node-mac-recorder 2.21.3 → 2.21.5

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.5",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -109,34 +109,40 @@ 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
- if (allowContinuity) {
123
- if (@available(macOS 14.0, *)) {
124
- [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
125
- [deviceTypes addObject:AVCaptureDeviceTypeExternal];
126
- } else {
127
- [deviceTypes addObject:AVCaptureDeviceTypeExternalUnknown];
128
- }
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];
129
128
  }
130
-
129
+
130
+ // ALWAYS add Continuity Camera type - filtering happens later
131
+ if (@available(macOS 14.0, *)) {
132
+ [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
133
+ }
134
+
131
135
  AVCaptureDeviceDiscoverySession *discoverySession =
132
136
  [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
133
137
  mediaType:AVMediaTypeVideo
134
138
  position:AVCaptureDevicePositionUnspecified];
135
-
139
+
136
140
  for (AVCaptureDevice *device in discoverySession.devices) {
137
141
  BOOL continuityCamera = MRIsContinuityCamera(device);
142
+ // ONLY skip Continuity cameras when permission is missing
143
+ // Regular USB/external cameras should ALWAYS be listed
138
144
  if (continuityCamera && !allowContinuity) {
139
- // Skip Continuity cameras when entitlement/env flag is missing
145
+ MRLog(@"⏭️ Skipping Continuity Camera (permission required): %@", device.localizedName);
140
146
  continue;
141
147
  }
142
148
 
@@ -328,29 +328,29 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
328
328
  (NSBundle.mainBundle.bundlePath &&
329
329
  [NSBundle.mainBundle.bundlePath containsString:@"Electron"]);
330
330
 
331
- if (isElectron) {
332
- MRLog(@"⚡ Electron environment detected - continuing with ScreenCaptureKit");
333
- MRLog(@"⚠️ Warning: ScreenCaptureKit in Electron may require additional stability measures");
334
- }
335
-
336
331
  // Check macOS version for ScreenCaptureKit compatibility
337
332
  NSOperatingSystemVersion osVersion = [[NSProcessInfo processInfo] operatingSystemVersion];
338
333
  BOOL isM15Plus = (osVersion.majorVersion >= 15);
339
334
  BOOL isM14Plus = (osVersion.majorVersion >= 14);
340
335
  BOOL isM13Plus = (osVersion.majorVersion >= 13);
341
-
342
- MRLog(@"🖥️ macOS Version: %ld.%ld.%ld",
336
+
337
+ MRLog(@"🖥️ macOS Version: %ld.%ld.%ld",
343
338
  (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
344
-
345
- // Force AVFoundation for debugging/testing
346
- BOOL forceAVFoundation = (getenv("FORCE_AVFOUNDATION") != NULL);
347
- if (forceAVFoundation) {
348
- MRLog(@"🔧 FORCE_AVFOUNDATION environment variable detected - skipping ScreenCaptureKit");
339
+
340
+ if (isElectron) {
341
+ MRLog(@"⚡ Electron environment detected");
342
+ MRLog(@"🔧 CRITICAL FIX: Forcing AVFoundation for Electron stability");
343
+ MRLog(@" Reason: ScreenCaptureKit has thread safety issues in Electron (SIGTRAP crashes)");
349
344
  }
350
-
351
- // Electron-first priority: This application is built for Electron.js
352
- // macOS 15+ ScreenCaptureKit (including Electron)
353
- // macOS 14/13 AVFoundation (including Electron)
345
+
346
+ // Force AVFoundation for debugging/testing OR Electron
347
+ BOOL forceAVFoundation = (getenv("FORCE_AVFOUNDATION") != NULL) || isElectron;
348
+ if (getenv("FORCE_AVFOUNDATION") != NULL) {
349
+ MRLog(@"🔧 FORCE_AVFOUNDATION environment variable detected");
350
+ }
351
+
352
+ // Electron-first priority: ALWAYS use AVFoundation in Electron for stability
353
+ // ScreenCaptureKit has severe thread safety issues in Electron causing SIGTRAP crashes
354
354
  if (isM15Plus && !forceAVFoundation) {
355
355
  if (isElectron) {
356
356
  MRLog(@"⚡ ELECTRON PRIORITY: macOS 15+ Electron → ScreenCaptureKit with full support");
@@ -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
 
@@ -613,7 +608,15 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
613
608
  BOOL shouldCaptureMic = includeMicrophone ? [includeMicrophone boolValue] : NO;
614
609
  BOOL shouldCaptureSystemAudio = includeSystemAudio ? [includeSystemAudio boolValue] : NO;
615
610
  g_shouldCaptureAudio = shouldCaptureMic || shouldCaptureSystemAudio;
616
- g_audioOutputPath = audioOutputPath;
611
+
612
+ // SAFETY: Ensure audioOutputPath is NSString, not NSURL or other type
613
+ if (audioOutputPath && ![audioOutputPath isKindOfClass:[NSString class]]) {
614
+ MRLog(@"⚠️ audioOutputPath type mismatch: %@, converting...", NSStringFromClass([audioOutputPath class]));
615
+ g_audioOutputPath = nil;
616
+ } else {
617
+ g_audioOutputPath = audioOutputPath;
618
+ }
619
+
617
620
  if (g_shouldCaptureAudio && (!g_audioOutputPath || [g_audioOutputPath length] == 0)) {
618
621
  NSLog(@"⚠️ Audio capture requested but no audio output path supplied – audio will be disabled");
619
622
  g_shouldCaptureAudio = NO;
@@ -680,9 +683,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
680
683
  NSError *writerError = nil;
681
684
  if (![ScreenCaptureKitRecorder prepareVideoWriterWithWidth:recordingWidth height:recordingHeight error:&writerError]) {
682
685
  NSLog(@"❌ Failed to prepare video writer: %@", writerError);
683
- @synchronized([ScreenCaptureKitRecorder class]) {
684
- g_isRecording = NO;
685
- }
686
+ // No need to set g_isRecording=NO since it was never set to YES
686
687
  return; // Early return from completion handler block
687
688
  }
688
689
 
@@ -695,29 +696,30 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
695
696
  g_audioStreamOutput = nil;
696
697
  }
697
698
 
699
+ // Create stream outputs and delegate
698
700
  g_streamDelegate = [[PureScreenCaptureDelegate alloc] init];
699
701
  g_stream = [[SCStream alloc] initWithFilter:filter configuration:streamConfig delegate:g_streamDelegate];
700
-
702
+
703
+ // Check if stream was created successfully
701
704
  if (!g_stream) {
702
705
  NSLog(@"❌ Failed to create pure stream");
703
706
  CleanupWriters();
704
- @synchronized([ScreenCaptureKitRecorder class]) {
705
- g_isRecording = NO;
706
- }
707
707
  return; // Early return from completion handler block
708
708
  }
709
-
709
+
710
+ MRLog(@"✅ Stream created successfully");
711
+
710
712
  NSError *outputError = nil;
711
713
  BOOL videoOutputAdded = [g_stream addStreamOutput:g_videoStreamOutput type:SCStreamOutputTypeScreen sampleHandlerQueue:g_videoQueue error:&outputError];
712
714
  if (!videoOutputAdded || outputError) {
713
715
  NSLog(@"❌ Failed to add video output: %@", outputError);
714
716
  CleanupWriters();
715
717
  @synchronized([ScreenCaptureKitRecorder class]) {
716
- g_isRecording = NO;
718
+ g_stream = nil;
717
719
  }
718
720
  return; // Early return from completion handler block
719
721
  }
720
-
722
+
721
723
  if (g_shouldCaptureAudio) {
722
724
  if (@available(macOS 13.0, *)) {
723
725
  NSError *audioError = nil;
@@ -726,7 +728,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
726
728
  NSLog(@"❌ Failed to add audio output: %@", audioError);
727
729
  CleanupWriters();
728
730
  @synchronized([ScreenCaptureKitRecorder class]) {
729
- g_isRecording = NO;
731
+ g_stream = nil;
730
732
  }
731
733
  return; // Early return from completion handler block
732
734
  }
@@ -735,27 +737,32 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
735
737
  g_shouldCaptureAudio = NO;
736
738
  }
737
739
  }
738
-
740
+
739
741
  MRLog(@"✅ Stream outputs configured (audio=%d)", g_shouldCaptureAudio);
740
742
  if (sessionTimestampNumber) {
741
743
  MRLog(@"🕒 Session timestamp: %@", sessionTimestampNumber);
742
744
  }
743
745
 
744
- // ELECTRON FIX: Start capture FULLY ASYNCHRONOUSLY - NO blocking
746
+ // Start capture - can be async
745
747
  [g_stream startCaptureWithCompletionHandler:^(NSError *startError) {
746
748
  if (startError) {
747
749
  NSLog(@"❌ Failed to start pure capture: %@", startError);
748
750
  CleanupWriters();
749
751
  @synchronized([ScreenCaptureKitRecorder class]) {
750
752
  g_isRecording = NO;
753
+ g_stream = nil;
751
754
  }
752
755
  } else {
753
756
  MRLog(@"🎉 PURE ScreenCaptureKit recording started successfully!");
754
- // g_isRecording already set to YES at the beginning
757
+ // NOW set recording flag - stream is actually running
758
+ @synchronized([ScreenCaptureKitRecorder class]) {
759
+ g_isRecording = YES;
760
+ }
755
761
  }
756
- }];
762
+ }]; // End of startCaptureWithCompletionHandler
757
763
  } // End of autoreleasepool
758
- }]; // End of getShareableContentWithCompletionHandler
764
+ }]; // End of getShareableContentWithCompletionHandler
765
+ }); // End of dispatch_async
759
766
 
