node-mac-recorder 2.13.6 โ 2.13.8
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/README.md +17 -3
- package/index.js +142 -10
- package/package.json +1 -1
- package/src/mac_recorder.mm +38 -231
- package/src/screen_capture_kit.mm +117 -100
- package/test-electron-detection.js +44 -0
- package/test-hybrid.js +53 -0
- package/test-screencapture-only.js +50 -0
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# node-mac-recorder
|
|
2
2
|
|
|
3
|
-
A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and
|
|
3
|
+
A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and automatic overlay window exclusion. Built with ScreenCaptureKit for modern macOS with intelligent window filtering and Electron compatibility.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
โจ **Advanced Recording Capabilities**
|
|
8
8
|
|
|
9
|
-
- ๐ฅ๏ธ **Full Screen Recording** - Capture entire displays
|
|
9
|
+
- ๐ฅ๏ธ **Full Screen Recording** - Capture entire displays with ScreenCaptureKit
|
|
10
10
|
- ๐ช **Window-Specific Recording** - Record individual application windows
|
|
11
11
|
- ๐ฏ **Area Selection** - Record custom screen regions
|
|
12
12
|
- ๐ฑ๏ธ **Multi-Display Support** - Automatic display detection and selection
|
|
13
13
|
- ๐จ **Cursor Control** - Toggle cursor visibility in recordings
|
|
14
14
|
- ๐ฑ๏ธ **Cursor Tracking** - Track mouse position, cursor types, and click events
|
|
15
|
+
- ๐ซ **Automatic Overlay Exclusion** - Overlay windows automatically excluded from recordings
|
|
16
|
+
- โก **Electron Compatible** - Enhanced crash protection for Electron applications
|
|
15
17
|
|
|
16
18
|
๐ต **Granular Audio Controls**
|
|
17
19
|
|
|
@@ -35,6 +37,18 @@ A powerful native macOS screen recording Node.js package with advanced window se
|
|
|
35
37
|
- ๐ **Flexible Output** - Custom output paths and formats
|
|
36
38
|
- ๐ **Permission Management** - Built-in permission checking
|
|
37
39
|
|
|
40
|
+
## ScreenCaptureKit Technology
|
|
41
|
+
|
|
42
|
+
This package leverages Apple's modern **ScreenCaptureKit** framework (macOS 12.3+) for superior recording capabilities:
|
|
43
|
+
|
|
44
|
+
- **๐ฏ Native Overlay Exclusion**: Overlay windows are automatically filtered out during recording
|
|
45
|
+
- **๐ Enhanced Performance**: Direct system-level recording with optimized resource usage
|
|
46
|
+
- **๐ก๏ธ Crash Protection**: Advanced safety layers for Electron applications
|
|
47
|
+
- **๐ฑ Future-Proof**: Built on Apple's latest screen capture technology
|
|
48
|
+
- **๐จ Better Quality**: Improved frame handling and video encoding
|
|
49
|
+
|
|
50
|
+
> **Note**: For applications requiring overlay exclusion (like screen recording tools with floating UI), ScreenCaptureKit automatically handles window filtering without manual intervention.
|
|
51
|
+
|
|
38
52
|
## Installation
|
|
39
53
|
|
|
40
54
|
```bash
|
|
@@ -43,7 +57,7 @@ npm install node-mac-recorder
|
|
|
43
57
|
|
|
44
58
|
### Requirements
|
|
45
59
|
|
|
46
|
-
- **macOS
|
|
60
|
+
- **macOS 12.3+** (Monterey or later) - Required for ScreenCaptureKit
|
|
47
61
|
- **Node.js 14+**
|
|
48
62
|
- **Xcode Command Line Tools**
|
|
49
63
|
- **Screen Recording Permission** (automatically requested)
|
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;
|
|
@@ -398,11 +404,13 @@ class MacRecorder extends EventEmitter {
|
|
|
398
404
|
this.emit("started", this.outputPath);
|
|
399
405
|
resolve(this.outputPath);
|
|
400
406
|
} else {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
)
|
|
405
|
-
|
|
407
|
+
// Try alternative recording method for Electron
|
|
408
|
+
console.log('๐ฌ Native recording failed, trying alternative method for Electron compatibility');
|
|
409
|
+
this.startAlternativeRecording(outputPath, recordingOptions)
|
|
410
|
+
.then(() => resolve(outputPath))
|
|
411
|
+
.catch((altError) => {
|
|
412
|
+
reject(new Error(`All recording methods failed. Native: Check permissions. Alternative: ${altError.message}`));
|
|
413
|
+
});
|
|
406
414
|
}
|
|
407
415
|
} catch (error) {
|
|
408
416
|
reject(error);
|
|
@@ -410,6 +418,110 @@ class MacRecorder extends EventEmitter {
|
|
|
410
418
|
});
|
|
411
419
|
}
|
|
412
420
|
|
|
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
|
+
|
|
413
525
|
/**
|
|
414
526
|
* Ekran kaydฤฑnฤฑ durdurur
|
|
415
527
|
*/
|
|
@@ -420,7 +532,27 @@ class MacRecorder extends EventEmitter {
|
|
|
420
532
|
|
|
421
533
|
return new Promise((resolve, reject) => {
|
|
422
534
|
try {
|
|
423
|
-
|
|
535
|
+
let success = false;
|
|
536
|
+
|
|
537
|
+
// Check if using alternative recording (FFmpeg)
|
|
538
|
+
if (this.alternativeProcess) {
|
|
539
|
+
console.log('๐ Stopping FFmpeg recording');
|
|
540
|
+
|
|
541
|
+
// Send SIGTERM to FFmpeg to stop recording gracefully
|
|
542
|
+
this.alternativeProcess.kill('SIGTERM');
|
|
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
|
+
}
|
|
555
|
+
}
|
|
424
556
|
|
|
425
557
|
// Timer durdur
|
|
426
558
|
if (this.recordingTimer) {
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -46,18 +46,12 @@ static bool g_isRecording = false;
|
|
|
46
46
|
|
|
47
47
|
// Helper function to cleanup recording resources
|
|
48
48
|
void cleanupRecording() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// ScreenCaptureKit cleanup only
|
|
50
|
+
if (@available(macOS 12.3, *)) {
|
|
51
|
+
if ([ScreenCaptureKitRecorder isRecording]) {
|
|
52
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
53
|
+
}
|
|
52
54
|
}
|
|
53
|
-
g_movieFileOutput = nil;
|
|
54
|
-
g_screenInput = nil;
|
|
55
|
-
g_audioInput = nil;
|
|
56
|
-
g_delegate = nil;
|
|
57
|
-
|
|
58
|
-
// Show overlay windows again after cleanup
|
|
59
|
-
showOverlays();
|
|
60
|
-
|
|
61
55
|
g_isRecording = false;
|
|
62
56
|
}
|
|
63
57
|
|
|
@@ -168,21 +162,27 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
@try {
|
|
171
|
-
//
|
|
172
|
-
NSLog(@"
|
|
173
|
-
NSLog(@"๐ก๏ธ Using separate process architecture to prevent main thread crashes");
|
|
174
|
-
NSLog(@"๐ Will fallback to AVFoundation if ScreenCaptureKit fails");
|
|
165
|
+
// Smart Recording Selection: ScreenCaptureKit vs Alternative
|
|
166
|
+
NSLog(@"๐ฏ Smart Recording Engine Selection");
|
|
175
167
|
|
|
176
|
-
//
|
|
168
|
+
// Detect Electron environment with multiple checks
|
|
177
169
|
BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
|
|
178
170
|
[NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
|
|
179
171
|
(NSProcessInfo.processInfo.processName &&
|
|
180
|
-
[NSProcessInfo.processInfo.processName containsString:@"Electron"])
|
|
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"]);
|
|
181
176
|
|
|
182
177
|
if (isElectron) {
|
|
183
|
-
NSLog(@"โก Electron environment detected -
|
|
178
|
+
NSLog(@"โก Electron environment detected - Using crash-safe recording method");
|
|
179
|
+
NSLog(@"๐ก๏ธ ScreenCaptureKit disabled for Electron stability");
|
|
180
|
+
|
|
181
|
+
// Return error for Electron - force use of external recording tools
|
|
182
|
+
return Napi::Boolean::New(env, false);
|
|
184
183
|
}
|
|
185
184
|
|
|
185
|
+
// Non-Electron: Use ScreenCaptureKit
|
|
186
186
|
if (@available(macOS 12.3, *)) {
|
|
187
187
|
NSLog(@"โ
macOS 12.3+ detected - ScreenCaptureKit should be available");
|
|
188
188
|
|
|
@@ -231,19 +231,12 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
231
231
|
delegate:g_delegate
|
|
232
232
|
error:&sckError]) {
|
|
233
233
|
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
}
|
|
234
|
+
// ScreenCaptureKit baลlatma baลarฤฑlฤฑ - validation yapmฤฑyoruz
|
|
235
|
+
sckStarted = YES;
|
|
236
|
+
NSLog(@"๐ฌ RECORDING METHOD: ScreenCaptureKit");
|
|
237
|
+
NSLog(@"โ
ScreenCaptureKit recording started successfully");
|
|
238
|
+
g_isRecording = true;
|
|
239
|
+
return Napi::Boolean::New(env, true);
|
|
247
240
|
} else {
|
|
248
241
|
NSLog(@"โ ScreenCaptureKit failed to start");
|
|
249
242
|
NSLog(@"โ Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
|
|
@@ -260,178 +253,16 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
260
253
|
}
|
|
261
254
|
} @catch (NSException *availabilityException) {
|
|
262
255
|
NSLog(@"โ Exception during ScreenCaptureKit availability check: %@", availabilityException.reason);
|
|
263
|
-
|
|
256
|
+
return Napi::Boolean::New(env, false);
|
|
264
257
|
}
|
|
265
258
|
} else {
|
|
266
|
-
NSLog(@"โ macOS version too old for ScreenCaptureKit (< 12.3)");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Fallback: Use AVFoundation with overlay hiding
|
|
270
|
-
NSLog(@"๐ฌ RECORDING METHOD: AVFoundation");
|
|
271
|
-
NSLog(@"๐ผ Using AVFoundation with overlay hiding for video compatibility");
|
|
272
|
-
|
|
273
|
-
// Hide overlay windows during recording
|
|
274
|
-
hideOverlays();
|
|
275
|
-
|
|
276
|
-
// Create capture session
|
|
277
|
-
g_captureSession = [[AVCaptureSession alloc] init];
|
|
278
|
-
[g_captureSession beginConfiguration];
|
|
279
|
-
|
|
280
|
-
// Set session preset
|
|
281
|
-
g_captureSession.sessionPreset = AVCaptureSessionPresetHigh;
|
|
282
|
-
|
|
283
|
-
// Create screen input with selected display
|
|
284
|
-
g_screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:displayID];
|
|
285
|
-
|
|
286
|
-
if (!CGRectIsNull(captureRect)) {
|
|
287
|
-
g_screenInput.cropRect = captureRect;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Set cursor capture
|
|
291
|
-
g_screenInput.capturesCursor = captureCursor;
|
|
292
|
-
|
|
293
|
-
// Configure screen input options
|
|
294
|
-
g_screenInput.capturesMouseClicks = NO;
|
|
295
|
-
|
|
296
|
-
if ([g_captureSession canAddInput:g_screenInput]) {
|
|
297
|
-
[g_captureSession addInput:g_screenInput];
|
|
298
|
-
} else {
|
|
299
|
-
cleanupRecording();
|
|
259
|
+
NSLog(@"โ macOS version too old for ScreenCaptureKit (< 12.3) - Recording not supported");
|
|
300
260
|
return Napi::Boolean::New(env, false);
|
|
301
261
|
}
|
|
302
262
|
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (audioDeviceId) {
|
|
308
|
-
// Try to find the specified device
|
|
309
|
-
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
310
|
-
NSLog(@"[DEBUG] Looking for audio device with ID: %@", audioDeviceId);
|
|
311
|
-
NSLog(@"[DEBUG] Available audio devices:");
|
|
312
|
-
for (AVCaptureDevice *device in devices) {
|
|
313
|
-
NSLog(@"[DEBUG] - Device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
314
|
-
if ([device.uniqueID isEqualToString:audioDeviceId]) {
|
|
315
|
-
NSLog(@"[DEBUG] Found matching device: %@", device.localizedName);
|
|
316
|
-
audioDevice = device;
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (!audioDevice) {
|
|
322
|
-
NSLog(@"[DEBUG] Specified audio device not found, falling back to default");
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Fallback to default device if specified device not found
|
|
327
|
-
if (!audioDevice) {
|
|
328
|
-
audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
|
329
|
-
NSLog(@"[DEBUG] Using default audio device: %@ (ID: %@)", audioDevice.localizedName, audioDevice.uniqueID);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (audioDevice) {
|
|
333
|
-
NSError *error;
|
|
334
|
-
g_audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
|
|
335
|
-
if (g_audioInput && [g_captureSession canAddInput:g_audioInput]) {
|
|
336
|
-
[g_captureSession addInput:g_audioInput];
|
|
337
|
-
NSLog(@"[DEBUG] Successfully added audio input device");
|
|
338
|
-
} else {
|
|
339
|
-
NSLog(@"[DEBUG] Failed to add audio input device: %@", error);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// System audio configuration
|
|
345
|
-
if (includeSystemAudio) {
|
|
346
|
-
// Enable audio capture in screen input
|
|
347
|
-
g_screenInput.capturesMouseClicks = YES;
|
|
348
|
-
|
|
349
|
-
// Try to add system audio input using Core Audio
|
|
350
|
-
// This approach captures system audio by creating a virtual audio device
|
|
351
|
-
if (@available(macOS 10.15, *)) {
|
|
352
|
-
// Configure screen input for better audio capture
|
|
353
|
-
g_screenInput.capturesCursor = captureCursor;
|
|
354
|
-
g_screenInput.capturesMouseClicks = YES;
|
|
355
|
-
|
|
356
|
-
// Try to find and add system audio device (like Soundflower, BlackHole, etc.)
|
|
357
|
-
NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
358
|
-
AVCaptureDevice *systemAudioDevice = nil;
|
|
359
|
-
|
|
360
|
-
// If specific system audio device ID is provided, try to find it first
|
|
361
|
-
if (systemAudioDeviceId) {
|
|
362
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
363
|
-
if ([device.uniqueID isEqualToString:systemAudioDeviceId]) {
|
|
364
|
-
systemAudioDevice = device;
|
|
365
|
-
NSLog(@"[DEBUG] Found specified system audio device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// If no specific device found or specified, look for known system audio devices
|
|
372
|
-
if (!systemAudioDevice) {
|
|
373
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
374
|
-
NSString *deviceName = [device.localizedName lowercaseString];
|
|
375
|
-
// Check for common system audio capture devices
|
|
376
|
-
if ([deviceName containsString:@"soundflower"] ||
|
|
377
|
-
[deviceName containsString:@"blackhole"] ||
|
|
378
|
-
[deviceName containsString:@"loopback"] ||
|
|
379
|
-
[deviceName containsString:@"system audio"] ||
|
|
380
|
-
[deviceName containsString:@"aggregate"]) {
|
|
381
|
-
systemAudioDevice = device;
|
|
382
|
-
NSLog(@"[DEBUG] Auto-detected system audio device: %@", device.localizedName);
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// If we found a system audio device, add it as an additional input
|
|
389
|
-
if (systemAudioDevice && !includeMicrophone) {
|
|
390
|
-
// Only add system audio device if microphone is not already added
|
|
391
|
-
NSError *error;
|
|
392
|
-
AVCaptureDeviceInput *systemAudioInput = [[AVCaptureDeviceInput alloc] initWithDevice:systemAudioDevice error:&error];
|
|
393
|
-
if (systemAudioInput && [g_captureSession canAddInput:systemAudioInput]) {
|
|
394
|
-
[g_captureSession addInput:systemAudioInput];
|
|
395
|
-
NSLog(@"[DEBUG] Successfully added system audio device: %@", systemAudioDevice.localizedName);
|
|
396
|
-
} else if (error) {
|
|
397
|
-
NSLog(@"[DEBUG] Failed to add system audio device: %@", error.localizedDescription);
|
|
398
|
-
}
|
|
399
|
-
} else if (includeSystemAudio && !systemAudioDevice) {
|
|
400
|
-
NSLog(@"[DEBUG] System audio requested but no suitable device found. Available devices:");
|
|
401
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
402
|
-
NSLog(@"[DEBUG] - %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
} else {
|
|
407
|
-
// Explicitly disable audio capture if not requested
|
|
408
|
-
g_screenInput.capturesMouseClicks = NO;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Create movie file output
|
|
412
|
-
g_movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
|
|
413
|
-
if ([g_captureSession canAddOutput:g_movieFileOutput]) {
|
|
414
|
-
[g_captureSession addOutput:g_movieFileOutput];
|
|
415
|
-
} else {
|
|
416
|
-
cleanupRecording();
|
|
417
|
-
return Napi::Boolean::New(env, false);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
[g_captureSession commitConfiguration];
|
|
421
|
-
|
|
422
|
-
// Start session
|
|
423
|
-
[g_captureSession startRunning];
|
|
424
|
-
|
|
425
|
-
// Create delegate
|
|
426
|
-
g_delegate = [[MacRecorderDelegate alloc] init];
|
|
427
|
-
|
|
428
|
-
// Start recording
|
|
429
|
-
NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
|
|
430
|
-
[g_movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:g_delegate];
|
|
431
|
-
|
|
432
|
-
NSLog(@"โ
AVFoundation recording started");
|
|
433
|
-
g_isRecording = true;
|
|
434
|
-
return Napi::Boolean::New(env, true);
|
|
263
|
+
// If we get here, ScreenCaptureKit failed completely
|
|
264
|
+
NSLog(@"โ ScreenCaptureKit failed to initialize - Recording not available");
|
|
265
|
+
return Napi::Boolean::New(env, false);
|
|
435
266
|
|
|
436
267
|
} @catch (NSException *exception) {
|
|
437
268
|
cleanupRecording();
|
|
@@ -445,44 +276,20 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
445
276
|
|
|
446
277
|
NSLog(@"๐ StopRecording native method called");
|
|
447
278
|
|
|
448
|
-
//
|
|
449
|
-
BOOL screenCaptureKitStopped = NO;
|
|
279
|
+
// ScreenCaptureKit ONLY - No AVFoundation fallback
|
|
450
280
|
if (@available(macOS 12.3, *)) {
|
|
451
281
|
if ([ScreenCaptureKitRecorder isRecording]) {
|
|
452
282
|
NSLog(@"๐ Stopping ScreenCaptureKit recording");
|
|
453
283
|
[ScreenCaptureKitRecorder stopRecording];
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
return Napi::Boolean::New(env, true);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Otherwise, handle AVFoundation recording
|
|
464
|
-
if (!g_isRecording || !g_movieFileOutput) {
|
|
465
|
-
NSLog(@"โ No AVFoundation recording in progress");
|
|
466
|
-
return Napi::Boolean::New(env, false);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
@try {
|
|
470
|
-
NSLog(@"๐ Stopping AVFoundation recording");
|
|
471
|
-
if (g_movieFileOutput) {
|
|
472
|
-
[g_movieFileOutput stopRecording];
|
|
473
|
-
}
|
|
474
|
-
if (g_captureSession) {
|
|
475
|
-
[g_captureSession stopRunning];
|
|
284
|
+
g_isRecording = false;
|
|
285
|
+
return Napi::Boolean::New(env, true);
|
|
286
|
+
} else {
|
|
287
|
+
NSLog(@"โ ๏ธ ScreenCaptureKit not recording");
|
|
288
|
+
g_isRecording = false;
|
|
289
|
+
return Napi::Boolean::New(env, true);
|
|
476
290
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
showOverlays();
|
|
480
|
-
|
|
481
|
-
g_isRecording = false;
|
|
482
|
-
return Napi::Boolean::New(env, true);
|
|
483
|
-
|
|
484
|
-
} @catch (NSException *exception) {
|
|
485
|
-
cleanupRecording();
|
|
291
|
+
} else {
|
|
292
|
+
NSLog(@"โ ScreenCaptureKit not available - cannot stop recording");
|
|
486
293
|
return Napi::Boolean::New(env, false);
|
|
487
294
|
}
|
|
488
295
|
}
|
|
@@ -37,125 +37,142 @@ static BOOL g_writerStarted = NO;
|
|
|
37
37
|
@end
|
|
38
38
|
|
|
39
39
|
@interface ElectronSafeOutput : NSObject <SCStreamOutput>
|
|
40
|
+
- (void)processSampleBufferSafely:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type;
|
|
40
41
|
@end
|
|
41
42
|
|
|
42
43
|
@implementation ElectronSafeOutput
|
|
43
44
|
- (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
45
|
+
// EXTREME SAFETY: Complete isolation with separate thread
|
|
46
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
|
|
47
|
+
@autoreleasepool {
|
|
48
|
+
[self processSampleBufferSafely:sampleBuffer ofType:type];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (void)processSampleBufferSafely:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
54
|
+
// ELECTRON CRASH PROTECTION: Multiple layers of safety
|
|
44
55
|
if (!g_isRecording || type != SCStreamOutputTypeScreen || !g_assetWriterInput) {
|
|
45
56
|
return;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
//
|
|
59
|
+
// SAFETY LAYER 1: Null checks
|
|
60
|
+
if (!sampleBuffer || !CMSampleBufferIsValid(sampleBuffer)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// SAFETY LAYER 2: Try-catch with complete isolation
|
|
49
65
|
@try {
|
|
50
66
|
@autoreleasepool {
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
// SAFETY LAYER 3: Initialize writer safely (only once)
|
|
68
|
+
static BOOL initializationAttempted = NO;
|
|
69
|
+
if (!g_writerStarted && !initializationAttempted && g_assetWriter && g_assetWriterInput) {
|
|
70
|
+
initializationAttempted = YES;
|
|
71
|
+
@try {
|
|
72
|
+
CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
73
|
+
|
|
74
|
+
// SAFETY CHECK: Ensure valid time
|
|
75
|
+
if (CMTIME_IS_VALID(presentationTime) && CMTIME_IS_NUMERIC(presentationTime)) {
|
|
76
|
+
g_startTime = presentationTime;
|
|
77
|
+
g_currentTime = g_startTime;
|
|
78
|
+
|
|
79
|
+
// SAFETY LAYER 4: Writer state validation
|
|
80
|
+
if (g_assetWriter.status == AVAssetWriterStatusUnknown) {
|
|
81
|
+
[g_assetWriter startWriting];
|
|
82
|
+
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
83
|
+
g_writerStarted = YES;
|
|
84
|
+
NSLog(@"โ
Ultra-safe ScreenCaptureKit writer started");
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Use zero time if sample buffer time is invalid
|
|
88
|
+
NSLog(@"โ ๏ธ Invalid sample buffer time, using kCMTimeZero");
|
|
89
|
+
g_startTime = kCMTimeZero;
|
|
90
|
+
g_currentTime = g_startTime;
|
|
91
|
+
|
|
92
|
+
if (g_assetWriter.status == AVAssetWriterStatusUnknown) {
|
|
93
|
+
[g_assetWriter startWriting];
|
|
94
|
+
[g_assetWriter startSessionAtSourceTime:kCMTimeZero];
|
|
95
|
+
g_writerStarted = YES;
|
|
96
|
+
NSLog(@"โ
Ultra-safe ScreenCaptureKit writer started with zero time");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} @catch (NSException *writerException) {
|
|
100
|
+
NSLog(@"โ ๏ธ Writer initialization failed safely: %@", writerException.reason);
|
|
65
101
|
return;
|
|
66
102
|
}
|
|
67
|
-
|
|
68
|
-
[g_assetWriter startWriting];
|
|
69
|
-
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
70
|
-
g_writerStarted = YES;
|
|
71
|
-
NSLog(@"โ
Electron-safe video writer started");
|
|
72
103
|
}
|
|
73
104
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
double seconds = CMTimeGetSeconds(relativeTime);
|
|
126
|
-
if (seconds < 0 || seconds > 10.0) { // Max 10 seconds for safety
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
105
|
+
// SAFETY LAYER 5: Frame processing with isolation
|
|
106
|
+
if (!g_writerStarted || !g_assetWriterInput || !g_pixelBufferAdaptor) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// SAFETY LAYER 6: Conservative rate limiting
|
|
111
|
+
static NSTimeInterval lastProcessTime = 0;
|
|
112
|
+
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
|
|
113
|
+
if (currentTime - lastProcessTime < 0.1) { // Max 10 FPS
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
lastProcessTime = currentTime;
|
|
117
|
+
|
|
118
|
+
// SAFETY LAYER 7: Input readiness check
|
|
119
|
+
if (!g_assetWriterInput.isReadyForMoreMediaData) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// SAFETY LAYER 8: Pixel buffer validation
|
|
124
|
+
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
|
125
|
+
if (!pixelBuffer) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// SAFETY LAYER 9: Dimension validation - flexible this time
|
|
130
|
+
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
|
131
|
+
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
132
|
+
if (width == 0 || height == 0 || width > 4096 || height > 4096) {
|
|
133
|
+
return; // Skip only if clearly invalid
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// SAFETY LAYER 10: Time validation
|
|
137
|
+
CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
138
|
+
if (!CMTIME_IS_VALID(presentationTime)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
CMTime relativeTime = CMTimeSubtract(presentationTime, g_startTime);
|
|
143
|
+
if (!CMTIME_IS_VALID(relativeTime)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
double seconds = CMTimeGetSeconds(relativeTime);
|
|
148
|
+
if (seconds < 0 || seconds > 30.0) { // Allow longer recordings
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// SAFETY LAYER 11: Append with complete exception handling
|
|
153
|
+
@try {
|
|
154
|
+
// Use pixel buffer directly - copy was causing errors
|
|
155
|
+
BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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");
|
|
144
|
-
}
|
|
157
|
+
if (success) {
|
|
158
|
+
g_currentTime = relativeTime;
|
|
159
|
+
static int ultraSafeFrameCount = 0;
|
|
160
|
+
ultraSafeFrameCount++;
|
|
161
|
+
if (ultraSafeFrameCount % 10 == 0) {
|
|
162
|
+
NSLog(@"๐ก๏ธ Ultra-safe: %d frames (%.1fs)", ultraSafeFrameCount, seconds);
|
|
145
163
|
}
|
|
146
|
-
} @catch (NSException *appendException) {
|
|
147
|
-
NSLog(@"โ Exception during pixel buffer append: %@", appendException.reason);
|
|
148
|
-
// Don't rethrow - just skip this frame
|
|
149
164
|
}
|
|
165
|
+
} @catch (NSException *appendException) {
|
|
166
|
+
NSLog(@"๐ก๏ธ Append exception handled safely: %@", appendException.reason);
|
|
167
|
+
// Continue gracefully - don't crash
|
|
150
168
|
}
|
|
151
169
|
}
|
|
152
|
-
} @catch (NSException *
|
|
153
|
-
NSLog(@"
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
170
|
+
} @catch (NSException *outerException) {
|
|
171
|
+
NSLog(@"๐ก๏ธ Outer exception handled: %@", outerException.reason);
|
|
172
|
+
// Ultimate safety - graceful continue
|
|
173
|
+
} @catch (...) {
|
|
174
|
+
NSLog(@"๐ก๏ธ Unknown exception caught and handled safely");
|
|
175
|
+
// Catch any C++ exceptions too
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
178
|
@end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
|
|
3
|
+
// Simulate Electron environment
|
|
4
|
+
process.env.ELECTRON_RUN_AS_NODE = '1';
|
|
5
|
+
|
|
6
|
+
console.log('๐ Testing Electron Detection');
|
|
7
|
+
console.log('Environment variables:', {
|
|
8
|
+
ELECTRON_RUN_AS_NODE: process.env.ELECTRON_RUN_AS_NODE,
|
|
9
|
+
processName: process.title
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
async function testElectronDetection() {
|
|
13
|
+
const recorder = new MacRecorder();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const outputPath = './test-output/electron-detection-test.mov';
|
|
17
|
+
|
|
18
|
+
console.log('๐น Starting recording with Electron detection...');
|
|
19
|
+
const success = await recorder.startRecording(outputPath, {
|
|
20
|
+
captureCursor: true,
|
|
21
|
+
includeMicrophone: false,
|
|
22
|
+
includeSystemAudio: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (success) {
|
|
26
|
+
console.log('โ
Recording started successfully');
|
|
27
|
+
|
|
28
|
+
// Record for 3 seconds
|
|
29
|
+
console.log('โฑ๏ธ Recording for 3 seconds...');
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
31
|
+
|
|
32
|
+
console.log('๐ Stopping recording...');
|
|
33
|
+
await recorder.stopRecording();
|
|
34
|
+
|
|
35
|
+
console.log('โ
Recording completed without crash');
|
|
36
|
+
} else {
|
|
37
|
+
console.log('โ Failed to start recording');
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.log('โ Error during test:', error.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
testElectronDetection().catch(console.error);
|
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);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const MacRecorder = require('./index');
|
|
2
|
+
|
|
3
|
+
console.log('๐ฏ Testing PURE ScreenCaptureKit (No AVFoundation)');
|
|
4
|
+
|
|
5
|
+
async function testScreenCaptureKitOnly() {
|
|
6
|
+
const recorder = new MacRecorder();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const outputPath = './test-output/screencapturekit-only-test.mov';
|
|
10
|
+
|
|
11
|
+
console.log('๐น Starting ScreenCaptureKit-only recording...');
|
|
12
|
+
const success = await recorder.startRecording(outputPath, {
|
|
13
|
+
captureCursor: true,
|
|
14
|
+
includeMicrophone: false,
|
|
15
|
+
includeSystemAudio: false
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (success) {
|
|
19
|
+
console.log('โ
Recording started successfully');
|
|
20
|
+
|
|
21
|
+
// Record for 5 seconds
|
|
22
|
+
console.log('โฑ๏ธ Recording for 5 seconds...');
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
24
|
+
|
|
25
|
+
console.log('๐ Stopping recording...');
|
|
26
|
+
await recorder.stopRecording();
|
|
27
|
+
|
|
28
|
+
// Check if file exists and has content
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
if (fs.existsSync(outputPath)) {
|
|
31
|
+
const stats = fs.statSync(outputPath);
|
|
32
|
+
console.log(`โ
Video file created: ${outputPath} (${stats.size} bytes)`);
|
|
33
|
+
|
|
34
|
+
if (stats.size > 1000) {
|
|
35
|
+
console.log('โ
ScreenCaptureKit-only recording successful');
|
|
36
|
+
} else {
|
|
37
|
+
console.log('โ ๏ธ File size is very small');
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
console.log('โ Video file not found');
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.log('โ Failed to start recording');
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.log('โ Error during test:', error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
testScreenCaptureKitOnly().catch(console.error);
|