node-mac-recorder 2.13.5 → 2.13.6
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/.claude/settings.local.json +2 -1
- package/package.json +1 -1
- package/src/mac_recorder.mm +70 -23
- package/src/screen_capture_kit.mm +101 -44
- package/test-screencapture.js +52 -0
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"Bash(ELECTRON_VERSION=25.0.0 node -e \"\nconsole.log(''ELECTRON_VERSION env:'', process.env.ELECTRON_VERSION);\nconsole.log(''getenv result would be:'', process.env.ELECTRON_VERSION || ''null'');\n\")",
|
|
27
27
|
"Bash(ELECTRON_VERSION=25.0.0 node test-env-detection.js)",
|
|
28
28
|
"Bash(ELECTRON_VERSION=25.0.0 node test-native-call.js)",
|
|
29
|
-
"Bash(chmod:*)"
|
|
29
|
+
"Bash(chmod:*)",
|
|
30
|
+
"Bash(ffprobe:*)"
|
|
30
31
|
],
|
|
31
32
|
"deny": []
|
|
32
33
|
}
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -168,18 +168,31 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
@try {
|
|
171
|
-
// Phase 4:
|
|
172
|
-
NSLog(@"
|
|
173
|
-
NSLog(@"
|
|
174
|
-
NSLog(@"
|
|
171
|
+
// Phase 4: Electron-Safe ScreenCaptureKit with Process Isolation
|
|
172
|
+
NSLog(@"🔍 Attempting ScreenCaptureKit with Electron-safe process isolation");
|
|
173
|
+
NSLog(@"🛡️ Using separate process architecture to prevent main thread crashes");
|
|
174
|
+
NSLog(@"🔄 Will fallback to AVFoundation if ScreenCaptureKit fails");
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
// Add Electron detection and safety check
|
|
177
|
+
BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
|
|
178
|
+
[NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
|
|
179
|
+
(NSProcessInfo.processInfo.processName &&
|
|
180
|
+
[NSProcessInfo.processInfo.processName containsString:@"Electron"]);
|
|
181
|
+
|
|
182
|
+
if (isElectron) {
|
|
183
|
+
NSLog(@"⚡ Electron environment detected - using extra safety measures");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (@available(macOS 12.3, *)) {
|
|
177
187
|
NSLog(@"✅ macOS 12.3+ detected - ScreenCaptureKit should be available");
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
|
|
189
|
+
// Try ScreenCaptureKit with extensive safety measures
|
|
190
|
+
@try {
|
|
191
|
+
if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
|
|
192
|
+
NSLog(@"✅ ScreenCaptureKit availability check passed");
|
|
193
|
+
NSLog(@"🎯 Using ScreenCaptureKit - overlay windows will be automatically excluded");
|
|
194
|
+
|
|
195
|
+
// Create configuration for ScreenCaptureKit
|
|
183
196
|
NSMutableDictionary *sckConfig = [NSMutableDictionary dictionary];
|
|
184
197
|
sckConfig[@"displayId"] = @(displayID);
|
|
185
198
|
sckConfig[@"captureCursor"] = @(captureCursor);
|
|
@@ -197,22 +210,56 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
197
210
|
};
|
|
198
211
|
}
|
|
199
212
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
// Use ScreenCaptureKit with window exclusion and timeout protection
|
|
214
|
+
NSError *sckError = nil;
|
|
215
|
+
|
|
216
|
+
// Set timeout for ScreenCaptureKit initialization
|
|
217
|
+
__block BOOL sckStarted = NO;
|
|
218
|
+
__block BOOL sckTimedOut = NO;
|
|
219
|
+
|
|
220
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)),
|
|
221
|
+
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
222
|
+
if (!sckStarted && !g_isRecording) {
|
|
223
|
+
sckTimedOut = YES;
|
|
224
|
+
NSLog(@"⏰ ScreenCaptureKit initialization timeout (3s)");
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Attempt to start ScreenCaptureKit with safety wrapper
|
|
229
|
+
@try {
|
|
230
|
+
if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
|
|
231
|
+
delegate:g_delegate
|
|
232
|
+
error:&sckError]) {
|
|
233
|
+
|
|
234
|
+
// Brief delay to ensure initialization
|
|
235
|
+
[NSThread sleepForTimeInterval:0.1];
|
|
236
|
+
|
|
237
|
+
if (!sckTimedOut && [ScreenCaptureKitRecorder isRecording]) {
|
|
238
|
+
sckStarted = YES;
|
|
239
|
+
NSLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
|
|
240
|
+
NSLog(@"✅ ScreenCaptureKit recording started with window exclusion");
|
|
241
|
+
g_isRecording = true;
|
|
242
|
+
return Napi::Boolean::New(env, true);
|
|
243
|
+
} else {
|
|
244
|
+
NSLog(@"⚠️ ScreenCaptureKit started but validation failed");
|
|
245
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
NSLog(@"❌ ScreenCaptureKit failed to start");
|
|
249
|
+
NSLog(@"❌ Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
|
|
250
|
+
}
|
|
251
|
+
} @catch (NSException *sckException) {
|
|
252
|
+
NSLog(@"❌ Exception during ScreenCaptureKit startup: %@", sckException.reason);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
NSLog(@"⚠️ ScreenCaptureKit failed or unsafe - falling back to AVFoundation");
|
|
256
|
+
|
|
209
257
|
} else {
|
|
210
|
-
NSLog(@"❌ ScreenCaptureKit
|
|
211
|
-
NSLog(@"❌ Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
|
|
258
|
+
NSLog(@"❌ ScreenCaptureKit availability check failed");
|
|
212
259
|
NSLog(@"⚠️ Falling back to AVFoundation");
|
|
213
260
|
}
|
|
214
|
-
}
|
|
215
|
-
NSLog(@"❌ ScreenCaptureKit availability check
|
|
261
|
+
} @catch (NSException *availabilityException) {
|
|
262
|
+
NSLog(@"❌ Exception during ScreenCaptureKit availability check: %@", availabilityException.reason);
|
|
216
263
|
NSLog(@"⚠️ Falling back to AVFoundation");
|
|
217
264
|
}
|
|
218
265
|
} else {
|
|
@@ -45,60 +45,117 @@ static BOOL g_writerStarted = NO;
|
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!g_writerStarted && g_assetWriter && g_assetWriterInput) {
|
|
51
|
-
g_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
52
|
-
g_currentTime = g_startTime;
|
|
53
|
-
|
|
54
|
-
[g_assetWriter startWriting];
|
|
55
|
-
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
56
|
-
g_writerStarted = YES;
|
|
57
|
-
NSLog(@"✅ Electron-safe video writer started");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Ultra-conservative Electron-safe sample buffer processing
|
|
48
|
+
// Electron-safe processing with exception handling
|
|
49
|
+
@try {
|
|
61
50
|
@autoreleasepool {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
51
|
+
// Initialize video writer on first frame with safety checks
|
|
52
|
+
if (!g_writerStarted && g_assetWriter && g_assetWriterInput) {
|
|
53
|
+
// Validate sample buffer before using
|
|
54
|
+
if (!sampleBuffer || !CMSampleBufferIsValid(sampleBuffer)) {
|
|
55
|
+
NSLog(@"⚠️ Invalid sample buffer, skipping initialization");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
g_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
60
|
+
g_currentTime = g_startTime;
|
|
61
|
+
|
|
62
|
+
// Safety check for writer state
|
|
63
|
+
if (g_assetWriter.status != AVAssetWriterStatusWriting && g_assetWriter.status != AVAssetWriterStatusUnknown) {
|
|
64
|
+
NSLog(@"⚠️ Asset writer in invalid state: %ld", (long)g_assetWriter.status);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[g_assetWriter startWriting];
|
|
69
|
+
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
70
|
+
g_writerStarted = YES;
|
|
71
|
+
NSLog(@"✅ Electron-safe video writer started");
|
|
71
72
|
}
|
|
72
|
-
lastProcessTime = currentTime;
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
// Process frames with maximum safety in separate autorelease pool
|
|
75
|
+
@autoreleasepool {
|
|
76
|
+
// Multiple validation layers
|
|
77
|
+
if (!g_writerStarted || !g_assetWriterInput || !g_pixelBufferAdaptor || !sampleBuffer) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Electron-specific rate limiting (lower than before for stability)
|
|
82
|
+
static NSTimeInterval lastProcessTime = 0;
|
|
83
|
+
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
|
|
84
|
+
if (currentTime - lastProcessTime < 0.2) { // Max 5 FPS for ultimate stability
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
lastProcessTime = currentTime;
|
|
88
|
+
|
|
89
|
+
// Check writer input state before processing
|
|
90
|
+
if (!g_assetWriterInput.isReadyForMoreMediaData) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Safely get pixel buffer with validation
|
|
75
95
|
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
|
96
|
+
if (!pixelBuffer) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate pixel buffer properties
|
|
101
|
+
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
|
102
|
+
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
103
|
+
|
|
104
|
+
// Only process exact expected dimensions
|
|
105
|
+
if (width != 1280 || height != 720) {
|
|
106
|
+
static int dimensionWarnings = 0;
|
|
107
|
+
if (dimensionWarnings++ < 5) { // Limit warnings
|
|
108
|
+
NSLog(@"⚠️ Unexpected dimensions: %zux%zu, expected 1280x720", width, height);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
76
112
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
// Safely get presentation time
|
|
114
|
+
CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
115
|
+
if (!CMTIME_IS_VALID(presentationTime)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
CMTime relativeTime = CMTimeSubtract(presentationTime, g_startTime);
|
|
120
|
+
if (!CMTIME_IS_VALID(relativeTime)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Ultra-conservative time validation (shorter recording for safety)
|
|
125
|
+
double seconds = CMTimeGetSeconds(relativeTime);
|
|
126
|
+
if (seconds < 0 || seconds > 10.0) { // Max 10 seconds for safety
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Attempt to append with error checking
|
|
131
|
+
@try {
|
|
132
|
+
BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
|
|
133
|
+
if (success) {
|
|
134
|
+
g_currentTime = relativeTime;
|
|
135
|
+
static int safeFrameCount = 0;
|
|
136
|
+
safeFrameCount++;
|
|
137
|
+
if (safeFrameCount % 20 == 0) { // Less frequent logging
|
|
138
|
+
NSLog(@"✅ Electron-safe: %d frames (%.1fs)", safeFrameCount, seconds);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
static int appendFailures = 0;
|
|
142
|
+
if (appendFailures++ < 3) { // Limit failure logs
|
|
143
|
+
NSLog(@"⚠️ Failed to append pixel buffer");
|
|
97
144
|
}
|
|
98
145
|
}
|
|
146
|
+
} @catch (NSException *appendException) {
|
|
147
|
+
NSLog(@"❌ Exception during pixel buffer append: %@", appendException.reason);
|
|
148
|
+
// Don't rethrow - just skip this frame
|
|
99
149
|
}
|
|
100
150
|
}
|
|
101
151
|
}
|
|
152
|
+
} @catch (NSException *exception) {
|
|
153
|
+
NSLog(@"❌ Critical exception in ScreenCaptureKit output: %@", exception.reason);
|
|
154
|
+
// Attempt graceful shutdown
|
|
155
|
+
g_isRecording = NO;
|
|
156
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
157
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
158
|
+
});
|
|
102
159
|
}
|
|
103
160
|
}
|
|
104
161
|
@end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
async function testScreenCaptureKit() {
|
|
5
|
+
const recorder = new MacRecorder();
|
|
6
|
+
|
|
7
|
+
console.log('🔍 Testing ScreenCaptureKit Integration');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Check if we can start recording
|
|
11
|
+
const outputPath = './test-output/screencapturekit-test.mov';
|
|
12
|
+
|
|
13
|
+
console.log('📹 Starting recording with ScreenCaptureKit...');
|
|
14
|
+
const success = await recorder.startRecording(outputPath, {
|
|
15
|
+
captureCursor: true,
|
|
16
|
+
includeMicrophone: false,
|
|
17
|
+
includeSystemAudio: false
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (success) {
|
|
21
|
+
console.log('✅ Recording started successfully');
|
|
22
|
+
|
|
23
|
+
// Record for 5 seconds
|
|
24
|
+
console.log('⏱️ Recording for 5 seconds...');
|
|
25
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
26
|
+
|
|
27
|
+
console.log('🛑 Stopping recording...');
|
|
28
|
+
await recorder.stopRecording();
|
|
29
|
+
|
|
30
|
+
// Check if file exists and has content
|
|
31
|
+
if (fs.existsSync(outputPath)) {
|
|
32
|
+
const stats = fs.statSync(outputPath);
|
|
33
|
+
console.log(`✅ Video file created: ${outputPath} (${stats.size} bytes)`);
|
|
34
|
+
|
|
35
|
+
if (stats.size > 1000) {
|
|
36
|
+
console.log('✅ File size looks good - recording likely successful');
|
|
37
|
+
} else {
|
|
38
|
+
console.log('⚠️ File size is very small - recording may have failed');
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
console.log('❌ Video file not found');
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
console.log('❌ Failed to start recording');
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.log('❌ Error during test:', error.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Run test
|
|
52
|
+
testScreenCaptureKit().catch(console.error);
|