node-mac-recorder 2.21.23 → 2.21.24

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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "setup-worktree": [
3
+ "npm install"
4
+ ]
5
+ }
@@ -0,0 +1,85 @@
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
+
@@ -0,0 +1,138 @@
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
+
package/index.js CHANGED
@@ -551,7 +551,8 @@ class MacRecorder extends EventEmitter {
551
551
  console.warn('❌ Native recording failed to start:', error.message);
552
552
  }
553
553
 
554
- // Only start cursor if native recording started successfully
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
556
  if (success) {
556
557
  const standardCursorOptions = {
557
558
  videoRelative: true,
@@ -560,11 +561,36 @@ class MacRecorder extends EventEmitter {
560
561
  this.options.captureArea ? 'area' : 'display',
561
562
  captureArea: this.options.captureArea,
562
563
  windowId: this.options.windowId,
563
- startTimestamp: sessionTimestamp // Use the same timestamp base
564
+ startTimestamp: sessionTimestamp // Will be updated with actual start time
564
565
  };
565
566
 
566
567
  try {
567
- console.log('🎯 SYNC: Starting cursor tracking at timestamp:', sessionTimestamp);
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
+
568
594
  await this.startCursorCapture(cursorFilePath, standardCursorOptions);
569
595
  console.log('✅ SYNC: Cursor tracking started successfully');
570
596
  } catch (cursorError) {
@@ -1007,21 +1033,23 @@ class MacRecorder extends EventEmitter {
1007
1033
 
1008
1034
  const last = this.lastCapturedData;
1009
1035
 
1010
- // Event type değişmişse
1036
+ // Event type değişmişse (click, drag, vs)
1011
1037
  if (currentData.type !== last.type) {
1012
1038
  return true;
1013
1039
  }
1014
1040
 
1015
- // Pozisyon değişmişse (minimum 2 pixel tolerans)
1016
- if (
1017
- Math.abs(currentData.x - last.x) >= 2 ||
1018
- Math.abs(currentData.y - last.y) >= 2
1019
- ) {
1041
+ // Cursor type değişmişse (pointer, text, vs)
1042
+ if (currentData.cursorType !== last.cursorType) {
1020
1043
  return true;
1021
1044
  }
1022
1045
 
1023
- // Cursor type değişmişse
1024
- if (currentData.cursorType !== last.cursorType) {
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
+ ) {
1025
1053
  return true;
1026
1054
  }
1027
1055
 
@@ -1042,11 +1070,14 @@ class MacRecorder extends EventEmitter {
1042
1070
  */
1043
1071
  async startCursorCapture(intervalOrFilepath = 100, options = {}) {
1044
1072
  let filepath;
1045
- let interval = 20; // Default 50 FPS
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
1046
1077
 
1047
1078
  // Parameter parsing: number = interval, string = filepath
1048
1079
  if (typeof intervalOrFilepath === "number") {
1049
- interval = Math.max(10, intervalOrFilepath); // Min 10ms
1080
+ interval = Math.max(5, intervalOrFilepath); // Min 5ms for sync
1050
1081
  filepath = `cursor-data-${Date.now()}.json`;
1051
1082
  } else if (typeof intervalOrFilepath === "string") {
1052
1083
  filepath = intervalOrFilepath;
@@ -1141,6 +1172,10 @@ class MacRecorder extends EventEmitter {
1141
1172
 
1142
1173
  return new Promise((resolve, reject) => {
1143
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
+
1144
1179
  // Dosyayı oluştur ve temizle
1145
1180
  const fs = require("fs");
1146
1181
  fs.writeFileSync(filepath, "[");
@@ -1245,7 +1280,7 @@ class MacRecorder extends EventEmitter {
1245
1280
  return resolve(false);
1246
1281
  }
1247
1282
 
1248
- // Interval'ı durdur
1283
+ // Stop JavaScript interval
1249
1284
  clearInterval(this.cursorCaptureInterval);
1250
1285
  this.cursorCaptureInterval = null;
1251
1286
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.23",
3
+ "version": "2.21.24",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -25,6 +25,9 @@ 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
+
28
31
  // AVFoundation screen recording implementation
29
32
  extern "C" bool startAVFoundationRecording(const std::string& outputPath,
30
33
  CGDirectDisplayID displayID,
@@ -380,6 +383,11 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
380
383
  CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(((double)g_avFrameNumber) / fps, 600));
381
384
  BOOL appendSuccess = [localPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
382
385
  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
+ }
383
391
  g_avFrameNumber++;
384
392
  } else {
385
393
  NSLog(@"⚠️ Failed to append pixel buffer");
@@ -479,6 +487,7 @@ extern "C" bool stopAVFoundationRecording() {
479
487
  g_avVideoInput = nil;
480
488
  g_avPixelBufferAdaptor = nil;
481
489
  g_avFrameNumber = 0;
490
+ g_avActualRecordingStartTime = 0;
482
491
 
483
492
  MRLog(@"✅ AVFoundation recording stopped");
484
493
  return true;
@@ -493,6 +502,11 @@ extern "C" bool isAVFoundationRecording() {
493
502
  return g_avIsRecording;
494
503
  }
495
504
 
505
+ // SYNC FIX: Get actual recording start time for AVFoundation
506
+ extern "C" NSTimeInterval getAVFoundationActualStartTime() {
507
+ return g_avActualRecordingStartTime;
508
+ }
509
+
496
510
  extern "C" NSString* getAVFoundationAudioPath() {
497
511
  return g_avAudioOutputPath;
498
512
  }
@@ -1032,7 +1032,9 @@ 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
- g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
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
1036
1038
  target:g_timerTarget
1037
1039
  selector:@selector(timerCallback:)
1038
1040
  userInfo:nil
@@ -23,6 +23,7 @@ extern "C" {
23
23
  double frameRate);
24
24
  bool stopAVFoundationRecording();
25
25
  bool isAVFoundationRecording();
26
+ NSTimeInterval getAVFoundationActualStartTime();
26
27
  NSString* getAVFoundationAudioPath();
27
28
 
28
29
  NSArray<NSDictionary *> *listCameraDevices();
@@ -1057,6 +1058,26 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
1057
1058
  return Napi::Boolean::New(env, isRecording);
1058
1059
  }
1059
1060
 
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
+
1060
1081
  // NAPI Function: Get Window Thumbnail
1061
1082
  Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
1062
1083
  Napi::Env env = info.Env();
@@ -1385,6 +1406,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1385
1406
  exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
1386
1407
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
1387
1408
  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));
1388
1410
  exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
1389
1411
 
1390
1412
  // Thumbnail functions
@@ -15,5 +15,6 @@ API_AVAILABLE(macos(12.3))
15
15
  + (void)finalizeRecording;
16
16
  + (void)finalizeVideoWriter;
17
17
  + (void)cleanupVideoWriter;
18
+ + (NSTimeInterval)getActualRecordingStartTime;
18
19
 
19
20
  @end
@@ -38,6 +38,9 @@ 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
+
41
44
  static void CleanupWriters(void);
42
45
  static AVAssetWriterInputPixelBufferAdaptor * _Nullable CurrentPixelBufferAdaptor(void) {
43
46
  if (!g_pixelBufferAdaptorRef) {
@@ -99,6 +102,7 @@ static void CleanupWriters(void) {
99
102
  // Reset frame counting
100
103
  g_frameCount = 0;
101
104
  g_firstFrameTime = 0;
105
+ g_actualRecordingStartTime = 0;
102
106
  }
103
107
 
104
108
  if (g_audioWriter) {
@@ -190,7 +194,12 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
190
194
  [g_videoWriter startSessionAtSourceTime:presentationTime];
191
195
  g_videoStartTime = presentationTime;
192
196
  g_videoWriterStarted = YES;
193
- MRLog(@"🎞️ Video writer session started @ %.3f", CMTimeGetSeconds(presentationTime));
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);
194
203
  }
195
204
 
196
205
  if (!g_videoInput.readyForMoreMediaData) {
@@ -945,9 +954,16 @@ BOOL isScreenCaptureKitCleaningUp() API_AVAILABLE(macos(12.3)) {
945
954
  g_isRecording = NO;
946
955
  g_isCleaningUp = NO; // Reset cleanup flag
947
956
  g_outputPath = nil;
957
+ g_actualRecordingStartTime = 0;
948
958
 
949
959
  MRLog(@"🧹 Pure ScreenCaptureKit cleanup complete");
950
960
  }
951
961
  }
952
962
 
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
+
953
969
  @end