node-mac-recorder 2.21.29 → 2.21.31

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.29",
3
+ "version": "2.21.31",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -724,27 +724,43 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
724
724
  MRLog(@"🛑 CameraRecorder: Stopping session (external device safe)...");
725
725
 
726
726
  self.isShuttingDown = YES;
727
- self.isRecording = NO;
728
-
729
- // Stop delegate FIRST to prevent new frames
730
- [self.videoOutput setSampleBufferDelegate:nil queue:nil];
731
727
 
732
- // Stop session on background thread to avoid blocking
728
+ // Stop session on background thread to avoid blocking, but wait briefly so we capture trailing frames.
733
729
  AVCaptureSession *sessionToStop = self.session;
734
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
735
- @autoreleasepool {
736
- @try {
737
- if ([sessionToStop isRunning]) {
738
- MRLog(@"🛑 Stopping AVCaptureSession (camera)...");
739
- [sessionToStop stopRunning];
740
- MRLog(@"✅ AVCaptureSession stopped (camera)");
730
+
731
+ dispatch_group_t sessionStopGroup = NULL;
732
+ if (sessionToStop) {
733
+ sessionStopGroup = dispatch_group_create();
734
+ dispatch_group_enter(sessionStopGroup);
735
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
736
+ @autoreleasepool {
737
+ @try {
738
+ if ([sessionToStop isRunning]) {
739
+ MRLog(@"🛑 Stopping AVCaptureSession (camera)...");
740
+ [sessionToStop stopRunning];
741
+ MRLog(@"✅ AVCaptureSession stopped (camera)");
742
+ }
743
+ } @catch (NSException *exception) {
744
+ MRLog(@"⚠️ CameraRecorder: Exception while stopping session: %@", exception.reason);
741
745
  }
742
- } @catch (NSException *exception) {
743
- MRLog(@"⚠️ CameraRecorder: Exception while stopping session: %@", exception.reason);
746
+ dispatch_group_leave(sessionStopGroup);
744
747
  }
745
- // Release happens automatically when block completes
748
+ });
749
+ }
750
+
751
+ if (sessionStopGroup) {
752
+ dispatch_time_t sessionTimeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
753
+ long waitResult = dispatch_group_wait(sessionStopGroup, sessionTimeout);
754
+ if (waitResult != 0) {
755
+ MRLog(@"⚠️ CameraRecorder: AVCaptureSession stop timed out after 2s (continuing shutdown)");
746
756
  }
747
- });
757
+ }
758
+
759
+ // Now detach delegate to prevent further callbacks
760
+ if (self.videoOutput) {
761
+ [self.videoOutput setSampleBufferDelegate:nil queue:nil];
762
+ }
763
+ self.isRecording = NO;
748
764
 
749
765
  // CRITICAL FIX: Check if assetWriter exists before trying to finish it
750
766
  // If no frames were captured, assetWriter will be nil
@@ -754,31 +770,41 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
754
770
  return YES; // Success - nothing to finish
755
771
  }
756
772
 
757
- if (self.assetWriterInput) {
758
- [self.assetWriterInput markAsFinished];
773
+ AVAssetWriter *writer = self.assetWriter;
774
+ AVAssetWriterInput *writerInput = self.assetWriterInput;
775
+
776
+ if (writerInput) {
777
+ [writerInput markAsFinished];
759
778
  }
760
779
 
761
780
  __block BOOL finished = NO;
762
781
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
763
782
 
764
- [self.assetWriter finishWritingWithCompletionHandler:^{
783
+ [writer finishWritingWithCompletionHandler:^{
765
784
  finished = YES;
766
785
  dispatch_semaphore_signal(semaphore);
767
786
  }];
768
787
 
769
- // Reduced timeout to 1 second for external devices
770
- dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
788
+ // Allow generous flush time so final frames are preserved.
789
+ const int64_t primaryWaitSeconds = 3;
790
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(primaryWaitSeconds * NSEC_PER_SEC));
771
791
  long result = dispatch_semaphore_wait(semaphore, timeout);
772
792
 
773
793
  if (result != 0 || !finished) {
774
- MRLog(@"⚠️ CameraRecorder: Timed out waiting for writer (external device?) - forcing cancel");
775
- // Force cancel if timeout
776
- [self.assetWriter cancelWriting];
794
+ MRLog(@"⚠️ CameraRecorder: Writer still finishing after %ds waiting longer", (int)primaryWaitSeconds);
795
+ const int64_t extendedWaitSeconds = 5;
796
+ dispatch_time_t extendedTimeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(extendedWaitSeconds * NSEC_PER_SEC));
797
+ result = dispatch_semaphore_wait(semaphore, extendedTimeout);
777
798
  }
778
799
 
779
- BOOL success = (self.assetWriter.status == AVAssetWriterStatusCompleted);
800
+ if (result != 0 || !finished) {
801
+ MRLog(@"⚠️ CameraRecorder: Writer did not finish after extended wait – forcing cancel");
802
+ [writer cancelWriting];
803
+ }
804
+
805
+ BOOL success = (writer.status == AVAssetWriterStatusCompleted);
780
806
  if (!success) {
781
- MRLog(@"⚠️ CameraRecorder: Writer finished with status %ld error %@", (long)self.assetWriter.status, self.assetWriter.error);
807
+ MRLog(@"⚠️ CameraRecorder: Writer finished with status %ld error %@", (long)writer.status, writer.error);
782
808
  } else {
783
809
  MRLog(@"✅ CameraRecorder writer finished successfully");
784
810
  }
@@ -791,7 +817,7 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
791
817
  - (void)captureOutput:(AVCaptureOutput *)output
792
818
  didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
793
819
  fromConnection:(AVCaptureConnection *)connection {
794
- if (!self.isRecording || self.isShuttingDown) {
820
+ if (!self.isRecording) {
795
821
  return;
796
822
  }
797
823