node-mac-recorder 2.21.24 → 2.21.25
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 +14 -49
- package/package.json +1 -1
- package/src/avfoundation_recorder.mm +0 -14
- package/src/cursor_tracker.mm +1 -3
- package/src/mac_recorder.mm +0 -22
- package/src/screen_capture_kit.h +0 -1
- package/src/screen_capture_kit.mm +1 -17
- package/.cursor/worktrees.json +0 -5
- package/CURSOR-SYNC-FIX.md +0 -85
- package/CURSOR-SYNC-PERFECT.md +0 -138
package/index.js
CHANGED
|
@@ -551,8 +551,7 @@ class MacRecorder extends EventEmitter {
|
|
|
551
551
|
console.warn('❌ Native recording failed to start:', error.message);
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
//
|
|
555
|
-
// This ensures cursor and video start at the EXACT same time
|
|
554
|
+
// Only start cursor if native recording started successfully
|
|
556
555
|
if (success) {
|
|
557
556
|
const standardCursorOptions = {
|
|
558
557
|
videoRelative: true,
|
|
@@ -561,36 +560,11 @@ class MacRecorder extends EventEmitter {
|
|
|
561
560
|
this.options.captureArea ? 'area' : 'display',
|
|
562
561
|
captureArea: this.options.captureArea,
|
|
563
562
|
windowId: this.options.windowId,
|
|
564
|
-
startTimestamp: sessionTimestamp //
|
|
563
|
+
startTimestamp: sessionTimestamp // Use the same timestamp base
|
|
565
564
|
};
|
|
566
565
|
|
|
567
566
|
try {
|
|
568
|
-
|
|
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
|
-
|
|
567
|
+
console.log('🎯 SYNC: Starting cursor tracking at timestamp:', sessionTimestamp);
|
|
594
568
|
await this.startCursorCapture(cursorFilePath, standardCursorOptions);
|
|
595
569
|
console.log('✅ SYNC: Cursor tracking started successfully');
|
|
596
570
|
} catch (cursorError) {
|
|
@@ -1033,23 +1007,21 @@ class MacRecorder extends EventEmitter {
|
|
|
1033
1007
|
|
|
1034
1008
|
const last = this.lastCapturedData;
|
|
1035
1009
|
|
|
1036
|
-
// Event type değişmişse
|
|
1010
|
+
// Event type değişmişse
|
|
1037
1011
|
if (currentData.type !== last.type) {
|
|
1038
1012
|
return true;
|
|
1039
1013
|
}
|
|
1040
1014
|
|
|
1041
|
-
//
|
|
1042
|
-
if (
|
|
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
|
+
) {
|
|
1043
1020
|
return true;
|
|
1044
1021
|
}
|
|
1045
1022
|
|
|
1046
|
-
//
|
|
1047
|
-
|
|
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
|
-
) {
|
|
1023
|
+
// Cursor type değişmişse
|
|
1024
|
+
if (currentData.cursorType !== last.cursorType) {
|
|
1053
1025
|
return true;
|
|
1054
1026
|
}
|
|
1055
1027
|
|
|
@@ -1070,14 +1042,11 @@ class MacRecorder extends EventEmitter {
|
|
|
1070
1042
|
*/
|
|
1071
1043
|
async startCursorCapture(intervalOrFilepath = 100, options = {}) {
|
|
1072
1044
|
let filepath;
|
|
1073
|
-
|
|
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
|
|
1045
|
+
let interval = 20; // Default 50 FPS
|
|
1077
1046
|
|
|
1078
1047
|
// Parameter parsing: number = interval, string = filepath
|
|
1079
1048
|
if (typeof intervalOrFilepath === "number") {
|
|
1080
|
-
interval = Math.max(
|
|
1049
|
+
interval = Math.max(10, intervalOrFilepath); // Min 10ms
|
|
1081
1050
|
filepath = `cursor-data-${Date.now()}.json`;
|
|
1082
1051
|
} else if (typeof intervalOrFilepath === "string") {
|
|
1083
1052
|
filepath = intervalOrFilepath;
|
|
@@ -1172,10 +1141,6 @@ class MacRecorder extends EventEmitter {
|
|
|
1172
1141
|
|
|
1173
1142
|
return new Promise((resolve, reject) => {
|
|
1174
1143
|
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
1144
|
// Dosyayı oluştur ve temizle
|
|
1180
1145
|
const fs = require("fs");
|
|
1181
1146
|
fs.writeFileSync(filepath, "[");
|
|
@@ -1280,7 +1245,7 @@ class MacRecorder extends EventEmitter {
|
|
|
1280
1245
|
return resolve(false);
|
|
1281
1246
|
}
|
|
1282
1247
|
|
|
1283
|
-
//
|
|
1248
|
+
// Interval'ı durdur
|
|
1284
1249
|
clearInterval(this.cursorCaptureInterval);
|
|
1285
1250
|
this.cursorCaptureInterval = null;
|
|
1286
1251
|
|
package/package.json
CHANGED
|
@@ -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,
|
|
@@ -383,11 +380,6 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
383
380
|
CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(((double)g_avFrameNumber) / fps, 600));
|
|
384
381
|
BOOL appendSuccess = [localPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
|
|
385
382
|
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
383
|
g_avFrameNumber++;
|
|
392
384
|
} else {
|
|
393
385
|
NSLog(@"⚠️ Failed to append pixel buffer");
|
|
@@ -487,7 +479,6 @@ extern "C" bool stopAVFoundationRecording() {
|
|
|
487
479
|
g_avVideoInput = nil;
|
|
488
480
|
g_avPixelBufferAdaptor = nil;
|
|
489
481
|
g_avFrameNumber = 0;
|
|
490
|
-
g_avActualRecordingStartTime = 0;
|
|
491
482
|
|
|
492
483
|
MRLog(@"✅ AVFoundation recording stopped");
|
|
493
484
|
return true;
|
|
@@ -502,11 +493,6 @@ extern "C" bool isAVFoundationRecording() {
|
|
|
502
493
|
return g_avIsRecording;
|
|
503
494
|
}
|
|
504
495
|
|
|
505
|
-
// SYNC FIX: Get actual recording start time for AVFoundation
|
|
506
|
-
extern "C" NSTimeInterval getAVFoundationActualStartTime() {
|
|
507
|
-
return g_avActualRecordingStartTime;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
496
|
extern "C" NSString* getAVFoundationAudioPath() {
|
|
511
497
|
return g_avAudioOutputPath;
|
|
512
498
|
}
|
package/src/cursor_tracker.mm
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/mac_recorder.mm
CHANGED
|
@@ -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();
|
|
@@ -1058,26 +1057,6 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
|
|
|
1058
1057
|
return Napi::Boolean::New(env, isRecording);
|
|
1059
1058
|
}
|
|
1060
1059
|
|
|
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
1060
|
// NAPI Function: Get Window Thumbnail
|
|
1082
1061
|
Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
|
|
1083
1062
|
Napi::Env env = info.Env();
|
|
@@ -1406,7 +1385,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
1406
1385
|
exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
|
|
1407
1386
|
exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
|
|
1408
1387
|
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
1388
|
exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
|
|
1411
1389
|
|
|
1412
1390
|
// Thumbnail functions
|
package/src/screen_capture_kit.h
CHANGED
|
@@ -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
|
package/.cursor/worktrees.json
DELETED
package/CURSOR-SYNC-FIX.md
DELETED
|
@@ -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
|
-
|
package/CURSOR-SYNC-PERFECT.md
DELETED
|
@@ -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
|
-
|