node-mac-recorder 2.21.4 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.4",
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": [
@@ -127,11 +127,9 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
127
127
  [deviceTypes addObject:AVCaptureDeviceTypeExternalUnknown];
128
128
  }
129
129
 
130
- // Only add Continuity Camera type if allowed
131
- if (allowContinuity) {
132
- if (@available(macOS 14.0, *)) {
133
- [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
134
- }
130
+ // ALWAYS add Continuity Camera type - filtering happens later
131
+ if (@available(macOS 14.0, *)) {
132
+ [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
135
133
  }
136
134
 
137
135
  AVCaptureDeviceDiscoverySession *discoverySession =
@@ -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");
@@ -608,7 +608,15 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
608
608
  BOOL shouldCaptureMic = includeMicrophone ? [includeMicrophone boolValue] : NO;
609
609
  BOOL shouldCaptureSystemAudio = includeSystemAudio ? [includeSystemAudio boolValue] : NO;
610
610
  g_shouldCaptureAudio = shouldCaptureMic || shouldCaptureSystemAudio;
611
- 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
+
612
620
  if (g_shouldCaptureAudio && (!g_audioOutputPath || [g_audioOutputPath length] == 0)) {
613
621
  NSLog(@"⚠️ Audio capture requested but no audio output path supplied – audio will be disabled");
614
622
  g_shouldCaptureAudio = NO;
@@ -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
- });