760
767
  // Return immediately - async completion will handle success/failure
761
768
  return YES;
@@ -1,103 +0,0 @@
1
- const MacRecorder = require("../index");
2
- const path = require("path");
3
- const fs = require("fs");
4
-
5
- async function main() {
6
- const recorder = new MacRecorder();
7
-
8
- // Optional: list audio and camera devices for reference
9
- const audioDevices = await recorder.getAudioDevices();
10
- const cameraDevices = await recorder.getCameraDevices();
11
-
12
- console.log("Audio devices:");
13
- audioDevices.forEach((device, idx) => {
14
- console.log(`${idx + 1}. ${device.name} (id: ${device.id})`);
15
- });
16
-
17
- console.log("\nCamera devices:");
18
- cameraDevices.forEach((device, idx) => {
19
- console.log(`${idx + 1}. ${device.name} (id: ${device.id})`);
20
- });
21
-
22
- // Pick the first available devices (customize as needed)
23
- const preferredCamera = cameraDevices.find(device => !device.requiresContinuityCameraPermission);
24
- const selectedCameraId = preferredCamera ? preferredCamera.id : null;
25
- if (!selectedCameraId && cameraDevices.length > 0) {
26
- console.warn("Skipping camera capture: only Continuity Camera devices detected. Add NSCameraUseContinuityCameraDeviceType to Info.plist or set ALLOW_CONTINUITY_CAMERA=1.");
27
- }
28
-
29
- if (selectedCameraId) {
30
- console.log(`\nSelected camera: ${preferredCamera.name} (id: ${selectedCameraId})`);
31
- } else {
32
- console.log("\nSelected camera: none (camera capture disabled)");
33
- }
34
- const selectedMicId = audioDevices[0]?.id || null;
35
-
36
- if (selectedCameraId) {
37
- recorder.setCameraDevice(selectedCameraId);
38
- recorder.setCameraEnabled(true);
39
- }
40
-
41
- recorder.setAudioSettings({
42
- microphone: !!selectedMicId,
43
- systemAudio: true,
44
- });
45
-
46
- if (selectedMicId) {
47
- recorder.setAudioDevice(selectedMicId);
48
- }
49
-
50
- const outputDir = path.resolve(__dirname, "../tmp-tests");
51
- if (!fs.existsSync(outputDir)) {
52
- fs.mkdirSync(outputDir, { recursive: true });
53
- }
54
-
55
- const outputPath = path.join(outputDir, `test_capture_${Date.now()}.mov`);
56
- console.log("\nStarting recording to:", outputPath);
57
-
58
- recorder.on("recordingStarted", (payload) => {
59
- console.log("recordingStarted", payload);
60
- });
61
- recorder.on("cameraCaptureStarted", (payload) => {
62
- console.log("cameraCaptureStarted", payload);
63
- });
64
- recorder.on("audioCaptureStarted", (payload) => {
65
- console.log("audioCaptureStarted", payload);
66
- });
67
- recorder.on("cameraCaptureStopped", (payload) => {
68
- console.log("cameraCaptureStopped", payload);
69
- });
70
- recorder.on("audioCaptureStopped", (payload) => {
71
- console.log("audioCaptureStopped", payload);
72
- });
73
- recorder.on("stopped", (payload) => {
74
- console.log("stopped", payload);
75
- });
76
- recorder.on("completed", (filePath) => {
77
- console.log("completed", filePath);
78
- });
79
-
80
- await recorder.startRecording(outputPath, {
81
- includeMicrophone: !!selectedMicId,
82
- includeSystemAudio: true,
83
- captureCursor: true,
84
- captureCamera: !!selectedCameraId,
85
- });
86
-
87
- console.log("Recording for 10 seconds...");
88
- await new Promise((resolve) => setTimeout(resolve, 10_000));
89
-
90
- const result = await recorder.stopRecording();
91
- console.log("\nRecording finished:", result);
92
-
93
- console.log("\nArtifacts:");
94
- console.log("Video:", result.outputPath);
95
- console.log("Camera:", result.cameraOutputPath);
96
- console.log("Audio:", result.audioOutputPath);
97
- console.log("Session timestamp:", result.sessionTimestamp);
98
- }
99
-
100
- main().catch((error) => {
101
- console.error("Test capture failed:", error);
102
- process.exit(1);
103
- });
@@ -1,90 +0,0 @@
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
- });