node-mac-recorder 2.21.24 → 2.21.26

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
@@ -37,6 +37,7 @@ class MacRecorder extends EventEmitter {
37
37
  this.cameraCaptureFile = null;
38
38
  this.cameraCaptureActive = false;
39
39
  this.sessionTimestamp = null;
40
+ this.syncTimestamp = null;
40
41
  this.audioCaptureFile = null;
41
42
  this.audioCaptureActive = false;
42
43
 
@@ -551,9 +552,13 @@ class MacRecorder extends EventEmitter {
551
552
  console.warn('❌ Native recording failed to start:', error.message);
552
553
  }
553
554
 
554
- // CRITICAL SYNC FIX: Wait for first video frame before starting cursor
555
- // This ensures cursor and video start at the EXACT same time
555
+ // Only start cursor if native recording started successfully
556
556
  if (success) {
557
+ this.sessionTimestamp = sessionTimestamp;
558
+ const syncTimestamp = Date.now();
559
+ this.syncTimestamp = syncTimestamp;
560
+ this.recordingStartTime = syncTimestamp;
561
+
557
562
  const standardCursorOptions = {
558
563
  videoRelative: true,
559
564
  displayInfo: this.recordingDisplayInfo,
@@ -561,36 +566,11 @@ class MacRecorder extends EventEmitter {
561
566
  this.options.captureArea ? 'area' : 'display',
562
567
  captureArea: this.options.captureArea,
563
568
  windowId: this.options.windowId,
564
- startTimestamp: sessionTimestamp // Will be updated with actual start time
569
+ startTimestamp: syncTimestamp // Align cursor timeline to actual start
565
570
  };
566
571
 
567
572
  try {
568
- // Poll for actual recording start (when first frame is captured)
569
- console.log('⏳ SYNC: Waiting for first video frame...');
570
- const maxWaitMs = 2000; // Max 2 seconds wait
571
- const pollInterval = 10; // Check every 10ms
572
- let waitedMs = 0;
573
- let actualStartTime = 0;
574
-
575
- while (waitedMs < maxWaitMs) {
576
- actualStartTime = nativeBinding.getActualRecordingStartTime();
577
- if (actualStartTime > 0) {
578
- console.log(`✅ SYNC: First frame captured at ${actualStartTime}ms (waited ${waitedMs}ms)`);
579
- break;
580
- }
581
- await new Promise(resolve => setTimeout(resolve, pollInterval));
582
- waitedMs += pollInterval;
583
- }
584
-
585
- if (actualStartTime > 0) {
586
- // Use actual start time for perfect sync
587
- standardCursorOptions.startTimestamp = actualStartTime;
588
- console.log('🎯 SYNC: Starting cursor tracking at ACTUAL recording start:', actualStartTime);
589
- } else {
590
- // Fallback to session timestamp if first frame not detected
591
- console.warn('⚠️ SYNC: First frame not detected, using session timestamp');
592
- }
593
-
573
+ console.log('🎯 SYNC: Starting cursor tracking at timestamp:', syncTimestamp);
594
574
  await this.startCursorCapture(cursorFilePath, standardCursorOptions);
595
575
  console.log('✅ SYNC: Cursor tracking started successfully');
596
576
  } catch (cursorError) {
@@ -600,6 +580,9 @@ class MacRecorder extends EventEmitter {
600
580
  }
601
581
 
602
582
  if (success) {
583
+ const timelineTimestamp = this.syncTimestamp || sessionTimestamp;
584
+ const fileTimestamp = this.sessionTimestamp || sessionTimestamp;
585
+
603
586
  if (this.options.captureCamera === true) {
604
587
  try {
605
588
  const nativeCameraPath = nativeBinding.getCameraRecordingPath
@@ -627,31 +610,33 @@ class MacRecorder extends EventEmitter {
627
610
  }
628
611
  }
629
612
  this.isRecording = true;
630
- // SYNC FIX: Use session timestamp for consistent timing across all components
631
- this.recordingStartTime = sessionTimestamp;
632
613
 
633
614
  if (this.options.captureCamera === true && cameraFilePath) {
634
615
  this.cameraCaptureActive = true;
635
- console.log('📹 SYNC: Camera recording started at timestamp:', sessionTimestamp);
616
+ console.log('📹 SYNC: Camera recording started at timestamp:', timelineTimestamp);
636
617
  this.emit("cameraCaptureStarted", {
637
618
  outputPath: cameraFilePath,
638
619
  deviceId: this.options.cameraDeviceId || null,
639
- timestamp: sessionTimestamp,
640
- sessionTimestamp,
620
+ timestamp: timelineTimestamp,
621
+ sessionTimestamp: fileTimestamp,
622
+ syncTimestamp: timelineTimestamp,
623
+ fileTimestamp,
641
624
  });
642
625
  }
643
626
 
644
627
  if (captureAudio && audioFilePath) {
645
628
  this.audioCaptureActive = true;
646
- console.log('🎙️ SYNC: Audio recording started at timestamp:', sessionTimestamp);
629
+ console.log('🎙️ SYNC: Audio recording started at timestamp:', timelineTimestamp);
647
630
  this.emit("audioCaptureStarted", {
648
631
  outputPath: audioFilePath,
649
632
  deviceIds: {
650
633
  microphone: this.options.audioDeviceId || null,
651
634
  system: this.options.systemAudioDeviceId || null,
652
635
  },
653
- timestamp: sessionTimestamp,
654
- sessionTimestamp,
636
+ timestamp: timelineTimestamp,
637
+ sessionTimestamp: fileTimestamp,
638
+ syncTimestamp: timelineTimestamp,
639
+ fileTimestamp,
655
640
  });
656
641
  }
657
642
 
@@ -664,7 +649,7 @@ class MacRecorder extends EventEmitter {
664
649
  if (this.cursorCaptureInterval) activeComponents.push('Cursor');
665
650
  if (this.cameraCaptureActive) activeComponents.push('Camera');
666
651
  if (this.audioCaptureActive) activeComponents.push('Audio');
667
- console.log(`✅ SYNC COMPLETE: All components synchronized at timestamp ${sessionTimestamp}`);
652
+ console.log(`✅ SYNC COMPLETE: All components synchronized at timestamp ${timelineTimestamp}`);
668
653
  console.log(` Active components: ${activeComponents.join(', ')}`);
669
654
 
670
655
  // Timer başlat (progress tracking için)
@@ -685,15 +670,19 @@ class MacRecorder extends EventEmitter {
685
670
  clearInterval(checkRecordingStatus);
686
671
 
687
672
  // Kayıt gerçekten başladığı anda event emit et
673
+ const startTimestampPayload = this.syncTimestamp || this.recordingStartTime || Date.now();
674
+ const fileTimestampPayload = this.sessionTimestamp;
688
675
  this.emit("recordingStarted", {
689
676
  outputPath: this.outputPath,
690
- timestamp: Date.now(), // Gerçek başlangıç zamanı
677
+ timestamp: startTimestampPayload,
691
678
  options: this.options,
692
679
  nativeConfirmed: true,
693
680
  cameraOutputPath: this.cameraCaptureFile || null,
694
681
  audioOutputPath: this.audioCaptureFile || null,
695
682
  cursorOutputPath: cursorFilePath,
696
- sessionTimestamp: this.sessionTimestamp,
683
+ sessionTimestamp: fileTimestampPayload,
684
+ syncTimestamp: startTimestampPayload,
685
+ fileTimestamp: fileTimestampPayload,
697
686
  });
698
687
  }
699
688
  } catch (error) {
@@ -701,15 +690,19 @@ class MacRecorder extends EventEmitter {
701
690
  if (!recordingStartedEmitted) {
702
691
  recordingStartedEmitted = true;
703
692
  clearInterval(checkRecordingStatus);
693
+ const startTimestampPayload = this.syncTimestamp || this.recordingStartTime || Date.now();
694
+ const fileTimestampPayload = this.sessionTimestamp;
704
695
  this.emit("recordingStarted", {
705
696
  outputPath: this.outputPath,
706
- timestamp: this.recordingStartTime,
697
+ timestamp: startTimestampPayload,
707
698
  options: this.options,
708
699
  nativeConfirmed: false,
709
700
  cameraOutputPath: this.cameraCaptureFile || null,
710
701
  audioOutputPath: this.audioCaptureFile || null,
711
702
  cursorOutputPath: cursorFilePath,
712
- sessionTimestamp: this.sessionTimestamp,
703
+ sessionTimestamp: fileTimestampPayload,
704
+ syncTimestamp: startTimestampPayload,
705
+ fileTimestamp: fileTimestampPayload,
713
706
  });
714
707
  }
715
708
  }
@@ -720,15 +713,19 @@ class MacRecorder extends EventEmitter {
720
713
  if (!recordingStartedEmitted) {
721
714
  recordingStartedEmitted = true;
722
715
  clearInterval(checkRecordingStatus);
716
+ const startTimestampPayload = this.syncTimestamp || this.recordingStartTime || Date.now();
717
+ const fileTimestampPayload = this.sessionTimestamp;
723
718
  this.emit("recordingStarted", {
724
719
  outputPath: this.outputPath,
725
- timestamp: this.recordingStartTime,
720
+ timestamp: startTimestampPayload,
726
721
  options: this.options,
727
722
  nativeConfirmed: false,
728
723
  cameraOutputPath: this.cameraCaptureFile || null,
729
724
  audioOutputPath: this.audioCaptureFile || null,
730
725
  cursorOutputPath: cursorFilePath,
731
- sessionTimestamp: this.sessionTimestamp,
726
+ sessionTimestamp: fileTimestampPayload,
727
+ syncTimestamp: startTimestampPayload,
728
+ fileTimestamp: fileTimestampPayload,
732
729
  });
733
730
  }
734
731
  }, 5000);
@@ -761,6 +758,7 @@ class MacRecorder extends EventEmitter {
761
758
  }
762
759
 
763
760
  this.sessionTimestamp = null;
761
+ this.syncTimestamp = null;
764
762
 
765
763
  reject(
766
764
  new Error(
@@ -770,6 +768,7 @@ class MacRecorder extends EventEmitter {
770
768
  }
771
769
  } catch (error) {
772
770
  this.sessionTimestamp = null;
771
+ this.syncTimestamp = null;
773
772
  reject(error);
774
773
  }
775
774
  });
@@ -848,6 +847,7 @@ class MacRecorder extends EventEmitter {
848
847
  outputPath: this.cameraCaptureFile || null,
849
848
  success: success === true,
850
849
  sessionTimestamp: this.sessionTimestamp,
850
+ syncTimestamp: this.syncTimestamp,
851
851
  });
852
852
  }
853
853
 
@@ -858,6 +858,7 @@ class MacRecorder extends EventEmitter {
858
858
  outputPath: this.audioCaptureFile || null,
859
859
  success: success === true,
860
860
  sessionTimestamp: this.sessionTimestamp,
861
+ syncTimestamp: this.syncTimestamp,
861
862
  });
862
863
  }
863
864
 
@@ -883,6 +884,7 @@ class MacRecorder extends EventEmitter {
883
884
  cameraOutputPath: this.cameraCaptureFile || null,
884
885
  audioOutputPath: this.audioCaptureFile || null,
885
886
  sessionTimestamp: sessionId,
887
+ syncTimestamp: this.syncTimestamp,
886
888
  };
887
889
 
888
890
  this.emit("stopped", result);
@@ -897,6 +899,7 @@ class MacRecorder extends EventEmitter {
897
899
  }
898
900
 
899
901
  this.sessionTimestamp = null;
902
+ this.syncTimestamp = null;
900
903
  resolve(result);
901
904
  } catch (error) {
902
905
  this.isRecording = false;
@@ -905,6 +908,7 @@ class MacRecorder extends EventEmitter {
905
908
  this.audioCaptureActive = false;
906
909
  this.audioCaptureFile = null;
907
910
  this.sessionTimestamp = null;
911
+ this.syncTimestamp = null;
908
912
  if (this.recordingTimer) {
909
913
  clearInterval(this.recordingTimer);
910
914
  this.recordingTimer = null;
@@ -927,6 +931,7 @@ class MacRecorder extends EventEmitter {
927
931
  cameraCapturing: this.cameraCaptureActive,
928
932
  audioCapturing: this.audioCaptureActive,
929
933
  sessionTimestamp: this.sessionTimestamp,
934
+ syncTimestamp: this.syncTimestamp,
930
935
  options: this.options,
931
936
  recordingTime: this.recordingStartTime
932
937
  ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
@@ -1033,23 +1038,21 @@ class MacRecorder extends EventEmitter {
1033
1038
 
1034
1039
  const last = this.lastCapturedData;
1035
1040
 
1036
- // Event type değişmişse (click, drag, vs)
1041
+ // Event type değişmişse
1037
1042
  if (currentData.type !== last.type) {
1038
1043
  return true;
1039
1044
  }
1040
1045
 
1041
- // Cursor type değişmişse (pointer, text, vs)
1042
- if (currentData.cursorType !== last.cursorType) {
1046
+ // Pozisyon değişmişse (minimum 2 pixel tolerans)
1047
+ if (
1048
+ Math.abs(currentData.x - last.x) >= 2 ||
1049
+ Math.abs(currentData.y - last.y) >= 2
1050
+ ) {
1043
1051
  return true;
1044
1052
  }
1045
1053
 
1046
- // SYNC FIX: Reduced threshold for better sync (1 pixel instead of 2)
1047
- // With 200 FPS sampling, we can afford more granular position tracking
1048
- // Pozisyon değişmişse (minimum 1 pixel - hassas tracking)
1049
- if (
1050
- Math.abs(currentData.x - last.x) >= 1 ||
1051
- Math.abs(currentData.y - last.y) >= 1
1052
- ) {
1054
+ // Cursor type değişmişse
1055
+ if (currentData.cursorType !== last.cursorType) {
1053
1056
  return true;
1054
1057
  }
1055
1058
 
@@ -1070,14 +1073,11 @@ class MacRecorder extends EventEmitter {
1070
1073
  */
1071
1074
  async startCursorCapture(intervalOrFilepath = 100, options = {}) {
1072
1075
  let filepath;
1073
- // SYNC FIX: Use 5ms interval (200 FPS) for ultra-smooth cursor tracking
1074
- // This high sampling rate ensures cursor is always in sync with 60 FPS video
1075
- // Even if we sample 200 times per second, we only write on position/event changes (efficient)
1076
- let interval = 5; // Default 200 FPS for perfect sync
1076
+ let interval = 20; // Default 50 FPS
1077
1077
 
1078
1078
  // Parameter parsing: number = interval, string = filepath
1079
1079
  if (typeof intervalOrFilepath === "number") {
1080
- interval = Math.max(5, intervalOrFilepath); // Min 5ms for sync
1080
+ interval = Math.max(10, intervalOrFilepath); // Min 10ms
1081
1081
  filepath = `cursor-data-${Date.now()}.json`;
1082
1082
  } else if (typeof intervalOrFilepath === "string") {
1083
1083
  filepath = intervalOrFilepath;
@@ -1172,10 +1172,6 @@ class MacRecorder extends EventEmitter {
1172
1172
 
1173
1173
  return new Promise((resolve, reject) => {
1174
1174
  try {
1175
- // NOTE: Native cursor tracking (NSTimer/CFRunLoop) doesn't work with Node.js event loop
1176
- // Using JavaScript setInterval with high frequency (5ms = 200 FPS) instead
1177
- // This provides excellent sync with minimal overhead due to change-detection filtering
1178
-
1179
1175
  // Dosyayı oluştur ve temizle
1180
1176
  const fs = require("fs");
1181
1177
  fs.writeFileSync(filepath, "[");
@@ -1280,7 +1276,7 @@ class MacRecorder extends EventEmitter {
1280
1276
  return resolve(false);
1281
1277
  }
1282
1278
 
1283
- // Stop JavaScript interval
1279
+ // Interval'ı durdur
1284
1280
  clearInterval(this.cursorCaptureInterval);
1285
1281
  this.cursorCaptureInterval = null;
1286
1282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.24",
3
+ "version": "2.21.26",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -25,9 +25,6 @@ static CMTime g_avStartTime;
25
25
  static void* g_avAudioRecorder = nil;
26
26
  static NSString* g_avAudioOutputPath = nil;
27
27
 
28
- // SYNC FIX: Track actual recording start time (when first frame is captured)
29
- static NSTimeInterval g_avActualRecordingStartTime = 0;
30
-
31
28
  // AVFoundation screen recording implementation
32
29
  extern "C" bool startAVFoundationRecording(const std::string& outputPath,
33
30
  CGDirectDisplayID displayID,
@@ -192,8 +189,7 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
192
189
  return false;
193
190
  }
194
191
 
195
- g_avStartTime = CMTimeMakeWithSeconds(CACurrentMediaTime(), 600);
196
- [g_avWriter startSessionAtSourceTime:g_avStartTime];
192
+ g_avStartTime = kCMTimeInvalid;
197
193
 
198
194
  // Store recording parameters with scaling correction
199
195
  g_avDisplayID = displayID;
@@ -380,14 +376,15 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
380
376
 
381
377
  // Write frame only if input is ready
382
378
  if (localVideoInput && localVideoInput.readyForMoreMediaData) {
379
+ if (CMTIME_IS_INVALID(g_avStartTime)) {
380
+ g_avStartTime = CMTimeMakeWithSeconds(CACurrentMediaTime(), 600);
381
+ [g_avWriter startSessionAtSourceTime:g_avStartTime];
382
+ MRLog(@"🎞️ AVFoundation writer session started @ %.3f", CMTimeGetSeconds(g_avStartTime));
383
+ }
384
+
383
385
  CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(((double)g_avFrameNumber) / fps, 600));
384
386
  BOOL appendSuccess = [localPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
385
387
  if (appendSuccess) {
386
- // SYNC FIX: Record actual start time when first frame is written
387
- if (g_avFrameNumber == 0) {
388
- g_avActualRecordingStartTime = [[NSDate date] timeIntervalSince1970] * 1000; // milliseconds
389
- MRLog(@"🎞️ AVFoundation first frame written (actual start time: %.0f)", g_avActualRecordingStartTime);
390
- }
391
388
  g_avFrameNumber++;
392
389
  } else {
393
390
  NSLog(@"⚠️ Failed to append pixel buffer");
@@ -487,7 +484,7 @@ extern "C" bool stopAVFoundationRecording() {
487
484
  g_avVideoInput = nil;
488
485
  g_avPixelBufferAdaptor = nil;
489
486
  g_avFrameNumber = 0;
490
- g_avActualRecordingStartTime = 0;
487
+ g_avStartTime = kCMTimeInvalid;
491
488
 
492
489
  MRLog(@"✅ AVFoundation recording stopped");
493
490
  return true;
@@ -502,11 +499,6 @@ extern "C" bool isAVFoundationRecording() {
502
499
  return g_avIsRecording;
503
500
  }
504
501
 
505
- // SYNC FIX: Get actual recording start time for AVFoundation
506
- extern "C" NSTimeInterval getAVFoundationActualStartTime() {
507
- return g_avActualRecordingStartTime;
508
- }
509
-
510
502
  extern "C" NSString* getAVFoundationAudioPath() {
511
503
  return g_avAudioOutputPath;
512
504
  }
@@ -1032,9 +1032,7 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
1032
1032
  // NSTimer kullan (main thread'de çalışır)
1033
1033
  g_timerTarget = [[CursorTimerTarget alloc] init];
1034
1034
 
1035
- // SYNC FIX: Match screen recording frame rate (60 FPS = 16.67ms)
1036
- // This ensures cursor tracking is synchronized with video frames
1037
- g_cursorTimer = [NSTimer timerWithTimeInterval:0.01667 // 16.67ms (60 FPS) - matches screen recording
1035
+ g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
1038
1036
  target:g_timerTarget
1039
1037
  selector:@selector(timerCallback:)
1040
1038
  userInfo:nil
@@ -23,7 +23,6 @@ extern "C" {
23
23
  double frameRate);
24
24
  bool stopAVFoundationRecording();
25
25
  bool isAVFoundationRecording();
26
- NSTimeInterval getAVFoundationActualStartTime();
27
26
  NSString* getAVFoundationAudioPath();
28
27
 
29
28
  NSArray<NSDictionary *> *listCameraDevices();
@@ -445,20 +444,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
445
444
  // Set timeout for ScreenCaptureKit initialization
446
445
  // Attempt to start ScreenCaptureKit with safety wrapper
447
446
  @try {
448
- // CRITICAL SYNC FIX: Start camera BEFORE ScreenCaptureKit for perfect sync
449
- bool cameraStarted = true;
450
- if (captureCamera) {
451
- MRLog(@"🎯 SYNC: Starting camera recording first for parallel sync");
452
- cameraStarted = startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp);
453
- if (!cameraStarted) {
454
- MRLog(@"❌ Camera start failed - aborting");
455
- return Napi::Boolean::New(env, false);
456
- }
457
- MRLog(@"✅ SYNC: Camera recording started");
458
- }
459
-
460
- // Now start ScreenCaptureKit immediately after camera
461
- MRLog(@"🎯 SYNC: Starting ScreenCaptureKit recording immediately");
447
+ MRLog(@"🎯 SYNC: Starting ScreenCaptureKit recording");
462
448
  if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
463
449
  delegate:g_delegate
464
450
  error:&sckError]) {
@@ -467,16 +453,22 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
467
453
  MRLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
468
454
  MRLog(@"✅ SYNC: ScreenCaptureKit recording started successfully");
469
455
 
456
+ if (captureCamera) {
457
+ MRLog(@"🎯 SYNC: Starting camera recording after screen start");
458
+ bool cameraStarted = startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp);
459
+ if (!cameraStarted) {
460
+ MRLog(@"❌ Camera start failed - stopping ScreenCaptureKit recording");
461
+ [ScreenCaptureKitRecorder stopRecording];
462
+ return Napi::Boolean::New(env, false);
463
+ }
464
+ MRLog(@"✅ SYNC: Camera recording started");
465
+ }
466
+
470
467
  g_isRecording = true;
471
468
  return Napi::Boolean::New(env, true);
472
469
  } else {
473
470
  NSLog(@"❌ ScreenCaptureKit failed to start");
474
471
  NSLog(@"❌ Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
475
-
476
- // Cleanup camera if ScreenCaptureKit failed
477
- if (cameraStarted && isCameraRecording()) {
478
- stopCameraRecording();
479
- }
480
472
  }
481
473
  } @catch (NSException *sckException) {
482
474
  NSLog(@"❌ Exception during ScreenCaptureKit startup: %@", sckException.reason);
@@ -543,21 +535,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
543
535
  NSString* audioOutputPath,
544
536
  double frameRate);
545
537
 
546
- // CRITICAL SYNC FIX: Start camera BEFORE screen recording for perfect sync
547
- // This ensures both capture their first frame at approximately the same time
548
- bool cameraStarted = true;
549
- if (captureCamera) {
550
- MRLog(@"🎯 SYNC: Starting camera recording first for parallel sync");
551
- cameraStarted = startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp);
552
- if (!cameraStarted) {
553
- MRLog(@"❌ Camera start failed - aborting");
554
- return Napi::Boolean::New(env, false);
555
- }
556
- MRLog(@"✅ SYNC: Camera recording started");
557
- }
558
-
559
- // Now start screen recording immediately after camera
560
- MRLog(@"🎯 SYNC: Starting screen recording immediately");
538
+ MRLog(@"🎯 SYNC: Starting screen recording");
561
539
  bool avResult = startAVFoundationRecording(outputPath, displayID, windowID, captureRect,
562
540
  captureCursor, includeMicrophone, includeSystemAudio,
563
541
  audioDeviceId, audioOutputPath, frameRate);
@@ -566,6 +544,17 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
566
544
  MRLog(@"🎥 RECORDING METHOD: AVFoundation");
567
545
  MRLog(@"✅ SYNC: Screen recording started successfully");
568
546
 
547
+ if (captureCamera) {
548
+ MRLog(@"🎯 SYNC: Starting camera recording after screen start");
549
+ bool cameraStarted = startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp);
550
+ if (!cameraStarted) {
551
+ MRLog(@"❌ Camera start failed - stopping screen recording");
552
+ stopAVFoundationRecording();
553
+ return Napi::Boolean::New(env, false);
554
+ }
555
+ MRLog(@"✅ SYNC: Camera recording started");
556
+ }
557
+
569
558
  // NOTE: Audio is handled internally by AVFoundation, no need for standalone audio
570
559
  // AVFoundation integrates audio recording directly
571
560
 
@@ -574,11 +563,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
574
563
  } else {
575
564
  NSLog(@"❌ AVFoundation recording failed to start");
576
565
  NSLog(@"❌ Check permissions and output path validity");
577
-
578
- // Cleanup camera if screen recording failed
579
- if (cameraStarted && isCameraRecording()) {
580
- stopCameraRecording();
581
- }
582
566
  }
583
567
  } @catch (NSException *avException) {
584
568
  NSLog(@"❌ Exception during AVFoundation startup: %@", avException.reason);
@@ -1058,26 +1042,6 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
1058
1042
  return Napi::Boolean::New(env, isRecording);
1059
1043
  }
1060
1044
 
1061
- // SYNC FIX: Get actual recording start time (when first frame was captured)
1062
- Napi::Value GetActualRecordingStartTime(const Napi::CallbackInfo& info) {
1063
- Napi::Env env = info.Env();
1064
-
1065
- NSTimeInterval startTime = 0;
1066
-
1067
- // Check ScreenCaptureKit first
1068
- if (@available(macOS 12.3, *)) {
1069
- startTime = [ScreenCaptureKitRecorder getActualRecordingStartTime];
1070
- }
1071
-
1072
- // Check AVFoundation if ScreenCaptureKit didn't return a time
1073
- if (startTime == 0) {
1074
- startTime = getAVFoundationActualStartTime();
1075
- }
1076
-
1077
- // Return 0 if not started yet, otherwise return the actual start time in milliseconds
1078
- return Napi::Number::New(env, startTime);
1079
- }
1080
-
1081
1045
  // NAPI Function: Get Window Thumbnail
1082
1046
  Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
1083
1047
  Napi::Env env = info.Env();
@@ -1406,7 +1370,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1406
1370
  exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
1407
1371
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
1408
1372
  exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
1409
- exports.Set(Napi::String::New(env, "getActualRecordingStartTime"), Napi::Function::New(env, GetActualRecordingStartTime));
1410
1373
  exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
1411
1374
 
1412
1375
  // Thumbnail functions
@@ -15,6 +15,5 @@ API_AVAILABLE(macos(12.3))
15
15
  + (void)finalizeRecording;
16
16
  + (void)finalizeVideoWriter;
17
17
  + (void)cleanupVideoWriter;
18
- + (NSTimeInterval)getActualRecordingStartTime;
19
18
 
20
19
  @end
@@ -38,9 +38,6 @@ static NSInteger g_targetFPS = 60;
38
38
  static NSInteger g_frameCount = 0;
39
39
  static CFAbsoluteTime g_firstFrameTime = 0;
40
40
 
41
- // SYNC FIX: Track actual recording start time (when first frame is captured)
42
- static NSTimeInterval g_actualRecordingStartTime = 0;
43
-
44
41
  static void CleanupWriters(void);
45
42
  static AVAssetWriterInputPixelBufferAdaptor * _Nullable CurrentPixelBufferAdaptor(void) {
46
43
  if (!g_pixelBufferAdaptorRef) {
@@ -102,7 +99,6 @@ static void CleanupWriters(void) {
102
99
  // Reset frame counting
103
100
  g_frameCount = 0;
104
101
  g_firstFrameTime = 0;
105
- g_actualRecordingStartTime = 0;
106
102
  }
107
103
 
108
104
  if (g_audioWriter) {
@@ -194,12 +190,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
194
190
  [g_videoWriter startSessionAtSourceTime:presentationTime];
195
191
  g_videoStartTime = presentationTime;
196
192
  g_videoWriterStarted = YES;
197
-
198
- // SYNC FIX: Record the ACTUAL recording start time (when first frame is captured)
199
- // This is the TRUE sync point - cursor tracking should use this timestamp
200
- g_actualRecordingStartTime = [[NSDate date] timeIntervalSince1970] * 1000; // milliseconds
201
- MRLog(@"🎞️ Video writer session started @ %.3f (actual start time: %.0f)",
202
- CMTimeGetSeconds(presentationTime), g_actualRecordingStartTime);
193
+ MRLog(@"🎞️ Video writer session started @ %.3f", CMTimeGetSeconds(presentationTime));
203
194
  }
204
195
 
205
196
  if (!g_videoInput.readyForMoreMediaData) {
@@ -954,16 +945,9 @@ BOOL isScreenCaptureKitCleaningUp() API_AVAILABLE(macos(12.3)) {
954
945
  g_isRecording = NO;
955
946
  g_isCleaningUp = NO; // Reset cleanup flag
956
947
  g_outputPath = nil;
957
- g_actualRecordingStartTime = 0;
958
948
 
959
949
  MRLog(@"🧹 Pure ScreenCaptureKit cleanup complete");
960
950
  }
961
951
  }
962
952
 
963
- // SYNC FIX: Get the actual recording start time (when first frame was captured)
964
- // This is the TRUE sync point for cursor tracking
965
- + (NSTimeInterval)getActualRecordingStartTime {
966
- return g_actualRecordingStartTime;
967
- }
968
-
969
953
  @end
@@ -1,5 +0,0 @@
1
- {
2
- "setup-worktree": [
3
- "npm install"
4
- ]
5
- }
@@ -1,85 +0,0 @@
1
- # Cursor-Video Senkronizasyon Düzeltmesi
2
-
3
- ## Problem
4
- Ekran videosu kaydedilirken custom cursor da kayıt ediliyor, ancak cursor'un tıklama/hareket kayıtları ekran videosundan ~0.5-1 saniye geriden geliyordu.
5
-
6
- ## Çözüm
7
-
8
- ### 1. Cursor Tracking Interval Azaltıldı
9
- - **Öncesi**: 20ms interval (50 FPS)
10
- - **Sonrası**: 5ms interval (200 FPS)
11
- - **Sonuç**: Cursor artık çok daha sık örnekleniyor, bu yüzden video frame'leri ile sync şansı çok daha yüksek
12
-
13
- ### 2. Position Threshold Azaltıldı
14
- - **Öncesi**: 2 pixel minimum hareket
15
- - **Sonrası**: 1 pixel minimum hareket
16
- - **Sonuç**: Daha hassas tracking, küçük mouse hareketleri bile kaydediliyor
17
-
18
- ### 3. Gerçek Test Sonuçları
19
-
20
- ```
21
- 📊 Cursor tracking analysis:
22
- Total events captured: 193
23
- Average capture rate: 41.5 FPS
24
- Timing analysis:
25
- - Average interval: 24.3ms (41.2 FPS)
26
- - Min interval: 1.0ms
27
- - Max interval: 765.0ms
28
- ✅ Smooth cursor tracking
29
- ```
30
-
31
- ## Test Etme
32
-
33
- ```bash
34
- # Kısa sync testi (5 saniye, mouse'u hareket ettir)
35
- node test-cursor-sync-mouse.js
36
-
37
- # Test sonrası video ve cursor dosyasını kontrol et:
38
- # - Video: test-output/sync-test-{timestamp}.mov
39
- # - Cursor: test-output/temp_cursor_{timestamp}.json
40
- ```
41
-
42
- ## Teknik Detaylar
43
-
44
- ### Neden Native Event Tracking Kullanılmadı?
45
- Native `NSTimer` ve `CGEventTap` çalışıyor ama Node.js event loop ile uyumsuz. Timer callback'leri çağrılmıyor çünkü:
46
- - Node.js kendi event loop'unu kullanıyor
47
- - macOS main run loop block olmuyor
48
- - Bu yüzden timer callback'leri tetiklenmiyor
49
-
50
- ### JavaScript Polling Neden Yeterli?
51
- - 5ms interval = 200 FPS sampling rate
52
- - Video 60 FPS = ~16.67ms per frame
53
- - Cursor her video frame'inde 3+ kez örnekleniyor
54
- - `shouldCaptureEvent` filtrelemesi sayesinde sadece değişiklikler kaydediliyor
55
- - Ortalama 40-50 FPS cursor data elde ediliyor (yeterli)
56
-
57
- ### Sync Mekanizması
58
- 1. **Unified Session Timestamp**: Hem video hem cursor aynı `sessionTimestamp` kullanıyor
59
- 2. **Synchronized Start**: Video başladıktan hemen sonra cursor tracking başlıyor (aynı timestamp)
60
- 3. **Relative Timestamps**: İkisi de başlangıçtan itibaren millisaniye cinsinden kaydediyor
61
-
62
- ## Beklenen Sonuç
63
-
64
- - ✅ Cursor ve video aynı anda başlıyor (0ms fark)
65
- - ✅ Cursor her ~24ms'de bir örnekleniyor (smooth)
66
- - ✅ Click event'leri doğru yakalanıyor
67
- - ✅ Video frame'leri ile mükemmel sync
68
-
69
- ## Ek Notlar
70
-
71
- ### Çoklu Ekran Kullanımı
72
- Eğer cursor başka bir ekrandaysa `coordinateSystem: "video-relative-outside"` olarak işaretlenir. Bu normal ve cursor overlay render'ında handle edilmelidir.
73
-
74
- ### Performance
75
- 200 FPS sampling yüksek görünse de:
76
- - Change detection filtrelemesi var (sadece hareket olunca yazıyor)
77
- - Dosya boyutu küçük kalıyor
78
- - CPU overhead minimal
79
-
80
- ### İleride İyileştirme
81
- Native event tracking için:
82
- - Ayrı thread'de CFRunLoop çalıştırılabilir
83
- - Veya GCD dispatch queue kullanılabilir
84
- - Ama şimdilik JavaScript polling yeterli ve güvenilir
85
-
@@ -1,138 +0,0 @@
1
- # ✅ Cursor-Video Senkronizasyon Tamamen Çözüldü!
2
-
3
- ## Asıl Sorun
4
-
5
- Cursor tracking **hemen** başlıyordu, ama video'nun ilk frame'ini yakalaması **~100-200ms** sürüyordu.
6
-
7
- ```
8
- ÖNCE:
9
- t=0ms: startRecording() çağrılır
10
- t=0ms: Cursor tracking başlar ✅
11
- t=150ms: İlk video frame yakalanır ❌ (CURSOR ÖNDE!)
12
- ```
13
-
14
- Bu yüzden cursor event'leri video'dan önce geliyordu - **cursor önde, video geride!**
15
-
16
- ## Çözüm: İlk Frame Senkronizasyonu
17
-
18
- ### 1. Native Tarafta: İlk Frame Timestamp'ini Kaydet
19
-
20
- **ScreenCaptureKit** (`screen_capture_kit.mm`):
21
- ```objc
22
- static NSTimeInterval g_actualRecordingStartTime = 0;
23
-
24
- - (void)stream:didOutputSampleBuffer:ofType: {
25
- if (!g_videoWriterStarted) {
26
- // İlk frame yakalandığında
27
- g_actualRecordingStartTime = [[NSDate date] timeIntervalSince1970] * 1000;
28
- MRLog(@"🎞️ First frame captured at %.0f", g_actualRecordingStartTime);
29
- }
30
- }
31
- ```
32
-
33
- **AVFoundation** (`avfoundation_recorder.mm`):
34
- ```objc
35
- static NSTimeInterval g_avActualRecordingStartTime = 0;
36
-
37
- if (g_avFrameNumber == 0) {
38
- // İlk frame yazıldığında
39
- g_avActualRecordingStartTime = [[NSDate date] timeIntervalSince1970] * 1000;
40
- MRLog(@"🎞️ AVFoundation first frame written at %.0f", g_avActualRecordingStartTime);
41
- }
42
- ```
43
-
44
- ### 2. JavaScript Tarafta: İlk Frame'i Bekle
45
-
46
- ```javascript
47
- // Poll for actual recording start (when first frame is captured)
48
- console.log('⏳ SYNC: Waiting for first video frame...');
49
- const maxWaitMs = 2000;
50
- const pollInterval = 10;
51
- let actualStartTime = 0;
52
-
53
- while (waitedMs < maxWaitMs) {
54
- actualStartTime = nativeBinding.getActualRecordingStartTime();
55
- if (actualStartTime > 0) {
56
- console.log(`✅ SYNC: First frame captured at ${actualStartTime}ms`);
57
- break;
58
- }
59
- await new Promise(resolve => setTimeout(resolve, pollInterval));
60
- waitedMs += pollInterval;
61
- }
62
-
63
- // Cursor tracking'i ACTUAL start time ile başlat
64
- await this.startCursorCapture(cursorFilePath, {
65
- startTimestamp: actualStartTime // ← PERFECT SYNC!
66
- });
67
- ```
68
-
69
- ## Sonuç: Mükemmel Senkronizasyon!
70
-
71
- ```
72
- ŞIMDI:
73
- t=0ms: startRecording() çağrılır
74
- t=150ms: İlk video frame yakalanır ✅
75
- t=150ms: Cursor tracking başlar ✅ (TAM SENKRON!)
76
- ```
77
-
78
- ### Test Sonuçları:
79
- ```
80
- ✅ SYNC: First frame captured at 1761818226539.994ms (waited 30ms)
81
- 🎯 SYNC: Starting cursor tracking at ACTUAL recording start: 1761818226539.994
82
- First cursor event: t=19.006ms (video'dan 19ms sonra - mükemmel!)
83
- ```
84
-
85
- ## Teknik Detaylar
86
-
87
- ### Neden Bu Yaklaşım Çalışıyor?
88
-
89
- 1. **Video recording başlatma** → Native sistem hazırlanıyor
90
- 2. **İlk frame yakalanıyor** → GERÇEK kayıt başlangıcı (100-200ms sonra)
91
- 3. **Cursor tracking başlıyor** → Aynı timestamp'ten başlıyor
92
- 4. **Sonuç**: Cursor ve video TAM SENKRON!
93
-
94
- ### Timing Analizi
95
-
96
- - **Bekleme süresi**: ~30ms (çok hızlı!)
97
- - **İlk cursor event**: İlk frame'den 19ms sonra
98
- - **Senkronizasyon farkı**: <20ms (algılanamaz!)
99
-
100
- ### Ek İyileştirmeler
101
-
102
- 1. **5ms cursor interval** (200 FPS sampling)
103
- 2. **1px minimum threshold** (hassas tracking)
104
- 3. **Change detection filtering** (sadece hareket varsa kaydet)
105
-
106
- ## Test Etme
107
-
108
- ```bash
109
- node test-cursor-sync-mouse.js
110
- ```
111
-
112
- Test sırasında mouse'u hareket ettir ve tıkla. Sonuçta:
113
- - ✅ Video ve cursor aynı anda başlıyor
114
- - ✅ Tıklama event'leri doğru timing'de
115
- - ✅ Mouse hareketi smooth ve senkronize
116
-
117
- ## Kullanım
118
-
119
- ```javascript
120
- const recorder = new MacRecorder();
121
-
122
- // Normal kullanım - senkronizasyon otomatik!
123
- await recorder.startRecording('output.mov', {
124
- captureCursor: false, // Sistem cursor'unu gizle
125
- frameRate: 60
126
- });
127
-
128
- // Cursor tracking otomatik olarak video'nun ilk frame'ini bekler
129
- // ve mükemmel senkronizasyonla başlar!
130
- ```
131
-
132
- ## Özet
133
-
134
- 🎯 **Problem Çözüldü**: Cursor artık video ile TAM SENKRONIZE!
135
- ✅ **Timing**: İlk frame'den <20ms sonra cursor tracking başlıyor
136
- ⚡ **Performance**: Sadece ~30ms bekleme süresi
137
- 🔥 **Sonuç**: Mükemmel cursor-video sync!
138
-