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.
- package/.cursor/worktrees.json +5 -0
- package/CURSOR-SYNC-FIX.md +85 -0
- package/CURSOR-SYNC-PERFECT.md +138 -0
- package/index.js +49 -14
- package/package.json +1 -1
- package/src/avfoundation_recorder.mm +14 -0
- package/src/cursor_tracker.mm +3 -1
- package/src/mac_recorder.mm +22 -0
- package/src/screen_capture_kit.h +1 -0
- package/src/screen_capture_kit.mm +17 -1
|
@@ -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
|
-
//
|
|
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 //
|
|
564
|
+
startTimestamp: sessionTimestamp // Will be updated with actual start time
|
|
564
565
|
};
|
|
565
566
|
|
|
566
567
|
try {
|
|
567
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1024
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
1283
|
+
// Stop JavaScript interval
|
|
1249
1284
|
clearInterval(this.cursorCaptureInterval);
|
|
1250
1285
|
this.cursorCaptureInterval = null;
|
|
1251
1286
|
|
package/package.json
CHANGED
|
@@ -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
|
}
|
package/src/cursor_tracker.mm
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/mac_recorder.mm
CHANGED
|
@@ -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
|
package/src/screen_capture_kit.h
CHANGED
|
@@ -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
|
-
|
|
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
|