node-mac-recorder 2.17.12 → 2.17.14

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 +182 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.17.12",
3
+ "version": "2.17.14",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -43,9 +43,9 @@ static NSTimeInterval g_lastCursorCheckTime = 0;
43
43
  static int g_sameCursorDetectionCount = 0;
44
44
  static NSString *g_pendingCursorType = nil;
45
45
 
46
- // Cursor stability constants
47
- static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.1; // 100ms
48
- static const int CURSOR_CONFIRMATION_COUNT = 2; // Need 2 consecutive detections
46
+ // Cursor stability constants - more responsive
47
+ static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.05; // 50ms (faster response)
48
+ static const int CURSOR_CONFIRMATION_COUNT = 1; // Need 1 detection (more responsive)
49
49
 
50
50
  // Mouse button state tracking
51
51
  static bool g_leftMouseDown = false;
@@ -57,12 +57,12 @@ static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEv
57
57
  return event;
58
58
  }
59
59
 
60
- // Enhanced cursor type detection with multi-layer approach and stability
60
+ // Enhanced cursor type detection with better NSCursor analysis
61
61
  NSString* detectCursorTypeFromNSCursor() {
62
62
  @try {
63
63
  NSCursor *currentCursor = [NSCursor currentSystemCursor];
64
64
  if (!currentCursor) {
65
- return @"default";
65
+ return nil; // Return nil to indicate we should try other methods
66
66
  }
67
67
 
68
68
  // Compare with known system cursors using identity comparison
@@ -86,24 +86,43 @@ NSString* detectCursorTypeFromNSCursor() {
86
86
  return @"not-allowed";
87
87
  }
88
88
 
89
- // Fallback to image-based comparison for custom cursors
89
+ // Check for additional resize cursors using image analysis
90
90
  NSImage *cursorImage = [currentCursor image];
91
91
  if (cursorImage) {
92
92
  NSSize imageSize = [cursorImage size];
93
+ NSPoint hotSpot = [currentCursor hotSpot];
94
+
95
+ // Analyze cursor image to detect resize cursors
96
+ if (imageSize.width > 10 && imageSize.height > 10) {
97
+ // Check for diagonal resize cursors (corner resize)
98
+ if (imageSize.width >= 15 && imageSize.height >= 15) {
99
+ // These are likely diagonal resize cursors
100
+ // hotSpot can help distinguish between nwse and nesw
101
+ if (hotSpot.x < imageSize.width / 2 && hotSpot.y < imageSize.height / 2) {
102
+ return @"nwse-resize"; // northwest-southeast
103
+ } else if (hotSpot.x > imageSize.width / 2 && hotSpot.y < imageSize.height / 2) {
104
+ return @"nesw-resize"; // northeast-southwest
105
+ }
106
+ }
107
+
108
+ // Check for horizontal/vertical resize cursors
109
+ if (imageSize.width > imageSize.height + 5) {
110
+ return @"col-resize"; // horizontal resize
111
+ } else if (imageSize.height > imageSize.width + 5) {
112
+ return @"ns-resize"; // vertical resize
113
+ }
114
+ }
93
115
 
94
116
  // Text cursors typically have I-beam shape (narrow width)
95
117
  if (imageSize.width < 8 && imageSize.height > 15) {
96
118
  return @"text";
97
119
  }
98
- // Pointer cursors are typically hand-shaped
99
- else if (imageSize.width > 15 && imageSize.height > 15) {
100
- return @"pointer";
101
- }
102
120
  }
103
121
 
104
- return @"default";
122
+ // Return nil to indicate we should use contextual detection
123
+ return nil;
105
124
  } @catch (NSException *exception) {
106
- return @"default";
125
+ return nil;
107
126
  }
108
127
  }
109
128
 
