node-mac-recorder 2.13.6 β 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/README.md +17 -3
- package/package.json +1 -1
- package/src/mac_recorder.mm +28 -238
- package/src/screen_capture_kit.mm +117 -100
- package/test-electron-detection.js +44 -0
- package/test-screencapture-only.js +50 -0
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,20 +162,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
@try {
|
|
171
|
-
//
|
|
172
|
-
NSLog(@"
|
|
173
|
-
NSLog(@"π‘οΈ
|
|
174
|
-
NSLog(@"π Will fallback to AVFoundation if ScreenCaptureKit fails");
|
|
175
|
-
|
|
176
|
-
// Add Electron detection and safety check
|
|
177
|
-
BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
|
|
178
|
-
[NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
|
|
179
|
-
(NSProcessInfo.processInfo.processName &&
|
|
180
|
-
[NSProcessInfo.processInfo.processName containsString:@"Electron"]);
|
|
181
|
-
|
|
182
|
-
if (isElectron) {
|
|
183
|
-
NSLog(@"β‘ Electron environment detected - using extra safety measures");
|
|
184
|
-
}
|
|
165
|
+
// ScreenCaptureKit ONLY - No more AVFoundation fallback
|
|
166
|
+
NSLog(@"π― PURE ScreenCaptureKit - No AVFoundation fallback");
|
|
167
|
+
NSLog(@"π‘οΈ Enhanced Electron crash protection active");
|
|
185
168
|
|
|
186
169
|
if (@available(macOS 12.3, *)) {
|
|
187
170
|
NSLog(@"β
macOS 12.3+ detected - ScreenCaptureKit should be available");
|
|
@@ -231,19 +214,12 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
231
214
|
delegate:g_delegate
|
|
232
215
|
error:&sckError]) {
|
|
233
216
|
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
NSLog(@"β
ScreenCaptureKit recording started with window exclusion");
|
|
241
|
-
g_isRecording = true;
|
|
242
|
-
return Napi::Boolean::New(env, true);
|
|
243
|
-
} else {
|
|
244
|
-
NSLog(@"β οΈ ScreenCaptureKit started but validation failed");
|
|
245
|
-
[ScreenCaptureKitRecorder stopRecording];
|
|
246
|
-
}
|
|
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);
|
|
247
223
|
} else {
|
|
248
224
|
NSLog(@"β ScreenCaptureKit failed to start");
|
|
249
225
|
NSLog(@"β Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
|
|
@@ -260,178 +236,16 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
260
236
|
}
|
|
261
237
|
} @catch (NSException *availabilityException) {
|
|
262
238
|
NSLog(@"β Exception during ScreenCaptureKit availability check: %@", availabilityException.reason);
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
NSLog(@"β macOS version too old for ScreenCaptureKit (< 12.3)");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Fallback: Use AVFoundation with overlay hiding
|
|
270
|
-
NSLog(@"π¬ RECORDING METHOD: AVFoundation");
|
|
271
|
-
NSLog(@"πΌ Using AVFoundation with overlay hiding for video compatibility");
|
|
272
|
-
|
|
273
|
-
// Hide overlay windows during recording
|
|
274
|
-
hideOverlays();
|
|
275
|
-
|
|
276
|
-
// Create capture session
|
|
277
|
-
g_captureSession = [[AVCaptureSession alloc] init];
|
|
278
|
-
[g_captureSession beginConfiguration];
|
|
279
|
-
|
|
280
|
-
// Set session preset
|
|
281
|
-
g_captureSession.sessionPreset = AVCaptureSessionPresetHigh;
|
|
282
|
-
|
|
283
|
-
// Create screen input with selected display
|
|
284
|
-
g_screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:displayID];
|
|
285
|
-
|
|
286
|
-
if (!CGRectIsNull(captureRect)) {
|
|
287
|
-
g_screenInput.cropRect = captureRect;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Set cursor capture
|
|
291
|
-
g_screenInput.capturesCursor = captureCursor;
|
|
292
|
-
|
|
293
|
-
// Configure screen input options
|
|
294
|
-
g_screenInput.capturesMouseClicks = NO;
|
|
295
|
-
|
|
296
|
-
if ([g_captureSession canAddInput:g_screenInput]) {
|
|
297
|
-
[g_captureSession addInput:g_screenInput];
|
|
298
|
-
} else {
|
|
299
|
-
cleanupRecording();
|
|
300
|
-
return Napi::Boolean::New(env, false);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Add microphone input if requested
|
|
304
|
-
if (includeMicrophone) {
|
|
305
|
-
AVCaptureDevice *audioDevice = nil;
|
|
306
|
-
|
|
307
|
-
if (audioDeviceId) {
|
|
308
|
-
// Try to find the specified device
|
|
309
|
-
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
310
|
-
NSLog(@"[DEBUG] Looking for audio device with ID: %@", audioDeviceId);
|
|
311
|
-
NSLog(@"[DEBUG] Available audio devices:");
|
|
312
|
-
for (AVCaptureDevice *device in devices) {
|
|
313
|
-
NSLog(@"[DEBUG] - Device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
314
|
-
if ([device.uniqueID isEqualToString:audioDeviceId]) {
|
|
315
|
-
NSLog(@"[DEBUG] Found matching device: %@", device.localizedName);
|
|
316
|
-
audioDevice = device;
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (!audioDevice) {
|
|
322
|
-
NSLog(@"[DEBUG] Specified audio device not found, falling back to default");
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Fallback to default device if specified device not found
|
|
327
|
-
if (!audioDevice) {
|
|
328
|
-
audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
|
329
|
-
NSLog(@"[DEBUG] Using default audio device: %@ (ID: %@)", audioDevice.localizedName, audioDevice.uniqueID);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (audioDevice) {
|
|
333
|
-
NSError *error;
|
|
334
|
-
g_audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
|
|
335
|
-
if (g_audioInput && [g_captureSession canAddInput:g_audioInput]) {
|
|
336
|
-
[g_captureSession addInput:g_audioInput];
|
|
337
|
-
NSLog(@"[DEBUG] Successfully added audio input device");
|
|
338
|
-
} else {
|
|
339
|
-
NSLog(@"[DEBUG] Failed to add audio input device: %@", error);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// System audio configuration
|
|
345
|
-
if (includeSystemAudio) {
|
|
346
|
-
// Enable audio capture in screen input
|
|
347
|
-
g_screenInput.capturesMouseClicks = YES;
|
|
348
|
-
|
|
349
|
-
// Try to add system audio input using Core Audio
|
|
350
|
-
// This approach captures system audio by creating a virtual audio device
|
|
351
|
-
if (@available(macOS 10.15, *)) {
|
|
352
|
-
// Configure screen input for better audio capture
|
|
353
|
-
g_screenInput.capturesCursor = captureCursor;
|
|
354
|
-
g_screenInput.capturesMouseClicks = YES;
|
|
355
|
-
|
|
356
|
-
// Try to find and add system audio device (like Soundflower, BlackHole, etc.)
|
|
357
|
-
NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
358
|
-
AVCaptureDevice *systemAudioDevice = nil;
|
|
359
|
-
|
|
360
|
-
// If specific system audio device ID is provided, try to find it first
|
|
361
|
-
if (systemAudioDeviceId) {
|
|
362
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
363
|
-
if ([device.uniqueID isEqualToString:systemAudioDeviceId]) {
|
|
364
|
-
systemAudioDevice = device;
|
|
365
|
-
NSLog(@"[DEBUG] Found specified system audio device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// If no specific device found or specified, look for known system audio devices
|
|
372
|
-
if (!systemAudioDevice) {
|
|
373
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
374
|
-
NSString *deviceName = [device.localizedName lowercaseString];
|
|
375
|
-
// Check for common system audio capture devices
|
|
376
|
-
if ([deviceName containsString:@"soundflower"] ||
|
|
377
|
-
[deviceName containsString:@"blackhole"] ||
|
|
378
|
-
[deviceName containsString:@"loopback"] ||
|
|
379
|
-
[deviceName containsString:@"system audio"] ||
|
|
380
|
-
[deviceName containsString:@"aggregate"]) {
|
|
381
|
-
systemAudioDevice = device;
|
|
382
|
-
NSLog(@"[DEBUG] Auto-detected system audio device: %@", device.localizedName);
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// If we found a system audio device, add it as an additional input
|
|
389
|
-
if (systemAudioDevice && !includeMicrophone) {
|
|
390
|
-
// Only add system audio device if microphone is not already added
|
|
391
|
-
NSError *error;
|
|
392
|
-
AVCaptureDeviceInput *systemAudioInput = [[AVCaptureDeviceInput alloc] initWithDevice:systemAudioDevice error:&error];
|
|
393
|
-
if (systemAudioInput && [g_captureSession canAddInput:systemAudioInput]) {
|
|
394
|
-
[g_captureSession addInput:systemAudioInput];
|
|
395
|
-
NSLog(@"[DEBUG] Successfully added system audio device: %@", systemAudioDevice.localizedName);
|
|
396
|
-
} else if (error) {
|
|
397
|
-
NSLog(@"[DEBUG] Failed to add system audio device: %@", error.localizedDescription);
|
|
398
|
-
}
|
|
399
|
-
} else if (includeSystemAudio && !systemAudioDevice) {
|
|
400
|
-
NSLog(@"[DEBUG] System audio requested but no suitable device found. Available devices:");
|
|
401
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
402
|
-
NSLog(@"[DEBUG] - %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
239
|
+
return Napi::Boolean::New(env, false);
|
|
405
240
|
}
|
|
406
241
|
} else {
|
|
407
|
-
|
|
408
|
-
g_screenInput.capturesMouseClicks = NO;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Create movie file output
|
|
412
|
-
g_movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
|
|
413
|
-
if ([g_captureSession canAddOutput:g_movieFileOutput]) {
|
|
414
|
-
[g_captureSession addOutput:g_movieFileOutput];
|
|
415
|
-
} else {
|
|
416
|
-
cleanupRecording();
|
|
242
|
+
NSLog(@"β macOS version too old for ScreenCaptureKit (< 12.3) - Recording not supported");
|
|
417
243
|
return Napi::Boolean::New(env, false);
|
|
418
244
|
}
|
|
419
245
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
[g_captureSession startRunning];
|
|
424
|
-
|
|
425
|
-
// Create delegate
|
|
426
|
-
g_delegate = [[MacRecorderDelegate alloc] init];
|
|
427
|
-
|
|
428
|
-
// Start recording
|
|
429
|
-
NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
|
|
430
|
-
[g_movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:g_delegate];
|
|
431
|
-
|
|
432
|
-
NSLog(@"β
AVFoundation recording started");
|
|
433
|
-
g_isRecording = true;
|
|
434
|
-
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);
|
|
435
249
|
|
|
436
250
|
} @catch (NSException *exception) {
|
|
437
251
|
cleanupRecording();
|
|
@@ -445,44 +259,20 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
445
259
|
|
|
446
260
|
NSLog(@"π StopRecording native method called");
|
|
447
261
|
|
|
448
|
-
//
|
|
449
|
-
BOOL screenCaptureKitStopped = NO;
|
|
262
|
+
// ScreenCaptureKit ONLY - No AVFoundation fallback
|
|
450
263
|
if (@available(macOS 12.3, *)) {
|
|
451
264
|
if ([ScreenCaptureKitRecorder isRecording]) {
|
|
452
265
|
NSLog(@"π Stopping ScreenCaptureKit recording");
|
|
453
266
|
[ScreenCaptureKitRecorder stopRecording];
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
return Napi::Boolean::New(env, true);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Otherwise, handle AVFoundation recording
|
|
464
|
-
if (!g_isRecording || !g_movieFileOutput) {
|
|
465
|
-
NSLog(@"β No AVFoundation recording in progress");
|
|
466
|
-
return Napi::Boolean::New(env, false);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
@try {
|
|
470
|
-
NSLog(@"π Stopping AVFoundation recording");
|
|
471
|
-
if (g_movieFileOutput) {
|
|
472
|
-
[g_movieFileOutput stopRecording];
|
|
473
|
-
}
|
|
474
|
-
if (g_captureSession) {
|
|
475
|
-
[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);
|
|
476
273
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
showOverlays();
|
|
480
|
-
|
|
481
|
-
g_isRecording = false;
|
|
482
|
-
return Napi::Boolean::New(env, true);
|
|
483
|
-
|
|
484
|
-
} @catch (NSException *exception) {
|
|
485
|
-
cleanupRecording();
|
|
274
|
+
} else {
|
|
275
|
+
NSLog(@"β ScreenCaptureKit not available - cannot stop recording");
|
|
486
276
|
return Napi::Boolean::New(env, false);
|
|
487
277
|
}
|
|
488
278
|
}
|
|
@@ -37,125 +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
|
-
//
|
|
59
|
+
// SAFETY LAYER 1: Null checks
|
|
60
|
+
if (!sampleBuffer || !CMSampleBufferIsValid(sampleBuffer)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// SAFETY LAYER 2: Try-catch with complete isolation
|
|
49
65
|
@try {
|
|
50
66
|
@autoreleasepool {
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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);
|
|
65
101
|
return;
|
|
66
102
|
}
|
|
67
|
-
|
|
68
|
-
[g_assetWriter startWriting];
|
|
69
|
-
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
70
|
-
g_writerStarted = YES;
|
|
71
|
-
NSLog(@"β
Electron-safe video writer started");
|
|
72
103
|
}
|
|
73
104
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
double seconds = CMTimeGetSeconds(relativeTime);
|
|
126
|
-
if (seconds < 0 || seconds > 10.0) { // Max 10 seconds for safety
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
105
|
+
// SAFETY LAYER 5: Frame processing with isolation
|
|
106
|
+
if (!g_writerStarted || !g_assetWriterInput || !g_pixelBufferAdaptor) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// SAFETY LAYER 6: Conservative rate limiting
|
|
111
|
+
static NSTimeInterval lastProcessTime = 0;
|
|
112
|
+
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
|
|
113
|
+
if (currentTime - lastProcessTime < 0.1) { // Max 10 FPS
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
lastProcessTime = currentTime;
|
|
117
|
+
|
|
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];
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
safeFrameCount++;
|
|
137
|
-
if (safeFrameCount % 20 == 0) { // Less frequent logging
|
|
138
|
-
NSLog(@"β
Electron-safe: %d frames (%.1fs)", safeFrameCount, seconds);
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
static int appendFailures = 0;
|
|
142
|
-
if (appendFailures++ < 3) { // Limit failure logs
|
|
143
|
-
NSLog(@"β οΈ Failed to append pixel buffer");
|
|
144
|
-
}
|
|
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);
|
|
145
163
|
}
|
|
146
|
-
} @catch (NSException *appendException) {
|
|
147
|
-
NSLog(@"β Exception during pixel buffer append: %@", appendException.reason);
|
|
148
|
-
// Don't rethrow - just skip this frame
|
|
149
164
|
}
|
|
165
|
+
} @catch (NSException *appendException) {
|
|
166
|
+
NSLog(@"π‘οΈ Append exception handled safely: %@", appendException.reason);
|
|
167
|
+
// Continue gracefully - don't crash
|
|
150
168
|
}
|
|
151
169
|
}
|
|
152
|
-
} @catch (NSException *
|
|
153
|
-
NSLog(@"
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
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
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
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);
|