node-mac-recorder 2.21.36 โ†’ 2.21.37

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.
@@ -4,7 +4,8 @@
4
4
  "Bash(cat:*)",
5
5
  "Bash(pkill:*)",
6
6
  "Bash(for f in test-output/*1761946670140.mov)",
7
- "Bash(node test.js:*)"
7
+ "Bash(node test.js:*)",
8
+ "Bash(sw_vers:*)"
8
9
  ],
9
10
  "deny": [],
10
11
  "ask": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.36",
3
+ "version": "2.21.37",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -282,18 +282,41 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
282
282
  }
283
283
  }
284
284
 
285
+ // ELECTRON SAFETY: Test image capture BEFORE starting timer
286
+ // This prevents crash if display capture doesn't work
287
+ MRLog(@"๐Ÿงช Testing display capture before starting timer...");
288
+ CGImageRef testCapture = CGDisplayCreateImage(g_avDisplayID);
289
+ if (!testCapture) {
290
+ NSLog(@"โŒ CRITICAL: Cannot capture display %u - aborting", g_avDisplayID);
291
+ if (g_avWriter) {
292
+ [g_avWriter cancelWriting];
293
+ g_avWriter = nil;
294
+ }
295
+ return false;
296
+ }
297
+ CGImageRelease(testCapture);
298
+ MRLog(@"โœ… Display capture test successful");
299
+
285
300
  // Start capture timer using target FPS
301
+ // ELECTRON SAFETY: Use a dedicated serial queue to avoid main thread conflicts
286
302
  dispatch_queue_t captureQueue = dispatch_queue_create("AVFoundationCaptureQueue", DISPATCH_QUEUE_SERIAL);
303
+ dispatch_set_target_queue(captureQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
304
+
287
305
  g_avTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, captureQueue);
288
-
306
+
289
307
  if (!g_avTimer) {
290
308
  NSLog(@"โŒ Failed to create dispatch timer");
309
+ if (g_avWriter) {
310
+ [g_avWriter cancelWriting];
311
+ g_avWriter = nil;
312
+ }
291
313
  return false;
292
314
  }
293
-
315
+
294
316
  uint64_t interval = (uint64_t)(NSEC_PER_SEC / fps);
295
- dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, interval / 10);
296
-
317
+ // ELECTRON SAFETY: Add small delay before first frame (100ms)
318
+ dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), interval, interval / 10);
319
+
297
320
  // Retain objects before passing to block to prevent deallocation
298
321
  AVAssetWriterInput *localVideoInput = g_avVideoInput;
299
322
  AVAssetWriterInputPixelBufferAdaptor *localPixelBufferAdaptor = g_avPixelBufferAdaptor;
@@ -448,12 +471,25 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
448
471
  }
449
472
  }
450
473
  });
451
-
452
- dispatch_resume(g_avTimer);
453
- g_avIsRecording = true;
454
-
455
- MRLog(@"๐ŸŽฅ AVFoundation recording started: %dx%d @ %.0ffps",
456
- (int)recordingSize.width, (int)recordingSize.height, fps);
474
+
475
+ // ELECTRON SAFETY: Start timer with exception handling
476
+ @try {
477
+ // Set recording flag BEFORE resuming timer
478
+ g_avIsRecording = true;
479
+
480
+ dispatch_resume(g_avTimer);
481
+
482
+ MRLog(@"๐ŸŽฅ AVFoundation recording started: %dx%d @ %.0ffps",
483
+ (int)recordingSize.width, (int)recordingSize.height, fps);
484
+ } @catch (NSException *exception) {
485
+ NSLog(@"โŒ Failed to start timer: %@", exception.reason);
486
+ g_avIsRecording = false;
487
+ if (g_avTimer) {
488
+ dispatch_source_cancel(g_avTimer);
489
+ g_avTimer = nil;
490
+ }
491
+ return false;
492
+ }
457
493
 
458
494
  return true;
