node-mac-recorder 2.21.1 โ 2.21.2
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 +1 -1
- package/src/screen_capture_kit.mm +82 -42
- package/test-electron-fix.js +90 -0
package/package.json
CHANGED
|
@@ -121,27 +121,30 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
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
|
MRLog(@"๐ Pure ScreenCapture stream stopped");
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
// Prevent recursive calls during cleanup
|
|
126
126
|
if (g_isCleaningUp) {
|
|
127
127
|
MRLog(@"โ ๏ธ Already cleaning up, ignoring delegate callback");
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
|
|
131
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
132
|
+
g_isRecording = NO;
|
|
133
|
+
}
|
|
134
|
+
|
|
133
135
|
if (error) {
|
|
134
136
|
NSLog(@"โ Stream error: %@", error);
|
|
135
137
|
} else {
|
|
136
138
|
MRLog(@"โ
Stream stopped cleanly");
|
|
137
139
|
}
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
|
|
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) {
|
|
142
145
|
[ScreenCaptureKitRecorder finalizeRecording];
|
|
143
146
|
}
|
|
144
|
-
}
|
|
147
|
+
}
|
|
145
148
|
}
|
|
146
149
|
@end
|
|
147
150
|
|
|
@@ -450,17 +453,23 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
450
453
|
+ (BOOL)startRecordingWithConfiguration:(NSDictionary *)config delegate:(id)delegate error:(NSError **)error {
|
|
451
454
|
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
452
455
|
if (g_isRecording || g_isCleaningUp) {
|
|
453
|
-
|
|
456
|
+
MRLog(@"โ ๏ธ Already recording or cleaning up (recording:%d cleaning:%d)", g_isRecording, g_isCleaningUp);
|
|
454
457
|
return NO;
|
|
455
458
|
}
|
|
456
|
-
|
|
459
|
+
|
|
457
460
|
// Reset any stale state
|
|
458
461
|
g_isCleaningUp = NO;
|
|
462
|
+
|
|
463
|
+
// Set flag early to prevent race conditions in Electron
|
|
464
|
+
g_isRecording = YES;
|
|
459
465
|
}
|
|
460
|
-
|
|
466
|
+
|
|
461
467
|
NSString *outputPath = config[@"outputPath"];
|
|
462
468
|
if (!outputPath || [outputPath length] == 0) {
|
|
463
469
|
NSLog(@"โ Invalid output path provided");
|
|
470
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
471
|
+
g_isRecording = NO;
|
|
472
|
+
}
|
|
464
473
|
return NO;
|
|
465
474
|
}
|
|
466
475
|
g_outputPath = outputPath;
|
|
@@ -483,12 +492,18 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
483
492
|
// CRITICAL DEBUG: Log EXACT audio parameter values
|
|
484
493
|
MRLog(@"๐ AUDIO DEBUG: includeMicrophone type=%@ value=%d", [includeMicrophone class], [includeMicrophone boolValue]);
|
|
485
494
|
MRLog(@"๐ AUDIO DEBUG: includeSystemAudio type=%@ value=%d", [includeSystemAudio class], [includeSystemAudio boolValue]);
|
|
486
|
-
|
|
487
|
-
// Get shareable content
|
|
495
|
+
|
|
496
|
+
// ELECTRON FIX: Get shareable content asynchronously without blocking
|
|
497
|
+
// This prevents deadlocks in Electron's event loop
|
|
488
498
|
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
|
|
499
|
+
// This block runs asynchronously - safe for Electron
|
|
500
|
+
@autoreleasepool {
|
|
489
501
|
if (contentError) {
|
|
490
502
|
NSLog(@"โ Content error: %@", contentError);
|
|
491
|
-
|
|
503
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
504
|
+
g_isRecording = NO;
|
|
505
|
+
}
|
|
506
|
+
return; // Early return from completion handler block
|
|
492
507
|
}
|
|
493
508
|
|
|
494
509
|
MRLog(@"โ
Got %lu displays, %lu windows for pure recording",
|
|
@@ -529,7 +544,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
529
544
|
recordingHeight = (NSInteger)targetWindow.frame.size.height;
|
|
530
545
|
} else {
|
|
531
546
|
NSLog(@"โ Window ID %@ not found", windowId);
|
|
532
|
-
|
|
547
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
548
|
+
g_isRecording = NO;
|
|
549
|
+
}
|
|
550
|
+
return; // Early return from completion handler block
|
|
533
551
|
}
|
|
534
552
|
}
|
|
535
553
|
// DISPLAY RECORDING
|
|
@@ -558,7 +576,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
558
576
|
|
|
559
577
|
if (!targetDisplay) {
|
|
560
578
|
NSLog(@"โ Display not found");
|
|
561
|
-
|
|
579
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
580
|
+
g_isRecording = NO;
|
|
581
|
+
}
|
|
582
|
+
return; // Early return from completion handler block
|
|
562
583
|
}
|
|
563
584
|
|
|
564
585
|
MRLog(@"๐ฅ๏ธ Recording display %u (%dx%d)",
|
|
@@ -660,7 +681,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
660
681
|
NSError *writerError = nil;
|
|
661
682
|
if (![ScreenCaptureKitRecorder prepareVideoWriterWithWidth:recordingWidth height:recordingHeight error:&writerError]) {
|
|
662
683
|
NSLog(@"โ Failed to prepare video writer: %@", writerError);
|
|
663
|
-
|
|
684
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
685
|
+
g_isRecording = NO;
|
|
686
|
+
}
|
|
687
|
+
return; // Early return from completion handler block
|
|
664
688
|
}
|
|
665
689
|
|
|
666
690
|
g_videoQueue = dispatch_queue_create("screen_capture_video_queue", DISPATCH_QUEUE_SERIAL);
|
|
@@ -678,7 +702,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
678
702
|
if (!g_stream) {
|
|
679
703
|
NSLog(@"โ Failed to create pure stream");
|
|
680
704
|
CleanupWriters();
|
|
681
|
-
|
|
705
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
706
|
+
g_isRecording = NO;
|
|
707
|
+
}
|
|
708
|
+
return; // Early return from completion handler block
|
|
682
709
|
}
|
|
683
710
|
|
|
684
711
|
NSError *outputError = nil;
|
|
@@ -686,7 +713,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
686
713
|
if (!videoOutputAdded || outputError) {
|
|
687
714
|
NSLog(@"โ Failed to add video output: %@", outputError);
|
|
688
715
|
CleanupWriters();
|
|
689
|
-
|
|
716
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
717
|
+
g_isRecording = NO;
|
|
718
|
+
}
|
|
719
|
+
return; // Early return from completion handler block
|
|
690
720
|
}
|
|
691
721
|
|
|
692
722
|
if (g_shouldCaptureAudio) {
|
|
@@ -696,7 +726,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
696
726
|
if (!audioOutputAdded || audioError) {
|
|
697
727
|
NSLog(@"โ Failed to add audio output: %@", audioError);
|
|
698
728
|
CleanupWriters();
|
|
699
|
-
|
|
729
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
730
|
+
g_isRecording = NO;
|
|
731
|
+
}
|
|
732
|
+
return; // Early return from completion handler block
|
|
700
733
|
}
|
|
701
734
|
} else {
|
|
702
735
|
NSLog(@"โ ๏ธ Audio capture requested but requires macOS 13.0+");
|
|
@@ -708,19 +741,24 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
708
741
|
if (sessionTimestampNumber) {
|
|
709
742
|
MRLog(@"๐ Session timestamp: %@", sessionTimestampNumber);
|
|
710
743
|
}
|
|
711
|
-
|
|
744
|
+
|
|
745
|
+
// ELECTRON FIX: Start capture asynchronously
|
|
712
746
|
[g_stream startCaptureWithCompletionHandler:^(NSError *startError) {
|
|
713
747
|
if (startError) {
|
|
714
748
|
NSLog(@"โ Failed to start pure capture: %@", startError);
|
|
715
|
-
g_isRecording = NO;
|
|
716
749
|
CleanupWriters();
|
|
750
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
751
|
+
g_isRecording = NO;
|
|
752
|
+
}
|
|
717
753
|
} else {
|
|
718
754
|
MRLog(@"๐ PURE ScreenCaptureKit recording started successfully!");
|
|
719
|
-
g_isRecording
|
|
755
|
+
// g_isRecording already set to YES at the beginning
|
|
720
756
|
}
|
|
721
757
|
}];
|
|
722
|
-
|
|
723
|
-
|
|
758
|
+
} // End of autoreleasepool
|
|
759
|
+
}]; // End of getShareableContentWithCompletionHandler
|
|
760
|
+
|
|
761
|
+
// Return immediately - async completion will handle success/failure
|
|
724
762
|
return YES;
|
|
725
763
|
}
|
|
726
764
|
|
|
@@ -729,26 +767,28 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
729
767
|
NSLog(@"โ ๏ธ Cannot stop: recording=%d stream=%@ cleaning=%d", g_isRecording, g_stream, g_isCleaningUp);
|
|
730
768
|
return;
|
|
731
769
|
}
|
|
732
|
-
|
|
770
|
+
|
|
733
771
|
MRLog(@"๐ Stopping pure ScreenCaptureKit recording");
|
|
734
|
-
|
|
772
|
+
|
|
735
773
|
// Store stream reference to prevent it from being deallocated
|
|
736
774
|
SCStream *streamToStop = g_stream;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
775
|
+
|
|
776
|
+
// ELECTRON FIX: Stop asynchronously without blocking
|
|
777
|
+
[streamToStop stopCaptureWithCompletionHandler:^(NSError *stopError) {
|
|
778
|
+
if (stopError) {
|
|
779
|
+
NSLog(@"โ Stop error: %@", stopError);
|
|
780
|
+
} else {
|
|
781
|
+
MRLog(@"โ
Pure stream stopped");
|
|
741
782
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
});
|
|
783
|
+
|
|
784
|
+
// Reset recording state to allow new recordings
|
|
785
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
786
|
+
g_isRecording = NO;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Cleanup after stop completes
|
|
790
|
+
CleanupWriters();
|
|
791
|
+
[ScreenCaptureKitRecorder cleanupVideoWriter];
|
|
752
792
|
}];
|
|
753
793
|
}
|
|
754
794
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Test script for Electron crash fix
|
|
4
|
+
* Tests ScreenCaptureKit recording with synchronous semaphore-based approach
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MacRecorder = require('./index.js');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
async function testRecording() {
|
|
12
|
+
console.log('๐งช Testing ScreenCaptureKit Electron crash fix...\n');
|
|
13
|
+
|
|
14
|
+
const recorder = new MacRecorder();
|
|
15
|
+
|
|
16
|
+
// Check permissions first
|
|
17
|
+
console.log('1๏ธโฃ Checking permissions...');
|
|
18
|
+
const permissions = await recorder.checkPermissions();
|
|
19
|
+
console.log(' Permissions:', permissions);
|
|
20
|
+
|
|
21
|
+
if (!permissions.screenRecording) {
|
|
22
|
+
console.error('โ Screen recording permission not granted');
|
|
23
|
+
console.log(' Please enable screen recording in System Settings > Privacy & Security');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get displays
|
|
28
|
+
console.log('\n2๏ธโฃ Getting displays...');
|
|
29
|
+
const displays = await recorder.getDisplays();
|
|
30
|
+
console.log(` Found ${displays.length} display(s):`);
|
|
31
|
+
displays.forEach(d => {
|
|
32
|
+
console.log(` - Display ${d.id}: ${d.width}x${d.height} (Primary: ${d.isPrimary})`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Prepare output path
|
|
36
|
+
const outputDir = path.join(__dirname, 'test-output');
|
|
37
|
+
if (!fs.existsSync(outputDir)) {
|
|
38
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const outputPath = path.join(outputDir, `electron-fix-test-${Date.now()}.mov`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Start recording
|
|
45
|
+
console.log('\n3๏ธโฃ Starting recording...');
|
|
46
|
+
console.log(` Output: ${outputPath}`);
|
|
47
|
+
|
|
48
|
+
await recorder.startRecording(outputPath, {
|
|
49
|
+
displayId: displays[0].id,
|
|
50
|
+
captureCursor: true,
|
|
51
|
+
includeMicrophone: false,
|
|
52
|
+
includeSystemAudio: false
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log('โ
Recording started successfully!');
|
|
56
|
+
console.log(' Recording for 3 seconds...\n');
|
|
57
|
+
|
|
58
|
+
// Record for 3 seconds
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
60
|
+
|
|
61
|
+
// Stop recording
|
|
62
|
+
console.log('4๏ธโฃ Stopping recording...');
|
|
63
|
+
const result = await recorder.stopRecording();
|
|
64
|
+
console.log('โ
Recording stopped successfully!');
|
|
65
|
+
console.log(' Result:', result);
|
|
66
|
+
|
|
67
|
+
// Check output file
|
|
68
|
+
if (fs.existsSync(outputPath)) {
|
|
69
|
+
const stats = fs.statSync(outputPath);
|
|
70
|
+
console.log(`\nโ
Output file created: ${outputPath}`);
|
|
71
|
+
console.log(` File size: ${(stats.size / 1024).toFixed(2)} KB`);
|
|
72
|
+
} else {
|
|
73
|
+
console.log('\nโ ๏ธ Output file not found (may still be finalizing)');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('\n๐ Test completed successfully! No crashes detected.');
|
|
77
|
+
console.log(' The Electron crash fix appears to be working.\n');
|
|
78
|
+
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('\nโ Test failed:', error.message);
|
|
81
|
+
console.error(' Stack:', error.stack);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Run test
|
|
87
|
+
testRecording().catch(error => {
|
|
88
|
+
console.error('Fatal error:', error);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|