node-mac-recorder 2.22.10 → 2.22.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.22.10",
3
+ "version": "2.22.12",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -814,6 +814,10 @@ static bool g_leftMouseDown = false;
814
814
  static bool g_rightMouseDown = false;
815
815
  static NSString *g_lastEventType = @"move";
816
816
 
817
+ // Text input (keyboard) tracking state
818
+ static NSTimeInterval g_lastTextInputEmitTime = 0; // Throttle: son textInput event zamanı
819
+ static const NSTimeInterval TEXT_INPUT_THROTTLE_MS = 50; // Min 50ms aralık (20 FPS)
820
+
817
821
  // Accessibility tabanlı cursor tip tespiti
818
822
  static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
819
823
  @autoreleasepool {
@@ -1942,6 +1946,125 @@ void writeToFile(NSDictionary *cursorData) {
1942
1946
  }
1943
1947
  }
1944
1948
 
1949
+ // Text input event: Klavye basıldığında focused text field'in caret pozisyonunu yakala
1950
+ static void emitTextInputEvent(NSTimeInterval timestamp, NSTimeInterval unixTimeMs, CGPoint mouseLocation) {
1951
+ // Throttle: Çok sık emit etme (performans için)
1952
+ if (unixTimeMs - g_lastTextInputEmitTime < TEXT_INPUT_THROTTLE_MS) {
1953
+ return;
1954
+ }
1955
+
1956
+ @autoreleasepool {
1957
+ @try {
1958
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
1959
+ if (!systemWide) return;
1960
+
1961
+ AXUIElementRef focusedElement = NULL;
1962
+ AXError focusErr = AXUIElementCopyAttributeValue(
1963
+ systemWide, kAXFocusedUIElementAttribute, (CFTypeRef *)&focusedElement);
1964
+
1965
+ if (focusErr != kAXErrorSuccess || !focusedElement) {
1966
+ CFRelease(systemWide);
1967
+ return;
1968
+ }
1969
+
1970
+ // Focused element text field mi kontrol et
1971
+ NSString *role = CopyAttributeString(focusedElement, kAXRoleAttribute);
1972
+ BOOL isEditable = NO;
1973
+ CopyAttributeBoolean(focusedElement, CFSTR("AXEditable"), &isEditable);
1974
+
1975
+ BOOL isTextField = StringEqualsAny(role, @[
1976
+ @"AXTextField", @"AXTextArea", @"AXTextView",
1977
+ @"AXTextEditor", @"AXSearchField",
1978
+ @"AXComboBox"
1979
+ ]) || isEditable;
1980
+
1981
+ if (!isTextField) {
1982
+ CFRelease(focusedElement);
1983
+ CFRelease(systemWide);
1984
+ return;
1985
+ }
1986
+
1987
+ // Input frame bilgisini al (AXPosition + AXSize)
1988
+ CGPoint inputOrigin = CGPointZero;
1989
+ CGSize inputSize = CGSizeZero;
1990
+ AXValueRef positionValue = NULL;
1991
+ AXValueRef sizeValue = NULL;
1992
+
1993
+ AXUIElementCopyAttributeValue(focusedElement, kAXPositionAttribute, (CFTypeRef *)&positionValue);
1994
+ AXUIElementCopyAttributeValue(focusedElement, kAXSizeAttribute, (CFTypeRef *)&sizeValue);
1995
+
1996
+ if (positionValue) {
1997
+ AXValueGetValue(positionValue, kAXValueTypeCGPoint, &inputOrigin);
1998
+ CFRelease(positionValue);
1999
+ }
2000
+ if (sizeValue) {
2001
+ AXValueGetValue(sizeValue, kAXValueTypeCGSize, &inputSize);
2002
+ CFRelease(sizeValue);
2003
+ }
2004
+
2005
+ // Caret pozisyonunu al (AXSelectedTextRange → AXBoundsForRange)
2006
+ CGPoint caretPos = CGPointMake(inputOrigin.x, inputOrigin.y);
2007
+ BOOL hasCaretPos = NO;
2008
+
2009
+ CFTypeRef selectedRangeValue = NULL;
2010
+ AXError rangeErr = AXUIElementCopyAttributeValue(
2011
+ focusedElement, CFSTR("AXSelectedTextRange"), &selectedRangeValue);
2012
+
2013
+ if (rangeErr == kAXErrorSuccess && selectedRangeValue) {
2014
+ CFTypeRef boundsValue = NULL;
2015
+ AXError boundsErr = AXUIElementCopyParameterizedAttributeValue(
2016
+ focusedElement, CFSTR("AXBoundsForRange"),
2017
+ selectedRangeValue, &boundsValue);
2018
+
2019
+ if (boundsErr == kAXErrorSuccess && boundsValue) {
2020
+ CGRect caretBounds = CGRectZero;
2021
+ if (AXValueGetValue((AXValueRef)boundsValue, kAXValueTypeCGRect, &caretBounds)) {
2022
+ // Caret'in dikey ortası
2023
+ caretPos = CGPointMake(
2024
+ caretBounds.origin.x,
2025
+ caretBounds.origin.y + caretBounds.size.height / 2.0
2026
+ );
2027
+ hasCaretPos = YES;
2028
+ }
2029
+ CFRelease(boundsValue);
2030
+ }
2031
+ CFRelease(selectedRangeValue);
2032
+ }
2033
+
2034
+ // Caret alınamazsa input frame'in sol ortasını kullan
2035
+ if (!hasCaretPos) {
2036
+ caretPos = CGPointMake(inputOrigin.x + 4, inputOrigin.y + inputSize.height / 2.0);
2037
+ }
2038
+
2039
+ // textInput event'i oluştur ve dosyaya yaz
2040
+ NSDictionary *textInputInfo = @{
2041
+ @"x": @((int)mouseLocation.x),
2042
+ @"y": @((int)mouseLocation.y),
2043
+ @"timestamp": @(timestamp),
2044
+ @"unixTimeMs": @(unixTimeMs),
2045
+ @"cursorType": @"text",
2046
+ @"type": @"textInput",
2047
+ @"caretX": @((int)caretPos.x),
2048
+ @"caretY": @((int)caretPos.y),
2049
+ @"inputFrame": @{
2050
+ @"x": @((int)inputOrigin.x),
2051
+ @"y": @((int)inputOrigin.y),
2052
+ @"width": @((int)inputSize.width),
2053
+ @"height": @((int)inputSize.height)
2054
+ }
2055
+ };
2056
+
2057
+ writeToFile(textInputInfo);
2058
+ g_lastTextInputEmitTime = unixTimeMs;
2059
+
2060
+ CFRelease(focusedElement);
2061
+ CFRelease(systemWide);
2062
+ } @catch (NSException *exception) {
2063
+ // Accessibility hata verirse sessizce devam et
2064
+ }
2065
+ }
2066
+ }
2067
+
1945
2068
  // Event callback for mouse events
