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 +60 -64
- package/package.json +1 -1
- package/src/avfoundation_recorder.mm +8 -16
- package/src/cursor_tracker.mm +1 -3
- package/src/mac_recorder.mm +24 -61
- 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
|
@@ -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
|
-
//
|
|
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:
|
|
569
|
+
startTimestamp: syncTimestamp // Align cursor timeline to actual start
|
|
565
570
|
};
|
|
566
571
|
|
|
567
572
|
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
|
-
|
|
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:',
|
|
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:
|
|
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:',
|
|
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:
|
|
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 ${
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
1041
|
+
// Event type değişmişse
|
|
1037
1042
|
if (currentData.type !== last.type) {
|
|
1038
1043
|
return true;
|
|
1039
1044
|
}
|
|
1040
1045
|
|
|
1041
|
-
//
|
|
1042
|
-
if (
|
|
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
|
-
//
|
|
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
|
-
) {
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
1279
|
+
// Interval'ı durdur
|
|
1284
1280
|
clearInterval(this.cursorCaptureInterval);
|
|
1285
1281
|
this.cursorCaptureInterval = null;
|
|
1286
1282
|
|
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,
|
|
@@ -192,8 +189,7 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
192
189
|
return false;
|
|
193
190
|
}
|
|
194
191
|
|
|
195
|
-
g_avStartTime =
|
|
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
|
-
|
|
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
|
}
|
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();
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
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
|
-
|