node-mac-recorder 2.21.37 โ 2.21.39
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/package.json +1 -1
- package/src/avfoundation_recorder.mm +22 -55
- package/src/mac_recorder.mm +82 -63
package/package.json
CHANGED
|
@@ -282,41 +282,18 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
// ELECTRON SAFETY: Test image capture BEFORE starting timer
|
|
286
|
-
// This prevents crash if display capture doesn't work
|
|
287
|
-
MRLog(@"๐งช Testing display capture before starting timer...");
|
|
288
|
-
CGImageRef testCapture = CGDisplayCreateImage(g_avDisplayID);
|
|
289
|
-
if (!testCapture) {
|
|
290
|
-
NSLog(@"โ CRITICAL: Cannot capture display %u - aborting", g_avDisplayID);
|
|
291
|
-
if (g_avWriter) {
|
|
292
|
-
[g_avWriter cancelWriting];
|
|
293
|
-
g_avWriter = nil;
|
|
294
|
-
}
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
CGImageRelease(testCapture);
|
|
298
|
-
MRLog(@"โ
Display capture test successful");
|
|
299
|
-
|
|
300
285
|
// Start capture timer using target FPS
|
|
301
|
-
// ELECTRON SAFETY: Use a dedicated serial queue to avoid main thread conflicts
|
|
302
286
|
dispatch_queue_t captureQueue = dispatch_queue_create("AVFoundationCaptureQueue", DISPATCH_QUEUE_SERIAL);
|
|
303
|
-
dispatch_set_target_queue(captureQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
|
|
304
|
-
|
|
305
287
|
g_avTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, captureQueue);
|
|
306
|
-
|
|
288
|
+
|
|
307
289
|
if (!g_avTimer) {
|
|
308
290
|
NSLog(@"โ Failed to create dispatch timer");
|
|
309
|
-
if (g_avWriter) {
|
|
310
|
-
[g_avWriter cancelWriting];
|
|
311
|
-
g_avWriter = nil;
|
|
312
|
-
}
|
|
313
291
|
return false;
|
|
314
292
|
}
|
|
315
|
-
|
|
293
|
+
|
|
316
294
|
uint64_t interval = (uint64_t)(NSEC_PER_SEC / fps);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
295
|
+
dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, interval / 10);
|
|
296
|
+
|
|
320
297
|
// Retain objects before passing to block to prevent deallocation
|
|
321
298
|
AVAssetWriterInput *localVideoInput = g_avVideoInput;
|
|
322
299
|
AVAssetWriterInputPixelBufferAdaptor *localPixelBufferAdaptor = g_avPixelBufferAdaptor;
|
|
@@ -471,25 +448,12 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
471
448
|
}
|
|
472
449
|
}
|
|
473
450
|
});
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
dispatch_resume(g_avTimer);
|
|
481
|
-
|
|
482
|
-
MRLog(@"๐ฅ AVFoundation recording started: %dx%d @ %.0ffps",
|
|
483
|
-
(int)recordingSize.width, (int)recordingSize.height, fps);
|
|
484
|
-
} @catch (NSException *exception) {
|
|
485
|
-
NSLog(@"โ Failed to start timer: %@", exception.reason);
|
|
486
|
-
g_avIsRecording = false;
|
|
487
|
-
if (g_avTimer) {
|
|
488
|
-
dispatch_source_cancel(g_avTimer);
|
|
489
|
-
g_avTimer = nil;
|
|
490
|
-
}
|
|
491
|
-
return false;
|
|
492
|
-
}
|
|
451
|
+
|
|
452
|
+
dispatch_resume(g_avTimer);
|
|
453
|
+
g_avIsRecording = true;
|
|
454
|
+
|
|
455
|
+
MRLog(@"๐ฅ AVFoundation recording started: %dx%d @ %.0ffps",
|
|
456
|
+
(int)recordingSize.width, (int)recordingSize.height, fps);
|
|
493
457
|
|
|
494
458
|
return true;
|
|
495
459
|
|
|
@@ -526,8 +490,14 @@ extern "C" bool stopAVFoundationRecording() {
|
|
|
526
490
|
// Mark as not recording FIRST to stop timer callbacks
|
|
527
491
|
g_avIsRecording = false;
|
|
528
492
|
|
|
529
|
-
// Cancel timer
|
|
493
|
+
// Cancel timer and wait a brief moment for completion
|
|
530
494
|
dispatch_source_cancel(g_avTimer);
|
|
495
|
+
|
|
496
|
+
// Use async to avoid deadlock in Electron
|
|
497
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
|
|
498
|
+
// Timer should be fully cancelled by now
|
|
499
|
+
});
|
|
500
|
+
|
|
531
501
|
g_avTimer = nil;
|
|
532
502
|
MRLog(@"โ
AVFoundation timer stopped safely");
|
|
533
503
|
}
|
|
@@ -540,16 +510,13 @@ extern "C" bool stopAVFoundationRecording() {
|
|
|
540
510
|
|
|
541
511
|
AVAssetWriter *writer = g_avWriter;
|
|
542
512
|
if (writer && writer.status == AVAssetWriterStatusWriting) {
|
|
543
|
-
|
|
544
|
-
// Just call finishWriting async and let it complete in background
|
|
513
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
545
514
|
[writer finishWritingWithCompletionHandler:^{
|
|
546
|
-
|
|
547
|
-
MRLog(@"โ
AVFoundation writer finished successfully");
|
|
548
|
-
} else if (writer.error) {
|
|
549
|
-
NSLog(@"โ ๏ธ AVFoundation writer error: %@", writer.error);
|
|
550
|
-
}
|
|
515
|
+
dispatch_semaphore_signal(semaphore);
|
|
551
516
|
}];
|
|
552
|
-
|
|
517
|
+
// Add timeout to prevent infinite wait in Electron
|
|
518
|
+
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
|
|
519
|
+
dispatch_semaphore_wait(semaphore, timeout);
|
|
553
520
|
}
|
|
554
521
|
|
|
555
522
|
// Cleanup
|
package/src/mac_recorder.mm
CHANGED
|
@@ -137,22 +137,29 @@ static bool startAudioIfRequested(bool captureAudio,
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Helper function to cleanup recording resources
|
|
140
|
-
// CRITICAL FIX: Made safer for Electron - only resets state flags, doesn't call stop
|
|
141
|
-
// Calling stop operations during startup causes crashes in Electron
|
|
142
140
|
void cleanupRecording() {
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
141
|
+
// ScreenCaptureKit cleanup
|
|
142
|
+
if (@available(macOS 12.3, *)) {
|
|
143
|
+
if ([ScreenCaptureKitRecorder isRecording]) {
|
|
144
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// AVFoundation cleanup (supports both Node.js and Electron)
|
|
149
|
+
if (isAVFoundationRecording()) {
|
|
150
|
+
stopAVFoundationRecording();
|
|
151
|
+
}
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
NSLog(@"โ ๏ธ Exception during state reset: %@", exception.reason);
|
|
153
|
+
if (isCameraRecording()) {
|
|
154
|
+
stopCameraRecording();
|
|
155
155
|
}
|
|
156
|
+
if (isStandaloneAudioRecording()) {
|
|
157
|
+
stopStandaloneAudioRecording();
|
|
158
|
+
}
|
|
159
|
+
g_usingStandaloneAudio = false;
|
|
160
|
+
|
|
161
|
+
g_isRecording = false;
|
|
162
|
+
MRSyncConfigure(NO);
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
// NAPI Function: Start Recording
|
|
@@ -164,16 +171,24 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
164
171
|
return env.Null();
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
174
|
+
// IMPORTANT: Clean up any stale recording state before starting
|
|
175
|
+
// This fixes the issue where macOS 14/13 users get "recording already in progress"
|
|
176
|
+
MRLog(@"๐งน Cleaning up any previous recording state...");
|
|
177
|
+
cleanupRecording();
|
|
178
|
+
|
|
169
179
|
if (g_isRecording) {
|
|
170
|
-
MRLog(@"โ ๏ธ
|
|
180
|
+
MRLog(@"โ ๏ธ Still recording after cleanup - forcing stop");
|
|
171
181
|
return Napi::Boolean::New(env, false);
|
|
172
182
|
}
|
|
173
183
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
// CRITICAL FIX: Check if ScreenCaptureKit is still cleaning up (async stop in progress)
|
|
185
|
+
if (@available(macOS 12.3, *)) {
|
|
186
|
+
extern BOOL isScreenCaptureKitCleaningUp();
|
|
187
|
+
if (isScreenCaptureKitCleaningUp()) {
|
|
188
|
+
MRLog(@"โ ๏ธ ScreenCaptureKit is still stopping previous recording - please wait");
|
|
189
|
+
return Napi::Boolean::New(env, false);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
177
192
|
g_usingStandaloneAudio = false;
|
|
178
193
|
|
|
179
194
|
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
@@ -362,23 +377,23 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
362
377
|
MRLog(@"๐ฅ๏ธ macOS Version: %ld.%ld.%ld",
|
|
363
378
|
(long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
|
|
364
379
|
|
|
365
|
-
// CRITICAL FIX FOR ELECTRON: ScreenCaptureKit crashes in Electron on macOS 15+
|
|
366
|
-
// Force AVFoundation for Electron regardless of macOS version
|
|
367
|
-
BOOL forceAVFoundation = isElectron ? YES : NO;
|
|
368
|
-
|
|
369
380
|
if (isElectron) {
|
|
370
381
|
MRLog(@"โก Electron environment detected");
|
|
371
|
-
MRLog(@"๐ง
|
|
372
|
-
|
|
382
|
+
MRLog(@"๐ง CRITICAL FIX: Forcing AVFoundation for Electron stability");
|
|
383
|
+
MRLog(@" Reason: ScreenCaptureKit has thread safety issues in Electron (SIGTRAP crashes)");
|
|
373
384
|
}
|
|
374
385
|
|
|
375
|
-
|
|
386
|
+
// CRITICAL FIX: Always use AVFoundation for stability
|
|
387
|
+
// ScreenCaptureKit has file writing issues in Node.js environment
|
|
388
|
+
// AVFoundation works reliably in both Node.js and Electron
|
|
389
|
+
BOOL forceAVFoundation = YES;
|
|
390
|
+
|
|
391
|
+
MRLog(@"๐ง FRAMEWORK SELECTION: Using AVFoundation for stability");
|
|
376
392
|
MRLog(@" Environment: %@", isElectron ? @"Electron" : @"Node.js");
|
|
377
393
|
MRLog(@" macOS: %ld.%ld.%ld", (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
|
|
378
394
|
|
|
379
|
-
// Electron-first priority:
|
|
380
|
-
//
|
|
381
|
-
// macOS 14/13: Use AVFoundation (ScreenCaptureKit not available)
|
|
395
|
+
// Electron-first priority: ALWAYS use AVFoundation in Electron for stability
|
|
396
|
+
// ScreenCaptureKit has severe thread safety issues in Electron causing SIGTRAP crashes
|
|
382
397
|
if (isM15Plus && !forceAVFoundation) {
|
|
383
398
|
if (isElectron) {
|
|
384
399
|
MRLog(@"โก ELECTRON PRIORITY: macOS 15+ Electron โ ScreenCaptureKit with full support");
|
|
@@ -590,41 +605,30 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
590
605
|
|
|
591
606
|
// Try ScreenCaptureKit first
|
|
592
607
|
if (@available(macOS 12.3, *)) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
MRLog(@"๐ Stopping ScreenCaptureKit recording");
|
|
608
|
+
if ([ScreenCaptureKitRecorder isRecording]) {
|
|
609
|
+
MRLog(@"๐ Stopping ScreenCaptureKit recording");
|
|
596
610
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
611
|
+
// CRITICAL FIX: Stop camera and audio FIRST (they are synchronous)
|
|
612
|
+
if (isCameraRecording()) {
|
|
613
|
+
MRLog(@"๐ Stopping camera recording...");
|
|
614
|
+
bool cameraStopped = stopCameraRecording();
|
|
615
|
+
if (cameraStopped) {
|
|
616
|
+
MRLog(@"โ
Camera stopped successfully");
|
|
617
|
+
} else {
|
|
618
|
+
MRLog(@"โ ๏ธ Camera stop may have timed out");
|
|
606
619
|
}
|
|
620
|
+
}
|
|
607
621
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
MRLog(@"โ
ScreenCaptureKit stop dispatched");
|
|
613
|
-
} @catch (NSException *exception) {
|
|
614
|
-
NSLog(@"โ ScreenCaptureKit stop exception: %@", exception.reason);
|
|
615
|
-
g_isRecording = false;
|
|
616
|
-
return Napi::Boolean::New(env, false);
|
|
617
|
-
}
|
|
622
|
+
// Now stop ScreenCaptureKit (asynchronous)
|
|
623
|
+
// WARNING: [ScreenCaptureKitRecorder stopRecording] is ASYNC!
|
|
624
|
+
// It will set g_isRecording = NO in its completion handler
|
|
625
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
618
626
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
return Napi::Boolean::New(env, true);
|
|
622
|
-
}
|
|
623
|
-
} @catch (NSException *exception) {
|
|
624
|
-
NSLog(@"โ Exception in ScreenCaptureKit stop: %@", exception.reason);
|
|
625
|
-
g_isRecording = false;
|
|
627
|
+
// DO NOT set g_isRecording here - let ScreenCaptureKit completion handler do it
|
|
628
|
+
// Otherwise we have a race condition where JS thinks recording stopped but it's still running
|
|
626
629
|
g_usingStandaloneAudio = false;
|
|
627
|
-
|
|
630
|
+
MRSyncSetStopLimitSeconds(-1.0);
|
|
631
|
+
return Napi::Boolean::New(env, true);
|
|
628
632
|
}
|
|
629
633
|
}
|
|
630
634
|
|
|
@@ -668,11 +672,26 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
668
672
|
|
|
669
673
|
bool avFoundationStopped = stopAVFoundationRecording();
|
|
670
674
|
|
|
671
|
-
//
|
|
672
|
-
// Instead, just fire async stop and continue
|
|
675
|
+
// SYNC FIX: Wait for both camera AND audio to finish (increased timeout to 5s)
|
|
673
676
|
if (cameraWasRecording || audioWasRecording) {
|
|
674
|
-
|
|
675
|
-
|
|
677
|
+
dispatch_time_t waitTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
|
|
678
|
+
long waitResult = dispatch_group_wait(stopGroup, waitTime);
|
|
679
|
+
if (waitResult != 0) {
|
|
680
|
+
MRLog(@"โ ๏ธ SYNC: Camera/Audio stop did not finish within 5 seconds");
|
|
681
|
+
if (cameraWasRecording && !cameraStopResult) {
|
|
682
|
+
MRLog(@" โ ๏ธ Camera stop timed out");
|
|
683
|
+
}
|
|
684
|
+
if (audioWasRecording && !audioStopResult) {
|
|
685
|
+
MRLog(@" โ ๏ธ Audio stop timed out");
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
if (cameraWasRecording) {
|
|
689
|
+
MRLog(@"โ
SYNC: Camera stopped successfully");
|
|
690
|
+
}
|
|
691
|
+
if (audioWasRecording) {
|
|
692
|
+
MRLog(@"โ
SYNC: Audio stopped successfully");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
676
695
|
}
|
|
677
696
|
|
|
678
697
|
g_isRecording = false;
|