node-mac-recorder 2.13.5 → 2.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/README.md +17 -3
- package/package.json +1 -1
- package/src/mac_recorder.mm +70 -233
- package/src/screen_capture_kit.mm +113 -39
- package/test-electron-detection.js +44 -0
- package/test-screencapture-only.js +50 -0
- package/test-screencapture.js +52 -0
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"Bash(ELECTRON_VERSION=25.0.0 node -e \"\nconsole.log(''ELECTRON_VERSION env:'', process.env.ELECTRON_VERSION);\nconsole.log(''getenv result would be:'', process.env.ELECTRON_VERSION || ''null'');\n\")",
|
|
27
27
|
"Bash(ELECTRON_VERSION=25.0.0 node test-env-detection.js)",
|
|
28
28
|
"Bash(ELECTRON_VERSION=25.0.0 node test-native-call.js)",
|
|
29
|
-
"Bash(chmod:*)"
|
|
29
|
+
"Bash(chmod:*)",
|
|
30
|
+
"Bash(ffprobe:*)"
|
|
30
31
|
],
|
|
31
32
|
"deny": []
|
|
32
33
|
}
|
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# node-mac-recorder
|
|
2
2
|
|
|
3
|
-
A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and
|
|
3
|
+
A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and automatic overlay window exclusion. Built with ScreenCaptureKit for modern macOS with intelligent window filtering and Electron compatibility.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
✨ **Advanced Recording Capabilities**
|
|
8
8
|
|
|
9
|
-
- 🖥️ **Full Screen Recording** - Capture entire displays
|
|
9
|
+
- 🖥️ **Full Screen Recording** - Capture entire displays with ScreenCaptureKit
|
|
10
10
|
- 🪟 **Window-Specific Recording** - Record individual application windows
|
|
11
11
|
- 🎯 **Area Selection** - Record custom screen regions
|
|
12
12
|
- 🖱️ **Multi-Display Support** - Automatic display detection and selection
|
|
13
13
|
- 🎨 **Cursor Control** - Toggle cursor visibility in recordings
|
|
14
14
|
- 🖱️ **Cursor Tracking** - Track mouse position, cursor types, and click events
|
|
15
|
+
- 🚫 **Automatic Overlay Exclusion** - Overlay windows automatically excluded from recordings
|
|
16
|
+
- ⚡ **Electron Compatible** - Enhanced crash protection for Electron applications
|
|
15
17
|
|
|
16
18
|
🎵 **Granular Audio Controls**
|
|
17
19
|
|
|
@@ -35,6 +37,18 @@ A powerful native macOS screen recording Node.js package with advanced window se
|
|
|
35
37
|
- 📁 **Flexible Output** - Custom output paths and formats
|
|
36
38
|
- 🔐 **Permission Management** - Built-in permission checking
|
|
37
39
|
|
|
40
|
+
## ScreenCaptureKit Technology
|
|
41
|
+
|
|
42
|
+
This package leverages Apple's modern **ScreenCaptureKit** framework (macOS 12.3+) for superior recording capabilities:
|
|
43
|
+
|
|
44
|
+
- **🎯 Native Overlay Exclusion**: Overlay windows are automatically filtered out during recording
|
|
45
|
+
- **🚀 Enhanced Performance**: Direct system-level recording with optimized resource usage
|
|
46
|
+
- **🛡️ Crash Protection**: Advanced safety layers for Electron applications
|
|
47
|
+
- **📱 Future-Proof**: Built on Apple's latest screen capture technology
|
|
48
|
+
- **🎨 Better Quality**: Improved frame handling and video encoding
|
|
49
|
+
|
|
50
|
+
> **Note**: For applications requiring overlay exclusion (like screen recording tools with floating UI), ScreenCaptureKit automatically handles window filtering without manual intervention.
|
|
51
|
+
|
|
38
52
|
## Installation
|
|
39
53
|
|
|
40
54
|
```bash
|
|
@@ -43,7 +57,7 @@ npm install node-mac-recorder
|
|
|
43
57
|
|
|
44
58
|
### Requirements
|
|
45
59
|
|
|
46
|
-
- **macOS
|
|
60
|
+
- **macOS 12.3+** (Monterey or later) - Required for ScreenCaptureKit
|
|
47
61
|
- **Node.js 14+**
|
|
48
62
|
- **Xcode Command Line Tools**
|
|
49
63
|
- **Screen Recording Permission** (automatically requested)
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -46,18 +46,12 @@ static bool g_isRecording = false;
|
|
|
46
46
|
|
|
47
47
|
// Helper function to cleanup recording resources
|
|
48
48
|
void cleanupRecording() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// ScreenCaptureKit cleanup only
|
|
50
|
+
if (@available(macOS 12.3, *)) {
|
|
51
|
+
if ([ScreenCaptureKitRecorder isRecording]) {
|
|
52
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
53
|
+
}
|
|
52
54
|
}
|
|
53
|
-
g_movieFileOutput = nil;
|
|
54
|
-
g_screenInput = nil;
|
|
55
|
-
g_audioInput = nil;
|
|
56
|
-
g_delegate = nil;
|
|
57
|
-
|
|
58
|
-
// Show overlay windows again after cleanup
|
|
59
|
-
showOverlays();
|
|
60
|
-
|
|
61
55
|
g_isRecording = false;
|
|
62
56
|
}
|
|
63
57
|
|
|
@@ -168,18 +162,20 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
@try {
|
|
171
|
-
//
|
|
172
|
-
NSLog(@"
|
|
173
|
-
NSLog(@"
|
|
174
|
-
NSLog(@"🎯 Using AVFoundation instead - stable in Electron environment");
|
|
165
|
+
// ScreenCaptureKit ONLY - No more AVFoundation fallback
|
|
166
|
+
NSLog(@"🎯 PURE ScreenCaptureKit - No AVFoundation fallback");
|
|
167
|
+
NSLog(@"🛡️ Enhanced Electron crash protection active");
|
|
175
168
|
|
|
176
|
-
if (
|
|
169
|
+
if (@available(macOS 12.3, *)) {
|
|
177
170
|
NSLog(@"✅ macOS 12.3+ detected - ScreenCaptureKit should be available");
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
171
|
+
|
|
172
|
+
// Try ScreenCaptureKit with extensive safety measures
|
|
173
|
+
@try {
|
|
174
|
+
if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
|
|
175
|
+
NSLog(@"✅ ScreenCaptureKit availability check passed");
|
|
176
|
+
NSLog(@"🎯 Using ScreenCaptureKit - overlay windows will be automatically excluded");
|
|
177
|
+
|
|
178
|
+
// Create configuration for ScreenCaptureKit
|
|
183
179
|
NSMutableDictionary *sckConfig = [NSMutableDictionary dictionary];
|
|
184
180
|
sckConfig[@"displayId"] = @(displayID);
|
|
185
181
|
sckConfig[@"captureCursor"] = @(captureCursor);
|
|
@@ -197,194 +193,59 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
197
193
|
};
|
|
198
194
|
}
|
|
199
195
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
NSLog(@"⚠️ Falling back to AVFoundation");
|
|
213
|
-
}
|
|
214
|
-
} else {
|
|
215
|
-
NSLog(@"❌ ScreenCaptureKit availability check failed");
|
|
216
|
-
NSLog(@"⚠️ Falling back to AVFoundation");
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
NSLog(@"❌ macOS version too old for ScreenCaptureKit (< 12.3)");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Fallback: Use AVFoundation with overlay hiding
|
|
223
|
-
NSLog(@"🎬 RECORDING METHOD: AVFoundation");
|
|
224
|
-
NSLog(@"📼 Using AVFoundation with overlay hiding for video compatibility");
|
|
225
|
-
|
|
226
|
-
// Hide overlay windows during recording
|
|
227
|
-
hideOverlays();
|
|
228
|
-
|
|
229
|
-
// Create capture session
|
|
230
|
-
g_captureSession = [[AVCaptureSession alloc] init];
|
|
231
|
-
[g_captureSession beginConfiguration];
|
|
232
|
-
|
|
233
|
-
// Set session preset
|
|
234
|
-
g_captureSession.sessionPreset = AVCaptureSessionPresetHigh;
|
|
235
|
-
|
|
236
|
-
// Create screen input with selected display
|
|
237
|
-
g_screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:displayID];
|
|
238
|
-
|
|
239
|
-
if (!CGRectIsNull(captureRect)) {
|
|
240
|
-
g_screenInput.cropRect = captureRect;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Set cursor capture
|
|
244
|
-
g_screenInput.capturesCursor = captureCursor;
|
|
245
|
-
|
|
246
|
-
// Configure screen input options
|
|
247
|
-
g_screenInput.capturesMouseClicks = NO;
|
|
248
|
-
|
|
249
|
-
if ([g_captureSession canAddInput:g_screenInput]) {
|
|
250
|
-
[g_captureSession addInput:g_screenInput];
|
|
251
|
-
} else {
|
|
252
|
-
cleanupRecording();
|
|
253
|
-
return Napi::Boolean::New(env, false);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Add microphone input if requested
|
|
257
|
-
if (includeMicrophone) {
|
|
258
|
-
AVCaptureDevice *audioDevice = nil;
|
|
259
|
-
|
|
260
|
-
if (audioDeviceId) {
|
|
261
|
-
// Try to find the specified device
|
|
262
|
-
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
263
|
-
NSLog(@"[DEBUG] Looking for audio device with ID: %@", audioDeviceId);
|
|
264
|
-
NSLog(@"[DEBUG] Available audio devices:");
|
|
265
|
-
for (AVCaptureDevice *device in devices) {
|
|
266
|
-
NSLog(@"[DEBUG] - Device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
267
|
-
if ([device.uniqueID isEqualToString:audioDeviceId]) {
|
|
268
|
-
NSLog(@"[DEBUG] Found matching device: %@", device.localizedName);
|
|
269
|
-
audioDevice = device;
|
|
270
|
-
break;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!audioDevice) {
|
|
275
|
-
NSLog(@"[DEBUG] Specified audio device not found, falling back to default");
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Fallback to default device if specified device not found
|
|
280
|
-
if (!audioDevice) {
|
|
281
|
-
audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
|
282
|
-
NSLog(@"[DEBUG] Using default audio device: %@ (ID: %@)", audioDevice.localizedName, audioDevice.uniqueID);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (audioDevice) {
|
|
286
|
-
NSError *error;
|
|
287
|
-
g_audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
|
|
288
|
-
if (g_audioInput && [g_captureSession canAddInput:g_audioInput]) {
|
|
289
|
-
[g_captureSession addInput:g_audioInput];
|
|
290
|
-
NSLog(@"[DEBUG] Successfully added audio input device");
|
|
291
|
-
} else {
|
|
292
|
-
NSLog(@"[DEBUG] Failed to add audio input device: %@", error);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// System audio configuration
|
|
298
|
-
if (includeSystemAudio) {
|
|
299
|
-
// Enable audio capture in screen input
|
|
300
|
-
g_screenInput.capturesMouseClicks = YES;
|
|
301
|
-
|
|
302
|
-
// Try to add system audio input using Core Audio
|
|
303
|
-
// This approach captures system audio by creating a virtual audio device
|
|
304
|
-
if (@available(macOS 10.15, *)) {
|
|
305
|
-
// Configure screen input for better audio capture
|
|
306
|
-
g_screenInput.capturesCursor = captureCursor;
|
|
307
|
-
g_screenInput.capturesMouseClicks = YES;
|
|
308
|
-
|
|
309
|
-
// Try to find and add system audio device (like Soundflower, BlackHole, etc.)
|
|
310
|
-
NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
311
|
-
AVCaptureDevice *systemAudioDevice = nil;
|
|
312
|
-
|
|
313
|
-
// If specific system audio device ID is provided, try to find it first
|
|
314
|
-
if (systemAudioDeviceId) {
|
|
315
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
316
|
-
if ([device.uniqueID isEqualToString:systemAudioDeviceId]) {
|
|
317
|
-
systemAudioDevice = device;
|
|
318
|
-
NSLog(@"[DEBUG] Found specified system audio device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
319
|
-
break;
|
|
196
|
+
// Use ScreenCaptureKit with window exclusion and timeout protection
|
|
197
|
+
NSError *sckError = nil;
|
|
198
|
+
|
|
199
|
+
// Set timeout for ScreenCaptureKit initialization
|
|
200
|
+
__block BOOL sckStarted = NO;
|
|
201
|
+
__block BOOL sckTimedOut = NO;
|
|
202
|
+
|
|
203
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)),
|
|
204
|
+
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
205
|
+
if (!sckStarted && !g_isRecording) {
|
|
206
|
+
sckTimedOut = YES;
|
|
207
|
+
NSLog(@"⏰ ScreenCaptureKit initialization timeout (3s)");
|
|
320
208
|
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Attempt to start ScreenCaptureKit with safety wrapper
|
|
212
|
+
@try {
|
|
213
|
+
if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
|
|
214
|
+
delegate:g_delegate
|
|
215
|
+
error:&sckError]) {
|
|
216
|
+
|
|
217
|
+
// ScreenCaptureKit başlatma başarılı - validation yapmıyoruz
|
|
218
|
+
sckStarted = YES;
|
|
219
|
+
NSLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
|
|
220
|
+
NSLog(@"✅ ScreenCaptureKit recording started successfully");
|
|
221
|
+
g_isRecording = true;
|
|
222
|
+
return Napi::Boolean::New(env, true);
|
|
223
|
+
} else {
|
|
224
|
+
NSLog(@"❌ ScreenCaptureKit failed to start");
|
|
225
|
+
NSLog(@"❌ Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
|
|
337
226
|
}
|
|
227
|
+
} @catch (NSException *sckException) {
|
|
228
|
+
NSLog(@"❌ Exception during ScreenCaptureKit startup: %@", sckException.reason);
|
|
338
229
|
}
|
|
230
|
+
|
|
231
|
+
NSLog(@"⚠️ ScreenCaptureKit failed or unsafe - falling back to AVFoundation");
|
|
232
|
+
|
|
233
|
+
} else {
|
|
234
|
+
NSLog(@"❌ ScreenCaptureKit availability check failed");
|
|
235
|
+
NSLog(@"⚠️ Falling back to AVFoundation");
|
|
339
236
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// Only add system audio device if microphone is not already added
|
|
344
|
-
NSError *error;
|
|
345
|
-
AVCaptureDeviceInput *systemAudioInput = [[AVCaptureDeviceInput alloc] initWithDevice:systemAudioDevice error:&error];
|
|
346
|
-
if (systemAudioInput && [g_captureSession canAddInput:systemAudioInput]) {
|
|
347
|
-
[g_captureSession addInput:systemAudioInput];
|
|
348
|
-
NSLog(@"[DEBUG] Successfully added system audio device: %@", systemAudioDevice.localizedName);
|
|
349
|
-
} else if (error) {
|
|
350
|
-
NSLog(@"[DEBUG] Failed to add system audio device: %@", error.localizedDescription);
|
|
351
|
-
}
|
|
352
|
-
} else if (includeSystemAudio && !systemAudioDevice) {
|
|
353
|
-
NSLog(@"[DEBUG] System audio requested but no suitable device found. Available devices:");
|
|
354
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
355
|
-
NSLog(@"[DEBUG] - %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
237
|
+
} @catch (NSException *availabilityException) {
|
|
238
|
+
NSLog(@"❌ Exception during ScreenCaptureKit availability check: %@", availabilityException.reason);
|
|
239
|
+
return Napi::Boolean::New(env, false);
|
|
358
240
|
}
|
|
359
241
|
} else {
|
|
360
|
-
|
|
361
|
-
g_screenInput.capturesMouseClicks = NO;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Create movie file output
|
|
365
|
-
g_movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
|
|
366
|
-
if ([g_captureSession canAddOutput:g_movieFileOutput]) {
|
|
367
|
-
[g_captureSession addOutput:g_movieFileOutput];
|
|
368
|
-
} else {
|
|
369
|
-
cleanupRecording();
|
|
242
|
+
NSLog(@"❌ macOS version too old for ScreenCaptureKit (< 12.3) - Recording not supported");
|
|
370
243
|
return Napi::Boolean::New(env, false);
|
|
371
244
|
}
|
|
372
245
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
[g_captureSession startRunning];
|
|
377
|
-
|
|
378
|
-
// Create delegate
|
|
379
|
-
g_delegate = [[MacRecorderDelegate alloc] init];
|
|
380
|
-
|
|
381
|
-
// Start recording
|
|
382
|
-
NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
|
|
383
|
-
[g_movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:g_delegate];
|
|
384
|
-
|
|
385
|
-
NSLog(@"✅ AVFoundation recording started");
|
|
386
|
-
g_isRecording = true;
|
|
387
|
-
return Napi::Boolean::New(env, true);
|
|
246
|
+
// If we get here, ScreenCaptureKit failed completely
|
|
247
|
+
NSLog(@"❌ ScreenCaptureKit failed to initialize - Recording not available");
|
|
248
|
+
return Napi::Boolean::New(env, false);
|
|
388
249
|
|
|
389
250
|
} @catch (NSException *exception) {
|
|
390
251
|
cleanupRecording();
|
|
@@ -398,44 +259,20 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
398
259
|
|
|
399
260
|
NSLog(@"📞 StopRecording native method called");
|
|
400
261
|
|
|
401
|
-
//
|
|
402
|
-
BOOL screenCaptureKitStopped = NO;
|
|
262
|
+
// ScreenCaptureKit ONLY - No AVFoundation fallback
|
|
403
263
|
if (@available(macOS 12.3, *)) {
|
|
404
264
|
if ([ScreenCaptureKitRecorder isRecording]) {
|
|
405
265
|
NSLog(@"🛑 Stopping ScreenCaptureKit recording");
|
|
406
266
|
[ScreenCaptureKitRecorder stopRecording];
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return Napi::Boolean::New(env, true);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Otherwise, handle AVFoundation recording
|
|
417
|
-
if (!g_isRecording || !g_movieFileOutput) {
|
|
418
|
-
NSLog(@"❌ No AVFoundation recording in progress");
|
|
419
|
-
return Napi::Boolean::New(env, false);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
@try {
|
|
423
|
-
NSLog(@"🛑 Stopping AVFoundation recording");
|
|
424
|
-
if (g_movieFileOutput) {
|
|
425
|
-
[g_movieFileOutput stopRecording];
|
|
426
|
-
}
|
|
427
|
-
if (g_captureSession) {
|
|
428
|
-
[g_captureSession stopRunning];
|
|
267
|
+
g_isRecording = false;
|
|
268
|
+
return Napi::Boolean::New(env, true);
|
|
269
|
+
} else {
|
|
270
|
+
NSLog(@"⚠️ ScreenCaptureKit not recording");
|
|
271
|
+
g_isRecording = false;
|
|
272
|
+
return Napi::Boolean::New(env, true);
|
|
429
273
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
showOverlays();
|
|
433
|
-
|
|
434
|
-
g_isRecording = false;
|
|
435
|
-
return Napi::Boolean::New(env, true);
|
|
436
|
-
|
|
437
|
-
} @catch (NSException *exception) {
|
|
438
|
-
cleanupRecording();
|
|
274
|
+
} else {
|
|
275
|
+
NSLog(@"❌ ScreenCaptureKit not available - cannot stop recording");
|
|
439
276
|
return Napi::Boolean::New(env, false);
|
|
440
277
|
}
|
|
441
278
|
}
|
|
@@ -37,68 +37,142 @@ static BOOL g_writerStarted = NO;
|
|
|
37
37
|
@end
|
|
38
38
|
|
|
39
39
|
@interface ElectronSafeOutput : NSObject <SCStreamOutput>
|
|
40
|
+
- (void)processSampleBufferSafely:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type;
|
|
40
41
|
@end
|
|
41
42
|
|
|
42
43
|
@implementation ElectronSafeOutput
|
|
43
44
|
- (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
45
|
+
// EXTREME SAFETY: Complete isolation with separate thread
|
|
46
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
|
|
47
|
+
@autoreleasepool {
|
|
48
|
+
[self processSampleBufferSafely:sampleBuffer ofType:type];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (void)processSampleBufferSafely:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
54
|
+
// ELECTRON CRASH PROTECTION: Multiple layers of safety
|
|
44
55
|
if (!g_isRecording || type != SCStreamOutputTypeScreen || !g_assetWriterInput) {
|
|
45
56
|
return;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
56
|
-
g_writerStarted = YES;
|
|
57
|
-
NSLog(@"✅ Electron-safe video writer started");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Ultra-conservative Electron-safe sample buffer processing
|
|
59
|
+
// SAFETY LAYER 1: Null checks
|
|
60
|
+
if (!sampleBuffer || !CMSampleBufferIsValid(sampleBuffer)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// SAFETY LAYER 2: Try-catch with complete isolation
|
|
65
|
+
@try {
|
|
61
66
|
@autoreleasepool {
|
|
67
|
+
// SAFETY LAYER 3: Initialize writer safely (only once)
|
|
68
|
+
static BOOL initializationAttempted = NO;
|
|
69
|
+
if (!g_writerStarted && !initializationAttempted && g_assetWriter && g_assetWriterInput) {
|
|
70
|
+
initializationAttempted = YES;
|
|
71
|
+
@try {
|
|
72
|
+
CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
73
|
+
|
|
74
|
+
// SAFETY CHECK: Ensure valid time
|
|
75
|
+
if (CMTIME_IS_VALID(presentationTime) && CMTIME_IS_NUMERIC(presentationTime)) {
|
|
76
|
+
g_startTime = presentationTime;
|
|
77
|
+
g_currentTime = g_startTime;
|
|
78
|
+
|
|
79
|
+
// SAFETY LAYER 4: Writer state validation
|
|
80
|
+
if (g_assetWriter.status == AVAssetWriterStatusUnknown) {
|
|
81
|
+
[g_assetWriter startWriting];
|
|
82
|
+
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
83
|
+
g_writerStarted = YES;
|
|
84
|
+
NSLog(@"✅ Ultra-safe ScreenCaptureKit writer started");
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Use zero time if sample buffer time is invalid
|
|
88
|
+
NSLog(@"⚠️ Invalid sample buffer time, using kCMTimeZero");
|
|
89
|
+
g_startTime = kCMTimeZero;
|
|
90
|
+
g_currentTime = g_startTime;
|
|
91
|
+
|
|
92
|
+
if (g_assetWriter.status == AVAssetWriterStatusUnknown) {
|
|
93
|
+
[g_assetWriter startWriting];
|
|
94
|
+
[g_assetWriter startSessionAtSourceTime:kCMTimeZero];
|
|
95
|
+
g_writerStarted = YES;
|
|
96
|
+
NSLog(@"✅ Ultra-safe ScreenCaptureKit writer started with zero time");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} @catch (NSException *writerException) {
|
|
100
|
+
NSLog(@"⚠️ Writer initialization failed safely: %@", writerException.reason);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// SAFETY LAYER 5: Frame processing with isolation
|
|
62
106
|
if (!g_writerStarted || !g_assetWriterInput || !g_pixelBufferAdaptor) {
|
|
63
|
-
return;
|
|
107
|
+
return;
|
|
64
108
|
}
|
|
65
109
|
|
|
66
|
-
//
|
|
110
|
+
// SAFETY LAYER 6: Conservative rate limiting
|
|
67
111
|
static NSTimeInterval lastProcessTime = 0;
|
|
68
112
|
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
|
|
69
|
-
if (currentTime - lastProcessTime < 0.1) { // Max 10 FPS
|
|
113
|
+
if (currentTime - lastProcessTime < 0.1) { // Max 10 FPS
|
|
70
114
|
return;
|
|
71
115
|
}
|
|
72
116
|
lastProcessTime = currentTime;
|
|
73
117
|
|
|
74
|
-
|
|
75
|
-
|
|
118
|
+
// SAFETY LAYER 7: Input readiness check
|
|
119
|
+
if (!g_assetWriterInput.isReadyForMoreMediaData) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// SAFETY LAYER 8: Pixel buffer validation
|
|
124
|
+
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
|
125
|
+
if (!pixelBuffer) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// SAFETY LAYER 9: Dimension validation - flexible this time
|
|
130
|
+
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
|
131
|
+
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
132
|
+
if (width == 0 || height == 0 || width > 4096 || height > 4096) {
|
|
133
|
+
return; // Skip only if clearly invalid
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// SAFETY LAYER 10: Time validation
|
|
137
|
+
CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
138
|
+
if (!CMTIME_IS_VALID(presentationTime)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
CMTime relativeTime = CMTimeSubtract(presentationTime, g_startTime);
|
|
143
|
+
if (!CMTIME_IS_VALID(relativeTime)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
double seconds = CMTimeGetSeconds(relativeTime);
|
|
148
|
+
if (seconds < 0 || seconds > 30.0) { // Allow longer recordings
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// SAFETY LAYER 11: Append with complete exception handling
|
|
153
|
+
@try {
|
|
154
|
+
// Use pixel buffer directly - copy was causing errors
|
|
155
|
+
BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
|
|
76
156
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
84
|
-
CMTime relativeTime = CMTimeSubtract(presentationTime, g_startTime);
|
|
85
|
-
|
|
86
|
-
// Conservative time validation
|
|
87
|
-
if (CMTimeGetSeconds(relativeTime) >= 0 && CMTimeGetSeconds(relativeTime) < 30) {
|
|
88
|
-
BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
|
|
89
|
-
if (success) {
|
|
90
|
-
g_currentTime = relativeTime;
|
|
91
|
-
static int safeFrameCount = 0;
|
|
92
|
-
safeFrameCount++;
|
|
93
|
-
if (safeFrameCount % 10 == 0) {
|
|
94
|
-
NSLog(@"✅ Electron-safe: %d frames", safeFrameCount);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
157
|
+
if (success) {
|
|
158
|
+
g_currentTime = relativeTime;
|
|
159
|
+
static int ultraSafeFrameCount = 0;
|
|
160
|
+
ultraSafeFrameCount++;
|
|
161
|
+
if (ultraSafeFrameCount % 10 == 0) {
|
|
162
|
+
NSLog(@"🛡️ Ultra-safe: %d frames (%.1fs)", ultraSafeFrameCount, seconds);
|
|
98
163
|
}
|
|
99
164
|
}
|
|
165
|
+
} @catch (NSException *appendException) {
|
|
166
|
+
NSLog(@"🛡️ Append exception handled safely: %@", appendException.reason);
|
|
167
|
+
// Continue gracefully - don't crash
|
|
100
168
|
}
|
|
101
169
|
}
|
|
170
|
+
} @catch (NSException *outerException) {
|
|
171
|
+
NSLog(@"🛡️ Outer exception handled: %@", outerException.reason);
|
|
172
|
+
// Ultimate safety - graceful continue
|
|
173
|
+
} @catch (...) {
|
|
174
|
+
NSLog(@"🛡️ Unknown exception caught and handled safely");
|
|
175
|
+
// Catch any C++ exceptions too
|
|
102
176
|
}
|
|
103
177
|
}
|
|
104
178
|
@end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
|
|
3
|
+
// Simulate Electron environment
|
|
4
|
+
process.env.ELECTRON_RUN_AS_NODE = '1';
|
|
5
|
+
|
|
6
|
+
console.log('🔍 Testing Electron Detection');
|
|
7
|
+
console.log('Environment variables:', {
|
|
8
|
+
ELECTRON_RUN_AS_NODE: process.env.ELECTRON_RUN_AS_NODE,
|
|
9
|
+
processName: process.title
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
async function testElectronDetection() {
|
|
13
|
+
const recorder = new MacRecorder();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const outputPath = './test-output/electron-detection-test.mov';
|
|
17
|
+
|
|
18
|
+
console.log('📹 Starting recording with Electron detection...');
|
|
19
|
+
const success = await recorder.startRecording(outputPath, {
|
|
20
|
+
captureCursor: true,
|
|
21
|
+
includeMicrophone: false,
|
|
22
|
+
includeSystemAudio: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (success) {
|
|
26
|
+
console.log('✅ Recording started successfully');
|
|
27
|
+
|
|
28
|
+
// Record for 3 seconds
|
|
29
|
+
console.log('⏱️ Recording for 3 seconds...');
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
31
|
+
|
|
32
|
+
console.log('🛑 Stopping recording...');
|
|
33
|
+
await recorder.stopRecording();
|
|
34
|
+
|
|
35
|
+
console.log('✅ Recording completed without crash');
|
|
36
|
+
} else {
|
|
37
|
+
console.log('❌ Failed to start recording');
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.log('❌ Error during test:', error.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
testElectronDetection().catch(console.error);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
|
|
3
|
+
console.log('🎯 Testing PURE ScreenCaptureKit (No AVFoundation)');
|
|
4
|
+
|
|
5
|
+
async function testScreenCaptureKitOnly() {
|
|
6
|
+
const recorder = new MacRecorder();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const outputPath = './test-output/screencapturekit-only-test.mov';
|
|
10
|
+
|
|
11
|
+
console.log('📹 Starting ScreenCaptureKit-only recording...');
|
|
12
|
+
const success = await recorder.startRecording(outputPath, {
|
|
13
|
+
captureCursor: true,
|
|
14
|
+
includeMicrophone: false,
|
|
15
|
+
includeSystemAudio: false
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (success) {
|
|
19
|
+
console.log('✅ Recording started successfully');
|
|
20
|
+
|
|
21
|
+
// Record for 5 seconds
|
|
22
|
+
console.log('⏱️ Recording for 5 seconds...');
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
24
|
+
|
|
25
|
+
console.log('🛑 Stopping recording...');
|
|
26
|
+
await recorder.stopRecording();
|
|
27
|
+
|
|
28
|
+
// Check if file exists and has content
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
if (fs.existsSync(outputPath)) {
|
|
31
|
+
const stats = fs.statSync(outputPath);
|
|
32
|
+
console.log(`✅ Video file created: ${outputPath} (${stats.size} bytes)`);
|
|
33
|
+
|
|
34
|
+
if (stats.size > 1000) {
|
|
35
|
+
console.log('✅ ScreenCaptureKit-only recording successful');
|
|
36
|
+
} else {
|
|
37
|
+
console.log('⚠️ File size is very small');
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
console.log('❌ Video file not found');
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.log('❌ Failed to start recording');
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.log('❌ Error during test:', error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
testScreenCaptureKitOnly().catch(console.error);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
async function testScreenCaptureKit() {
|
|
5
|
+
const recorder = new MacRecorder();
|
|
6
|
+
|
|
7
|
+
console.log('🔍 Testing ScreenCaptureKit Integration');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Check if we can start recording
|
|
11
|
+
const outputPath = './test-output/screencapturekit-test.mov';
|
|
12
|
+
|
|
13
|
+
console.log('📹 Starting recording with ScreenCaptureKit...');
|
|
14
|
+
const success = await recorder.startRecording(outputPath, {
|
|
15
|
+
captureCursor: true,
|
|
16
|
+
includeMicrophone: false,
|
|
17
|
+
includeSystemAudio: false
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (success) {
|
|
21
|
+
console.log('✅ Recording started successfully');
|
|
22
|
+
|
|
23
|
+
// Record for 5 seconds
|
|
24
|
+
console.log('⏱️ Recording for 5 seconds...');
|
|
25
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
26
|
+
|
|
27
|
+
console.log('🛑 Stopping recording...');
|
|
28
|
+
await recorder.stopRecording();
|
|
29
|
+
|
|
30
|
+
// Check if file exists and has content
|
|
31
|
+
if (fs.existsSync(outputPath)) {
|
|
32
|
+
const stats = fs.statSync(outputPath);
|
|
33
|
+
console.log(`✅ Video file created: ${outputPath} (${stats.size} bytes)`);
|
|
34
|
+
|
|
35
|
+
if (stats.size > 1000) {
|
|
36
|
+
console.log('✅ File size looks good - recording likely successful');
|
|
37
|
+
} else {
|
|
38
|
+
console.log('⚠️ File size is very small - recording may have failed');
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
console.log('❌ Video file not found');
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
console.log('❌ Failed to start recording');
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.log('❌ Error during test:', error.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Run test
|
|
52
|
+
testScreenCaptureKit().catch(console.error);
|