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 CHANGED
@@ -856,13 +856,7 @@ class MacRecorder extends EventEmitter {
856
856
  } catch (_) {}
857
857
  this.sessionTimestamp = sessionTimestamp;
858
858
 
859
- // CURSOR SYNC FIX: Wait additional 300ms for first frames to start
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.22.7",
3
+ "version": "2.22.8",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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);
@@ -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
- // Give session a brief moment to warm up
811
- [NSThread sleepForTimeInterval:0.5];
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];
@@ -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
- // CRITICAL SYNC FIX: Start camera BEFORE ScreenCaptureKit
623
- // This allows camera warmup to happen in parallel with async ScreenCaptureKit init
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 BEFORE ScreenCaptureKit (parallel warmup)");
626
- if (!startCameraWithConfirmation(true, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
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
- // Set timeout for ScreenCaptureKit initialization
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
- MRLog(@"🎥 Starting camera recording for AVFoundation fallback");
726
- if (!startCameraWithConfirmation(true, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
727
- MRLog(@"❌ Camera failed to start for AVFoundation - stopping");
728
- stopAVFoundationRecording();
729
- return Napi::Boolean::New(env, false);
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);
@@ -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);
@@ -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;