node-mac-recorder 2.21.37 โ†’ 2.21.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.37",
3
+ "version": "2.21.40",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -282,41 +282,18 @@ 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
-
300
285
  // Start capture timer using target FPS
301
- // ELECTRON SAFETY: Use a dedicated serial queue to avoid main thread conflicts
302
286
  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
-
305
287
  g_avTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, captureQueue);
306
-
288
+
307
289
  if (!g_avTimer) {
308
290
  NSLog(@"โŒ Failed to create dispatch timer");
309
- if (g_avWriter) {
310
- [g_avWriter cancelWriting];
311
- g_avWriter = nil;
312
- }
313
291
  return false;
314
292
  }
315
-
293
+
316
294
  uint64_t interval = (uint64_t)(NSEC_PER_SEC / fps);
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
-
295
+ dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, interval / 10);
296
+
320
297
  // Retain objects before passing to block to prevent deallocation
321
298
  AVAssetWriterInput *localVideoInput = g_avVideoInput;
322
299
  AVAssetWriterInputPixelBufferAdaptor *localPixelBufferAdaptor = g_avPixelBufferAdaptor;
@@ -471,25 +448,12 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
471
448
  }
472
449
  }
473
450
  });
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
- }
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);
493
457
 
494
458
  return true;
495
459
 
@@ -526,8 +490,14 @@ extern "C" bool stopAVFoundationRecording() {
526
490
  // Mark as not recording FIRST to stop timer callbacks
527
491
  g_avIsRecording = false;
528
492
 
529
- // Cancel timer immediately (Electron-safe)
493
+ // Cancel timer and wait a brief moment for completion
530
494
  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
+
531
501
  g_avTimer = nil;
532
502
  MRLog(@"โœ… AVFoundation timer stopped safely");
533
503
  }
@@ -540,16 +510,13 @@ extern "C" bool stopAVFoundationRecording() {
540
510
 
541
511
  AVAssetWriter *writer = g_avWriter;
542
512
  if (writer && writer.status == AVAssetWriterStatusWriting) {
543
- // ELECTRON SAFETY: Don't use dispatch_semaphore_wait - causes crashes in Electron
544
- // Just call finishWriting async and let it complete in background
513
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
545
514
  [writer finishWritingWithCompletionHandler:^{
546
- if (writer.status == AVAssetWriterStatusCompleted) {
547
- MRLog(@"โœ… AVFoundation writer finished successfully");
548
- } else if (writer.error) {
549
- NSLog(@"โš ๏ธ AVFoundation writer error: %@", writer.error);
550
- }
515
+ dispatch_semaphore_signal(semaphore);
551
516
  }];
552
- MRLog(@"๐Ÿ”„ AVFoundation writer finishing asynchronously (Electron-safe mode)");
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);
553
520
  }
554
521
 
555
522
  // Cleanup
@@ -922,15 +922,9 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
922
922
  if (CMTIME_IS_VALID(baseline)) {
923
923
  frameSeconds = CMTimeGetSeconds(CMTimeSubtract(bufferTime, baseline));
924
924
  }
925
- // Adjust camera stop limit by start offset relative to audio
925
+ // Do NOT extend camera stop limit by audio start offset.
926
+ // Clamping to the same stopLimit as audio ensures durations match.
926
927
  double effectiveStopLimit = stopLimit;
927
- if (hasAudioStart && CMTIME_IS_VALID(baseline)) {
928
- CMTime startDeltaTime = CMTimeSubtract(baseline, audioStart);
929
- double startDelta = CMTimeGetSeconds(startDeltaTime);
930
- if (startDelta > 0) {
931
- effectiveStopLimit += startDelta;
932
- }
933
- }
934
928
  double tolerance = self.expectedFrameRate > 0 ? (1.5 / self.expectedFrameRate) : 0.02;