@@ -114,48 +133,170 @@ NSString* getCursorType() {
114
133
  NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
115
134
 
116
135
  @try {
117
- // Layer 1: Fast NSCursor detection (most reliable)
136
+ // Layer 1: Fast NSCursor detection (high confidence cases)
118
137
  NSString *nsCursorType = detectCursorTypeFromNSCursor();
119
138
 
120
- // Layer 2: Accessibility API for context (when NSCursor isn't enough)
121
- NSString *contextualCursorType = nsCursorType;
139
+ // Layer 2: Contextual detection using Accessibility API
140
+ NSString *contextualCursorType = nil;
122
141
 
123
- // Only use expensive Accessibility API if NSCursor gives us "default"
124
- if ([nsCursorType isEqualToString:@"default"]) {
125
- CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
126
- AXUIElementRef systemWide = AXUIElementCreateSystemWide();
127
- AXUIElementRef elementAtPosition = NULL;
128
- AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
142
+ CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
143
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
144
+ AXUIElementRef elementAtPosition = NULL;
145
+ AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
129
146
 
130
- if (error == kAXErrorSuccess && elementAtPosition) {
131
- CFStringRef role = NULL;
132
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
147
+ if (error == kAXErrorSuccess && elementAtPosition) {
148
+ CFStringRef role = NULL;
149
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
133
150
 
134
- if (error == kAXErrorSuccess && role) {
135
- NSString *elementRole = (__bridge_transfer NSString*)role;
151
+ if (error == kAXErrorSuccess && role) {
152
+ NSString *elementRole = (__bridge_transfer NSString*)role;
136
153
 
137
- // Simplified, high-confidence role mappings only
138
- if ([elementRole isEqualToString:@"AXTextField"] ||
139
- [elementRole isEqualToString:@"AXTextArea"] ||
140
- [elementRole isEqualToString:@"AXSearchField"]) {
141
- contextualCursorType = @"text";
154
+ // TEXT CURSORS - high priority
155
+ if ([elementRole isEqualToString:@"AXTextField"] ||
156
+ [elementRole isEqualToString:@"AXTextArea"] ||
157
+ [elementRole isEqualToString:@"AXSearchField"]) {
158
+ contextualCursorType = @"text";
159
+ }
160
+ // POINTER CURSORS - interactive elements with broader detection
161
+ else if ([elementRole isEqualToString:@"AXLink"] ||
162
+ [elementRole isEqualToString:@"AXButton"] ||
163
+ [elementRole isEqualToString:@"AXMenuItem"] ||
164
+ [elementRole isEqualToString:@"AXRadioButton"] ||
165
+ [elementRole isEqualToString:@"AXCheckBox"] ||
166
+ [elementRole isEqualToString:@"AXPopUpButton"] ||
167
+ [elementRole isEqualToString:@"AXTab"]) {
168
+ contextualCursorType = @"pointer";
169
+
170
+ // Also check subroles for links and buttons
171
+ CFStringRef subrole = NULL;
172
+ AXError subroleError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
173
+ if (subroleError == kAXErrorSuccess && subrole) {
174
+ NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
175
+ if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
176
+ [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
177
+ [elementSubrole isEqualToString:@"AXZoomButton"] ||
178
+ [elementSubrole isEqualToString:@"AXToolbarButton"]) {
179
+ contextualCursorType = @"pointer";
180
+ }
142
181
  }
143
- else if ([elementRole isEqualToString:@"AXLink"] ||
144
- [elementRole isEqualToString:@"AXButton"] ||
145
- [elementRole isEqualToString:@"AXMenuItem"]) {
146
- contextualCursorType = @"pointer";
182
+ }
183
+ // WEB ELEMENTS - for web links that might not show as AXLink
184
+ else if ([elementRole isEqualToString:@"AXGroup"] ||
185
+ [elementRole isEqualToString:@"AXStaticText"]) {
186
+ // Check if it's clickable/has action
187
+ CFArrayRef actions = NULL;
188
+ AXError actionsError = AXUIElementCopyActionNames(elementAtPosition, &actions);
189
+ if (actionsError == kAXErrorSuccess && actions) {
190
+ CFIndex actionCount = CFArrayGetCount(actions);
191
+ for (CFIndex i = 0; i < actionCount; i++) {
192
+ CFStringRef action = (CFStringRef)CFArrayGetValueAtIndex(actions, i);
193
+ NSString *actionStr = (__bridge NSString*)action;
194
+ if ([actionStr isEqualToString:@"AXPress"] ||
195
+ [actionStr isEqualToString:@"AXShowMenu"]) {
196
+ contextualCursorType = @"pointer";
197
+ break;
198
+ }
199
+ }
200
+ CFRelease(actions);
147
201
  }
148
- else if ([elementRole isEqualToString:@"AXProgressIndicator"]) {
149
- contextualCursorType = @"progress";
202
+ }
203
+ // WINDOW BORDER RESIZE - critical for resize detection
204
+ else if ([elementRole isEqualToString:@"AXWindow"]) {
205
+ CFTypeRef position = NULL;
206
+ CFTypeRef size = NULL;
207
+ AXError posErr = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
208
+ AXError sizeErr = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
209
+
210
+ if (posErr == kAXErrorSuccess && sizeErr == kAXErrorSuccess && position && size) {
211
+ CGPoint windowPos;
212
+ CGSize windowSize;
213
+ AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
214
+ AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
215
+
216
+ CGFloat x = cursorPos.x - windowPos.x;
217
+ CGFloat y = cursorPos.y - windowPos.y;
218
+ CGFloat w = windowSize.width;
219
+ CGFloat h = windowSize.height;
220
+ CGFloat edge = 5.0; // 5px edge detection
221
+
222
+ // Corner resize detection
223
+ if (x <= edge && y <= edge) {
224
+ contextualCursorType = @"nwse-resize";
225
+ }
226
+ else if (x >= w-edge && y <= edge) {
227
+ contextualCursorType = @"nesw-resize";
228
+ }
229
+ else if (x <= edge && y >= h-edge) {
230
+ contextualCursorType = @"nesw-resize";
231
+ }
232
+ else if (x >= w-edge && y >= h-edge) {
233
+ contextualCursorType = @"nwse-resize";
234
+ }
235
+ // Edge resize detection
236
+ else if (x <= edge || x >= w-edge) {
237
+ contextualCursorType = @"col-resize";
238
+ }
239
+ else if (y <= edge || y >= h-edge) {
240
+ contextualCursorType = @"ns-resize";
241
+ }
242
+
243
+ if (position) CFRelease(position);
244
+ if (size) CFRelease(size);
150
245
  }
151
246
  }
152
- CFRelease(elementAtPosition);
247
+ // SPLITTER RESIZE
248
+ else if ([elementRole isEqualToString:@"AXSplitter"]) {
249
+ CFStringRef orientation = NULL;
250
+ error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
251
+ if (error == kAXErrorSuccess && orientation) {
252
+ NSString *orientationStr = (__bridge_transfer NSString*)orientation;
253
+ if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
254
+ contextualCursorType = @"ns-resize";
255
+ } else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
256
+ contextualCursorType = @"col-resize";
257
+ }
258
+ }
259
+ }
260
+ // PROGRESS INDICATORS
261
+ else if ([elementRole isEqualToString:@"AXProgressIndicator"]) {
262
+ contextualCursorType = @"progress";
263
+ }
264
+ // OTHER ELEMENTS - be conservative, don't assume pointer
265
+ else {
266
+ // Don't override NSCursor for unknown elements
267
+ contextualCursorType = nil;
268
+ }
153
269
  }
154
- if (systemWide) CFRelease(systemWide);
270
+ CFRelease(elementAtPosition);
271
+ }
272
+ if (systemWide) CFRelease(systemWide);
273
+
274
+ // Layer 3: Intelligent fusion of NSCursor and contextual results
275
+ NSString *detectedCursorType = @"default";
276
+
277
+ // Priority logic with better fallback:
278
+ // 1. If contextual gives resize cursor, always use it (resize has highest priority)
279
+ if (contextualCursorType != nil &&
280
+ ([contextualCursorType hasSuffix:@"resize"] ||
281
+ [contextualCursorType isEqualToString:@"col-resize"] ||
282
+ [contextualCursorType isEqualToString:@"ns-resize"])) {
283
+ detectedCursorType = contextualCursorType;
284
+ }
285
+ // 2. If NSCursor gives a definitive answer, use it
286
+ else if (nsCursorType != nil) {
287
+ detectedCursorType = nsCursorType;
288
+ }
289
+ // 3. If contextual gives specific non-resize cursor, use it
290
+ else if (contextualCursorType != nil &&
291
+ ![contextualCursorType isEqualToString:@"default"]) {
292
+ detectedCursorType = contextualCursorType;
293
+ }
294
+ // 4. If both are nil or default, use true default
295
+ else {
296
+ detectedCursorType = @"default";
155
297
  }
156
298
 
157
- // Layer 3: Stability filtering to prevent oscillation
158
- NSString *detectedCursorType = contextualCursorType;
299
+ // Layer 4: Stability filtering to prevent oscillation
159
300
 
160
301
  // Time-based stability check
161
302
  if (currentTime - g_lastCursorCheckTime > CURSOR_STABILITY_THRESHOLD) {