node-mac-recorder 2.13.8 β 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/index.js +11 -128
- package/package.json +1 -1
- package/src/mac_recorder.mm +3 -5
- package/src/screen_capture_kit.mm +169 -39
- package/test-quick.js +55 -0
- package/test-screencapture-pure.js +69 -0
package/index.js
CHANGED
|
@@ -404,13 +404,11 @@ class MacRecorder extends EventEmitter {
|
|
|
404
404
|
this.emit("started", this.outputPath);
|
|
405
405
|
resolve(this.outputPath);
|
|
406
406
|
} else {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
reject(new Error(`All recording methods failed. Native: Check permissions. Alternative: ${altError.message}`));
|
|
413
|
-
});
|
|
407
|
+
reject(
|
|
408
|
+
new Error(
|
|
409
|
+
"ScreenCaptureKit failed to start. Check permissions and try again."
|
|
410
|
+
)
|
|
411
|
+
);
|
|
414
412
|
}
|
|
415
413
|
} catch (error) {
|
|
416
414
|
reject(error);
|
|
@@ -418,109 +416,6 @@ class MacRecorder extends EventEmitter {
|
|
|
418
416
|
});
|
|
419
417
|
}
|
|
420
418
|
|
|
421
|
-
/**
|
|
422
|
-
* Alternative recording method for Electron compatibility - REAL VIDEO
|
|
423
|
-
*/
|
|
424
|
-
async startAlternativeRecording(outputPath, options = {}) {
|
|
425
|
-
const { spawn } = require('child_process');
|
|
426
|
-
const fs = require('fs');
|
|
427
|
-
const path = require('path');
|
|
428
|
-
|
|
429
|
-
try {
|
|
430
|
-
console.log('π¬ Starting REAL video recording with FFmpeg for Electron');
|
|
431
|
-
|
|
432
|
-
// Check if FFmpeg is available
|
|
433
|
-
const ffmpegArgs = [
|
|
434
|
-
'-f', 'avfoundation', // Use AVFoundation input
|
|
435
|
-
'-framerate', '30', // 30 FPS
|
|
436
|
-
'-video_size', '1280x720', // Resolution
|
|
437
|
-
'-i', '3:none', // Screen capture device 3, no audio
|
|
438
|
-
'-c:v', 'libx264', // H.264 codec
|
|
439
|
-
'-preset', 'ultrafast', // Fast encoding
|
|
440
|
-
'-crf', '23', // Quality
|
|
441
|
-
'-t', '30', // Max 30 seconds
|
|
442
|
-
'-y', // Overwrite output
|
|
443
|
-
outputPath
|
|
444
|
-
];
|
|
445
|
-
|
|
446
|
-
// Add capture area if specified (crop filter)
|
|
447
|
-
if (options.captureArea) {
|
|
448
|
-
const cropFilter = `crop=${options.captureArea.width}:${options.captureArea.height}:${options.captureArea.x}:${options.captureArea.y}`;
|
|
449
|
-
const index = ffmpegArgs.indexOf(outputPath);
|
|
450
|
-
ffmpegArgs.splice(index, 0, '-vf', cropFilter);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
console.log('π₯ Starting FFmpeg with args:', ffmpegArgs);
|
|
454
|
-
|
|
455
|
-
this.alternativeProcess = spawn('ffmpeg', ffmpegArgs);
|
|
456
|
-
|
|
457
|
-
this.alternativeProcess.stdout.on('data', (data) => {
|
|
458
|
-
console.log('FFmpeg stdout:', data.toString());
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
this.alternativeProcess.stderr.on('data', (data) => {
|
|
462
|
-
const output = data.toString();
|
|
463
|
-
if (output.includes('frame=')) {
|
|
464
|
-
// This indicates recording is working
|
|
465
|
-
console.log('π¬ Recording frames...');
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
this.alternativeProcess.on('close', (code) => {
|
|
470
|
-
console.log(`π¬ FFmpeg recording finished with code: ${code}`);
|
|
471
|
-
this.isRecording = false;
|
|
472
|
-
|
|
473
|
-
if (this.recordingTimer) {
|
|
474
|
-
clearInterval(this.recordingTimer);
|
|
475
|
-
this.recordingTimer = null;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
this.emit('stopped', { code, outputPath });
|
|
479
|
-
|
|
480
|
-
// Check if file was created
|
|
481
|
-
setTimeout(() => {
|
|
482
|
-
if (fs.existsSync(outputPath) && fs.statSync(outputPath).size > 1000) {
|
|
483
|
-
this.emit('completed', outputPath);
|
|
484
|
-
console.log('β
Real video file created with FFmpeg');
|
|
485
|
-
} else {
|
|
486
|
-
console.log('β FFmpeg video creation failed');
|
|
487
|
-
}
|
|
488
|
-
}, 500);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
this.alternativeProcess.on('error', (error) => {
|
|
492
|
-
console.error('β FFmpeg error:', error.message);
|
|
493
|
-
if (error.code === 'ENOENT') {
|
|
494
|
-
console.log('π‘ FFmpeg not found. Install with: brew install ffmpeg');
|
|
495
|
-
throw new Error('FFmpeg not installed. Run: brew install ffmpeg');
|
|
496
|
-
}
|
|
497
|
-
throw error;
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
this.isRecording = true;
|
|
501
|
-
this.recordingStartTime = Date.now();
|
|
502
|
-
|
|
503
|
-
// Timer baΕlat
|
|
504
|
-
this.recordingTimer = setInterval(() => {
|
|
505
|
-
const elapsed = Math.floor((Date.now() - this.recordingStartTime) / 1000);
|
|
506
|
-
this.emit("timeUpdate", elapsed);
|
|
507
|
-
}, 1000);
|
|
508
|
-
|
|
509
|
-
this.emit('recordingStarted', {
|
|
510
|
-
outputPath,
|
|
511
|
-
options,
|
|
512
|
-
timestamp: Date.now(),
|
|
513
|
-
method: 'ffmpeg'
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
this.emit('started');
|
|
517
|
-
return true;
|
|
518
|
-
|
|
519
|
-
} catch (error) {
|
|
520
|
-
console.error('Alternative recording failed:', error);
|
|
521
|
-
throw new Error(`FFmpeg recording failed: ${error.message}`);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
419
|
|
|
525
420
|
/**
|
|
526
421
|
* Ekran kaydΔ±nΔ± durdurur
|
|
@@ -534,24 +429,12 @@ class MacRecorder extends EventEmitter {
|
|
|
534
429
|
try {
|
|
535
430
|
let success = false;
|
|
536
431
|
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
// Wait for FFmpeg to finish
|
|
545
|
-
success = true;
|
|
546
|
-
this.alternativeProcess = null;
|
|
547
|
-
} else {
|
|
548
|
-
// Try native stop
|
|
549
|
-
try {
|
|
550
|
-
success = nativeBinding.stopRecording();
|
|
551
|
-
} catch (nativeError) {
|
|
552
|
-
console.log('Native stop failed:', nativeError.message);
|
|
553
|
-
success = true; // Assume success to avoid throwing
|
|
554
|
-
}
|
|
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
|
|
555
438
|
}
|
|
556
439
|
|
|
557
440
|
// Timer durdur
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -175,11 +175,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
175
175
|
[NSBundle.mainBundle.bundlePath containsString:@"Electron"]);
|
|
176
176
|
|
|
177
177
|
if (isElectron) {
|
|
178
|
-
NSLog(@"β‘ Electron environment detected - Using
|
|
179
|
-
NSLog(@"π‘οΈ
|
|
180
|
-
|
|
181
|
-
// Return error for Electron - force use of external recording tools
|
|
182
|
-
return Napi::Boolean::New(env, false);
|
|
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
|
|
183
181
|
}
|
|
184
182
|
|
|
185
183
|
// Non-Electron: Use ScreenCaptureKit
|
|
@@ -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-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);
|