node-mac-recorder 2.21.3 → 2.21.4

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.
@@ -5,9 +5,12 @@
5
5
  "Bash(node:*)",
6
6
  "Bash(timeout:*)",
7
7
  "Bash(open:*)",
8
- "Read(//Users/onur/codes/**)"
8
+ "Read(//Users/onur/codes/**)",
9
+ "Bash(log show:*)",
10
+ "Bash(MAC_RECORDER_DEBUG=1 node:*)",
11
+ "Read(//private/tmp/test-recording/**)"
9
12
  ],
10
13
  "deny": [],
11
14
  "ask": []
12
15
  }
13
- }
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.3",
3
+ "version": "2.21.4",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -109,34 +109,42 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
109
109
 
110
110
  + (NSArray<NSDictionary *> *)availableCameraDevices {
111
111
  NSMutableArray<NSDictionary *> *devicesInfo = [NSMutableArray array];
112
-
112
+
113
113
  NSMutableArray<AVCaptureDeviceType> *deviceTypes = [NSMutableArray array];
114
114
  BOOL allowContinuity = MRAllowContinuityCamera();
115
-
115
+
116
+ // Always include built-in and external cameras
116
117
  if (@available(macOS 10.15, *)) {
117
118
  [deviceTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera];
118
119
  } else {
119
120
  [deviceTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera];
120
121
  }
121
-
122
+
123
+ // ALWAYS add external cameras - they should be available regardless of Continuity permission
124
+ if (@available(macOS 14.0, *)) {
125
+ [deviceTypes addObject:AVCaptureDeviceTypeExternal];
126
+ } else {
127
+ [deviceTypes addObject:AVCaptureDeviceTypeExternalUnknown];
128
+ }
129
+
130
+ // Only add Continuity Camera type if allowed
122
131
  if (allowContinuity) {
123
132
  if (@available(macOS 14.0, *)) {
124
133
  [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
125
- [deviceTypes addObject:AVCaptureDeviceTypeExternal];
126
- } else {
127
- [deviceTypes addObject:AVCaptureDeviceTypeExternalUnknown];
128
134
  }
129
135
  }
130
-
136
+
131
137
  AVCaptureDeviceDiscoverySession *discoverySession =
132
138
  [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
133
139
  mediaType:AVMediaTypeVideo
134
140
  position:AVCaptureDevicePositionUnspecified];
135
-
141
+
136
142
  for (AVCaptureDevice *device in discoverySession.devices) {
137
143
  BOOL continuityCamera = MRIsContinuityCamera(device);
144
+ // ONLY skip Continuity cameras when permission is missing
145
+ // Regular USB/external cameras should ALWAYS be listed
138
146
  if (continuityCamera && !allowContinuity) {
139
- // Skip Continuity cameras when entitlement/env flag is missing
147
+ MRLog(@"⏭️ Skipping Continuity Camera (permission required): %@", device.localizedName);
140
148
  continue;
141
149
  }
142
150
 
@@ -120,31 +120,33 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
120
120
 
121
121
  @implementation PureScreenCaptureDelegate
122
122
  - (void)stream:(SCStream * API_AVAILABLE(macos(12.3)))stream didStopWithError:(NSError *)error API_AVAILABLE(macos(12.3)) {
123
- MRLog(@"🛑 Pure ScreenCapture stream stopped");
123
+ // ELECTRON FIX: Run cleanup on background thread to avoid blocking Electron
124
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
125
+ MRLog(@"🛑 Pure ScreenCapture stream stopped");
124
126
 
125
- // Prevent recursive calls during cleanup
126
- if (g_isCleaningUp) {
127
- MRLog(@"⚠️ Already cleaning up, ignoring delegate callback");
128
- return;
129
- }
127
+ // Prevent recursive calls during cleanup
128
+ if (g_isCleaningUp) {
129
+ MRLog(@"⚠️ Already cleaning up, ignoring delegate callback");
130
+ return;
131
+ }
130
132
 
131
- @synchronized([ScreenCaptureKitRecorder class]) {
132
- g_isRecording = NO;
133
- }
133
+ @synchronized([ScreenCaptureKitRecorder class]) {
134
+ g_isRecording = NO;
135
+ }
134
136
 
135
- if (error) {
136
- NSLog(@"❌ Stream error: %@", error);
137
- } else {
138
- MRLog(@"✅ Stream stopped cleanly");
139
- }
137
+ if (error) {
138
+ NSLog(@"❌ Stream error: %@", error);
139
+ } else {
140
+ MRLog(@"✅ Stream stopped cleanly");
141
+ }
140
142
 
141
- // ELECTRON FIX: Don't use dispatch_async to main queue - it can cause crashes
142
- // Instead, finalize directly on current thread with synchronization
143
- @synchronized([ScreenCaptureKitRecorder class]) {
144
- if (!g_isCleaningUp) {
145
- [ScreenCaptureKitRecorder finalizeRecording];
143
+ // Finalize on background thread with synchronization
144
+ @synchronized([ScreenCaptureKitRecorder class]) {
145
+ if (!g_isCleaningUp) {
146
+ [ScreenCaptureKitRecorder finalizeRecording];
147
+ }
146
148
  }
147
- }
149
+ });
148
150
  }
149
151
  @end
150
152
 
@@ -460,16 +462,13 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
460
462
  // Reset any stale state
461
463
  g_isCleaningUp = NO;
462
464
 
463
- // Set flag early to prevent race conditions in Electron
464
- g_isRecording = YES;
465
+ // DON'T set g_isRecording here - wait for stream to actually start
466
+ // This prevents the "recording=1 stream=null" issue
465
467
  }
466
468
 
467
469
  NSString *outputPath = config[@"outputPath"];
468
470
  if (!outputPath || [outputPath length] == 0) {
469
471
  NSLog(@"❌ Invalid output path provided");
470
- @synchronized([ScreenCaptureKitRecorder class]) {
471
- g_isRecording = NO;
472
- }
473
472
  return NO;
474
473
  }
475
474
  g_outputPath = outputPath;
@@ -495,15 +494,15 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
495
494
 
496
495
  // ELECTRON FIX: Get shareable content FULLY ASYNCHRONOUSLY
497
496
  // NO semaphores, NO blocking - pure async to prevent Electron crashes
498
- [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
499
- @autoreleasepool {
500
- if (contentError) {
501
- NSLog(@"❌ Content error: %@", contentError);
502
- @synchronized([ScreenCaptureKitRecorder class]) {
503
- g_isRecording = NO;
497
+ // CRITICAL: Run on background queue to avoid blocking Electron's main thread
498
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
499
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
500
+ @autoreleasepool {
501
+ if (contentError) {
502
+ NSLog(@"❌ Content error: %@", contentError);
503
+ // No need to set g_isRecording=NO since it was never set to YES
504
+ return; // Early return from completion handler block
504
505
  }
505
- return; // Early return from completion handler block
506
- }
507
506
 
508
507
  MRLog(@"✅ Got %lu displays, %lu windows for pure recording",
509
508
  content.displays.count, content.windows.count);
@@ -543,9 +542,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
543
542
  recordingHeight = (NSInteger)targetWindow.frame.size.height;
544
543
  } else {
545
544
  NSLog(@"❌ Window ID %@ not found", windowId);
546
- @synchronized([ScreenCaptureKitRecorder class]) {
547
- g_isRecording = NO;
548
- }
545
+ // No need to set g_isRecording=NO since it was never set to YES
549
546
  return; // Early return from completion handler block
550
547
  }
551
548
  }
@@ -575,9 +572,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
575
572
 
576
573
  if (!targetDisplay) {
577
574
  NSLog(@"❌ Display not found");
578
- @synchronized([ScreenCaptureKitRecorder class]) {
579
- g_isRecording = NO;
580
- }
575
+ // No need to set g_isRecording=NO since it was never set to YES
581
576
  return; // Early return from completion handler block
582
577
  }
583
578
 
@@ -680,9 +675,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
680
675
  NSError *writerError = nil;
681
676
  if (![ScreenCaptureKitRecorder prepareVideoWriterWithWidth:recordingWidth height:recordingHeight error:&writerError]) {
682
677
  NSLog(@"❌ Failed to prepare video writer: %@", writerError);
683
- @synchronized([ScreenCaptureKitRecorder class]) {
684
- g_isRecording = NO;
685
- }
678
+ // No need to set g_isRecording=NO since it was never set to YES
686
679
  return; // Early return from completion handler block
687
680
  }
688
681
 
@@ -695,29 +688,30 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
695
688
  g_audioStreamOutput = nil;
696
689
  }
697
690
 
691
+ // Create stream outputs and delegate
698
692
  g_streamDelegate = [[PureScreenCaptureDelegate alloc] init];
699
693
  g_stream = [[SCStream alloc] initWithFilter:filter configuration:streamConfig delegate:g_streamDelegate];
700
-
694
+
695
+ // Check if stream was created successfully
701
696
  if (!g_stream) {
702
697
  NSLog(@"❌ Failed to create pure stream");
703
698
  CleanupWriters();
704
- @synchronized([ScreenCaptureKitRecorder class]) {
705
- g_isRecording = NO;
706
- }
707
699
  return; // Early return from completion handler block
708
700
  }
709
-
701
+
702
+ MRLog(@"✅ Stream created successfully");
703
+
710
704
  NSError *outputError = nil;
711
705
  BOOL videoOutputAdded = [g_stream addStreamOutput:g_videoStreamOutput type:SCStreamOutputTypeScreen sampleHandlerQueue:g_videoQueue error:&outputError];
712
706
  if (!videoOutputAdded || outputError) {
713
707
  NSLog(@"❌ Failed to add video output: %@", outputError);
714
708
  CleanupWriters();
715
709
  @synchronized([ScreenCaptureKitRecorder class]) {
716
- g_isRecording = NO;
710
+ g_stream = nil;
717
711
  }
718
712
  return; // Early return from completion handler block
719
713
  }
720
-
714
+
721
715
  if (g_shouldCaptureAudio) {
722
716
  if (@available(macOS 13.0, *)) {
723
717
  NSError *audioError = nil;
@@ -726,7 +720,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
726
720
  NSLog(@"❌ Failed to add audio output: %@", audioError);
727
721
  CleanupWriters();
728
722
  @synchronized([ScreenCaptureKitRecorder class]) {
729
- g_isRecording = NO;
723
+ g_stream = nil;
730
724
  }
731
725
  return; // Early return from completion handler block
732
726
  }
@@ -735,27 +729,32 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
735
729
  g_shouldCaptureAudio = NO;
736
730
  }
737
731
  }
738
-
732
+
739
733
  MRLog(@"✅ Stream outputs configured (audio=%d)", g_shouldCaptureAudio);
740
734
  if (sessionTimestampNumber) {
741
735
  MRLog(@"🕒 Session timestamp: %@", sessionTimestampNumber);
742
736
  }
743
737
 
744
- // ELECTRON FIX: Start capture FULLY ASYNCHRONOUSLY - NO blocking
738
+ // Start capture - can be async
745
739
  [g_stream startCaptureWithCompletionHandler:^(NSError *startError) {
746
740
  if (startError) {
747
741
  NSLog(@"❌ Failed to start pure capture: %@", startError);
748
742
  CleanupWriters();
749
743
  @synchronized([ScreenCaptureKitRecorder class]) {
750
744
  g_isRecording = NO;
745
+ g_stream = nil;
751
746
  }
752
747
  } else {
753
748
  MRLog(@"🎉 PURE ScreenCaptureKit recording started successfully!");
754
- // g_isRecording already set to YES at the beginning
749
+ // NOW set recording flag - stream is actually running
750
+ @synchronized([ScreenCaptureKitRecorder class]) {
751
+ g_isRecording = YES;
752
+ }
755
753
  }
756
- }];
754
+ }]; // End of startCaptureWithCompletionHandler
757
755
  } // End of autoreleasepool
758
- }]; // End of getShareableContentWithCompletionHandler
756
+ }]; // End of getShareableContentWithCompletionHandler
757
+ }); // End of dispatch_async
759
758
 
760
759
  // Return immediately - async completion will handle success/failure
761
760
  return YES;