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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.1",
3
+ "version": "2.21.2",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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
- g_isRecording = NO;
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
- // Use dispatch_async to prevent potential deadlocks in Electron
140
- dispatch_async(dispatch_get_main_queue(), ^{
141
- if (!g_isCleaningUp) { // Double-check before finalizing
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
- MRLog(@"โš ๏ธ Already recording or cleaning up (recording:%d cleaning:%d)", g_isRecording, g_isCleaningUp);
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
- return;
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
- return;
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
- return;
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
- return;
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
- return;
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
- return;
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
- return;
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 = YES;
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
- [streamToStop stopCaptureWithCompletionHandler:^(NSError *error) {
739
- if (error) {
740
- NSLog(@"โŒ Stop error: %@", error);
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
- MRLog(@"โœ… Pure stream stopped");
743
-
744
- // Immediately reset recording state to allow new recordings
745
- g_isRecording = NO;
746
-
747
- // Finalize on main queue to prevent threading issues
748
- dispatch_async(dispatch_get_main_queue(), ^{
749
- CleanupWriters();
750
- [ScreenCaptureKitRecorder cleanupVideoWriter];
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
+ });