node-mac-recorder 2.20.2 → 2.20.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cursor_tracker.mm +232 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.20.2",
3
+ "version": "2.20.3",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -7,6 +7,10 @@
7
7
  #import <Accessibility/Accessibility.h>
8
8
  #import <dispatch/dispatch.h>
9
9
 
10
+ #ifndef kAXHitTestParameterizedAttribute
11
+ #define kAXHitTestParameterizedAttribute CFSTR("AXHitTest")
12
+ #endif
13
+
10
14
  // Global state for cursor tracking
11
15
  static bool g_isCursorTracking = false;
12
16
  static CFMachPortRef g_eventTap = NULL;
@@ -49,16 +53,194 @@ static NSString* CopyAndReleaseCFString(CFStringRef value) {
49
53
  return result;
50
54
  }
51
55
 
56
+ static BOOL pointInsideElementFrame(AXUIElementRef element, CGPoint point) {
57
+ if (!element) {
58
+ return NO;
59
+ }
60
+
61
+ AXValueRef positionValue = NULL;
62
+ AXValueRef sizeValue = NULL;
63
+
64
+ AXError positionError = AXUIElementCopyAttributeValue(element, kAXPositionAttribute, (CFTypeRef *)&positionValue);
65
+ AXError sizeError = AXUIElementCopyAttributeValue(element, kAXSizeAttribute, (CFTypeRef *)&sizeValue);
66
+
67
+ if (positionError != kAXErrorSuccess || sizeError != kAXErrorSuccess || !positionValue || !sizeValue) {
68
+ if (positionValue) CFRelease(positionValue);
69
+ if (sizeValue) CFRelease(sizeValue);
70
+ return NO;
71
+ }
72
+
73
+ CGPoint elementOrigin = CGPointZero;
74
+ CGSize elementSize = CGSizeZero;
75
+ AXValueGetValue(positionValue, kAXValueTypeCGPoint, &elementOrigin);
76
+ AXValueGetValue(sizeValue, kAXValueTypeCGSize, &elementSize);
77
+
78
+ CFRelease(positionValue);
79
+ CFRelease(sizeValue);
80
+
81
+ CGRect frame = CGRectMake(elementOrigin.x, elementOrigin.y, elementSize.width, elementSize.height);
82
+ return CGRectContainsPoint(frame, point);
83
+ }
84
+
85
+ static BOOL elementHasAction(AXUIElementRef element, CFStringRef actionName) {
86
+ if (!element || !actionName) {
87
+ return NO;
88
+ }
89
+
90
+ CFArrayRef actions = NULL;
91
+ AXError error = AXUIElementCopyActionNames(element, &actions);
92
+ if (error != kAXErrorSuccess || !actions) {
93
+ return NO;
94
+ }
95
+
96
+ BOOL hasAction = NO;
97
+ CFIndex count = CFArrayGetCount(actions);
98
+ for (CFIndex i = 0; i < count; i++) {
99
+ CFStringRef action = (CFStringRef)CFArrayGetValueAtIndex(actions, i);
100
+ if (CFStringCompare(action, actionName, 0) == kCFCompareEqualTo) {
101
+ hasAction = YES;
102
+ break;
103
+ }
104
+ }
105
+
106
+ CFRelease(actions);
107
+ return hasAction;
108
+ }
109
+
110
+ static BOOL elementIsClickable(AXUIElementRef element) {
111
+ if (!element) {
112
+ return NO;
113
+ }
114
+
115
+ CFStringRef roleRef = NULL;
116
+ if (AXUIElementCopyAttributeValue(element, kAXRoleAttribute, (CFTypeRef *)&roleRef) == kAXErrorSuccess && roleRef) {
117
+ NSString *role = CopyAndReleaseCFString(roleRef);
118
+ if ([role isEqualToString:@"AXButton"] ||
119
+ [role isEqualToString:@"AXLink"] ||
120
+ [role isEqualToString:@"AXMenuItem"] ||
121
+ [role isEqualToString:@"AXTab"] ||
122
+ [role isEqualToString:@"AXRadioButton"] ||
123
+ [role isEqualToString:@"AXCheckBox"] ||
124
+ [role isEqualToString:@"AXPopUpButton"]) {
125
+ return YES;
126
+ }
127
+ }
128
+
129
+ CFStringRef subroleRef = NULL;
130
+ if (AXUIElementCopyAttributeValue(element, kAXSubroleAttribute, (CFTypeRef *)&subroleRef) == kAXErrorSuccess && subroleRef) {
131
+ NSString *subrole = CopyAndReleaseCFString(subroleRef);
132
+ if ([subrole isEqualToString:@"AXLink"] ||
133
+ [subrole isEqualToString:@"AXFileDrop"] ||
134
+ [subrole isEqualToString:@"AXDropTarget"] ||
135
+ [subrole isEqualToString:@"AXToolbarButton"]) {
136
+ return YES;
137
+ }
138
+ }
139
+
140
+ if (elementHasAction(element, kAXPressAction) ||
141
+ elementHasAction(element, kAXShowMenuAction) ||
142
+ elementHasAction(element, CFSTR("AXConfirm"))) {
143
+ return YES;
144
+ }
145
+
146
+ CFTypeRef urlValue = NULL;
147
+ if (AXUIElementCopyAttributeValue(element, kAXURLAttribute, &urlValue) == kAXErrorSuccess && urlValue) {
148
+ CFRelease(urlValue);
149
+ return YES;
150
+ }
151
+ if (urlValue) {
152
+ CFRelease(urlValue);
153
+ urlValue = NULL;
154
+ }
155
+
156
+ CFBooleanRef isEnabled = NULL;
157
+ if (AXUIElementCopyAttributeValue(element, kAXEnabledAttribute, (CFTypeRef *)&isEnabled) == kAXErrorSuccess &&
158
+ isEnabled && CFBooleanGetValue(isEnabled)) {
159
+ CFRelease(isEnabled);
160
+ isEnabled = NULL;
161
+ if (elementHasAction(element, CFSTR("AXShowMenu")) || elementHasAction(element, CFSTR("AXDecrement")) || elementHasAction(element, CFSTR("AXIncrement"))) {
162
+ return YES;
163
+ }
164
+ }
165
+ if (isEnabled) {
166
+ CFRelease(isEnabled);
167
+ isEnabled = NULL;
168
+ }
169
+
170
+ return NO;
171
+ }
172
+
173
+ static BOOL elementSupportsText(AXUIElementRef element) {
174
+ if (!element) {
175
+ return NO;
176
+ }
177
+
178
+ BOOL supportsText = NO;
179
+
180
+ CFStringRef roleRef = NULL;
181
+ if (AXUIElementCopyAttributeValue(element, kAXRoleAttribute, (CFTypeRef *)&roleRef) == kAXErrorSuccess && roleRef) {
182
+ NSString *role = CopyAndReleaseCFString(roleRef);
183
+ if ([role isEqualToString:@"AXTextField"] ||
184
+ [role isEqualToString:@"AXTextArea"] ||
185
+ [role isEqualToString:@"AXSearchField"] ||
186
+ [role isEqualToString:@"AXStaticText"] ||
187
+ [role isEqualToString:@"AXDocument"] ||
188
+ [role isEqualToString:@"AXWebArea"]) {
189
+ supportsText = YES;
190
+ }
191
+ }
192
+
193
+ CFStringRef subroleRef = NULL;
194
+ if (!supportsText && AXUIElementCopyAttributeValue(element, kAXSubroleAttribute, (CFTypeRef *)&subroleRef) == kAXErrorSuccess && subroleRef) {
195
+ NSString *subrole = CopyAndReleaseCFString(subroleRef);
196
+ if ([subrole isEqualToString:@"AXSecureTextField"] ||
197
+ [subrole isEqualToString:@"AXTextAttachment"] ||
198
+ [subrole isEqualToString:@"AXTextField"]) {
199
+ supportsText = YES;
200
+ }
201
+ }
202
+
203
+ CFStringRef roleDescriptionRef = NULL;
204
+ if (!supportsText && AXUIElementCopyAttributeValue(element, kAXRoleDescriptionAttribute, (CFTypeRef *)&roleDescriptionRef) == kAXErrorSuccess && roleDescriptionRef) {
205
+ NSString *roleDescription = CopyAndReleaseCFString(roleDescriptionRef);
206
+ NSString *lower = [roleDescription lowercaseString];
207
+ if ([lower containsString:@"text"] ||
208
+ [lower containsString:@"editor"] ||
209
+ [lower containsString:@"code"] ||
210
+ [lower containsString:@"document"]) {
211
+ supportsText = YES;
212
+ }
213
+ }
214
+
215
+ CFBooleanRef editable = NULL;
216
+ if (!supportsText && AXUIElementCopyAttributeValue(element, CFSTR("AXEditable"), (CFTypeRef *)&editable) == kAXErrorSuccess && editable) {
217
+ supportsText = CFBooleanGetValue(editable);
218
+ CFRelease(editable);
219
+ }
220
+
221
+ CFBooleanRef supportsSelection = NULL;
222
+ if (!supportsText && AXUIElementCopyAttributeValue(element, CFSTR("AXSupportsTextSelection"), (CFTypeRef *)&supportsSelection) == kAXErrorSuccess && supportsSelection) {
223
+ supportsText = CFBooleanGetValue(supportsSelection);
224
+ CFRelease(supportsSelection);
225
+ }
226
+
227
+ CFTypeRef valueAttribute = NULL;
228
+ if (!supportsText && AXUIElementCopyAttributeValue(element, kAXValueAttribute, &valueAttribute) == kAXErrorSuccess && valueAttribute) {
229
+ CFTypeID typeId = CFGetTypeID(valueAttribute);
230
+ if (typeId == CFAttributedStringGetTypeID() || typeId == CFStringGetTypeID()) {
231
+ supportsText = YES;
232
+ }
233
+ CFRelease(valueAttribute);
234
+ }
235
+
236
+ return supportsText;
237
+ }
238
+
52
239
  // Mouse button state tracking