459
495
 
@@ -490,14 +526,8 @@ extern "C" bool stopAVFoundationRecording() {
490
526
  // Mark as not recording FIRST to stop timer callbacks
491
527
  g_avIsRecording = false;
492
528
 
493
- // Cancel timer and wait a brief moment for completion
529
+ // Cancel timer immediately (Electron-safe)
494
530
  dispatch_source_cancel(g_avTimer);
495
-
496
- // Use async to avoid deadlock in Electron
497
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
498
- // Timer should be fully cancelled by now
499
- });
500
-
501
531
  g_avTimer = nil;
502
532
  MRLog(@"โœ… AVFoundation timer stopped safely");
503
533
  }
@@ -510,13 +540,16 @@ extern "C" bool stopAVFoundationRecording() {
510
540
 
511
541
  AVAssetWriter *writer = g_avWriter;
512
542
  if (writer && writer.status == AVAssetWriterStatusWriting) {
513
- dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
543
+ // ELECTRON SAFETY: Don't use dispatch_semaphore_wait - causes crashes in Electron
544
+ // Just call finishWriting async and let it complete in background
514
545
  [writer finishWritingWithCompletionHandler:^{
515
- dispatch_semaphore_signal(semaphore);
546
+ if (writer.status == AVAssetWriterStatusCompleted) {
547
+ MRLog(@"โœ… AVFoundation writer finished successfully");
548
+ } else if (writer.error) {
549
+ NSLog(@"โš ๏ธ AVFoundation writer error: %@", writer.error);
550
+ }
516
551
  }];
517
- // Add timeout to prevent infinite wait in Electron
518
- dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
519
- dispatch_semaphore_wait(semaphore, timeout);
552
+ MRLog(@"๐Ÿ”„ AVFoundation writer finishing asynchronously (Electron-safe mode)");
520
553
  }
521
554
 
522
555
  // Cleanup
@@ -137,29 +137,22 @@ static bool startAudioIfRequested(bool captureAudio,
137
137
  }
138
138
 
139
139
  // Helper function to cleanup recording resources
140
+ // CRITICAL FIX: Made safer for Electron - only resets state flags, doesn't call stop
141
+ // Calling stop operations during startup causes crashes in Electron
140
142
  void cleanupRecording() {
141
- // ScreenCaptureKit cleanup
142
- if (@available(macOS 12.3, *)) {
143
- if ([ScreenCaptureKitRecorder isRecording]) {
144
- [ScreenCaptureKitRecorder stopRecording];
145
- }
146
- }
147
-
148
- // AVFoundation cleanup (supports both Node.js and Electron)
149
- if (isAVFoundationRecording()) {
150
- stopAVFoundationRecording();
151
- }
143
+ // ELECTRON SAFETY: Don't call stop operations here - they're async and cause crashes
144
+ // Just reset state flags if they're stuck
152
145
 
153
- if (isCameraRecording()) {
154
- stopCameraRecording();
155
- }
156
- if (isStandaloneAudioRecording()) {
157
- stopStandaloneAudioRecording();
146
+ @try {
147
+ // Only reset flags, don't actually stop anything
148
+ g_isRecording = false;
149
+ g_usingStandaloneAudio = false;
150
+ MRSyncConfigure(NO);
151
+
152
+ MRLog(@"๐Ÿงน State flags reset (safe mode - no actual stop operations)");
153
+ } @catch (NSException *exception) {
154
+ NSLog(@"โš ๏ธ Exception during state reset: %@", exception.reason);
158
155
  }
159
- g_usingStandaloneAudio = false;
160
-
161
- g_isRecording = false;
162
- MRSyncConfigure(NO);
163
156
  }
164
157
 
165
158
  // NAPI Function: Start Recording
@@ -171,24 +164,16 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
171
164
  return env.Null();
172
165
  }
173
166
 
174
- // IMPORTANT: Clean up any stale recording state before starting
175
- // This fixes the issue where macOS 14/13 users get "recording already in progress"
176
- MRLog(@"๐Ÿงน Cleaning up any previous recording state...");
177
- cleanupRecording();
178
-
167
+ // ELECTRON SAFETY: Just check if recording is active, don't try to clean up
168
+ // Cleanup operations during start cause crashes in Electron
179
169
  if (g_isRecording) {
180
- MRLog(@"โš ๏ธ Still recording after cleanup - forcing stop");
170
+ MRLog(@"โš ๏ธ Recording already in progress");
181
171
  return Napi::Boolean::New(env, false);
182
172
  }
183
173
 
184
- // CRITICAL FIX: Check if ScreenCaptureKit is still cleaning up (async stop in progress)
185
- if (@available(macOS 12.3, *)) {
186
- extern BOOL isScreenCaptureKitCleaningUp();
187
- if (isScreenCaptureKitCleaningUp()) {
188
- MRLog(@"โš ๏ธ ScreenCaptureKit is still stopping previous recording - please wait");
189
- return Napi::Boolean::New(env, false);
190
- }
191
- }
174
+ // ELECTRON SAFETY: Reset state flags in case they're stuck
175
+ MRLog(@"๐Ÿงน Resetting state flags (safe mode)...");
176
+ cleanupRecording(); // This now only resets flags, doesn't call stop operations
192
177
  g_usingStandaloneAudio = false;
193
178
 
194
179
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
@@ -377,16 +362,16 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
377
362
  MRLog(@"๐Ÿ–ฅ๏ธ macOS Version: %ld.%ld.%ld",
378
363
  (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
379
364
 
365
+ // CRITICAL FIX FOR ELECTRON: ScreenCaptureKit crashes in Electron on macOS 15+
366
+ // Force AVFoundation for Electron regardless of macOS version
367
+ BOOL forceAVFoundation = isElectron ? YES : NO;
368
+
380
369
  if (isElectron) {
381
370
  MRLog(@"โšก Electron environment detected");
382
- MRLog(@"โœ… ELECTRON-FIRST: Both ScreenCaptureKit and AVFoundation fully supported");
371
+ MRLog(@"๐Ÿ”ง FORCING AVFoundation for Electron stability (ScreenCaptureKit crashes in Electron)");
372
+ forceAVFoundation = YES; // CRITICAL: ScreenCaptureKit is unstable in Electron
383
373
  }
384
374
 
385
- // CRITICAL FIX: Use ScreenCaptureKit on macOS 15+ for best compatibility
386
- // Both ScreenCaptureKit and AVFoundation work in Electron (Electron-first design)
387
- // Only fallback to AVFoundation if ScreenCaptureKit is unavailable
388
- BOOL forceAVFoundation = NO; // Let system choose based on availability
389
-
390
375
  MRLog(@"๐Ÿ”ง FRAMEWORK SELECTION: Smart selection based on macOS version");
391
376
  MRLog(@" Environment: %@", isElectron ? @"Electron" : @"Node.js");
392
377
  MRLog(@" macOS: %ld.%ld.%ld", (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
@@ -605,30 +590,41 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
605
590
 
606
591
  // Try ScreenCaptureKit first
607
592
  if (@available(macOS 12.3, *)) {
608
- if ([ScreenCaptureKitRecorder isRecording]) {
609
- MRLog(@"๐Ÿ›‘ Stopping ScreenCaptureKit recording");
593
+ @try {
594
+ if ([ScreenCaptureKitRecorder isRecording]) {
595
+ MRLog(@"๐Ÿ›‘ Stopping ScreenCaptureKit recording");
610
596
 
611
- // CRITICAL FIX: Stop camera and audio FIRST (they are synchronous)
612
- if (isCameraRecording()) {
613
- MRLog(@"๐Ÿ›‘ Stopping camera recording...");
614
- bool cameraStopped = stopCameraRecording();
615
- if (cameraStopped) {
616
- MRLog(@"โœ… Camera stopped successfully");
617
- } else {
618
- MRLog(@"โš ๏ธ Camera stop may have timed out");
597
+ // ELECTRON SAFETY: Stop camera without waiting
598
+ if (isCameraRecording()) {
599
+ MRLog(@"๐Ÿ›‘ Stopping camera recording...");
600
+ @try {
601
+ stopCameraRecording();
602
+ MRLog(@"โœ… Camera stop dispatched");
603
+ } @catch (NSException *exception) {
604
+ NSLog(@"โš ๏ธ Camera stop exception: %@", exception.reason);
605
+ }
619
606
  }
620
- }
621
607
 
622
- // Now stop ScreenCaptureKit (asynchronous)
623
- // WARNING: [ScreenCaptureKitRecorder stopRecording] is ASYNC!
624
- // It will set g_isRecording = NO in its completion handler
625
- [ScreenCaptureKitRecorder stopRecording];
608
+ // Now stop ScreenCaptureKit (asynchronous)
609
+ // WARNING: [ScreenCaptureKitRecorder stopRecording] is ASYNC!
610
+ @try {
611
+ [ScreenCaptureKitRecorder stopRecording];
612
+ MRLog(@"โœ… ScreenCaptureKit stop dispatched");
613
+ } @catch (NSException *exception) {
614
+ NSLog(@"โŒ ScreenCaptureKit stop exception: %@", exception.reason);
615
+ g_isRecording = false;
616
+ return Napi::Boolean::New(env, false);
617
+ }
626
618
 
627
- // DO NOT set g_isRecording here - let ScreenCaptureKit completion handler do it
628
- // Otherwise we have a race condition where JS thinks recording stopped but it's still running
619
+ g_usingStandaloneAudio = false;
620
+ MRSyncSetStopLimitSeconds(-1.0);
621
+ return Napi::Boolean::New(env, true);
622
+ }
623
+ } @catch (NSException *exception) {
624
+ NSLog(@"โŒ Exception in ScreenCaptureKit stop: %@", exception.reason);
625
+ g_isRecording = false;
629
626
  g_usingStandaloneAudio = false;
630
- MRSyncSetStopLimitSeconds(-1.0);
631
- return Napi::Boolean::New(env, true);
627
+ return Napi::Boolean::New(env, false);
632
628
  }
633
629
  }
634
630
 
@@ -672,26 +668,11 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
672
668
 
673
669
  bool avFoundationStopped = stopAVFoundationRecording();
674
670
 
675
- // SYNC FIX: Wait for both camera AND audio to finish (increased timeout to 5s)
671
+ // ELECTRON SAFETY: Don't use dispatch_group_wait - it blocks main thread and causes crashes
672
+ // Instead, just fire async stop and continue
676
673
  if (cameraWasRecording || audioWasRecording) {
677
- dispatch_time_t waitTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
678
- long waitResult = dispatch_group_wait(stopGroup, waitTime);
679
- if (waitResult != 0) {
680
- MRLog(@"โš ๏ธ SYNC: Camera/Audio stop did not finish within 5 seconds");
681
- if (cameraWasRecording && !cameraStopResult) {
682
- MRLog(@" โš ๏ธ Camera stop timed out");
683
- }
684
- if (audioWasRecording && !audioStopResult) {
685
- MRLog(@" โš ๏ธ Audio stop timed out");
686
- }
687
- } else {
688
- if (cameraWasRecording) {
689
- MRLog(@"โœ… SYNC: Camera stopped successfully");
690
- }
691
- if (audioWasRecording) {
692
- MRLog(@"โœ… SYNC: Audio stopped successfully");
693
- }
694
- }
674
+ MRLog(@"๐Ÿ”„ Camera/Audio stop operations dispatched asynchronously (Electron-safe mode)");
675
+ // Operations will complete in background - don't wait for them
695
676
  }
696
677
 
697
678
  g_isRecording = false;