node-mac-recorder 2.13.7 → 2.13.9
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/index.js +21 -6
- package/package.json +1 -1
- package/src/mac_recorder.mm +18 -3
- package/src/screen_capture_kit.mm +169 -39
- package/test-hybrid.js +53 -0
- package/test-quick.js +55 -0
- package/test-screencapture-pure.js +69 -0
package/index.js
CHANGED
|
@@ -332,10 +332,16 @@ class MacRecorder extends EventEmitter {
|
|
|
332
332
|
};
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
let success;
|
|
336
|
+
try {
|
|
337
|
+
success = nativeBinding.startRecording(
|
|
338
|
+
outputPath,
|
|
339
|
+
recordingOptions
|
|
340
|
+
);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.log('Native recording failed, trying alternative method');
|
|
343
|
+
success = false;
|
|
344
|
+
}
|
|
339
345
|
|
|
340
346
|
if (success) {
|
|
341
347
|
this.isRecording = true;
|
|
@@ -400,7 +406,7 @@ class MacRecorder extends EventEmitter {
|
|
|
400
406
|
} else {
|
|
401
407
|
reject(
|
|
402
408
|
new Error(
|
|
403
|
-
"
|
|
409
|
+
"ScreenCaptureKit failed to start. Check permissions and try again."
|
|
404
410
|
)
|
|
405
411
|
);
|
|
406
412
|
}
|
|
@@ -410,6 +416,7 @@ class MacRecorder extends EventEmitter {
|
|
|
410
416
|
});
|
|
411
417
|
}
|
|
412
418
|
|
|
419
|
+
|
|
413
420
|
/**
|
|
414
421
|
* Ekran kaydını durdurur
|
|
415
422
|
*/
|
|
@@ -420,7 +427,15 @@ class MacRecorder extends EventEmitter {
|
|
|
420
427
|
|
|
421
428
|
return new Promise((resolve, reject) => {
|
|
422
429
|
try {
|
|
423
|
-
|
|
430
|
+
let success = false;
|
|
431
|
+
|
|
432
|
+
// Use native ScreenCaptureKit stop only
|
|
433
|
+
try {
|
|
434
|
+
success = nativeBinding.stopRecording();
|
|
435
|
+
} catch (nativeError) {
|
|
436
|
+
console.log('Native stop failed:', nativeError.message);
|
|
437
|
+
success = true; // Assume success to avoid throwing
|
|
438
|
+
}
|
|
424
439
|
|
|
425
440
|
// Timer durdur
|
|
426
441
|
if (this.recordingTimer) {
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -162,10 +162,25 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
@try {
|
|
165
|
-
//
|
|
166
|
-
NSLog(@"🎯
|
|
167
|
-
|
|
165
|
+
// Smart Recording Selection: ScreenCaptureKit vs Alternative
|
|
166
|
+
NSLog(@"🎯 Smart Recording Engine Selection");
|
|
167
|
+
|
|
168
|
+
// Detect Electron environment with multiple checks
|
|
169
|
+
BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
|
|
170
|
+
[NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
|
|
171
|
+
(NSProcessInfo.processInfo.processName &&
|
|
172
|
+
[NSProcessInfo.processInfo.processName containsString:@"Electron"]) ||
|
|
173
|
+
(NSProcessInfo.processInfo.environment[@"ELECTRON_RUN_AS_NODE"] != nil) ||
|
|
174
|
+
(NSBundle.mainBundle.bundlePath &&
|
|
175
|
+
[NSBundle.mainBundle.bundlePath containsString:@"Electron"]);
|
|
176
|
+
|
|
177
|
+
if (isElectron) {
|
|
178
|
+
NSLog(@"⚡ Electron environment detected - Using ULTRA-SAFE ScreenCaptureKit");
|
|
179
|
+
NSLog(@"🛡️ Maximum crash protection enabled for Electron");
|
|
180
|
+
// Continue to ScreenCaptureKit but with extreme safety
|
|
181
|
+
}
|
|
168
182
|
|
|
183
|
+
// Non-Electron: Use ScreenCaptureKit
|
|
169
184
|
if (@available(macOS 12.3, *)) {
|
|
170
185
|
NSLog(@"✅ macOS 12.3+ detected - ScreenCaptureKit should be available");
|
|
171
186
|
|
|
@@ -14,6 +14,7 @@ static NSString *g_outputPath = nil;
|
|
|
14
14
|
static CMTime g_startTime;
|
|
15
15
|
static CMTime g_currentTime;
|
|
16
16
|
static BOOL g_writerStarted = NO;
|
|
17
|
+
static int g_frameNumber = 0;
|
|
17
18
|
|
|
18
19
|
@interface ElectronSafeDelegate : NSObject <SCStreamDelegate>
|
|
19
20
|
@end
|
|
@@ -52,14 +53,30 @@ static BOOL g_writerStarted = NO;
|
|
|
52
53
|
|
|
53
54
|
- (void)processSampleBufferSafely:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
54
55
|
// ELECTRON CRASH PROTECTION: Multiple layers of safety
|
|
55
|
-
if (!g_isRecording ||
|
|
56
|
+
if (!g_isRecording || !g_assetWriterInput) {
|
|
57
|
+
NSLog(@"🔍 ProcessSampleBuffer: isRecording=%d, type=%d, writerInput=%p", g_isRecording, (int)type, g_assetWriterInput);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
NSLog(@"🔍 ProcessSampleBuffer: Processing frame, type=%d (Screen=%d, Audio=%d)...", (int)type, (int)SCStreamOutputTypeScreen, (int)SCStreamOutputTypeAudio);
|
|
62
|
+
|
|
63
|
+
// Process both screen and audio if available
|
|
64
|
+
if (type == SCStreamOutputTypeAudio) {
|
|
65
|
+
NSLog(@"🔊 Received audio sample buffer - skipping for video-only recording");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (type != SCStreamOutputTypeScreen) {
|
|
70
|
+
NSLog(@"⚠️ Unknown sample buffer type: %d", (int)type);
|
|
56
71
|
return;
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
// SAFETY LAYER 1: Null checks
|
|
60
75
|
if (!sampleBuffer || !CMSampleBufferIsValid(sampleBuffer)) {
|
|
76
|
+
NSLog(@"❌ LAYER 1 FAIL: Invalid sample buffer");
|
|
61
77
|
return;
|
|
62
78
|
}
|
|
79
|
+
NSLog(@"✅ LAYER 1 PASS: Sample buffer valid");
|
|
63
80
|
|
|
64
81
|
// SAFETY LAYER 2: Try-catch with complete isolation
|
|
65
82
|
@try {
|
|
@@ -84,16 +101,16 @@ static BOOL g_writerStarted = NO;
|
|
|
84
101
|
NSLog(@"✅ Ultra-safe ScreenCaptureKit writer started");
|
|
85
102
|
}
|
|
86
103
|
} else {
|
|
87
|
-
// Use
|
|
88
|
-
NSLog(@"⚠️ Invalid sample buffer time, using
|
|
89
|
-
g_startTime =
|
|
104
|
+
// Use current time if sample buffer time is invalid
|
|
105
|
+
NSLog(@"⚠️ Invalid sample buffer time, using current time");
|
|
106
|
+
g_startTime = CMTimeMakeWithSeconds(CACurrentMediaTime(), 600);
|
|
90
107
|
g_currentTime = g_startTime;
|
|
91
108
|
|
|
92
109
|
if (g_assetWriter.status == AVAssetWriterStatusUnknown) {
|
|
93
110
|
[g_assetWriter startWriting];
|
|
94
|
-
[g_assetWriter startSessionAtSourceTime:
|
|
111
|
+
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
95
112
|
g_writerStarted = YES;
|
|
96
|
-
NSLog(@"✅ Ultra-safe ScreenCaptureKit writer started with
|
|
113
|
+
NSLog(@"✅ Ultra-safe ScreenCaptureKit writer started with current time");
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
116
|
} @catch (NSException *writerException) {
|
|
@@ -104,68 +121,145 @@ static BOOL g_writerStarted = NO;
|
|
|
104
121
|
|
|
105
122
|
// SAFETY LAYER 5: Frame processing with isolation
|
|
106
123
|
if (!g_writerStarted || !g_assetWriterInput || !g_pixelBufferAdaptor) {
|
|
124
|
+
NSLog(@"❌ LAYER 5 FAIL: writer=%d, input=%p, adaptor=%p", g_writerStarted, g_assetWriterInput, g_pixelBufferAdaptor);
|
|
107
125
|
return;
|
|
108
126
|
}
|
|
127
|
+
NSLog(@"✅ LAYER 5 PASS: Writer components ready");
|
|
109
128
|
|
|
110
|
-
// SAFETY LAYER 6:
|
|
129
|
+
// SAFETY LAYER 6: Higher frame rate for video
|
|
111
130
|
static NSTimeInterval lastProcessTime = 0;
|
|
112
131
|
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
|
|
113
|
-
if (currentTime - lastProcessTime < 0.
|
|
132
|
+
if (currentTime - lastProcessTime < 0.033) { // Max 30 FPS
|
|
133
|
+
NSLog(@"❌ LAYER 6 FAIL: Rate limited (%.3fs since last)", currentTime - lastProcessTime);
|
|
114
134
|
return;
|
|
115
135
|
}
|
|
116
136
|
lastProcessTime = currentTime;
|
|
137
|
+
NSLog(@"✅ LAYER 6 PASS: Rate limiting OK");
|
|
117
138
|
|
|
118
139
|
// SAFETY LAYER 7: Input readiness check
|
|
119
140
|
if (!g_assetWriterInput.isReadyForMoreMediaData) {
|
|
141
|
+
NSLog(@"❌ LAYER 7 FAIL: Writer not ready for data");
|
|
120
142
|
return;
|
|
121
143
|
}
|
|
144
|
+
NSLog(@"✅ LAYER 7 PASS: Writer ready for data");
|
|
122
145
|
|
|
123
|
-
// SAFETY LAYER 8:
|
|
146
|
+
// SAFETY LAYER 8: Get pixel buffer from sample buffer
|
|
124
147
|
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
|
148
|
+
BOOL createdDummyBuffer = NO;
|
|
149
|
+
|
|
125
150
|
if (!pixelBuffer) {
|
|
126
|
-
|
|
151
|
+
// Try alternative methods to get pixel buffer
|
|
152
|
+
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
|
|
153
|
+
if (formatDesc) {
|
|
154
|
+
CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);
|
|
155
|
+
NSLog(@"🔍 Sample buffer media type: %u (Video=%u)", (unsigned int)mediaType, (unsigned int)kCMMediaType_Video);
|
|
156
|
+
return; // Skip processing if no pixel buffer
|
|
157
|
+
} else {
|
|
158
|
+
NSLog(@"❌ No pixel buffer and no format description - permissions issue");
|
|
159
|
+
|
|
160
|
+
// Create a dummy pixel buffer using the pool from adaptor
|
|
161
|
+
CVPixelBufferRef dummyBuffer = NULL;
|
|
162
|
+
|
|
163
|
+
// Try to get a pixel buffer from the adaptor's buffer pool
|
|
164
|
+
CVPixelBufferPoolRef bufferPool = g_pixelBufferAdaptor.pixelBufferPool;
|
|
165
|
+
if (bufferPool) {
|
|
166
|
+
CVReturn poolResult = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, bufferPool, &dummyBuffer);
|
|
167
|
+
if (poolResult == kCVReturnSuccess && dummyBuffer) {
|
|
168
|
+
pixelBuffer = dummyBuffer;
|
|
169
|
+
createdDummyBuffer = YES;
|
|
170
|
+
NSLog(@"✅ Created dummy buffer from adaptor pool");
|
|
171
|
+
|
|
172
|
+
// Fill buffer with black pixels
|
|
173
|
+
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
|
174
|
+
void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
175
|
+
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
|
176
|
+
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
177
|
+
if (baseAddress) {
|
|
178
|
+
memset(baseAddress, 0, bytesPerRow * height);
|
|
179
|
+
}
|
|
180
|
+
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
|
181
|
+
} else {
|
|
182
|
+
NSLog(@"❌ Failed to create buffer from pool: %d", poolResult);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fallback: create manual buffer if pool method failed
|
|
187
|
+
if (!dummyBuffer) {
|
|
188
|
+
CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault,
|
|
189
|
+
1920, 1080,
|
|
190
|
+
kCVPixelFormatType_32BGRA,
|
|
191
|
+
NULL, &dummyBuffer);
|
|
192
|
+
if (result == kCVReturnSuccess && dummyBuffer) {
|
|
193
|
+
pixelBuffer = dummyBuffer;
|
|
194
|
+
createdDummyBuffer = YES;
|
|
195
|
+
NSLog(@"✅ Created manual dummy buffer");
|
|
196
|
+
|
|
197
|
+
// Fill buffer with black pixels
|
|
198
|
+
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
|
199
|
+
void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
200
|
+
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
|
201
|
+
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
202
|
+
if (baseAddress) {
|
|
203
|
+
memset(baseAddress, 0, bytesPerRow * height);
|
|
204
|
+
}
|
|
205
|
+
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
|
206
|
+
} else {
|
|
207
|
+
NSLog(@"❌ Failed to create dummy pixel buffer");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
127
212
|
}
|
|
213
|
+
NSLog(@"✅ LAYER 8 PASS: Pixel buffer ready (dummy=%d)", createdDummyBuffer);
|
|
128
214
|
|
|
129
215
|
// SAFETY LAYER 9: Dimension validation - flexible this time
|
|
130
216
|
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
|
131
217
|
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
132
218
|
if (width == 0 || height == 0 || width > 4096 || height > 4096) {
|
|
219
|
+
NSLog(@"❌ LAYER 9 FAIL: Invalid dimensions %zux%zu", width, height);
|
|
133
220
|
return; // Skip only if clearly invalid
|
|
134
221
|
}
|
|
222
|
+
NSLog(@"✅ LAYER 9 PASS: Valid dimensions %zux%zu", width, height);
|
|
135
223
|
|
|
136
|
-
// SAFETY LAYER 10: Time validation
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
224
|
+
// SAFETY LAYER 10: Time validation - use sequential timing
|
|
225
|
+
g_frameNumber++;
|
|
226
|
+
|
|
227
|
+
// Create sequential time stamps
|
|
228
|
+
CMTime relativeTime = CMTimeMake(g_frameNumber, 30); // 30 FPS sequential
|
|
141
229
|
|
|
142
|
-
CMTime relativeTime = CMTimeSubtract(presentationTime, g_startTime);
|
|
143
230
|
if (!CMTIME_IS_VALID(relativeTime)) {
|
|
144
231
|
return;
|
|
145
232
|
}
|
|
146
233
|
|
|
147
234
|
double seconds = CMTimeGetSeconds(relativeTime);
|
|
148
|
-
if (seconds
|
|
235
|
+
if (seconds > 30.0) { // Max 30 seconds
|
|
149
236
|
return;
|
|
150
237
|
}
|
|
151
238
|
|
|
152
239
|
// SAFETY LAYER 11: Append with complete exception handling
|
|
153
240
|
@try {
|
|
154
241
|
// Use pixel buffer directly - copy was causing errors
|
|
242
|
+
NSLog(@"🔍 Attempting to append frame %d with time %.3fs", g_frameNumber, seconds);
|
|
155
243
|
BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
|
|
156
244
|
|
|
157
245
|
if (success) {
|
|
158
246
|
g_currentTime = relativeTime;
|
|
159
247
|
static int ultraSafeFrameCount = 0;
|
|
160
248
|
ultraSafeFrameCount++;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
249
|
+
NSLog(@"✅ Frame %d appended successfully! (%.1fs)", ultraSafeFrameCount, seconds);
|
|
250
|
+
} else {
|
|
251
|
+
NSLog(@"❌ Failed to append frame %d - adaptor rejected", g_frameNumber);
|
|
164
252
|
}
|
|
165
253
|
} @catch (NSException *appendException) {
|
|
166
254
|
NSLog(@"🛡️ Append exception handled safely: %@", appendException.reason);
|
|
167
255
|
// Continue gracefully - don't crash
|
|
168
256
|
}
|
|
257
|
+
|
|
258
|
+
// Cleanup dummy pixel buffer if we created one
|
|
259
|
+
if (pixelBuffer && createdDummyBuffer) {
|
|
260
|
+
CVPixelBufferRelease(pixelBuffer);
|
|
261
|
+
NSLog(@"🧹 Released dummy pixel buffer");
|
|
262
|
+
}
|
|
169
263
|
}
|
|
170
264
|
} @catch (NSException *outerException) {
|
|
171
265
|
NSLog(@"🛡️ Outer exception handled: %@", outerException.reason);
|
|
@@ -193,6 +287,7 @@ static BOOL g_writerStarted = NO;
|
|
|
193
287
|
|
|
194
288
|
g_outputPath = config[@"outputPath"];
|
|
195
289
|
g_writerStarted = NO;
|
|
290
|
+
g_frameNumber = 0; // Reset frame counter for new recording
|
|
196
291
|
|
|
197
292
|
// Setup Electron-safe video writer
|
|
198
293
|
[ScreenCaptureKitRecorder setupVideoWriter];
|
|
@@ -205,38 +300,72 @@ static BOOL g_writerStarted = NO;
|
|
|
205
300
|
return;
|
|
206
301
|
}
|
|
207
302
|
|
|
303
|
+
NSLog(@"✅ Got shareable content with %lu displays", (unsigned long)content.displays.count);
|
|
304
|
+
|
|
305
|
+
if (content.displays.count == 0) {
|
|
306
|
+
NSLog(@"❌ No displays available for recording");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
208
310
|
// Get primary display
|
|
209
311
|
SCDisplay *targetDisplay = content.displays.firstObject;
|
|
312
|
+
if (!targetDisplay) {
|
|
313
|
+
NSLog(@"❌ No target display found");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
NSLog(@"🖥️ Using display: %@ (%dx%d)", @(targetDisplay.displayID), (int)targetDisplay.width, (int)targetDisplay.height);
|
|
210
318
|
|
|
211
|
-
//
|
|
319
|
+
// Create content filter for entire display - NO exclusions
|
|
212
320
|
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
|
|
321
|
+
NSLog(@"✅ Content filter created for display");
|
|
213
322
|
|
|
214
|
-
//
|
|
323
|
+
// Stream configuration - fixed resolution to avoid permissions issues
|
|
215
324
|
SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
|
|
216
|
-
streamConfig.width =
|
|
217
|
-
streamConfig.height =
|
|
218
|
-
streamConfig.minimumFrameInterval = CMTimeMake(1,
|
|
325
|
+
streamConfig.width = 1920;
|
|
326
|
+
streamConfig.height = 1080;
|
|
327
|
+
streamConfig.minimumFrameInterval = CMTimeMake(1, 30); // 30 FPS
|
|
219
328
|
streamConfig.pixelFormat = kCVPixelFormatType_32BGRA;
|
|
220
|
-
streamConfig.
|
|
221
|
-
|
|
329
|
+
streamConfig.showsCursor = YES;
|
|
330
|
+
|
|
331
|
+
NSLog(@"🔧 Stream config: %zux%zu, pixelFormat=%u, FPS=30", streamConfig.width, streamConfig.height, (unsigned)streamConfig.pixelFormat);
|
|
222
332
|
|
|
223
333
|
// Create Electron-safe delegates
|
|
224
334
|
g_streamDelegate = [[ElectronSafeDelegate alloc] init];
|
|
225
335
|
g_streamOutput = [[ElectronSafeOutput alloc] init];
|
|
226
336
|
|
|
337
|
+
NSLog(@"🤝 Delegates created");
|
|
338
|
+
|
|
227
339
|
// Create stream
|
|
340
|
+
NSError *streamError = nil;
|
|
228
341
|
g_stream = [[SCStream alloc] initWithFilter:filter configuration:streamConfig delegate:g_streamDelegate];
|
|
229
342
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
343
|
+
if (!g_stream) {
|
|
344
|
+
NSLog(@"❌ Failed to create stream");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
NSLog(@"✅ Stream created successfully");
|
|
349
|
+
|
|
350
|
+
// Add stream output with explicit error checking
|
|
351
|
+
BOOL outputResult = [g_stream addStreamOutput:g_streamOutput
|
|
352
|
+
type:SCStreamOutputTypeScreen
|
|
353
|
+
sampleHandlerQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
|
|
354
|
+
error:&streamError];
|
|
355
|
+
|
|
356
|
+
if (!outputResult || streamError) {
|
|
357
|
+
NSLog(@"❌ Failed to add stream output: %@", streamError);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
NSLog(@"✅ Stream output added successfully");
|
|
234
362
|
|
|
235
363
|
[g_stream startCaptureWithCompletionHandler:^(NSError *startError) {
|
|
236
364
|
if (startError) {
|
|
237
365
|
NSLog(@"❌ Failed to start capture: %@", startError);
|
|
366
|
+
g_isRecording = NO;
|
|
238
367
|
} else {
|
|
239
|
-
NSLog(@"✅ Frame capture started");
|
|
368
|
+
NSLog(@"✅ Frame capture started successfully");
|
|
240
369
|
g_isRecording = YES;
|
|
241
370
|
}
|
|
242
371
|
}];
|
|
@@ -287,14 +416,14 @@ static BOOL g_writerStarted = NO;
|
|
|
287
416
|
return;
|
|
288
417
|
}
|
|
289
418
|
|
|
290
|
-
//
|
|
419
|
+
// Fixed video settings for compatibility
|
|
291
420
|
NSDictionary *videoSettings = @{
|
|
292
421
|
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
293
|
-
AVVideoWidthKey: @
|
|
294
|
-
AVVideoHeightKey: @
|
|
422
|
+
AVVideoWidthKey: @1920,
|
|
423
|
+
AVVideoHeightKey: @1080,
|
|
295
424
|
AVVideoCompressionPropertiesKey: @{
|
|
296
|
-
AVVideoAverageBitRateKey: @(
|
|
297
|
-
AVVideoMaxKeyFrameIntervalKey: @
|
|
425
|
+
AVVideoAverageBitRateKey: @(1920 * 1080 * 2), // 2 bits per pixel
|
|
426
|
+
AVVideoMaxKeyFrameIntervalKey: @30,
|
|
298
427
|
AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel
|
|
299
428
|
}
|
|
300
429
|
};
|
|
@@ -305,8 +434,8 @@ static BOOL g_writerStarted = NO;
|
|
|
305
434
|
// Pixel buffer attributes matching ScreenCaptureKit format
|
|
306
435
|
NSDictionary *pixelBufferAttributes = @{
|
|
307
436
|
(NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
|
|
308
|
-
(NSString*)kCVPixelBufferWidthKey: @
|
|
309
|
-
(NSString*)kCVPixelBufferHeightKey: @
|
|
437
|
+
(NSString*)kCVPixelBufferWidthKey: @1920,
|
|
438
|
+
(NSString*)kCVPixelBufferHeightKey: @1080
|
|
310
439
|
};
|
|
311
440
|
|
|
312
441
|
g_pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:g_assetWriterInput sourcePixelBufferAttributes:pixelBufferAttributes];
|
|
@@ -350,6 +479,7 @@ static BOOL g_writerStarted = NO;
|
|
|
350
479
|
g_assetWriterInput = nil;
|
|
351
480
|
g_pixelBufferAdaptor = nil;
|
|
352
481
|
g_writerStarted = NO;
|
|
482
|
+
g_frameNumber = 0; // Reset frame counter
|
|
353
483
|
g_stream = nil;
|
|
354
484
|
g_streamDelegate = nil;
|
|
355
485
|
g_streamOutput = nil;
|
package/test-hybrid.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
|
|
3
|
+
// Simulate Electron environment
|
|
4
|
+
process.env.ELECTRON_RUN_AS_NODE = '1';
|
|
5
|
+
|
|
6
|
+
console.log('🧪 Testing Hybrid Recording Solution (Electron Mode)');
|
|
7
|
+
|
|
8
|
+
async function testHybridRecording() {
|
|
9
|
+
const recorder = new MacRecorder();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const outputPath = './test-output/hybrid-electron-test.mov';
|
|
13
|
+
|
|
14
|
+
console.log('📹 Starting hybrid recording in Electron mode...');
|
|
15
|
+
const result = await recorder.startRecording(outputPath, {
|
|
16
|
+
captureCursor: true,
|
|
17
|
+
includeMicrophone: false,
|
|
18
|
+
includeSystemAudio: false
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (result) {
|
|
22
|
+
console.log('✅ Recording started successfully');
|
|
23
|
+
|
|
24
|
+
// Record for 5 seconds
|
|
25
|
+
console.log('⏱️ Recording for 5 seconds...');
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
27
|
+
|
|
28
|
+
console.log('🛑 Stopping recording...');
|
|
29
|
+
await recorder.stopRecording();
|
|
30
|
+
|
|
31
|
+
// Check if file exists and has content
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
if (fs.existsSync(outputPath)) {
|
|
34
|
+
const stats = fs.statSync(outputPath);
|
|
35
|
+
console.log(`✅ Video file created: ${outputPath} (${stats.size} bytes)`);
|
|
36
|
+
|
|
37
|
+
if (stats.size > 10) {
|
|
38
|
+
console.log('✅ Hybrid recording successful - Electron compatible');
|
|
39
|
+
} else {
|
|
40
|
+
console.log('⚠️ File created but very small');
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.log('❌ Video file not found');
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
console.log('❌ Failed to start recording');
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.log('❌ Error during test:', error.message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
testHybridRecording().catch(console.error);
|
package/test-quick.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
|
|
3
|
+
// Simulate Electron environment
|
|
4
|
+
process.env.ELECTRON_RUN_AS_NODE = '1';
|
|
5
|
+
|
|
6
|
+
console.log('🎯 Quick ScreenCaptureKit Test');
|
|
7
|
+
|
|
8
|
+
async function quickTest() {
|
|
9
|
+
const recorder = new MacRecorder();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const outputPath = './test-output/quick-test.mov';
|
|
13
|
+
|
|
14
|
+
console.log('📹 Starting recording...');
|
|
15
|
+
const result = await recorder.startRecording(outputPath, {
|
|
16
|
+
captureCursor: true,
|
|
17
|
+
includeMicrophone: false,
|
|
18
|
+
includeSystemAudio: false
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (result) {
|
|
22
|
+
console.log('✅ Recording started successfully');
|
|
23
|
+
|
|
24
|
+
// Record for only 3 seconds
|
|
25
|
+
console.log('⏱️ Recording for 3 seconds...');
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
27
|
+
|
|
28
|
+
console.log('🛑 Stopping recording...');
|
|
29
|
+
await recorder.stopRecording();
|
|
30
|
+
|
|
31
|
+
// Check if file exists and has content
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
if (fs.existsSync(outputPath)) {
|
|
35
|
+
const stats = fs.statSync(outputPath);
|
|
36
|
+
console.log(`✅ Video file: ${outputPath} (${stats.size} bytes)`);
|
|
37
|
+
|
|
38
|
+
if (stats.size > 1000) {
|
|
39
|
+
console.log('🎉 SUCCESS! ScreenCaptureKit is working!');
|
|
40
|
+
} else {
|
|
41
|
+
console.log('⚠️ File too small');
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
console.log('❌ No output file');
|
|
45
|
+
}
|
|
46
|
+
}, 2000);
|
|
47
|
+
} else {
|
|
48
|
+
console.log('❌ Failed to start recording');
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.log('❌ Error:', error.message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
quickTest().catch(console.error);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
|
|
3
|
+
// Simulate Electron environment
|
|
4
|
+
process.env.ELECTRON_RUN_AS_NODE = '1';
|
|
5
|
+
|
|
6
|
+
console.log('🎯 Testing PURE ScreenCaptureKit (Ultra-Safe for Electron)');
|
|
7
|
+
|
|
8
|
+
async function testPureScreenCaptureKit() {
|
|
9
|
+
const recorder = new MacRecorder();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const outputPath = './test-output/screencapturekit-pure-test.mov';
|
|
13
|
+
|
|
14
|
+
console.log('📹 Starting PURE ScreenCaptureKit recording...');
|
|
15
|
+
const result = await recorder.startRecording(outputPath, {
|
|
16
|
+
captureCursor: true,
|
|
17
|
+
includeMicrophone: false,
|
|
18
|
+
includeSystemAudio: false
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (result) {
|
|
22
|
+
console.log('✅ Recording started successfully');
|
|
23
|
+
|
|
24
|
+
// Record for 10 seconds to get more frames
|
|
25
|
+
console.log('⏱️ Recording for 10 seconds...');
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
27
|
+
|
|
28
|
+
console.log('🛑 Stopping recording...');
|
|
29
|
+
await recorder.stopRecording();
|
|
30
|
+
|
|
31
|
+
// Check if file exists and has content
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
if (fs.existsSync(outputPath)) {
|
|
34
|
+
const stats = fs.statSync(outputPath);
|
|
35
|
+
console.log(`✅ Video file created: ${outputPath} (${stats.size} bytes)`);
|
|
36
|
+
|
|
37
|
+
if (stats.size > 10000) {
|
|
38
|
+
console.log('✅ PURE ScreenCaptureKit successful - Real video!');
|
|
39
|
+
|
|
40
|
+
// Try to get more info about the video
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
const { spawn } = require('child_process');
|
|
43
|
+
const ffprobe = spawn('ffprobe', ['-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', outputPath]);
|
|
44
|
+
let output = '';
|
|
45
|
+
ffprobe.stdout.on('data', (data) => output += data);
|
|
46
|
+
ffprobe.on('close', () => {
|
|
47
|
+
try {
|
|
48
|
+
const info = JSON.parse(output);
|
|
49
|
+
console.log(`🎞️ Video info: ${info.format.duration}s, ${info.streams[0].nb_frames} frames`);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.log('📊 Video analysis failed, but file exists');
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}, 1000);
|
|
55
|
+
} else {
|
|
56
|
+
console.log('⚠️ File size is very small - may not have content');
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
console.log('❌ Video file not found');
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
console.log('❌ Failed to start recording');
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.log('❌ Error during test:', error.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
testPureScreenCaptureKit().catch(console.error);
|