1946
2069
  CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
1947
2070
  @autoreleasepool {
@@ -1982,6 +2105,10 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
1982
2105
  case kCGEventOtherMouseDragged:
1983
2106
  eventType = @"drag";
1984
2107
  break;
2108
+ case kCGEventKeyDown:
2109
+ // Klavye event'i — text caret tracking için
2110
+ emitTextInputEvent(timestamp, unixTimeMs, location);
2111
+ return event; // Mouse event olarak işleme, ayrı handle edildi
1985
2112
  case kCGEventMouseMoved:
1986
2113
  default:
1987
2114
  eventType = @"move";
@@ -1991,7 +2118,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
1991
2118
  if (!ShouldEmitCursorEvent(location, cursorType, eventType)) {
1992
2119
  return event;
1993
2120
  }
1994
-
2121
+
1995
2122
  // Cursor data oluştur
1996
2123
  NSDictionary *cursorInfo = @{
1997
2124
  @"x": @((int)location.x),
@@ -2001,7 +2128,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
2001
2128
  @"cursorType": cursorType,
2002
2129
  @"type": eventType
2003
2130
  };
2004
-
2131
+
2005
2132
  // Direkt dosyaya yaz
2006
2133
  writeToFile(cursorInfo);
2007
2134
  RememberCursorEvent(location, cursorType, eventType);
@@ -2140,6 +2267,7 @@ void cleanupCursorTracking() {
2140
2267
  g_lastDetectedCursorType = nil;
2141
2268
  g_cursorTypeCounter = 0;
2142
2269
  g_isFirstWrite = true;
2270
+ g_lastTextInputEmitTime = 0;
2143
2271
  ResetCursorEventHistory();
2144
2272
  }
2145
2273
 
@@ -2180,7 +2308,7 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
2180
2308
  g_trackingStartTime = [NSDate date];
2181
2309
  ResetCursorEventHistory();
2182
2310
 
2183
- // Create event tap for mouse events
2311
+ // Create event tap for mouse + keyboard events
2184
2312
  CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
2185
2313
  CGEventMaskBit(kCGEventLeftMouseUp) |
2186
2314
  CGEventMaskBit(kCGEventRightMouseDown) |
@@ -2190,7 +2318,8 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
2190
2318
  CGEventMaskBit(kCGEventMouseMoved) |
2191
2319
  CGEventMaskBit(kCGEventLeftMouseDragged) |
2192
2320
  CGEventMaskBit(kCGEventRightMouseDragged) |
2193
- CGEventMaskBit(kCGEventOtherMouseDragged));
2321
+ CGEventMaskBit(kCGEventOtherMouseDragged) |
2322
+ CGEventMaskBit(kCGEventKeyDown));
2194
2323
 