53
240
  static bool g_leftMouseDown = false;
54
241
  static bool g_rightMouseDown = false;
55
242
  static NSString *g_lastEventType = @"move";
56
243
 
57
- // Event tap callback
58
- static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo) {
59
- return event;
60
- }
61
-
62
244
  // Accessibility tabanlı cursor tip tespiti
63
245
  static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
64
246
  @autoreleasepool {
@@ -334,6 +516,43 @@ static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
334
516
  }
335
517
 
336
518
  if (elementAtPosition) {
519
+ if ([cursorType isEqualToString:@"default"] && elementSupportsText(elementAtPosition)) {
520
+ cursorType = @"text";
521
+ }
522
+
523
+ if ([cursorType isEqualToString:@"default"] && elementIsClickable(elementAtPosition)) {
524
+ cursorType = @"pointer";
525
+ }
526
+
527
+ if ([cursorType isEqualToString:@"default"]) {
528
+ AXValueRef pointValue = AXValueCreate(kAXValueTypeCGPoint, &cursorPos);
529
+ if (pointValue) {
530
+ AXUIElementRef deepElement = NULL;
531
+ AXError hitError = AXUIElementCopyParameterizedAttributeValue(systemWide, kAXHitTestParameterizedAttribute, pointValue, (CFTypeRef *)&deepElement);
532
+ CFRelease(pointValue);
533
+ if (hitError == kAXErrorSuccess && deepElement) {
534
+ if (elementSupportsText(deepElement)) {
535
+ cursorType = @"text";
536
+ } else if (elementIsClickable(deepElement)) {
537
+ cursorType = @"pointer";
538
+ }
539
+ CFRelease(deepElement);
540
+ }
541
+ }
542
+ }
543
+
544
+ if ([cursorType isEqualToString:@"default"]) {
545
+ AXUIElementRef focusedElement = NULL;
546
+ if (AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute, (CFTypeRef *)&focusedElement) == kAXErrorSuccess && focusedElement) {
547
+ if (elementSupportsText(focusedElement) && pointInsideElementFrame(focusedElement, cursorPos)) {
548
+ cursorType = @"text";
549
+ } else if (elementIsClickable(focusedElement) && pointInsideElementFrame(focusedElement, cursorPos)) {
550
+ cursorType = @"pointer";
551
+ }
552
+ CFRelease(focusedElement);
553
+ }
554
+ }
555
+
337
556
  CFRelease(elementAtPosition);
