node-mac-recorder 2.23.3 → 2.24.1
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/binding.gyp +1 -0
- package/index.js +173 -0
- package/package.json +1 -1
- package/src/keyboard_tracker.mm +291 -0
- package/src/mac_recorder.mm +8 -2
package/binding.gyp
CHANGED
package/index.js
CHANGED
|
@@ -65,6 +65,9 @@ class MacRecorder extends EventEmitter {
|
|
|
65
65
|
this.syncTimestamp = null;
|
|
66
66
|
this.audioCaptureFile = null;
|
|
67
67
|
this.audioCaptureActive = false;
|
|
68
|
+
// Keyboard (shortcut) capture variables
|
|
69
|
+
this.keyboardCaptureFile = null;
|
|
70
|
+
this.keyboardCaptureActive = false;
|
|
68
71
|
|
|
69
72
|
this.options = {
|
|
70
73
|
includeMicrophone: false, // Default olarak mikrofon kapalı
|
|
@@ -747,6 +750,7 @@ class MacRecorder extends EventEmitter {
|
|
|
747
750
|
this.outputPath = outputPath;
|
|
748
751
|
|
|
749
752
|
const cursorFilePath = path.join(outputDir, `temp_cursor_${sessionTimestamp}.json`);
|
|
753
|
+
const keyboardFilePath = path.join(outputDir, `temp_keyboard_${sessionTimestamp}.json`);
|
|
750
754
|
// CRITICAL FIX: Use .mov extension for camera (native recorder uses .mov, not .webm)
|
|
751
755
|
let cameraFilePath =
|
|
752
756
|
this.options.captureCamera === true
|
|
@@ -862,6 +866,31 @@ class MacRecorder extends EventEmitter {
|
|
|
862
866
|
this.recordingStartTime = syncTimestamp;
|
|
863
867
|
console.log(`🎯 CURSOR SYNC: Cursor tracking will use timestamp: ${syncTimestamp}`);
|
|
864
868
|
|
|
869
|
+
// CURSOR/VIDEO TIME ALIGNMENT:
|
|
870
|
+
// Cursor timeline'in t=0'i bu andir (syncTimestamp), ama videonun
|
|
871
|
+
// ILK KARESI daha once yakalanmis olabilir (ScreenCaptureKit
|
|
872
|
+
// baslatma gecikmesi + yukaridaki hazir-olma beklemesi). Editor
|
|
873
|
+
// "cursor'un ilk ornegi = video t=0" varsayarsa aradaki fark sabit
|
|
874
|
+
// bir zaman kaymasi olarak kalir. Native gercek video baslangicini
|
|
875
|
+
// biliyor; okuyup sakla ki stop'ta cursor JSON'una yazabilelim.
|
|
876
|
+
this.videoStartTimestamp = 0;
|
|
877
|
+
try {
|
|
878
|
+
const nativeVideoStart =
|
|
879
|
+
typeof nativeBinding.getVideoStartTimestamp === 'function'
|
|
880
|
+
? Number(nativeBinding.getVideoStartTimestamp())
|
|
881
|
+
: 0;
|
|
882
|
+
if (Number.isFinite(nativeVideoStart) && nativeVideoStart > 0) {
|
|
883
|
+
this.videoStartTimestamp = nativeVideoStart;
|
|
884
|
+
console.log(
|
|
885
|
+
`🎯 SYNC: Video first-frame timestamp: ${nativeVideoStart} (cursor starts ${(syncTimestamp - nativeVideoStart).toFixed(0)}ms later)`
|
|
886
|
+
);
|
|
887
|
+
} else {
|
|
888
|
+
console.warn('⚠️ SYNC: Video start timestamp unavailable — cursor sync metadata will be skipped');
|
|
889
|
+
}
|
|
890
|
+
} catch (videoStartError) {
|
|
891
|
+
console.warn('⚠️ SYNC: Video start timestamp read failed:', videoStartError.message);
|
|
892
|
+
}
|
|
893
|
+
|
|
865
894
|
const standardCursorOptions = {
|
|
866
895
|
videoRelative: true,
|
|
867
896
|
displayInfo: this.recordingDisplayInfo,
|
|
@@ -880,6 +909,16 @@ class MacRecorder extends EventEmitter {
|
|
|
880
909
|
console.warn('⚠️ Cursor tracking failed to start:', cursorError.message);
|
|
881
910
|
// Continue with recording even if cursor fails - don't stop native recording
|
|
882
911
|
}
|
|
912
|
+
|
|
913
|
+
// Klavye kısayolu yakalama (cursor ile AYNI zaman referansı)
|
|
914
|
+
try {
|
|
915
|
+
console.log('⌨️ SYNC: Starting keyboard shortcut capture at timestamp:', syncTimestamp);
|
|
916
|
+
await this.startKeyboardCapture(keyboardFilePath, { startTimestamp: syncTimestamp });
|
|
917
|
+
console.log('✅ SYNC: Keyboard shortcut capture started successfully');
|
|
918
|
+
} catch (keyboardError) {
|
|
919
|
+
console.warn('⚠️ Keyboard capture failed to start:', keyboardError.message);
|
|
920
|
+
// Klavye başlamazsa kayıt devam etsin
|
|
921
|
+
}
|
|
883
922
|
}
|
|
884
923
|
|
|
885
924
|
if (success) {
|
|
@@ -983,6 +1022,7 @@ class MacRecorder extends EventEmitter {
|
|
|
983
1022
|
cameraOutputPath: this.cameraCaptureFile || null,
|
|
984
1023
|
audioOutputPath: this.audioCaptureFile || null,
|
|
985
1024
|
cursorOutputPath: cursorFilePath,
|
|
1025
|
+
keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
|
|
986
1026
|
sessionTimestamp: fileTimestampPayload,
|
|
987
1027
|
syncTimestamp: startTimestampPayload,
|
|
988
1028
|
fileTimestamp: fileTimestampPayload,
|
|
@@ -1003,6 +1043,7 @@ class MacRecorder extends EventEmitter {
|
|
|
1003
1043
|
cameraOutputPath: this.cameraCaptureFile || null,
|
|
1004
1044
|
audioOutputPath: this.audioCaptureFile || null,
|
|
1005
1045
|
cursorOutputPath: cursorFilePath,
|
|
1046
|
+
keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
|
|
1006
1047
|
sessionTimestamp: fileTimestampPayload,
|
|
1007
1048
|
syncTimestamp: startTimestampPayload,
|
|
1008
1049
|
fileTimestamp: fileTimestampPayload,
|
|
@@ -1026,6 +1067,7 @@ class MacRecorder extends EventEmitter {
|
|
|
1026
1067
|
cameraOutputPath: this.cameraCaptureFile || null,
|
|
1027
1068
|
audioOutputPath: this.audioCaptureFile || null,
|
|
1028
1069
|
cursorOutputPath: cursorFilePath,
|
|
1070
|
+
keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
|
|
1029
1071
|
sessionTimestamp: fileTimestampPayload,
|
|
1030
1072
|
syncTimestamp: startTimestampPayload,
|
|
1031
1073
|
fileTimestamp: fileTimestampPayload,
|
|
@@ -1107,6 +1149,17 @@ class MacRecorder extends EventEmitter {
|
|
|
1107
1149
|
}
|
|
1108
1150
|
}
|
|
1109
1151
|
|
|
1152
|
+
// Klavye kısayolu yakalamayı durdur (instant)
|
|
1153
|
+
if (this.keyboardCaptureActive) {
|
|
1154
|
+
try {
|
|
1155
|
+
console.log('🛑 SYNC: Stopping keyboard shortcut capture');
|
|
1156
|
+
await this.stopKeyboardCapture();
|
|
1157
|
+
console.log('✅ SYNC: Keyboard shortcut capture stopped');
|
|
1158
|
+
} catch (keyboardError) {
|
|
1159
|
+
console.warn('⚠️ Keyboard capture failed to stop:', keyboardError.message);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1110
1163
|
let success = false;
|
|
1111
1164
|
|
|
1112
1165
|
// 2. Stop native screen recording
|
|
@@ -1681,12 +1734,20 @@ class MacRecorder extends EventEmitter {
|
|
|
1681
1734
|
this.cursorCaptureInterval = null;
|
|
1682
1735
|
|
|
1683
1736
|
// Dosyayı kapat
|
|
1737
|
+
let closedCursorFile = null;
|
|
1684
1738
|
if (this.cursorCaptureFile) {
|
|
1685
1739
|
const fs = require("fs");
|
|
1686
1740
|
fs.appendFileSync(this.cursorCaptureFile, "]");
|
|
1741
|
+
closedCursorFile = this.cursorCaptureFile;
|
|
1687
1742
|
this.cursorCaptureFile = null;
|
|
1688
1743
|
}
|
|
1689
1744
|
|
|
1745
|
+
// Cursor/video zaman hizalama bilgisini dosyaya yaz (editör bunu
|
|
1746
|
+
// okuyup sabit kaymayı telafi ediyor).
|
|
1747
|
+
if (closedCursorFile) {
|
|
1748
|
+
this._writeCursorSyncMetadata(closedCursorFile);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1690
1751
|
// Değişkenleri temizle
|
|
1691
1752
|
this.lastCapturedData = null;
|
|
1692
1753
|
this.cursorCaptureStartTime = null;
|
|
@@ -1701,6 +1762,118 @@ class MacRecorder extends EventEmitter {
|
|
|
1701
1762
|
});
|
|
1702
1763
|
}
|
|
1703
1764
|
|
|
1765
|
+
/**
|
|
1766
|
+
* Cursor JSON'una video/cursor zaman hizalama bilgisini yazar.
|
|
1767
|
+
*
|
|
1768
|
+
* NEDEN: Cursor timeline'inin t=0'i `syncTimestamp` (kayit hazir olduktan
|
|
1769
|
+
* sonraki an), videonun t=0'i ise ILK KARENIN yakalandigi an. ScreenCaptureKit
|
|
1770
|
+
* baslatma gecikmesi yuzunden bu ikisi ayni degil; fark bilinmezse oynatimda
|
|
1771
|
+
* sabit bir zaman kaymasi olusur (cursor video ile "sync tutmuyor").
|
|
1772
|
+
* Editor tarafi `_syncMetadata.videoStartTime` / `cursorStartTime` okuyup
|
|
1773
|
+
* tam telafi ediyor — burada sadece gercek degerleri yaziyoruz.
|
|
1774
|
+
*
|
|
1775
|
+
* Dosyanin ilk noktasina yazilir; okuyucu ilk metadata'yi bulup kullanir.
|
|
1776
|
+
*/
|
|
1777
|
+
_writeCursorSyncMetadata(cursorFilePath) {
|
|
1778
|
+
const videoStartTime = Number(this.videoStartTimestamp);
|
|
1779
|
+
const cursorStartTime = Number(this.syncTimestamp);
|
|
1780
|
+
if (
|
|
1781
|
+
!Number.isFinite(videoStartTime) ||
|
|
1782
|
+
videoStartTime <= 0 ||
|
|
1783
|
+
!Number.isFinite(cursorStartTime) ||
|
|
1784
|
+
cursorStartTime <= 0
|
|
1785
|
+
) {
|
|
1786
|
+
// Native video baslangici okunamadiysa metadata yazma — editor eski
|
|
1787
|
+
// (telafisiz) davranisa duser, yanlis bir offset uygulamaktan iyidir.
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
try {
|
|
1792
|
+
const fs = require("fs");
|
|
1793
|
+
const raw = fs.readFileSync(cursorFilePath, "utf8");
|
|
1794
|
+
const positions = JSON.parse(raw);
|
|
1795
|
+
if (!Array.isArray(positions) || positions.length === 0) return;
|
|
1796
|
+
|
|
1797
|
+
positions[0]._syncMetadata = {
|
|
1798
|
+
videoStartTime,
|
|
1799
|
+
cursorStartTime,
|
|
1800
|
+
startDelayMs: cursorStartTime - videoStartTime,
|
|
1801
|
+
recordingType: this.options?.windowId
|
|
1802
|
+
? "window"
|
|
1803
|
+
: this.options?.captureArea
|
|
1804
|
+
? "area"
|
|
1805
|
+
: "display",
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
fs.writeFileSync(cursorFilePath, JSON.stringify(positions));
|
|
1809
|
+
console.log(
|
|
1810
|
+
`🎯 SYNC: Cursor sync metadata written (video→cursor delay ${(cursorStartTime - videoStartTime).toFixed(0)}ms)`
|
|
1811
|
+
);
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
console.warn("⚠️ SYNC: Cursor sync metadata write failed:", error.message);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Klavye kısayolu (shortcut) yakalamayı başlatır.
|
|
1819
|
+
* Native CGEventTap keyDown olaylarını dinler; yalnızca ⌘/⌃/⌥ modifier'lı
|
|
1820
|
+
* basımları zaman damgalı JSON olarak dosyaya yazar (gizlilik: düz yazım kaydedilmez).
|
|
1821
|
+
* @param {string} filepath - Keyboard data JSON dosya yolu
|
|
1822
|
+
* @param {Object} options
|
|
1823
|
+
* @param {number} options.startTimestamp - Cursor/video ile hizalamak için başlangıç zamanı (ms)
|
|
1824
|
+
*/
|
|
1825
|
+
async startKeyboardCapture(filepath, options = {}) {
|
|
1826
|
+
if (typeof filepath !== "string" || !filepath) {
|
|
1827
|
+
throw new Error("Keyboard capture filepath (string) required");
|
|
1828
|
+
}
|
|
1829
|
+
if (
|
|
1830
|
+
!nativeBinding ||
|
|
1831
|
+
typeof nativeBinding.startKeyboardTracking !== "function"
|
|
1832
|
+
) {
|
|
1833
|
+
// Eski native binary — sessizce atla
|
|
1834
|
+
throw new Error("Native keyboard tracking not available in this build");
|
|
1835
|
+
}
|
|
1836
|
+
if (this.keyboardCaptureActive) {
|
|
1837
|
+
throw new Error("Keyboard capture is already running");
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
const startTimestamp = Number(options.startTimestamp) || Date.now();
|
|
1841
|
+
const started = nativeBinding.startKeyboardTracking(filepath, startTimestamp);
|
|
1842
|
+
if (!started) {
|
|
1843
|
+
throw new Error(
|
|
1844
|
+
"Failed to start keyboard tracking (Accessibility permission may be required)"
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
this.keyboardCaptureFile = filepath;
|
|
1849
|
+
this.keyboardCaptureActive = true;
|
|
1850
|
+
this.emit("keyboardCaptureStarted", filepath);
|
|
1851
|
+
return true;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* Klavye kısayolu yakalamayı durdurur ve JSON dosyasını kapatır.
|
|
1856
|
+
*/
|
|
1857
|
+
async stopKeyboardCapture() {
|
|
1858
|
+
if (
|
|
1859
|
+
!nativeBinding ||
|
|
1860
|
+
typeof nativeBinding.stopKeyboardTracking !== "function"
|
|
1861
|
+
) {
|
|
1862
|
+
this.keyboardCaptureActive = false;
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
if (!this.keyboardCaptureActive) {
|
|
1866
|
+
return false;
|
|
1867
|
+
}
|
|
1868
|
+
try {
|
|
1869
|
+
nativeBinding.stopKeyboardTracking();
|
|
1870
|
+
} finally {
|
|
1871
|
+
this.keyboardCaptureActive = false;
|
|
1872
|
+
this.emit("keyboardCaptureStopped");
|
|
1873
|
+
}
|
|
1874
|
+
return true;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1704
1877
|
/**
|
|
1705
1878
|
* Anlık cursor pozisyonunu ve tipini döndürür
|
|
1706
1879
|
* Display-relative koordinatlar döner (her zaman pozitif)
|
package/package.json
CHANGED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// keyboard_tracker.mm
|
|
2
|
+
// Kayıt sırasında kullanıcının klavye KISAYOLLARINI (shortcut) yakalar.
|
|
3
|
+
// cursor_tracker.mm'deki CGEventTap altyapısıyla aynı desende çalışır: global bir
|
|
4
|
+
// keyDown event tap kurar, her kısayol basımını zaman damgalı JSON olarak dosyaya
|
|
5
|
+
// yazar. GİZLİLİK: yalnızca bir modifier (⌘ / ⌃ / ⌥) ile birlikte basılan tuşlar
|
|
6
|
+
// yakalanır; düz metin yazımı (parola vb.) KAYDEDİLMEZ.
|
|
7
|
+
|
|
8
|
+
#import <napi.h>
|
|
9
|
+
#import <AppKit/AppKit.h>
|
|
10
|
+
#import <Foundation/Foundation.h>
|
|
11
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
12
|
+
#import <Carbon/Carbon.h>
|
|
13
|
+
#import "logging.h"
|
|
14
|
+
|
|
15
|
+
// ---- Global durum (tek aktif oturum) --------------------------------------
|
|
16
|
+
static CFMachPortRef g_kbEventTap = NULL;
|
|
17
|
+
static CFRunLoopSourceRef g_kbRunLoopSource = NULL;
|
|
18
|
+
static NSFileHandle *g_kbFileHandle = nil;
|
|
19
|
+
static NSString *g_kbOutputPath = nil;
|
|
20
|
+
static BOOL g_kbIsTracking = NO;
|
|
21
|
+
static BOOL g_kbIsFirstWrite = YES;
|
|
22
|
+
static double g_kbStartUnixMs = 0.0; // Senkron için referans başlangıç (ms)
|
|
23
|
+
|
|
24
|
+
static double NowUnixMs() {
|
|
25
|
+
return [[NSDate date] timeIntervalSince1970] * 1000.0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Özel (yazılamayan) tuşların okunabilir isimleri. Diğer tuşlar için
|
|
29
|
+
// charactersIgnoringModifiers kullanılır.
|
|
30
|
+
static NSString *SpecialKeyName(unsigned short keyCode) {
|
|
31
|
+
switch (keyCode) {
|
|
32
|
+
case kVK_Return: return @"Enter";
|
|
33
|
+
case kVK_ANSI_KeypadEnter: return @"Enter";
|
|
34
|
+
case kVK_Tab: return @"Tab";
|
|
35
|
+
case kVK_Space: return @"Space";
|
|
36
|
+
case kVK_Delete: return @"Delete"; // Backspace
|
|
37
|
+
case kVK_ForwardDelete: return @"ForwardDelete";
|
|
38
|
+
case kVK_Escape: return @"Esc";
|
|
39
|
+
case kVK_Home: return @"Home";
|
|
40
|
+
case kVK_End: return @"End";
|
|
41
|
+
case kVK_PageUp: return @"PageUp";
|
|
42
|
+
case kVK_PageDown: return @"PageDown";
|
|
43
|
+
case kVK_LeftArrow: return @"Left";
|
|
44
|
+
case kVK_RightArrow: return @"Right";
|
|
45
|
+
case kVK_DownArrow: return @"Down";
|
|
46
|
+
case kVK_UpArrow: return @"Up";
|
|
47
|
+
case kVK_F1: return @"F1";
|
|
48
|
+
case kVK_F2: return @"F2";
|
|
49
|
+
case kVK_F3: return @"F3";
|
|
50
|
+
case kVK_F4: return @"F4";
|
|
51
|
+
case kVK_F5: return @"F5";
|
|
52
|
+
case kVK_F6: return @"F6";
|
|
53
|
+
case kVK_F7: return @"F7";
|
|
54
|
+
case kVK_F8: return @"F8";
|
|
55
|
+
case kVK_F9: return @"F9";
|
|
56
|
+
case kVK_F10: return @"F10";
|
|
57
|
+
case kVK_F11: return @"F11";
|
|
58
|
+
case kVK_F12: return @"F12";
|
|
59
|
+
default: return nil;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static NSString *JsonEscape(NSString *input) {
|
|
64
|
+
if (!input) return @"";
|
|
65
|
+
NSMutableString *out = [NSMutableString stringWithCapacity:input.length + 2];
|
|
66
|
+
NSUInteger len = input.length;
|
|
67
|
+
for (NSUInteger i = 0; i < len; i++) {
|
|
68
|
+
unichar c = [input characterAtIndex:i];
|
|
69
|
+
switch (c) {
|
|
70
|
+
case '"': [out appendString:@"\\\""]; break;
|
|
71
|
+
case '\\': [out appendString:@"\\\\"]; break;
|
|
72
|
+
case '\n': [out appendString:@"\\n"]; break;
|
|
73
|
+
case '\r': [out appendString:@"\\r"]; break;
|
|
74
|
+
case '\t': [out appendString:@"\\t"]; break;
|
|
75
|
+
default:
|
|
76
|
+
if (c < 0x20) {
|
|
77
|
+
[out appendFormat:@"\\u%04x", c];
|
|
78
|
+
} else {
|
|
79
|
+
[out appendFormat:@"%C", c];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static void WriteKeyboardEvent(NSString *jsonObject) {
|
|
87
|
+
if (!g_kbFileHandle || !jsonObject) return;
|
|
88
|
+
@try {
|
|
89
|
+
NSString *chunk = g_kbIsFirstWrite ? jsonObject : [@"," stringByAppendingString:jsonObject];
|
|
90
|
+
[g_kbFileHandle writeData:[chunk dataUsingEncoding:NSUTF8StringEncoding]];
|
|
91
|
+
g_kbIsFirstWrite = NO;
|
|
92
|
+
} @catch (NSException *e) {
|
|
93
|
+
// Sessizce devam et
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// keyDown event callback — yalnızca modifier'lı basımları (kısayol) yazar.
|
|
98
|
+
static CGEventRef KeyboardEventCallback(CGEventTapProxy proxy,
|
|
99
|
+
CGEventType type,
|
|
100
|
+
CGEventRef event,
|
|
101
|
+
void *refcon) {
|
|
102
|
+
if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) {
|
|
103
|
+
if (g_kbEventTap) {
|
|
104
|
+
CGEventTapEnable(g_kbEventTap, true);
|
|
105
|
+
}
|
|
106
|
+
return event;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (type != kCGEventKeyDown || !g_kbIsTracking) {
|
|
110
|
+
return event;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@autoreleasepool {
|
|
114
|
+
CGEventFlags flags = CGEventGetFlags(event);
|
|
115
|
+
BOOL hasCommand = (flags & kCGEventFlagMaskCommand) != 0;
|
|
116
|
+
BOOL hasControl = (flags & kCGEventFlagMaskControl) != 0;
|
|
117
|
+
BOOL hasOption = (flags & kCGEventFlagMaskAlternate) != 0;
|
|
118
|
+
BOOL hasShift = (flags & kCGEventFlagMaskShift) != 0;
|
|
119
|
+
BOOL hasFn = (flags & kCGEventFlagMaskSecondaryFn) != 0;
|
|
120
|
+
|
|
121
|
+
// GİZLİLİK: Kısayol = ⌘ / ⌃ / ⌥ içermeli. Sadece Shift veya düz tuş → atla.
|
|
122
|
+
if (!(hasCommand || hasControl || hasOption)) {
|
|
123
|
+
return event;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
unsigned short keyCode = (unsigned short)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
|
|
127
|
+
|
|
128
|
+
NSString *keyName = SpecialKeyName(keyCode);
|
|
129
|
+
NSString *chars = @"";
|
|
130
|
+
NSEvent *nsEvent = nil;
|
|
131
|
+
@try {
|
|
132
|
+
nsEvent = [NSEvent eventWithCGEvent:event];
|
|
133
|
+
} @catch (NSException *e) {
|
|
134
|
+
nsEvent = nil;
|
|
135
|
+
}
|
|
136
|
+
if (!keyName) {
|
|
137
|
+
NSString *raw = nsEvent ? [nsEvent charactersIgnoringModifiers] : nil;
|
|
138
|
+
if (raw.length > 0) {
|
|
139
|
+
chars = raw;
|
|
140
|
+
keyName = [raw uppercaseString];
|
|
141
|
+
} else {
|
|
142
|
+
keyName = [NSString stringWithFormat:@"Key%d", (int)keyCode];
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
chars = keyName;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
double unixMs = NowUnixMs();
|
|
149
|
+
double relMs = g_kbStartUnixMs > 0 ? (unixMs - g_kbStartUnixMs) : 0.0;
|
|
150
|
+
if (relMs < 0) relMs = 0;
|
|
151
|
+
|
|
152
|
+
NSString *json = [NSString stringWithFormat:
|
|
153
|
+
@"{\"timestamp\":%.0f,\"unixTimeMs\":%.0f,\"type\":\"keydown\",\"keyCode\":%d,"
|
|
154
|
+
@"\"key\":\"%@\",\"chars\":\"%@\",\"modifiers\":{\"meta\":%@,\"control\":%@,"
|
|
155
|
+
@"\"alt\":%@,\"shift\":%@,\"fn\":%@}}",
|
|
156
|
+
relMs, unixMs, (int)keyCode,
|
|
157
|
+
JsonEscape(keyName), JsonEscape(chars),
|
|
158
|
+
hasCommand ? @"true" : @"false",
|
|
159
|
+
hasControl ? @"true" : @"false",
|
|
160
|
+
hasOption ? @"true" : @"false",
|
|
161
|
+
hasShift ? @"true" : @"false",
|
|
162
|
+
hasFn ? @"true" : @"false"];
|
|
163
|
+
|
|
164
|
+
WriteKeyboardEvent(json);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return event;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static void CleanupKeyboardTracking() {
|
|
171
|
+
g_kbIsTracking = NO;
|
|
172
|
+
|
|
173
|
+
if (g_kbFileHandle) {
|
|
174
|
+
@try {
|
|
175
|
+
// Açılış "[" başlangıçta yazıldığı için burada her zaman sadece "]" ile kapat.
|
|
176
|
+
// (boş durumda dosya "[]" olur, dolu durumda "[{...},{...}]")
|
|
177
|
+
[g_kbFileHandle writeData:[@"]" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
178
|
+
[g_kbFileHandle synchronizeFile];
|
|
179
|
+
[g_kbFileHandle closeFile];
|
|
180
|
+
} @catch (NSException *e) {
|
|
181
|
+
// yut
|
|
182
|
+
}
|
|
183
|
+
g_kbFileHandle = nil;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (g_kbEventTap) {
|
|
187
|
+
CGEventTapEnable(g_kbEventTap, false);
|
|
188
|
+
// CFRelease yapmıyoruz — cursor_tracker.mm ile aynı yaklaşım (sistem yönetsin)
|
|
189
|
+
g_kbEventTap = NULL;
|
|
190
|
+
}
|
|
191
|
+
if (g_kbRunLoopSource) {
|
|
192
|
+
g_kbRunLoopSource = NULL;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
g_kbOutputPath = nil;
|
|
196
|
+
g_kbIsFirstWrite = YES;
|
|
197
|
+
g_kbStartUnixMs = 0.0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- NAPI: startKeyboardTracking(outputPath, startTimestampMs?) -----------
|
|
201
|
+
Napi::Value StartKeyboardTracking(const Napi::CallbackInfo& info) {
|
|
202
|
+
Napi::Env env = info.Env();
|
|
203
|
+
|
|
204
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
205
|
+
Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
|
|
206
|
+
return env.Null();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (g_kbIsTracking) {
|
|
210
|
+
return Napi::Boolean::New(env, false);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
214
|
+
double startTs = 0.0;
|
|
215
|
+
if (info.Length() >= 2 && info[1].IsNumber()) {
|
|
216
|
+
startTs = info[1].As<Napi::Number>().DoubleValue();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@try {
|
|
220
|
+
g_kbOutputPath = [NSString stringWithUTF8String:outputPath.c_str()];
|
|
221
|
+
|
|
222
|
+
[[NSFileManager defaultManager] createFileAtPath:g_kbOutputPath contents:nil attributes:nil];
|
|
223
|
+
g_kbFileHandle = [[NSFileHandle fileHandleForWritingAtPath:g_kbOutputPath] retain];
|
|
224
|
+
if (!g_kbFileHandle) {
|
|
225
|
+
return Napi::Boolean::New(env, false);
|
|
226
|
+
}
|
|
227
|
+
[g_kbFileHandle truncateFileAtOffset:0];
|
|
228
|
+
[g_kbFileHandle writeData:[@"[" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
229
|
+
|
|
230
|
+
g_kbIsFirstWrite = YES;
|
|
231
|
+
g_kbStartUnixMs = startTs > 0 ? startTs : NowUnixMs();
|
|
232
|
+
|
|
233
|
+
CGEventMask eventMask = CGEventMaskBit(kCGEventKeyDown);
|
|
234
|
+
g_kbEventTap = CGEventTapCreate(kCGSessionEventTap,
|
|
235
|
+
kCGHeadInsertEventTap,
|
|
236
|
+
kCGEventTapOptionListenOnly,
|
|
237
|
+
eventMask,
|
|
238
|
+
KeyboardEventCallback,
|
|
239
|
+
NULL);
|
|
240
|
+
|
|
241
|
+
if (!g_kbEventTap) {
|
|
242
|
+
NSLog(@"⚠️ Failed to create keyboard event tap (requires Accessibility permission)");
|
|
243
|
+
CleanupKeyboardTracking();
|
|
244
|
+
return Napi::Boolean::New(env, false);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
g_kbRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_kbEventTap, 0);
|
|
248
|
+
CFRunLoopAddSource(CFRunLoopGetMain(), g_kbRunLoopSource, kCFRunLoopCommonModes);
|
|
249
|
+
CGEventTapEnable(g_kbEventTap, true);
|
|
250
|
+
|
|
251
|
+
g_kbIsTracking = YES;
|
|
252
|
+
NSLog(@"✅ Keyboard shortcut tracking active");
|
|
253
|
+
return Napi::Boolean::New(env, true);
|
|
254
|
+
} @catch (NSException *exception) {
|
|
255
|
+
CleanupKeyboardTracking();
|
|
256
|
+
return Napi::Boolean::New(env, false);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---- NAPI: stopKeyboardTracking() -----------------------------------------
|
|
261
|
+
Napi::Value StopKeyboardTracking(const Napi::CallbackInfo& info) {
|
|
262
|
+
Napi::Env env = info.Env();
|
|
263
|
+
if (!g_kbIsTracking) {
|
|
264
|
+
return Napi::Boolean::New(env, false);
|
|
265
|
+
}
|
|
266
|
+
@try {
|
|
267
|
+
CleanupKeyboardTracking();
|
|
268
|
+
return Napi::Boolean::New(env, true);
|
|
269
|
+
} @catch (NSException *exception) {
|
|
270
|
+
CleanupKeyboardTracking();
|
|
271
|
+
return Napi::Boolean::New(env, false);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---- NAPI: getKeyboardTrackingStatus() ------------------------------------
|
|
276
|
+
Napi::Value GetKeyboardTrackingStatus(const Napi::CallbackInfo& info) {
|
|
277
|
+
Napi::Env env = info.Env();
|
|
278
|
+
Napi::Object status = Napi::Object::New(env);
|
|
279
|
+
status.Set("isTracking", Napi::Boolean::New(env, g_kbIsTracking));
|
|
280
|
+
status.Set("outputPath",
|
|
281
|
+
g_kbOutputPath ? Napi::String::New(env, [g_kbOutputPath UTF8String])
|
|
282
|
+
: env.Null());
|
|
283
|
+
return status;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Napi::Object InitKeyboardTracker(Napi::Env env, Napi::Object exports) {
|
|
287
|
+
exports.Set("startKeyboardTracking", Napi::Function::New(env, StartKeyboardTracking));
|
|
288
|
+
exports.Set("stopKeyboardTracking", Napi::Function::New(env, StopKeyboardTracking));
|
|
289
|
+
exports.Set("getKeyboardTrackingStatus", Napi::Function::New(env, GetKeyboardTrackingStatus));
|
|
290
|
+
return exports;
|
|
291
|
+
}
|
package/src/mac_recorder.mm
CHANGED
|
@@ -48,7 +48,10 @@ extern "C" {
|
|
|
48
48
|
// Cursor tracker function declarations
|
|
49
49
|
Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
50
50
|
|
|
51
|
-
//
|
|
51
|
+
// Keyboard (shortcut) tracker function declarations
|
|
52
|
+
Napi::Object InitKeyboardTracker(Napi::Env env, Napi::Object exports);
|
|
53
|
+
|
|
54
|
+
// Window selector function declarations
|
|
52
55
|
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
|
|
53
56
|
|
|
54
57
|
// Window selector overlay functions (external)
|
|
@@ -1717,7 +1720,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
1717
1720
|
|
|
1718
1721
|
// Initialize cursor tracker
|
|
1719
1722
|
InitCursorTracker(env, exports);
|
|
1720
|
-
|
|
1723
|
+
|
|
1724
|
+
// Initialize keyboard (shortcut) tracker
|
|
1725
|
+
InitKeyboardTracker(env, exports);
|
|
1726
|
+
|
|
1721
1727
|
// Initialize window selector
|
|
1722
1728
|
InitWindowSelector(env, exports);
|
|
1723
1729
|
|