2195
2324
  bool eventTapActive = false;
2196
2325
  g_eventTap = CGEventTapCreate(kCGSessionEventTap,
@@ -50,6 +50,30 @@ static void initializeSafeQueue() {
50
50
  initializeSafeQueue();
51
51
  }
52
52
 
53
+ + (BOOL)shouldAllowElectronWindows {
54
+ NSString *flag = [[[NSProcessInfo processInfo] environment] objectForKey:@"CREAVIT_ALLOW_ELECTRON_WINDOWS"];
55
+ if (!flag) return NO;
56
+
57
+ NSString *normalized = [[flag lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
58
+ return [normalized isEqualToString:@"1"] ||
59
+ [normalized isEqualToString:@"true"] ||
60
+ [normalized isEqualToString:@"yes"] ||
61
+ [normalized isEqualToString:@"on"];
62
+ }
63
+
64
+ + (BOOL)shouldSkipWindowOwner:(NSString *)appName {
65
+ if (!appName || appName.length == 0) return YES;
66
+ if ([appName containsString:@"WindowServer"] || [appName containsString:@"Dock"]) return YES;
67
+
68
+ if (![self shouldAllowElectronWindows]) {
69
+ if ([appName containsString:@"Electron"] || [appName containsString:@"node"]) {
70
+ return YES;
71
+ }
72
+ }
73
+
74
+ return NO;
75
+ }
76
+
53
77
  + (BOOL)startRecordingWithPath:(NSString *)outputPath options:(NSDictionary *)options {
54
78
  if (@available(macOS 12.3, *)) {
55
79
  return [self startRecordingModern:outputPath options:options];
@@ -432,8 +456,7 @@ static void initializeSafeQueue() {
432
456
 
433
457
  NSString *appName = window.owningApplication.applicationName ?: @"Unknown";
434
458
 
435
- // Skip Electron windows (our overlay)
436
- if ([appName containsString:@"Electron"] || [appName containsString:@"node"]) continue;
459
+ if ([self shouldSkipWindowOwner:appName]) continue;
437
460
 
438
461
  NSDictionary *windowInfo = @{
439
462
  @"id": @(window.windowID),
@@ -12,6 +12,30 @@ static void initializeWindowQueue() {
12
12
  });
13
13
  }
14
14
 
15
+ static BOOL ShouldAllowElectronWindows(void) {
16
+ NSString *flag = [[[NSProcessInfo processInfo] environment] objectForKey:@"CREAVIT_ALLOW_ELECTRON_WINDOWS"];
17
+ if (!flag) return NO;
18
+
19
+ NSString *normalized = [[flag lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
20
+ return [normalized isEqualToString:@"1"] ||
21
+ [normalized isEqualToString:@"true"] ||
22
+ [normalized isEqualToString:@"yes"] ||
23
+ [normalized isEqualToString:@"on"];
24
+ }
25
+
26
+ static BOOL ShouldSkipWindowOwner(NSString *appName) {
27
+ if (!appName || appName.length == 0) return YES;
28
+ if ([appName containsString:@"WindowServer"] || [appName containsString:@"Dock"]) return YES;
29
+
30
+ if (!ShouldAllowElectronWindows()) {
31
+ if ([appName containsString:@"Electron"] || [appName containsString:@"node"]) {
32
+ return YES;
33
+ }
34
+ }
35
+
36
+ return NO;
37
+ }
38
+
15
39
  // NAPI Function: Get Windows (Electron-safe)
16
40
  Napi::Value GetWindowsElectronSafe(const Napi::CallbackInfo& info) {
17
41
  Napi::Env env = info.Env();
@@ -38,11 +62,7 @@ Napi::Value GetWindowsElectronSafe(const Napi::CallbackInfo& info) {
38
62
 
39
63
  NSString *appName = window.owningApplication.applicationName ?: @"Unknown";
40
64
 
41
- // Skip Electron windows (our overlay) and system windows
42
- if ([appName containsString:@"Electron"] ||
43
- [appName containsString:@"node"] ||
44
- [appName containsString:@"WindowServer"] ||
45
- [appName containsString:@"Dock"]) continue;
65
+ if (ShouldSkipWindowOwner(appName)) continue;
46
66
 
47
67
  NSDictionary *windowInfo = @{
48
68
  @"id": @(window.windowID),
@@ -94,11 +114,7 @@ Napi::Value GetWindowsElectronSafe(const Napi::CallbackInfo& info) {
94
114
  NSString *appName = (__bridge NSString*)ownerName;
95
115
  NSString *windowTitle = windowName ? (__bridge NSString*)windowName : @"";
96
116
 
97
- // Skip Electron windows and system windows
98
- if ([appName containsString:@"Electron"] ||
99
- [appName containsString:@"node"] ||
100
- [appName containsString:@"WindowServer"] ||
101
- [appName containsString:@"Dock"]) continue;
117
+ if (ShouldSkipWindowOwner(appName)) continue;
102
118
 
103
119
  // Get window bounds
104
120
  CGRect bounds = CGRectZero;
@@ -56,6 +56,31 @@ static id g_screenKeyEventMonitor = nil;
56
56
  static NSTimer *g_screenTrackingTimer = nil;
57
57
  static NSInteger g_currentActiveScreenIndex = -1;
58
58
 
59
+ static bool shouldAllowElectronWindows() {
60
+ NSString *flag = [[[NSProcessInfo processInfo] environment] objectForKey:@"CREAVIT_ALLOW_ELECTRON_WINDOWS"];
61
+ if (!flag) return false;
62
+
63
+ NSString *normalized = [[flag lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
64
+ return [normalized isEqualToString:@"1"] ||
65
+ [normalized isEqualToString:@"true"] ||
66
+ [normalized isEqualToString:@"yes"] ||
67
+ [normalized isEqualToString:@"on"];
68
+ }
69
+
70
+ static bool shouldSkipSelectableWindowOwner(NSString *windowOwner) {
71
+ if (!windowOwner || [windowOwner length] == 0) return true;
72
+ if ([windowOwner isEqualToString:@"WindowServer"]) return true;
73
+ if ([windowOwner isEqualToString:@"Dock"]) return true;
74
+
75
+ if (!shouldAllowElectronWindows()) {
76
+ if ([windowOwner containsString:@"Electron"] || [windowOwner containsString:@"node"]) {
77
+ return true;
78
+ }
79
+ }
80
+
81
+ return false;
82
+ }
83
+
59
84
  // Record icon helpers
60
85
  static NSImage *CreateRecordIconImage(CGFloat size) {
61
86
  const CGFloat leadingInset = 24.0;
@@ -757,12 +782,7 @@ NSArray* getAllSelectableWindows() {
757
782
 
758
783
  // Skip system windows, dock, menu bar, etc.
759
784
  if ([windowLayer intValue] != 0) continue; // Only normal windows
760
- if (!windowOwner || [windowOwner length] == 0) continue;
761
- if ([windowOwner isEqualToString:@"WindowServer"]) continue;
762
- if ([windowOwner isEqualToString:@"Dock"]) continue;
763
-
764
- // Skip Electron windows (our own overlay)
765
- if ([windowOwner containsString:@"Electron"] || [windowOwner containsString:@"node"]) continue;
785
+ if (shouldSkipSelectableWindowOwner(windowOwner)) continue;
766
786
 
767
787
  // Extract bounds
768
788
  int x = [[bounds objectForKey:@"X"] intValue];
@@ -802,8 +822,7 @@ NSDictionary* getWindowUnderCursor(CGPoint point) {
802
822
  for (NSDictionary *window in g_allWindows) {
803
823
  NSString *appName = [window objectForKey:@"appName"];
804
824
 
805
- // Skip Electron windows (our own overlay)
806
- if (appName && ([appName containsString:@"Electron"] || [appName containsString:@"node"])) {
825
+ if (shouldSkipSelectableWindowOwner(appName)) {
807
826
  continue;
808
827
  }
809
828