node-mac-recorder 2.22.7 → 2.22.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/index.js +1 -7
- package/package.json +1 -1
- package/src/audio_recorder.mm +8 -1
- package/src/camera_recorder.mm +5 -2
- package/src/mac_recorder.mm +46 -12
- package/src/screen_capture_kit.mm +7 -1
- package/src/sync_timeline.h +6 -0
- package/src/sync_timeline.mm +100 -0
package/index.js
CHANGED
|
@@ -856,13 +856,7 @@ class MacRecorder extends EventEmitter {
|
|
|
856
856
|
} catch (_) {}
|
|
857
857
|
this.sessionTimestamp = sessionTimestamp;
|
|
858
858
|
|
|
859
|
-
//
|
|
860
|
-
// This ensures cursor tracking aligns with actual video timeline
|
|
861
|
-
// ScreenCaptureKit needs ~200-350ms to actually start capturing frames
|
|
862
|
-
// We wait 300ms to ensure cursor starts AFTER first video frame
|
|
863
|
-
console.log('⏳ CURSOR SYNC: Waiting 300ms for first video frames...');
|
|
864
|
-
await new Promise(r => setTimeout(r, 300));
|
|
865
|
-
|
|
859
|
+
// Native sync_timeline handles A/V alignment - no JS-level delay needed
|
|
866
860
|
const syncTimestamp = Date.now();
|
|
867
861
|
this.syncTimestamp = syncTimestamp;
|
|
868
862
|
this.recordingStartTime = syncTimestamp;
|
package/package.json
CHANGED
package/src/audio_recorder.mm
CHANGED
|
@@ -336,8 +336,15 @@ static NSString *g_lastStandaloneAudioOutputPath = nil;
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
339
|
+
|
|
340
|
+
// A/V SYNC: Hold audio samples until camera produces first frame
|
|
341
|
+
// This ensures both audio and camera files start from the same wall-clock moment
|
|
342
|
+
if (MRSyncShouldHoldAudioSample(timestamp)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
339
346
|
MRSyncMarkAudioSample(timestamp);
|
|
340
|
-
|
|
347
|
+
|
|
341
348
|
if (!self.writerStarted) {
|
|
342
349
|
if (![self.writer startWriting]) {
|
|
343
350
|
NSLog(@"❌ Audio writer failed to start: %@", self.writer.error);
|
package/src/camera_recorder.mm
CHANGED
|
@@ -526,6 +526,9 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
|
|
526
526
|
|
|
527
527
|
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
528
528
|
|
|
529
|
+
// A/V SYNC: Signal camera's first frame to release audio hold
|
|
530
|
+
MRSyncMarkCameraFirstFrame(timestamp);
|
|
531
|
+
|
|
529
532
|
// Hold camera frames until we see audio so timelines stay aligned
|
|
530
533
|
if (MRSyncShouldHoldVideoFrame(timestamp)) {
|
|
531
534
|
return;
|
|
@@ -807,8 +810,8 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
|
|
807
810
|
|
|
808
811
|
[session startRunning];
|
|
809
812
|
|
|
810
|
-
//
|
|
811
|
-
|
|
813
|
+
// A/V SYNC FIX: Removed 500ms warmup delay that was causing lip sync issues.
|
|
814
|
+
// The delegate pattern starts writing on first frame, no warmup needed.
|
|
812
815
|
|
|
813
816
|
if (self.stopInFlight || token != self.activeToken) {
|
|
814
817
|
[session stopRunning];
|
package/src/mac_recorder.mm
CHANGED
|
@@ -476,6 +476,10 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
476
476
|
bool captureScreenAudio = captureSystemAudio || (screenCaptureSupportsMic && captureMicrophone);
|
|
477
477
|
bool captureAnyAudio = captureScreenAudio || captureStandaloneMic;
|
|
478
478
|
MRSyncConfigure(captureAnyAudio);
|
|
479
|
+
// A/V SYNC: Configure bidirectional barrier when camera is active
|
|
480
|
+
if (captureCamera) {
|
|
481
|
+
MRSyncConfigureCamera(YES);
|
|
482
|
+
}
|
|
479
483
|
NSString *preferredAudioDeviceId = nil;
|
|
480
484
|
if (captureSystemAudio && systemAudioDeviceId && [systemAudioDeviceId length] > 0) {
|
|
481
485
|
preferredAudioDeviceId = systemAudioDeviceId;
|
|
@@ -619,28 +623,42 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
619
623
|
// Use ScreenCaptureKit with window exclusion and timeout protection
|
|
620
624
|
NSError *sckError = nil;
|
|
621
625
|
|
|
622
|
-
//
|
|
623
|
-
//
|
|
626
|
+
// A/V SYNC: Start camera non-blocking BEFORE ScreenCaptureKit
|
|
627
|
+
// Camera warmup overlaps with async SCK initialization
|
|
624
628
|
if (captureCamera) {
|
|
625
|
-
MRLog(@"🎥 Starting camera recording
|
|
626
|
-
if (!
|
|
629
|
+
MRLog(@"🎥 Starting camera recording non-blocking (parallel with SCK init)");
|
|
630
|
+
if (!startCameraIfRequested(true, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
|
|
627
631
|
MRLog(@"❌ Camera failed to start - aborting recording");
|
|
628
632
|
return Napi::Boolean::New(env, false);
|
|
629
633
|
}
|
|
630
634
|
}
|
|
631
635
|
|
|
632
|
-
//
|
|
633
|
-
// Attempt to start ScreenCaptureKit with safety wrapper
|
|
636
|
+
// Start SCK immediately - don't wait for camera confirmation
|
|
634
637
|
@try {
|
|
635
638
|
MRLog(@"🎯 SYNC: Starting ScreenCaptureKit recording");
|
|
636
639
|
if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
|
|
637
640
|
delegate:g_delegate
|
|
638
641
|
error:&sckError]) {
|
|
639
642
|
|
|
640
|
-
// ScreenCaptureKit başlatma başarılı - validation yapmıyoruz
|
|
641
643
|
MRLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
|
|
642
644
|
MRLog(@"✅ SYNC: ScreenCaptureKit recording started successfully");
|
|
643
645
|
|
|
646
|
+
// A/V SYNC: Wait for camera AFTER SCK started
|
|
647
|
+
if (captureCamera) {
|
|
648
|
+
if (!waitForCameraRecordingStart(8.0)) {
|
|
649
|
+
double cameraStartTs = currentCameraRecordingStartTime();
|
|
650
|
+
if (cameraStartTs > 0 || isCameraRecording()) {
|
|
651
|
+
MRLog(@"⚠️ Camera did not confirm start within 8.0s but appears running; continuing");
|
|
652
|
+
} else {
|
|
653
|
+
MRLog(@"❌ Camera did not signal recording start within 8.0s");
|
|
654
|
+
stopCameraRecording();
|
|
655
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
656
|
+
return Napi::Boolean::New(env, false);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
MRLog(@"✅ Camera recording confirmed (started in parallel with SCK)");
|
|
660
|
+
}
|
|
661
|
+
|
|
644
662
|
g_isRecording = true;
|
|
645
663
|
MRMarkRecordingStartTimestamp();
|
|
646
664
|
|
|
@@ -712,6 +730,15 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
712
730
|
NSString* audioOutputPath,
|
|
713
731
|
double frameRate);
|
|
714
732
|
|
|
733
|
+
// A/V SYNC: Start camera non-blocking BEFORE AVFoundation
|
|
734
|
+
if (captureCamera) {
|
|
735
|
+
MRLog(@"🎥 Starting camera recording non-blocking (parallel with AVF init)");
|
|
736
|
+
if (!startCameraIfRequested(true, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
|
|
737
|
+
MRLog(@"❌ Camera failed to start - aborting recording");
|
|
738
|
+
return Napi::Boolean::New(env, false);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
715
742
|
MRLog(@"🎯 SYNC: Starting screen recording");
|
|
716
743
|
bool avResult = startAVFoundationRecording(outputPath, displayID, windowID, captureRect,
|
|
717
744
|
captureCursor, includeMicrophone, includeSystemAudio,
|
|
@@ -721,13 +748,20 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
721
748
|
MRLog(@"🎥 RECORDING METHOD: AVFoundation");
|
|
722
749
|
MRLog(@"✅ SYNC: Screen recording started successfully");
|
|
723
750
|
|
|
751
|
+
// A/V SYNC: Wait for camera AFTER AVF started
|
|
724
752
|
if (captureCamera) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
753
|
+
if (!waitForCameraRecordingStart(8.0)) {
|
|
754
|
+
double cameraStartTs = currentCameraRecordingStartTime();
|
|
755
|
+
if (cameraStartTs > 0 || isCameraRecording()) {
|
|
756
|
+
MRLog(@"⚠️ Camera did not confirm start within 8.0s but appears running; continuing");
|
|
757
|
+
} else {
|
|
758
|
+
MRLog(@"❌ Camera did not signal recording start within 8.0s");
|
|
759
|
+
stopCameraRecording();
|
|
760
|
+
stopAVFoundationRecording();
|
|
761
|
+
return Napi::Boolean::New(env, false);
|
|
762
|
+
}
|
|
730
763
|
}
|
|
764
|
+
MRLog(@"✅ Camera recording confirmed (started in parallel with AVF)");
|
|
731
765
|
}
|
|
732
766
|
|
|
733
767
|
// NOTE: Audio is handled internally by AVFoundation, no need for standalone audio
|
|
@@ -631,8 +631,14 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
631
631
|
}
|
|
632
632
|
|
|
633
633
|
CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
634
|
+
|
|
635
|
+
// A/V SYNC: Hold audio samples until camera produces first frame
|
|
636
|
+
if (MRSyncShouldHoldAudioSample(presentationTime)) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
634
640
|
MRSyncMarkAudioSample(presentationTime);
|
|
635
|
-
|
|
641
|
+
|
|
636
642
|
if (!g_audioWriterStarted) {
|
|
637
643
|
if (![g_audioWriter startWriting]) {
|
|
638
644
|
NSLog(@"❌ Audio writer failed to start: %@", g_audioWriter.error);
|
package/src/sync_timeline.h
CHANGED
|
@@ -26,6 +26,12 @@ CMTime MRSyncVideoAlignmentOffset(void);
|
|
|
26
26
|
// Returns the first audio timestamp observed for the current session.
|
|
27
27
|
CMTime MRSyncAudioFirstTimestamp(void);
|
|
28
28
|
|
|
29
|
+
// Bidirectional camera-audio barrier: ensures both start writing from the
|
|
30
|
+
// same wall-clock moment for perfect lip sync.
|
|
31
|
+
void MRSyncConfigureCamera(BOOL expectCamera);
|
|
32
|
+
void MRSyncMarkCameraFirstFrame(CMTime timestamp);
|
|
33
|
+
BOOL MRSyncShouldHoldAudioSample(CMTime timestamp);
|
|
34
|
+
|
|
29
35
|
// Optional hard stop limit (seconds) shared across capture components.
|
|
30
36
|
void MRSyncSetStopLimitSeconds(double seconds);
|
|
31
37
|
double MRSyncGetStopLimitSeconds(void);
|
package/src/sync_timeline.mm
CHANGED
|
@@ -18,6 +18,13 @@ static CMTime g_audioFirstTimestamp = kCMTimeInvalid;
|
|
|
18
18
|
static CMTime g_alignmentDelta = kCMTimeInvalid;
|
|
19
19
|
static double g_stopLimitSeconds = -1.0;
|
|
20
20
|
|
|
21
|
+
// Bidirectional barrier: camera side
|
|
22
|
+
static BOOL g_expectCamera = NO;
|
|
23
|
+
static BOOL g_cameraReady = YES;
|
|
24
|
+
static CMTime g_cameraFirstTimestamp = kCMTimeInvalid;
|
|
25
|
+
static CMTime g_audioHoldFirstTimestamp = kCMTimeInvalid;
|
|
26
|
+
static BOOL g_audioHoldLogged = NO;
|
|
27
|
+
|
|
21
28
|
void MRSyncConfigure(BOOL expectAudio) {
|
|
22
29
|
dispatch_sync(MRSyncQueue(), ^{
|
|
23
30
|
g_expectAudio = expectAudio;
|
|
@@ -27,6 +34,12 @@ void MRSyncConfigure(BOOL expectAudio) {
|
|
|
27
34
|
g_audioFirstTimestamp = kCMTimeInvalid;
|
|
28
35
|
g_alignmentDelta = kCMTimeInvalid;
|
|
29
36
|
g_stopLimitSeconds = -1.0;
|
|
37
|
+
// Reset camera barrier state
|
|
38
|
+
g_expectCamera = NO;
|
|
39
|
+
g_cameraReady = YES;
|
|
40
|
+
g_cameraFirstTimestamp = kCMTimeInvalid;
|
|
41
|
+
g_audioHoldFirstTimestamp = kCMTimeInvalid;
|
|
42
|
+
g_audioHoldLogged = NO;
|
|
30
43
|
});
|
|
31
44
|
}
|
|
32
45
|
|
|
@@ -145,6 +158,93 @@ CMTime MRSyncAudioFirstTimestamp(void) {
|
|
|
145
158
|
return ts;
|
|
146
159
|
}
|
|
147
160
|
|
|
161
|
+
void MRSyncConfigureCamera(BOOL expectCamera) {
|
|
162
|
+
dispatch_sync(MRSyncQueue(), ^{
|
|
163
|
+
g_expectCamera = expectCamera;
|
|
164
|
+
g_cameraReady = expectCamera ? NO : YES;
|
|
165
|
+
g_cameraFirstTimestamp = kCMTimeInvalid;
|
|
166
|
+
g_audioHoldFirstTimestamp = kCMTimeInvalid;
|
|
167
|
+
g_audioHoldLogged = NO;
|
|
168
|
+
});
|
|
169
|
+
if (expectCamera) {
|
|
170
|
+
MRLog(@"🔄 A/V SYNC: Bidirectional barrier enabled - audio will wait for camera");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
void MRSyncMarkCameraFirstFrame(CMTime timestamp) {
|
|
175
|
+
if (!CMTIME_IS_VALID(timestamp)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
__block BOOL logRelease = NO;
|
|
180
|
+
dispatch_sync(MRSyncQueue(), ^{
|
|
181
|
+
if (g_cameraReady) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!CMTIME_IS_VALID(g_cameraFirstTimestamp)) {
|
|
185
|
+
g_cameraFirstTimestamp = timestamp;
|
|
186
|
+
}
|
|
187
|
+
g_cameraReady = YES;
|
|
188
|
+
g_audioHoldFirstTimestamp = kCMTimeInvalid;
|
|
189
|
+
g_audioHoldLogged = NO;
|
|
190
|
+
logRelease = YES;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (logRelease) {
|
|
194
|
+
MRLog(@"🎥 A/V SYNC: Camera first frame received - releasing audio hold");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
BOOL MRSyncShouldHoldAudioSample(CMTime timestamp) {
|
|
199
|
+
if (!CMTIME_IS_VALID(timestamp)) {
|
|
200
|
+
return NO;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
__block BOOL shouldHold = NO;
|
|
204
|
+
__block BOOL logHold = NO;
|
|
205
|
+
__block BOOL logRelease = NO;
|
|
206
|
+
|
|
207
|
+
dispatch_sync(MRSyncQueue(), ^{
|
|
208
|
+
if (!g_expectCamera || g_cameraReady) {
|
|
209
|
+
shouldHold = NO;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Camera not yet ready - hold audio samples
|
|
214
|
+
if (!CMTIME_IS_VALID(g_audioHoldFirstTimestamp)) {
|
|
215
|
+
g_audioHoldFirstTimestamp = timestamp;
|
|
216
|
+
shouldHold = YES;
|
|
217
|
+
if (!g_audioHoldLogged) {
|
|
218
|
+
g_audioHoldLogged = YES;
|
|
219
|
+
logHold = YES;
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Safety timeout: release after 1.0s even if camera hasn't started
|
|
225
|
+
CMTime elapsed = CMTimeSubtract(timestamp, g_audioHoldFirstTimestamp);
|
|
226
|
+
CMTime maxWait = CMTimeMakeWithSeconds(1.0, 600);
|
|
227
|
+
if (CMTIME_COMPARE_INLINE(elapsed, >, maxWait)) {
|
|
228
|
+
g_cameraReady = YES;
|
|
229
|
+
g_audioHoldFirstTimestamp = kCMTimeInvalid;
|
|
230
|
+
g_audioHoldLogged = NO;
|
|
231
|
+
shouldHold = NO;
|
|
232
|
+
logRelease = YES;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
shouldHold = YES;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (logHold) {
|
|
240
|
+
MRLog(@"⏸️ A/V SYNC: Audio holding samples until camera produces first frame (max 1.0s)");
|
|
241
|
+
} else if (logRelease) {
|
|
242
|
+
MRLog(@"▶️ A/V SYNC: Audio hold released by timeout (camera not detected within 1.0s)");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return shouldHold;
|
|
246
|
+
}
|
|
247
|
+
|
|
148
248
|
void MRSyncSetStopLimitSeconds(double seconds) {
|
|
149
249
|
dispatch_sync(MRSyncQueue(), ^{
|
|
150
250
|
g_stopLimitSeconds = seconds;
|