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 +1 -1
- package/src/camera_recorder.mm +3 -5
- package/src/mac_recorder.mm +16 -16
- package/src/screen_capture_kit.mm +9 -1
- package/scripts/test-multi-capture.js +0 -103
- package/test-electron-fix.js +0 -90
package/package.json
CHANGED
package/src/camera_recorder.mm
CHANGED
|
@@ -127,11 +127,9 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
127
127
|
[deviceTypes addObject:AVCaptureDeviceTypeExternalUnknown];
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
//
|
|
131
|
-
if (
|
|
132
|
-
|
|
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 =
|
package/src/mac_recorder.mm
CHANGED
|
@@ -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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
MRLog(@"
|
|
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
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
});
|
package/test-electron-fix.js
DELETED
|
@@ -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
|
-
});
|