935
929
  if (tolerance < 0.02) {
936
930
  tolerance = 0.02;
@@ -1022,16 +1016,8 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
1022
1016
 
1023
1017
  double stopLimit = MRSyncGetStopLimitSeconds();
1024
1018
  if (stopLimit > 0) {
1025
- // Adjust by camera start vs audio start so durations align closely
1026
- CMTime audioStartTS = MRSyncAudioFirstTimestamp();
1027
- if (CMTIME_IS_VALID(audioStartTS) && CMTIME_IS_VALID(self.firstSampleTime)) {
1028
- CMTime startDeltaTS = CMTimeSubtract(self.firstSampleTime, audioStartTS);
1029
- double startDelta = CMTimeGetSeconds(startDeltaTS);
1030
- if (startDelta > 0) {
1031
- stopLimit += startDelta;
1032
- }
1033
- }
1034
-
1019
+ // Do NOT extend camera stop limit by audio start offset.
1020
+ // Using the same stopLimit as audio keeps durations aligned.
1035
1021
  double frameSeconds = CMTimeGetSeconds(relativeTimestamp);
1036
1022
  double tolerance = self.expectedFrameRate > 0 ? (1.5 / self.expectedFrameRate) : 0.02;
1037
1023
  if (tolerance < 0.02) {
@@ -137,22 +137,29 @@ 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
142
140
  void cleanupRecording() {
143
- // ELECTRON SAFETY: Don't call stop operations here - they're async and cause crashes
144
- // Just reset state flags if they're stuck
145
-
146
- @try {
147
- // Only reset flags, don't actually stop anything
148
- g_isRecording = false;
149
- g_usingStandaloneAudio = false;
150
- MRSyncConfigure(NO);
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
+ }
151
152
 
152
- MRLog(@"๐Ÿงน State flags reset (safe mode - no actual stop operations)");
153
- } @catch (NSException *exception) {
154
- NSLog(@"โš ๏ธ Exception during state reset: %@", exception.reason);
153
+ if (isCameraRecording()) {
154
+ stopCameraRecording();
155
155
  }
156
+ if (isStandaloneAudioRecording()) {
157
+ stopStandaloneAudioRecording();
158
+ }
159
+ g_usingStandaloneAudio = false;
160
+
161
+ g_isRecording = false;
162
+ MRSyncConfigure(NO);
156
163
  }
157
164
 
158
165
  // NAPI Function: Start Recording
@@ -164,16 +171,24 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
164
171
  return env.Null();
165
172
  }
166
173
 
167
- // ELECTRON SAFETY: Just check if recording is active, don't try to clean up
168
- // Cleanup operations during start cause crashes in Electron
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
+
169
179
  if (g_isRecording) {
170
- MRLog(@"โš ๏ธ Recording already in progress");
180
+ MRLog(@"โš ๏ธ Still recording after cleanup - forcing stop");
171
181
  return Napi::Boolean::New(env, false);
172
182
  }
173
183
 
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
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
+ }
177
192
  g_usingStandaloneAudio = false;
178
193
 
179
194
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
@@ -362,23 +377,23 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
362
377
  MRLog(@"๐Ÿ–ฅ๏ธ macOS Version: %ld.%ld.%ld",
363
378
  (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
364
379
 
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
-
369
380
  if (isElectron) {
370
381
  MRLog(@"โšก Electron environment detected");
371
- MRLog(@"๐Ÿ”ง FORCING AVFoundation for Electron stability (ScreenCaptureKit crashes in Electron)");
372
- forceAVFoundation = YES; // CRITICAL: ScreenCaptureKit is unstable in Electron
382
+ MRLog(@"๐Ÿ”ง CRITICAL FIX: Forcing AVFoundation for Electron stability");
383
+ MRLog(@" Reason: ScreenCaptureKit has thread safety issues in Electron (SIGTRAP crashes)");
373
384
  }
374
385
 
375
- MRLog(@"๐Ÿ”ง FRAMEWORK SELECTION: Smart selection based on macOS version");
386
+ // CRITICAL FIX: Always use AVFoundation for stability
387
+ // ScreenCaptureKit has file writing issues in Node.js environment
388
+ // AVFoundation works reliably in both Node.js and Electron
389
+ BOOL forceAVFoundation = YES;
390
+
391
+ MRLog(@"๐Ÿ”ง FRAMEWORK SELECTION: Using AVFoundation for stability");
376
392
  MRLog(@" Environment: %@", isElectron ? @"Electron" : @"Node.js");
377
393
  MRLog(@" macOS: %ld.%ld.%ld", (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
378
394
 
379
- // Electron-first priority: Both frameworks fully supported in Electron
380
- // macOS 15+: Try ScreenCaptureKit first (better performance)
381
- // macOS 14/13: Use AVFoundation (ScreenCaptureKit not available)
395
+ // Electron-first priority: ALWAYS use AVFoundation in Electron for stability
396
+ // ScreenCaptureKit has severe thread safety issues in Electron causing SIGTRAP crashes
382
397
  if (isM15Plus && !forceAVFoundation) {
383
398
  if (isElectron) {
384
399
  MRLog(@"โšก ELECTRON PRIORITY: macOS 15+ Electron โ†’ ScreenCaptureKit with full support");
@@ -590,41 +605,30 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
590
605
 
591
606
  // Try ScreenCaptureKit first
592
607
  if (@available(macOS 12.3, *)) {
593
- @try {
594
- if ([ScreenCaptureKitRecorder isRecording]) {
595
- MRLog(@"๐Ÿ›‘ Stopping ScreenCaptureKit recording");
608
+ if ([ScreenCaptureKitRecorder isRecording]) {
609
+ MRLog(@"๐Ÿ›‘ Stopping ScreenCaptureKit recording");
596
610
 
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
- }
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");
606
619
  }
620
+ }
607
621
 
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
- }
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];
618
626
 
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;
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
626
629
  g_usingStandaloneAudio = false;
627
- return Napi::Boolean::New(env, false);
630
+ MRSyncSetStopLimitSeconds(-1.0);
631
+ return Napi::Boolean::New(env, true);
628
632
  }
629
633
  }
630
634
 
@@ -668,11 +672,26 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
668
672
 
669
673
  bool avFoundationStopped = stopAVFoundationRecording();
670
674
 
671
- // ELECTRON SAFETY: Don't use dispatch_group_wait - it blocks main thread and causes crashes
672
- // Instead, just fire async stop and continue
675
+ // SYNC FIX: Wait for both camera AND audio to finish (increased timeout to 5s)
673
676
  if (cameraWasRecording || audioWasRecording) {
674
- MRLog(@"๐Ÿ”„ Camera/Audio stop operations dispatched asynchronously (Electron-safe mode)");
675
- // Operations will complete in background - don't wait for them
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
+ }
676
695
  }
677
696
 
678
697
  g_isRecording = false;