node-mac-recorder 2.21.36 โ 2.21.37
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/package.json +1 -1
- package/src/avfoundation_recorder.mm +55 -22
- package/src/mac_recorder.mm +59 -78
package/package.json
CHANGED
|
@@ -282,18 +282,41 @@ 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
|
+
|
|
285
300
|
// Start capture timer using target FPS
|
|
301
|
+
// ELECTRON SAFETY: Use a dedicated serial queue to avoid main thread conflicts
|
|
286
302
|
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
|
+
|
|
287
305
|
g_avTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, captureQueue);
|
|
288
|
-
|
|
306
|
+
|
|
289
307
|
if (!g_avTimer) {
|
|
290
308
|
NSLog(@"โ Failed to create dispatch timer");
|
|
309
|
+
if (g_avWriter) {
|
|
310
|
+
[g_avWriter cancelWriting];
|
|
311
|
+
g_avWriter = nil;
|
|
312
|
+
}
|
|
291
313
|
return false;
|
|
292
314
|
}
|
|
293
|
-
|
|
315
|
+
|
|
294
316
|
uint64_t interval = (uint64_t)(NSEC_PER_SEC / fps);
|
|
295
|
-
|
|
296
|
-
|
|
317
|
+
// ELECTRON SAFETY: Add small delay before first frame (100ms)
|
|
318
|
+
dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), interval, interval / 10);
|
|
319
|
+
|
|
297
320
|
// Retain objects before passing to block to prevent deallocation
|
|
298
321
|
AVAssetWriterInput *localVideoInput = g_avVideoInput;
|
|
299
322
|
AVAssetWriterInputPixelBufferAdaptor *localPixelBufferAdaptor = g_avPixelBufferAdaptor;
|
|
@@ -448,12 +471,25 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
448
471
|
}
|
|
449
472
|
}
|
|
450
473
|
});
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
474
|
+
|
|
475
|
+
// ELECTRON SAFETY: Start timer with exception handling
|
|
476
|
+
@try {
|
|
477
|
+
// Set recording flag BEFORE resuming timer
|
|
478
|
+
g_avIsRecording = true;
|
|
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
|
+
}
|
|
457
493
|
|
|
458
494
|
return true;
|
|
459
495
|
|
|
@@ -490,14 +526,8 @@ extern "C" bool stopAVFoundationRecording() {
|
|
|
490
526
|
// Mark as not recording FIRST to stop timer callbacks
|
|
491
527
|
g_avIsRecording = false;
|
|
492
528
|
|
|
493
|
-
// Cancel timer
|
|
529
|
+
// Cancel timer immediately (Electron-safe)
|
|
494
530
|
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
|
-
|
|
501
531
|
g_avTimer = nil;
|
|
502
532
|
MRLog(@"โ
AVFoundation timer stopped safely");
|
|
503
533
|
}
|
|
@@ -510,13 +540,16 @@ extern "C" bool stopAVFoundationRecording() {
|
|
|
510
540
|
|
|
511
541
|
AVAssetWriter *writer = g_avWriter;
|
|
512
542
|
if (writer && writer.status == AVAssetWriterStatusWriting) {
|
|
513
|
-
|
|
543
|
+
// ELECTRON SAFETY: Don't use dispatch_semaphore_wait - causes crashes in Electron
|
|
544
|
+
// Just call finishWriting async and let it complete in background
|
|
514
545
|
[writer finishWritingWithCompletionHandler:^{
|
|
515
|
-
|
|
546
|
+
if (writer.status == AVAssetWriterStatusCompleted) {
|
|
547
|
+
MRLog(@"โ
AVFoundation writer finished successfully");
|
|
548
|
+
} else if (writer.error) {
|
|
549
|
+
NSLog(@"โ ๏ธ AVFoundation writer error: %@", writer.error);
|
|
550
|
+
}
|
|
516
551
|
}];
|
|
517
|
-
|
|
518
|
-
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
|
|
519
|
-
dispatch_semaphore_wait(semaphore, timeout);
|
|
552
|
+
MRLog(@"๐ AVFoundation writer finishing asynchronously (Electron-safe mode)");
|
|
520
553
|
}
|
|
521
554
|
|
|
522
555
|
// Cleanup
|
package/src/mac_recorder.mm
CHANGED
|
@@ -137,29 +137,22 @@ 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
|
|
140
142
|
void cleanupRecording() {
|
|
141
|
-
//
|
|
142
|
-
|
|
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
|
-
}
|
|
143
|
+
// ELECTRON SAFETY: Don't call stop operations here - they're async and cause crashes
|
|
144
|
+
// Just reset state flags if they're stuck
|
|
152
145
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
@try {
|
|
147
|
+
// Only reset flags, don't actually stop anything
|
|
148
|
+
g_isRecording = false;
|
|
149
|
+
g_usingStandaloneAudio = false;
|
|
150
|
+
MRSyncConfigure(NO);
|
|
151
|
+
|
|
152
|
+
MRLog(@"๐งน State flags reset (safe mode - no actual stop operations)");
|
|
153
|
+
} @catch (NSException *exception) {
|
|
154
|
+
NSLog(@"โ ๏ธ Exception during state reset: %@", exception.reason);
|
|
158
155
|
}
|
|
159
|
-
g_usingStandaloneAudio = false;
|
|
160
|
-
|
|
161
|
-
g_isRecording = false;
|
|
162
|
-
MRSyncConfigure(NO);
|
|
163
156
|
}
|
|
164
157
|
|
|
165
158
|
// NAPI Function: Start Recording
|
|
@@ -171,24 +164,16 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
171
164
|
return env.Null();
|
|
172
165
|
}
|
|
173
166
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
MRLog(@"๐งน Cleaning up any previous recording state...");
|
|
177
|
-
cleanupRecording();
|
|
178
|
-
|
|
167
|
+
// ELECTRON SAFETY: Just check if recording is active, don't try to clean up
|
|
168
|
+
// Cleanup operations during start cause crashes in Electron
|
|
179
169
|
if (g_isRecording) {
|
|
180
|
-
MRLog(@"โ ๏ธ
|
|
170
|
+
MRLog(@"โ ๏ธ Recording already in progress");
|
|
181
171
|
return Napi::Boolean::New(env, false);
|
|
182
172
|
}
|
|
183
173
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (isScreenCaptureKitCleaningUp()) {
|
|
188
|
-
MRLog(@"โ ๏ธ ScreenCaptureKit is still stopping previous recording - please wait");
|
|
189
|
-
return Napi::Boolean::New(env, false);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
174
|
+
// ELECTRON SAFETY: Reset state flags in case they're stuck
|
|
175
|
+
MRLog(@"๐งน Resetting state flags (safe mode)...");
|
|
176
|
+
cleanupRecording(); // This now only resets flags, doesn't call stop operations
|
|
192
177
|
g_usingStandaloneAudio = false;
|
|
193
178
|
|
|
194
179
|
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
@@ -377,16 +362,16 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
377
362
|
MRLog(@"๐ฅ๏ธ macOS Version: %ld.%ld.%ld",
|
|
378
363
|
(long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
|
|
379
364
|
|
|
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
|
+
|
|
380
369
|
if (isElectron) {
|
|
381
370
|
MRLog(@"โก Electron environment detected");
|
|
382
|
-
MRLog(@"
|
|
371
|
+
MRLog(@"๐ง FORCING AVFoundation for Electron stability (ScreenCaptureKit crashes in Electron)");
|
|
372
|
+
forceAVFoundation = YES; // CRITICAL: ScreenCaptureKit is unstable in Electron
|
|
383
373
|
}
|
|
384
374
|
|
|
385
|
-
// CRITICAL FIX: Use ScreenCaptureKit on macOS 15+ for best compatibility
|
|
386
|
-
// Both ScreenCaptureKit and AVFoundation work in Electron (Electron-first design)
|
|
387
|
-
// Only fallback to AVFoundation if ScreenCaptureKit is unavailable
|
|
388
|
-
BOOL forceAVFoundation = NO; // Let system choose based on availability
|
|
389
|
-
|
|
390
375
|
MRLog(@"๐ง FRAMEWORK SELECTION: Smart selection based on macOS version");
|
|
391
376
|
MRLog(@" Environment: %@", isElectron ? @"Electron" : @"Node.js");
|
|
392
377
|
MRLog(@" macOS: %ld.%ld.%ld", (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
|
|
@@ -605,30 +590,41 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
605
590
|
|
|
606
591
|
// Try ScreenCaptureKit first
|
|
607
592
|
if (@available(macOS 12.3, *)) {
|
|
608
|
-
|
|
609
|
-
|
|
593
|
+
@try {
|
|
594
|
+
if ([ScreenCaptureKitRecorder isRecording]) {
|
|
595
|
+
MRLog(@"๐ Stopping ScreenCaptureKit recording");
|
|
610
596
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
597
|
+
// ELECTRON SAFETY: Stop camera without waiting
|
|
598
|
+
if (isCameraRecording()) {
|
|
599
|
+
MRLog(@"๐ Stopping camera recording...");
|
|
600
|
+
@try {
|
|
601
|
+
stopCameraRecording();
|
|
602
|
+
MRLog(@"โ
Camera stop dispatched");
|
|
603
|
+
} @catch (NSException *exception) {
|
|
604
|
+
NSLog(@"โ ๏ธ Camera stop exception: %@", exception.reason);
|
|
605
|
+
}
|
|
619
606
|
}
|
|
620
|
-
}
|
|
621
607
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
608
|
+
// Now stop ScreenCaptureKit (asynchronous)
|
|
609
|
+
// WARNING: [ScreenCaptureKitRecorder stopRecording] is ASYNC!
|
|
610
|
+
@try {
|
|
611
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
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
|
+
}
|
|
626
618
|
|
|
627
|
-
|
|
628
|
-
|
|
619
|
+
g_usingStandaloneAudio = false;
|
|
620
|
+
MRSyncSetStopLimitSeconds(-1.0);
|
|
621
|
+
return Napi::Boolean::New(env, true);
|
|
622
|
+
}
|
|
623
|
+
} @catch (NSException *exception) {
|
|
624
|
+
NSLog(@"โ Exception in ScreenCaptureKit stop: %@", exception.reason);
|
|
625
|
+
g_isRecording = false;
|
|
629
626
|
g_usingStandaloneAudio = false;
|
|
630
|
-
|
|
631
|
-
return Napi::Boolean::New(env, true);
|
|
627
|
+
return Napi::Boolean::New(env, false);
|
|
632
628
|
}
|
|
633
629
|
}
|
|
634
630
|
|
|
@@ -672,26 +668,11 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
672
668
|
|
|
673
669
|
bool avFoundationStopped = stopAVFoundationRecording();
|
|
674
670
|
|
|
675
|
-
//
|
|
671
|
+
// ELECTRON SAFETY: Don't use dispatch_group_wait - it blocks main thread and causes crashes
|
|
672
|
+
// Instead, just fire async stop and continue
|
|
676
673
|
if (cameraWasRecording || audioWasRecording) {
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
}
|
|
674
|
+
MRLog(@"๐ Camera/Audio stop operations dispatched asynchronously (Electron-safe mode)");
|
|
675
|
+
// Operations will complete in background - don't wait for them
|
|
695
676
|
}
|
|
696
677
|
|
|
697
678
|
g_isRecording = false;
|