338
557
  }
339
558
  if (systemWide) {
@@ -569,12 +788,10 @@ NSString* getCursorType() {
569
788
  if (axCursorType && ![axCursorType isEqualToString:@"default"]) {
570
789
  finalType = axCursorType;
571
790
  } else if (systemCursorType && [systemCursorType length] > 0) {
572
- if ([systemCursorType isEqualToString:@"pointer"] &&
573
- (!axCursorType || [axCursorType isEqualToString:@"default"])) {
574
- finalType = @"default";
575
- } else {
576
- finalType = systemCursorType;
577
- }
791
+ // Prefer the system cursor when accessibility reports a generic value.
792
+ finalType = systemCursorType;
793
+ } else if (axCursorType && [axCursorType length] > 0) {
794
+ finalType = axCursorType;
578
795
  } else {
579
796
  finalType = @"default";
580
797
  }
@@ -629,17 +846,8 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
629
846
 
630
847
  CGPoint rawLocation = CGEventGetLocation(event);
631
848
 
632
- // Apply DPR scaling correction for Retina displays
633
- NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
849
+ // Coordinates are already in logical space; no additional scaling needed here.
634
850
  CGPoint location = rawLocation;
635
-
636
- if (scalingInfo) {
637
- CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
638
- NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
639
-
640
- // Keep logical coordinates - no scaling needed here
641
- location = rawLocation;
642
- }
643
851
  NSDate *currentDate = [NSDate date];
644
852
  NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
645
853
  NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
@@ -702,18 +910,9 @@ void cursorTimerCallback() {
702
910
  CFRelease(event);
703
911
  }
704
912
 
705
- // Apply DPR scaling correction for Retina displays
706
- NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
913
+ // Coordinates are already in logical space; no additional scaling needed here.
707
914
  CGPoint location = rawLocation;
708
915
 
709
- if (scalingInfo) {
710
- CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
711
- NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
712
-
713
- // Keep logical coordinates - no scaling needed here
714
- location = rawLocation;
715
- }
716
-
717
916
  NSDate *currentDate = [NSDate date];
718
917
  NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
719
918
  NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
@@ -1042,16 +1241,8 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
1042
1241
  // Get display scaling information
1043
1242
  NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
1044
1243
  CGPoint logicalLocation = rawLocation;
1045
-
1046
- if (scalingInfo) {
1047
- CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
1048
- NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
1049
-
1050
- // CGEventGetLocation returns LOGICAL coordinates (correct for JS layer)
1051
- // Keep logical coordinates - transformation happens in JS layer
1052
- logicalLocation = rawLocation;
1053
- }
1054
-
1244
+ // CGEventGetLocation already returns logical coordinates; additional scaling happens in JS layer.
1245
+
1055
1246
  NSString *cursorType = getCursorType();
1056
1247
 
1057
1248
  // Mouse button state'ini kontrol et