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.
- package/.claude/settings.local.json +5 -2
- package/package.json +1 -1
- package/src/camera_recorder.mm +17 -9
- package/src/screen_capture_kit.mm +54 -55
|
@@ -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
package/src/camera_recorder.mm
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
134
|
+
g_isRecording = NO;
|
|
135
|
+
}
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
if (error) {
|
|
138
|
+
NSLog(@"❌ Stream error: %@", error);
|
|
139
|
+
} else {
|
|
140
|
+
MRLog(@"✅ Stream stopped cleanly");
|
|
141
|
+
}
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
//
|
|
464
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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;
|