node-mac-recorder 2.21.5 → 2.21.7
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 +4 -1
- package/package.json +1 -1
- package/src/mac_recorder.mm +8 -0
- package/src/screen_capture_kit.mm +52 -36
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
"Read(//Users/onur/codes/**)",
|
|
9
9
|
"Bash(log show:*)",
|
|
10
10
|
"Bash(MAC_RECORDER_DEBUG=1 node:*)",
|
|
11
|
-
"Read(//private/tmp/test-recording/**)"
|
|
11
|
+
"Read(//private/tmp/test-recording/**)",
|
|
12
|
+
"Bash(MAC_RECORDER_DEBUG=1 timeout 5 node:*)",
|
|
13
|
+
"Read(//private/tmp/**)",
|
|
14
|
+
"Bash(ffprobe:*)"
|
|
12
15
|
],
|
|
13
16
|
"deny": [],
|
|
14
17
|
"ask": []
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -349,6 +349,14 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
349
349
|
MRLog(@"🔧 FORCE_AVFOUNDATION environment variable detected");
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
// CRITICAL: ScreenCaptureKit causes segmentation faults in both Node.js and Electron
|
|
353
|
+
// FORCE AVFoundation for ALL environments until Apple fixes stability issues
|
|
354
|
+
forceAVFoundation = YES;
|
|
355
|
+
if (forceAVFoundation) {
|
|
356
|
+
MRLog(@"🔧 CRITICAL: ScreenCaptureKit disabled due to segmentation faults");
|
|
357
|
+
MRLog(@" Using AVFoundation for stability in ALL environments");
|
|
358
|
+
}
|
|
359
|
+
|
|
352
360
|
// Electron-first priority: ALWAYS use AVFoundation in Electron for stability
|
|
353
361
|
// ScreenCaptureKit has severe thread safety issues in Electron causing SIGTRAP crashes
|
|
354
362
|
if (isM15Plus && !forceAVFoundation) {
|
|
@@ -225,10 +225,15 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
225
225
|
|
|
226
226
|
@implementation ScreenCaptureAudioOutput
|
|
227
227
|
- (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type API_AVAILABLE(macos(12.3)) {
|
|
228
|
+
static dispatch_once_t onceToken;
|
|
229
|
+
dispatch_once(&onceToken, ^{
|
|
230
|
+
MRLog(@"🎤 First audio sample callback received from ScreenCaptureKit");
|
|
231
|
+
});
|
|
232
|
+
|
|
228
233
|
if (!g_isRecording || !g_shouldCaptureAudio) {
|
|
229
234
|
return;
|
|
230
235
|
}
|
|
231
|
-
|
|
236
|
+
|
|
232
237
|
if (@available(macOS 13.0, *)) {
|
|
233
238
|
if (type != SCStreamOutputTypeAudio) {
|
|
234
239
|
return;
|
|
@@ -236,8 +241,9 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
236
241
|
} else {
|
|
237
242
|
return;
|
|
238
243
|
}
|
|
239
|
-
|
|
244
|
+
|
|
240
245
|
if (!CMSampleBufferDataIsReady(sampleBuffer)) {
|
|
246
|
+
MRLog(@"⚠️ Audio sample buffer data not ready");
|
|
241
247
|
return;
|
|
242
248
|
}
|
|
243
249
|
|
|
@@ -263,11 +269,21 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
if (!g_audioInput.readyForMoreMediaData) {
|
|
272
|
+
static int notReadyCount = 0;
|
|
273
|
+
if (notReadyCount++ % 100 == 0) {
|
|
274
|
+
MRLog(@"⚠️ Audio input not ready for data (count: %d)", notReadyCount);
|
|
275
|
+
}
|
|
266
276
|
return;
|
|
267
277
|
}
|
|
268
|
-
|
|
269
|
-
|
|
278
|
+
|
|
279
|
+
BOOL success = [g_audioInput appendSampleBuffer:sampleBuffer];
|
|
280
|
+
if (!success) {
|
|
270
281
|
NSLog(@"⚠️ Failed appending audio sample buffer: %@", g_audioWriter.error);
|
|
282
|
+
} else {
|
|
283
|
+
static int appendCount = 0;
|
|
284
|
+
if (appendCount++ % 100 == 0) {
|
|
285
|
+
MRLog(@"✅ Audio sample appended successfully (count: %d)", appendCount);
|
|
286
|
+
}
|
|
271
287
|
}
|
|
272
288
|
}
|
|
273
289
|
@end
|
|
@@ -365,13 +381,20 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
365
381
|
[[NSFileManager defaultManager] removeItemAtURL:audioURL error:nil];
|
|
366
382
|
|
|
367
383
|
NSError *writerError = nil;
|
|
384
|
+
// CRITICAL FIX: AVAssetWriter does NOT support WebM for audio
|
|
385
|
+
// Always use QuickTime Movie format (.mov) for audio files
|
|
368
386
|
AVFileType requestedFileType = AVFileTypeQuickTimeMovie;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
387
|
+
|
|
388
|
+
// Ensure path has .mov extension for audio
|
|
389
|
+
NSString *audioPath = originalPath;
|
|
390
|
+
if (![audioPath.pathExtension.lowercaseString isEqualToString:@"mov"]) {
|
|
391
|
+
MRLog(@"⚠️ Audio path has wrong extension '%@', changing to .mov", audioPath.pathExtension);
|
|
392
|
+
audioPath = [[audioPath stringByDeletingPathExtension] stringByAppendingPathExtension:@"mov"];
|
|
393
|
+
g_audioOutputPath = audioPath;
|
|
373
394
|
}
|
|
374
|
-
|
|
395
|
+
audioURL = [NSURL fileURLWithPath:audioPath];
|
|
396
|
+
[[NSFileManager defaultManager] removeItemAtURL:audioURL error:nil];
|
|
397
|
+
|
|
375
398
|
@try {
|
|
376
399
|
g_audioWriter = [[AVAssetWriter alloc] initWithURL:audioURL fileType:requestedFileType error:&writerError];
|
|
377
400
|
} @catch (NSException *exception) {
|
|
@@ -382,28 +405,6 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
382
405
|
g_audioWriter = nil;
|
|
383
406
|
}
|
|
384
407
|
|
|
385
|
-
if ((!g_audioWriter || writerError) && requestedWebM) {
|
|
386
|
-
MRLog(@"⚠️ ScreenCaptureKit audio writer unavailable (%@) – falling back to QuickTime container", writerError.localizedDescription);
|
|
387
|
-
NSString *fallbackPath = [[originalPath stringByDeletingPathExtension] stringByAppendingPathExtension:@"mov"];
|
|
388
|
-
if (!fallbackPath || [fallbackPath length] == 0) {
|
|
389
|
-
fallbackPath = [originalPath stringByAppendingString:@".mov"];
|
|
390
|
-
}
|
|
391
|
-
[[NSFileManager defaultManager] removeItemAtPath:fallbackPath error:nil];
|
|
392
|
-
NSURL *fallbackURL = [NSURL fileURLWithPath:fallbackPath];
|
|
393
|
-
g_audioOutputPath = fallbackPath;
|
|
394
|
-
writerError = nil;
|
|
395
|
-
@try {
|
|
396
|
-
g_audioWriter = [[AVAssetWriter alloc] initWithURL:fallbackURL fileType:AVFileTypeQuickTimeMovie error:&writerError];
|
|
397
|
-
} @catch (NSException *exception) {
|
|
398
|
-
NSDictionary *info = @{
|
|
399
|
-
NSLocalizedDescriptionKey: exception.reason ?: @"Failed to initialize audio writer"
|
|
400
|
-
};
|
|
401
|
-
writerError = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-202 userInfo:info];
|
|
402
|
-
g_audioWriter = nil;
|
|
403
|
-
}
|
|
404
|
-
audioURL = fallbackURL;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
408
|
if (!g_audioWriter || writerError) {
|
|
408
409
|
NSLog(@"❌ Failed to create audio writer: %@", writerError);
|
|
409
410
|
return NO;
|
|
@@ -433,7 +434,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
433
434
|
|
|
434
435
|
g_audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
|
|
435
436
|
g_audioInput.expectsMediaDataInRealTime = YES;
|
|
436
|
-
|
|
437
|
+
|
|
438
|
+
MRLog(@"🎙️ Audio input created: sampleRate=%ld, channels=%ld, bitrate=192k",
|
|
439
|
+
(long)g_configuredSampleRate, (long)channelCount);
|
|
440
|
+
|
|
437
441
|
if (![g_audioWriter canAddInput:g_audioInput]) {
|
|
438
442
|
NSLog(@"❌ Audio writer cannot add input");
|
|
439
443
|
return NO;
|
|
@@ -441,7 +445,8 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
441
445
|
[g_audioWriter addInput:g_audioInput];
|
|
442
446
|
g_audioWriterStarted = NO;
|
|
443
447
|
g_audioStartTime = kCMTimeInvalid;
|
|
444
|
-
|
|
448
|
+
|
|
449
|
+
MRLog(@"✅ Audio writer prepared successfully (path: %@)", g_audioOutputPath);
|
|
445
450
|
return YES;
|
|
446
451
|
}
|
|
447
452
|
|
|
@@ -623,17 +628,28 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
623
628
|
}
|
|
624
629
|
|
|
625
630
|
if (@available(macos 13.0, *)) {
|
|
631
|
+
// capturesAudio enables audio capture (both mic and system audio)
|
|
626
632
|
streamConfig.capturesAudio = g_shouldCaptureAudio;
|
|
627
|
-
streamConfig.sampleRate = g_configuredSampleRate;
|
|
628
|
-
streamConfig.channelCount = g_configuredChannelCount;
|
|
633
|
+
streamConfig.sampleRate = g_configuredSampleRate ?: 48000;
|
|
634
|
+
streamConfig.channelCount = g_configuredChannelCount ?: 2;
|
|
635
|
+
|
|
636
|
+
// excludesCurrentProcessAudio = YES means ONLY microphone
|
|
637
|
+
// excludesCurrentProcessAudio = NO means system audio + mic
|
|
629
638
|
streamConfig.excludesCurrentProcessAudio = !shouldCaptureSystemAudio;
|
|
639
|
+
|
|
640
|
+
MRLog(@"🎤 Audio config (macOS 13+): capturesAudio=%d, excludeProcess=%d (mic=%d sys=%d)",
|
|
641
|
+
g_shouldCaptureAudio, streamConfig.excludesCurrentProcessAudio,
|
|
642
|
+
shouldCaptureMic, shouldCaptureSystemAudio);
|
|
630
643
|
}
|
|
631
|
-
|
|
644
|
+
|
|
632
645
|
if (@available(macos 15.0, *)) {
|
|
646
|
+
// macOS 15+ has explicit microphone control
|
|
633
647
|
streamConfig.captureMicrophone = shouldCaptureMic;
|
|
634
648
|
if (microphoneDeviceId && microphoneDeviceId.length > 0) {
|
|
635
649
|
streamConfig.microphoneCaptureDeviceID = microphoneDeviceId;
|
|
636
650
|
}
|
|
651
|
+
MRLog(@"🎤 Microphone (macOS 15+): enabled=%d, deviceID=%@",
|
|
652
|
+
shouldCaptureMic, microphoneDeviceId ?: @"default");
|
|
637
653
|
}
|
|
638
654
|
|
|
639
655
|
// Apply crop area using sourceRect - CONVERT GLOBAL TO DISPLAY-RELATIVE COORDINATES
|