node-mac-recorder 2.22.33 → 2.22.34

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.
@@ -223,7 +223,6 @@ class MultiWindowRecorder extends EventEmitter {
223
223
  } : null,
224
224
  recordingType: 'multi-window', // Multi-window recording type
225
225
  startTimestamp: startTimestamp, // Use same timestamp as video
226
- interval: 33,
227
226
  // Pass window information for location detection
228
227
  multiWindowBounds: windowBounds
229
228
  };
package/index.js CHANGED
@@ -869,8 +869,7 @@ class MacRecorder extends EventEmitter {
869
869
  this.options.captureArea ? 'area' : 'display',
870
870
  captureArea: this.options.captureArea,
871
871
  windowId: this.options.windowId,
872
- startTimestamp: syncTimestamp,
873
- interval: 33,
872
+ startTimestamp: syncTimestamp // Align cursor timeline to actual start
874
873
  };
875
874
 
876
875
  try {
@@ -1349,13 +1348,6 @@ class MacRecorder extends EventEmitter {
1349
1348
  return true; // İlk event
1350
1349
  }
1351
1350
 
1352
- if (
1353
- currentData.type === "drag" ||
1354
- currentData.type === "rightdrag"
1355
- ) {
1356
- return true;
1357
- }
1358
-
1359
1351
  const last = this.lastCapturedData;
1360
1352
 
1361
1353
  // Event type değişmişse
@@ -1390,11 +1382,10 @@ class MacRecorder extends EventEmitter {
1390
1382
  * @param {Object} options.captureArea - Capture area for area recording coordinate transformation
1391
1383
  * @param {number} options.windowId - Window ID for window recording coordinate transformation
1392
1384
  * @param {number} options.startTimestamp - Pre-defined start timestamp for synchronization (optional)
1393
- * @param {number} options.interval - Örnekleme aralığı (ms), filepath ile çağrıda kullanılır; varsayılan 33ms (~30 Hz)
1394
1385
  */
1395
1386
  async startCursorCapture(intervalOrFilepath = 100, options = {}) {
1396
1387
  let filepath;
1397
- let interval = 33;
1388
+ let interval = 20; // Default 50 FPS
1398
1389
 
1399
1390
  // Parameter parsing: number = interval, string = filepath
1400
1391
  if (typeof intervalOrFilepath === "number") {
@@ -1408,10 +1399,6 @@ class MacRecorder extends EventEmitter {
1408
1399
  );
1409
1400
  }
1410
1401
 
1411
- if (typeof options.interval === "number") {
1412
- interval = Math.max(10, options.interval);
1413
- }
1414
-
1415
1402
  if (this.cursorCaptureInterval) {
1416
1403
  throw new Error("Cursor capture is already running");
1417
1404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.22.33",
3
+ "version": "2.22.34",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -43,9 +43,7 @@
43
43
  "build:electron-safe": "node build-electron-safe.js",
44
44
  "test:electron-safe": "node test-electron-safe.js",
45
45
  "clean:electron-safe": "node-gyp clean && rm -rf build",
46
- "canvas": "node make-canvas.js",
47
- "cursor:live": "node scripts/cursor-type-live.js",
48
- "test:cursor-types": "node scripts/cursor-types-15s-test.js"
46
+ "canvas": "node make-canvas.js"
49
47
  },
50
48
  "dependencies": {
51
49
  "node-addon-api": "^7.0.0"
@@ -8,7 +8,6 @@
8
8
  #import <dispatch/dispatch.h>
9
9
  #import "logging.h"
10
10
  #include <vector>
11
- #include <cstring>
12
11
  #include <math.h>
13
12
 
14
13
  #ifndef kAXHitTestParameterizedAttribute
@@ -310,180 +309,6 @@ static NSCursor* CursorFromSelector(SEL selector) {
310
309
  return func([NSCursor class], selector);
311
310
  }
312
311
 
313
- static BOOL CursorEqualsFactoryNamed(NSCursor *cursor, NSString *factoryMethodName) {
314
- if (!cursor || !factoryMethodName || [factoryMethodName length] == 0) {
315
- return NO;
316
- }
317
- SEL sel = NSSelectorFromString(factoryMethodName);
318
- NSCursor *ref = CursorFromSelector(sel);
319
- return ref != nil && cursor == ref;
320
- }
321
-
322
- static uint64_t CursorImagePixelHash(NSImage *image) {
323
- if (!image) {
324
- return 0;
325
- }
326
- NSRect imageRect = NSMakeRect(0, 0, [image size].width, [image size].height);
327
- CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:nil hints:nil];
328
- if (!cgImage) {
329
- for (NSImageRep *rep in [image representations]) {
330
- if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
331
- cgImage = [(NSBitmapImageRep *)rep CGImage];
332
- if (cgImage) {
333
- break;
334
- }
335
- }
336
- }
337
- }
338
- if (!cgImage) {
339
- return 0;
340
- }
341
- size_t width = CGImageGetWidth(cgImage);
342
- size_t height = CGImageGetHeight(cgImage);
343
- if (width == 0 || height == 0) {
344
- return 0;
345
- }
346
- size_t bytesPerPixel = 4;
347
- size_t bytesPerRow = width * bytesPerPixel;
348
- size_t bufferSize = bytesPerRow * height;
349
- if (bufferSize == 0) {
350
- return 0;
351
- }
352
- std::vector<unsigned char> buffer(bufferSize);
353
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
354
- if (!colorSpace) {
355
- return 0;
356
- }
357
- CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Little | (CGBitmapInfo)kCGImageAlphaPremultipliedLast;
358
- CGContextRef context = CGBitmapContextCreate(buffer.data(),
359
- width,
360
- height,
361
- 8,
362
- bytesPerRow,
363
- colorSpace,
364
- bitmapInfo);
365
- CGColorSpaceRelease(colorSpace);
366
- if (!context) {
367
- return 0;
368
- }
369
- CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
370
- CGContextRelease(context);
371
- return FNV1AHash(buffer.data(), buffer.size());
372
- }
373
-
374
- static uint64_t CursorImagePixelHashNormalized32(NSImage *image) {
375
- if (!image) {
376
- return 0;
377
- }
378
- const int dim = 32;
379
- size_t bytesPerPixel = 4;
380
- size_t bytesPerRow = (size_t)dim * bytesPerPixel;
381
- std::vector<unsigned char> buffer(bytesPerRow * (size_t)dim);
382
- memset(buffer.data(), 0, buffer.size());
383
-
384
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
385
- if (!colorSpace) {
386
- return 0;
387
- }
388
- CGContextRef ctx = CGBitmapContextCreate(buffer.data(),
389
- (size_t)dim,
390
- (size_t)dim,
391
- 8,
392
- bytesPerRow,
393
- colorSpace,
394
- kCGBitmapByteOrder32Little | (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
395
- CGColorSpaceRelease(colorSpace);
396
- if (!ctx) {
397
- return 0;
398
- }
399
-
400
- CGContextClearRect(ctx, CGRectMake(0, 0, dim, dim));
401
- NSGraphicsContext *nsgc = [NSGraphicsContext graphicsContextWithCGContext:ctx flipped:YES];
402
- [NSGraphicsContext saveGraphicsState];
403
- [NSGraphicsContext setCurrentContext:nsgc];
404
-
405
- NSSize sz = [image size];
406
- if (sz.width <= 0 || sz.height <= 0) {
407
- [NSGraphicsContext restoreGraphicsState];
408
- CGContextRelease(ctx);
409
- return 0;
410
- }
411
- CGFloat scale = MIN((CGFloat)dim / sz.width, (CGFloat)dim / sz.height);
412
- CGFloat rw = sz.width * scale;
413
- CGFloat rh = sz.height * scale;
414
- CGFloat ox = ((CGFloat)dim - rw) / 2.0;
415
- CGFloat oy = ((CGFloat)dim - rh) / 2.0;
416
- [image drawInRect:NSMakeRect(ox, oy, rw, rh)
417
- fromRect:NSZeroRect
418
- operation:NSCompositingOperationSourceOver
419
- fraction:1.0
420
- respectFlipped:YES
421
- hints:nil];
422
-
423
- [NSGraphicsContext restoreGraphicsState];
424
- CGContextRelease(ctx);
425
- return FNV1AHash(buffer.data(), buffer.size());
426
- }
427
-
428
- static NSString* DiagonalResizeTypeByVisualMatch(NSCursor *cursor) {
429
- if (!cursor) {
430
- return nil;
431
- }
432
- if (CursorEqualsFactoryNamed(cursor, @"resizeNorthWestSouthEastCursor")) {
433
- return @"nwse-resize";
434
- }
435
- if (CursorEqualsFactoryNamed(cursor, @"resizeNorthEastSouthWestCursor")) {
436
- return @"nesw-resize";
437
- }
438
-
439
- NSImage *curImg = [cursor image];
440
- if (!curImg) {
441
- return nil;
442
- }
443
-
444
- NSData *curTIFF = [curImg TIFFRepresentation];
445
- uint64_t curRaw = CursorImagePixelHash(curImg);
446
- uint64_t curNorm = CursorImagePixelHashNormalized32(curImg);
447
-
448
- struct DiagonalFactoryEntry {
449
- const char *selectorName;
450
- const char *type;
451
- };
452
- static const DiagonalFactoryEntry kDiagonalFactories[] = {
453
- { "resizeNorthWestSouthEastCursor", "nwse-resize" },
454
- { "resizeNorthEastSouthWestCursor", "nesw-resize" },
455
- };
456
-
457
- for (size_t i = 0; i < sizeof(kDiagonalFactories) / sizeof(kDiagonalFactories[0]); i++) {
458
- NSString *selStr = [NSString stringWithUTF8String:kDiagonalFactories[i].selectorName];
459
- SEL sel = NSSelectorFromString(selStr);
460
- if (![NSCursor respondsToSelector:sel]) {
461
- continue;
462
- }
463
- NSCursor *ref = CursorFromSelector(sel);
464
- if (!ref) {
465
- continue;
466
- }
467
- NSImage *refImg = [ref image];
468
- if (!refImg) {
469
- continue;
470
- }
471
- NSData *refTIFF = [refImg TIFFRepresentation];
472
- if (curTIFF && refTIFF && [curTIFF isEqualToData:refTIFF]) {
473
- return [NSString stringWithUTF8String:kDiagonalFactories[i].type];
474
- }
475
- uint64_t refRaw = CursorImagePixelHash(refImg);
476
- if (curRaw != 0 && curRaw == refRaw) {
477
- return [NSString stringWithUTF8String:kDiagonalFactories[i].type];
478
- }
479
- uint64_t refNorm = CursorImagePixelHashNormalized32(refImg);
480
- if (curNorm != 0 && curNorm == refNorm) {
481
- return [NSString stringWithUTF8String:kDiagonalFactories[i].type];
482
- }
483
- }
484
- return nil;
485
- }
486
-
487
312
  static void AddStandardCursorFingerprint(NSCursor *cursor, NSString *cursorType) {
488
313
  if (!cursor || !cursorType) {
489
314
  return;
@@ -534,7 +359,7 @@ static void InitializeCursorFingerprintMap(void) {
534
359
  AddCursorIfAvailable(@selector(dragCopyCursor), @"copy");
535
360
  AddCursorIfAvailable(@selector(dragLinkCursor), @"alias");
536
361
  AddCursorIfAvailable(@selector(resizeLeftRightCursor), @"col-resize");
537
- AddCursorIfAvailable(@selector(resizeUpDownCursor), @"ns-resize");
362
+ AddCursorIfAvailable(@selector(resizeUpDownCursor), @"row-resize");
538
363
  AddCursorIfAvailableByName(@"resizeLeftCursor", @"col-resize");
539
364
  AddCursorIfAvailableByName(@"resizeRightCursor", @"col-resize");
540
365
  AddCursorIfAvailableByName(@"resizeUpCursor", @"ns-resize");
@@ -677,26 +502,21 @@ static BOOL ShouldEmitCursorEvent(CGPoint location, NSString *cursorType, NSStri
677
502
  BOOL moved = fabs(location.x - g_lastCursorLocation.x) >= movementThreshold ||
678
503
  fabs(location.y - g_lastCursorLocation.y) >= movementThreshold;
679
504
  BOOL eventChanged = !StringsEqual(eventType, g_lastCursorEventType);
680
- BOOL isDragEvent = StringsEqual(eventType, @"drag") || StringsEqual(eventType, @"rightdrag");
681
- BOOL isMoveOnly = StringsEqual(eventType, @"move");
505
+ BOOL isMoveEvent = StringsEqual(eventType, @"move") || StringsEqual(eventType, @"drag");
682
506
  BOOL isClickEvent = StringsEqual(eventType, @"mousedown") ||
683
507
  StringsEqual(eventType, @"mouseup") ||
684
508
  StringsEqual(eventType, @"rightmousedown") ||
685
509
  StringsEqual(eventType, @"rightmouseup");
686
510
 
687
- if (isDragEvent) {
688
- return YES;
689
- }
690
-
691
- if (isMoveOnly) {
692
- BOOL cursorChanged = !StringsEqual(cursorType, g_lastCursorType);
693
- return moved || cursorChanged;
511
+ if (isMoveEvent) {
512
+ return moved;
694
513
  }
695
514
 
696
515
  if (isClickEvent) {
697
516
  return eventChanged || moved;
698
517
  }
699
518
 
519
+ // Fallback: only emit when something actually changed
700
520
  BOOL cursorChanged = !StringsEqual(cursorType, g_lastCursorType);
701
521
  return moved || cursorChanged || eventChanged;
702
522
  }
@@ -850,10 +670,7 @@ static NSString* CursorTypeForWindowBorder(AXUIElementRef element, CGPoint curso
850
670
  CFRelease(positionValue);
851
671
  CFRelease(sizeValue);
852
672
 
853
- // Köşe: köşe noktasına uzaklık (kutu sınırında col ↔ diagonal sıçramasını azaltır).
854
- const CGFloat cornerRadius = 9.0;
855
- const CGFloat edgeInset = 12.0;
856
-
673
+ CGFloat edge = 4.0;
857
674
  CGFloat x = cursorPos.x - windowOrigin.x;
858
675
  CGFloat y = cursorPos.y - windowOrigin.y;
859
676
  CGFloat w = windowSize.width;
@@ -863,44 +680,17 @@ static NSString* CursorTypeForWindowBorder(AXUIElementRef element, CGPoint curso
863
680
  return nil;
864
681
  }
865
682
 
866
- if (w < cornerRadius * 2.1 || h < cornerRadius * 2.1) {
867
- CGFloat edge = edgeInset;
868
- BOOL nearLeft = (x >= 0 && x <= edge);
869
- BOOL nearRight = (x >= w - edge && x <= w);
870
- BOOL nearTop = (y >= 0 && y <= edge);
871
- BOOL nearBottom = (y >= h - edge && y <= h);
872
- if ((nearLeft && nearTop) || (nearRight && nearBottom)) {
873
- return @"nwse-resize";
874
- }
875
- if ((nearRight && nearTop) || (nearLeft && nearBottom)) {
876
- return @"nesw-resize";
877
- }
878
- if (nearLeft || nearRight) {
879
- return @"col-resize";
880
- }
881
- if (nearTop || nearBottom) {
882
- return @"ns-resize";
883
- }
884
- return nil;
885
- }
886
-
887
- CGFloat dBottomLeft = hypot(x, y);
888
- CGFloat dTopRight = hypot(w - x, h - y);
889
- CGFloat dBottomRight = hypot(w - x, y);
890
- CGFloat dTopLeft = hypot(x, h - y);
683
+ BOOL nearLeft = (x >= 0 && x <= edge);
684
+ BOOL nearRight = (x >= w - edge && x <= w);
685
+ BOOL nearTop = (y >= 0 && y <= edge);
686
+ BOOL nearBottom = (y >= h - edge && y <= h);
891
687
 
892
- if (dBottomLeft <= cornerRadius || dTopRight <= cornerRadius) {
688
+ if ((nearLeft && nearTop) || (nearRight && nearBottom)) {
893
689
  return @"nwse-resize";
894
690
  }
895
- if (dBottomRight <= cornerRadius || dTopLeft <= cornerRadius) {
691
+ if ((nearRight && nearTop) || (nearLeft && nearBottom)) {
896
692
  return @"nesw-resize";
897
693
  }
898
-
899
- BOOL nearLeft = (x >= 0 && x <= edgeInset);
900
- BOOL nearRight = (x >= w - edgeInset && x <= w);
901
- BOOL nearTop = (y >= 0 && y <= edgeInset);
902
- BOOL nearBottom = (y >= h - edgeInset && y <= h);
903
-
904
694
  if (nearLeft || nearRight) {
905
695
  return @"col-resize";
906
696
  }
@@ -911,42 +701,6 @@ static NSString* CursorTypeForWindowBorder(AXUIElementRef element, CGPoint curso
911
701
  return nil;
912
702
  }
913
703
 
914
- static NSString* ResizeTypeFromWindowUnderCursor(CGPoint cursorPos) {
915
- @autoreleasepool {
916
- AXUIElementRef systemWide = AXUIElementCreateSystemWide();
917
- if (!systemWide) {
918
- return nil;
919
- }
920
- AXUIElementRef hit = NULL;
921
- AXError err = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &hit);
922
- CFRelease(systemWide);
923
- if (err != kAXErrorSuccess || !hit) {
924
- if (hit) {
925
- CFRelease(hit);
926
- }
927
- return nil;
928
- }
929
-
930
- AXUIElementRef node = hit;
931
- for (int depth = 0; depth < 24 && node; depth++) {
932
- NSString *role = CopyAttributeString(node, kAXRoleAttribute);
933
- if ([role isEqualToString:@"AXWindow"]) {
934
- NSString *borderType = CursorTypeForWindowBorder(node, cursorPos);
935
- CFRelease(node);
936
- return borderType;
937
- }
938
- AXUIElementRef parent = NULL;
939
- AXError perr = AXUIElementCopyAttributeValue(node, kAXParentAttribute, (CFTypeRef *)&parent);
940
- CFRelease(node);
941
- node = NULL;
942
- if (perr == kAXErrorSuccess && parent) {
943
- node = parent;
944
- }
945
- }
946
- return nil;
947
- }
948
- }
949
-
950
704
  static NSString* CursorTypeFromAccessibilityElement(AXUIElementRef element, CGPoint cursorPos) {
951
705
  if (!element) {
952
706
  return nil;
@@ -1183,14 +937,6 @@ static NSString* cursorTypeFromCursorName(NSString *value) {
1183
937
  return @"zoom-in";
1184
938
  }
1185
939
 
1186
- // CGS / tema kısa diagonal tokenları (nwse / nesw)
1187
- if ([normalized containsString:@"nesw"]) {
1188
- return @"nesw-resize";
1189
- }
1190
- if ([normalized containsString:@"nwse"]) {
1191
- return @"nwse-resize";
1192
- }
1193
-
1194
940
  // All-scroll pattern (move in all directions)
1195
941
  if ([normalized containsString:@"all-scroll"] ||
1196
942
  [normalized containsString:@"allscroll"] ||
@@ -1637,13 +1383,6 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1637
1383
  CGFloat relativeX = width > 0 ? hotspot.x / width : 0;
1638
1384
  CGFloat relativeY = height > 0 ? hotspot.y / height : 0;
1639
1385
 
1640
- if (cursor) {
1641
- NSString *diag = DiagonalResizeTypeByVisualMatch(cursor);
1642
- if (diag) {
1643
- return diag;
1644
- }
1645
- }
1646
-
1647
1386
  // Tolerance for floating point comparison
1648
1387
  CGFloat tolerance = 0.05;
1649
1388
  CGFloat tightTolerance = 0.02; // For precise hotspot matching
@@ -1683,35 +1422,29 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1683
1422
  // Distinguished by pointer equality
1684
1423
  if (approx(width, 32) && approx(height, 32) && approx(relativeY, 0.531)) {
1685
1424
  if (cursor) {
1686
- if ([NSCursor respondsToSelector:@selector(closedHandCursor)] &&
1687
- cursor == [NSCursor closedHandCursor]) {
1425
+ if (cursor == [NSCursor closedHandCursor]) {
1688
1426
  return @"grabbing";
1689
1427
  }
1690
- if ([NSCursor respondsToSelector:@selector(openHandCursor)] &&
1691
- cursor == [NSCursor openHandCursor]) {
1428
+ if (cursor == [NSCursor openHandCursor]) {
1692
1429
  return @"grab";
1693
1430
  }
1694
1431
  }
1695
1432
  return @"grab"; // Default to grab if can't distinguish
1696
1433
  }
1697
1434
 
1698
- // 24x24: köşe diagonal resize ile crosshair/move aynı boyutta — önce diagonal fabrika
1435
+ // 24x24 cursors: crosshair vs move/all-scroll
1436
+ // Distinguished by precise hotspot position
1699
1437
  if (approx(width, 24) && approx(height, 24)) {
1700
- if (cursor) {
1701
- NSString *diag = DiagonalResizeTypeByVisualMatch(cursor);
1702
- if (diag) {
1703
- return diag;
1704
- }
1705
- }
1706
1438
  // crosshair: hotspot rel=(0.458, 0.458)
1707
1439
  if (approxTight(relativeX, 0.458) && approxTight(relativeY, 0.458)) {
1708
1440
  return @"crosshair";
1709
1441
  }
1710
1442
  // move/all-scroll: hotspot rel=(0.5, 0.5)
1711
1443
  if (approxTight(relativeX, 0.5) && approxTight(relativeY, 0.5)) {
1712
- return @"move";
1444
+ return @"move"; // or all-scroll, they're identical
1713
1445
  }
1714
- return nil;
1446
+ // Fallback for 24x24
1447
+ return @"crosshair";
1715
1448
  }
1716
1449
 
1717
1450
  // help/cell: 18x18, ratio=1.0, hotspot rel=(0.5, 0.5)
@@ -1772,29 +1505,7 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1772
1505
 
1773
1506
  // ne-resize/nw-resize/se-resize/sw-resize/nesw-resize/nwse-resize: 22x22, ratio=1.0, hotspot rel=(0.5, 0.5)
1774
1507
  if (approx(width, 22) && approx(height, 22)) {
1775
- if (cursor) {
1776
- NSString *diag = DiagonalResizeTypeByVisualMatch(cursor);
1777
- if (diag) {
1778
- return diag;
1779
- }
1780
- if ([NSCursor respondsToSelector:@selector(resizeUpCursor)] &&
1781
- cursor == [NSCursor resizeUpCursor]) {
1782
- return @"n-resize";
1783
- }
1784
- if ([NSCursor respondsToSelector:@selector(resizeDownCursor)] &&
1785
- cursor == [NSCursor resizeDownCursor]) {
1786
- return @"s-resize";
1787
- }
1788
- if ([NSCursor respondsToSelector:@selector(resizeLeftCursor)] &&
1789
- cursor == [NSCursor resizeLeftCursor]) {
1790
- return @"w-resize";
1791
- }
1792
- if ([NSCursor respondsToSelector:@selector(resizeRightCursor)] &&
1793
- cursor == [NSCursor resizeRightCursor]) {
1794
- return @"e-resize";
1795
- }
1796
- }
1797
- return nil;
1508
+ return @"nwse-resize"; // Default to nwse-resize for all diagonal cursors
1798
1509
  }
1799
1510
 
1800
1511
  // zoom-in/zoom-out: 28x26, ratio=1.077, hotspot rel=(0.428, 0.423)
@@ -1816,12 +1527,10 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1816
1527
  if (approxTight(relativeX, 0.161) && approxTight(relativeY, 0.1)) {
1817
1528
  return @"default";
1818
1529
  }
1819
- // Ortak 28x40 ok/beachball hotspot bandı: spinner ile karışmasın diye önce ok
1530
+ // context-menu/progress/wait/copy/no-drop/not-allowed: hotspot rel=(0.179, 0.125) - hotspot at (5, 5)
1820
1531
  if (approxTight(relativeX, 0.179) && approxTight(relativeY, 0.125)) {
1532
+ // Try pointer equality for standard cursors
1821
1533
  if (cursor) {
1822
- if (cursor == [NSCursor arrowCursor]) {
1823
- return @"default";
1824
- }
1825
1534
  if (cursor == [NSCursor contextualMenuCursor]) {
1826
1535
  return @"context-menu";
1827
1536
  }
@@ -1832,7 +1541,10 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1832
1541
  return @"not-allowed";
1833
1542
  }
1834
1543
  }
1835
- return @"default";
1544
+ // NOTE: progress, wait, no-drop don't have standard NSCursor pointers
1545
+ // Return "progress" as default for this hotspot pattern (better than "default")
1546
+ // Let cursor name detection in caller distinguish between progress/wait
1547
+ return @"progress";
1836
1548
  }
1837
1549
  return @"default";
1838
1550
  }
@@ -1859,16 +1571,13 @@ static NSString* cursorTypeFromNSCursor(NSCursor *cursor) {
1859
1571
  if (cursor == [NSCursor pointingHandCursor]) {
1860
1572
  return @"pointer";
1861
1573
  }
1862
- if ([NSCursor respondsToSelector:@selector(crosshairCursor)] &&
1863
- cursor == [NSCursor crosshairCursor]) {
1574
+ if (cursor == [NSCursor crosshairCursor]) {
1864
1575
  return @"crosshair";
1865
1576
  }
1866
- if ([NSCursor respondsToSelector:@selector(openHandCursor)] &&
1867
- cursor == [NSCursor openHandCursor]) {
1577
+ if (cursor == [NSCursor openHandCursor]) {
1868
1578
  return @"grab";
1869
1579
  }
1870
- if ([NSCursor respondsToSelector:@selector(closedHandCursor)] &&
1871
- cursor == [NSCursor closedHandCursor]) {
1580
+ if (cursor == [NSCursor closedHandCursor]) {
1872
1581
  return @"grabbing";
1873
1582
  }
1874
1583
  if (cursor == [NSCursor operationNotAllowedCursor]) {
@@ -1884,42 +1593,14 @@ static NSString* cursorTypeFromNSCursor(NSCursor *cursor) {
1884
1593
  return @"context-menu";
1885
1594
  }
1886
1595
 
1887
- NSString *diagonalVisual = DiagonalResizeTypeByVisualMatch(cursor);
1888
- if (diagonalVisual) {
1889
- return diagonalVisual;
1890
- }
1891
-
1892
- if ([NSCursor respondsToSelector:@selector(resizeLeftCursor)] &&
1893
- cursor == [NSCursor resizeLeftCursor]) {
1894
- return @"col-resize";
1895
- }
1896
- if ([NSCursor respondsToSelector:@selector(resizeRightCursor)] &&
1897
- cursor == [NSCursor resizeRightCursor]) {
1898
- return @"col-resize";
1899
- }
1900
- if ([NSCursor respondsToSelector:@selector(resizeUpCursor)] &&
1901
- cursor == [NSCursor resizeUpCursor]) {
1902
- return @"n-resize";
1903
- }
1904
- if ([NSCursor respondsToSelector:@selector(resizeDownCursor)] &&
1905
- cursor == [NSCursor resizeDownCursor]) {
1906
- return @"s-resize";
1907
- }
1908
- if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)] &&
1909
- cursor == [NSCursor resizeLeftRightCursor]) {
1910
- return @"col-resize";
1911
- }
1912
- if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)] &&
1913
- cursor == [NSCursor resizeUpDownCursor]) {
1914
- return @"ns-resize";
1915
- }
1916
- if (@available(macOS 15.0, *)) {
1917
- if ([NSCursor respondsToSelector:@selector(columnResizeCursor)] &&
1918
- cursor == [NSCursor columnResizeCursor]) {
1596
+ // Resize cursors
1597
+ if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)]) {
1598
+ if (cursor == [NSCursor resizeLeftRightCursor]) {
1919
1599
  return @"col-resize";
1920
1600
  }
1921
- if ([NSCursor respondsToSelector:@selector(rowResizeCursor)] &&
1922
- cursor == [NSCursor rowResizeCursor]) {
1601
+ }
1602
+ if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)]) {
1603
+ if (cursor == [NSCursor resizeUpDownCursor]) {
1923
1604
  return @"row-resize";
1924
1605
  }
1925
1606
  }
@@ -1991,6 +1672,12 @@ static NSString* detectSystemCursorType(void) {
1991
1672
  }
1992
1673
 
1993
1674
  int cursorSeed = SafeCGSCurrentCursorSeed();
1675
+ if (cursorSeed > 0) {
1676
+ NSString *seedType = cursorTypeFromSeed(cursorSeed);
1677
+ if (seedType) {
1678
+ return seedType;
1679
+ }
1680
+ }
1994
1681
 
1995
1682
  void (^fetchCursorBlock)(void) = ^{
1996
1683
  NSCursor *currentCursor = nil;
@@ -2094,23 +1781,14 @@ static NSString* detectSystemCursorType(void) {
2094
1781
  dispatch_sync(dispatch_get_main_queue(), fetchCursorBlock);
2095
1782
  }
2096
1783
 
2097
- if (cursorType && ![cursorType isEqualToString:@"default"]) {
2098
- if (cursorSeed > 0) {
2099
- addCursorToSeedMap(cursorType, cursorSeed);
2100
- }
2101
- return cursorType;
1784
+ if (cursorType && ![cursorType isEqualToString:@"default"] && cursorSeed > 0) {
1785
+ addCursorToSeedMap(cursorType, cursorSeed);
2102
1786
  }
2103
1787
 
2104
- if (cursorSeed > 0) {
2105
- NSString *seedType = cursorTypeFromSeed(cursorSeed);
2106
- if (seedType) {
2107
- return seedType;
2108
- }
2109
- }
2110
-
2111
- return cursorType ?: @"default";
1788
+ return cursorType;
2112
1789
  }
2113
1790
 
1791
+ // Desktop'ta SVG karşılığı olmayan cursor tiplerini desteklenen tiplere normalize et
2114
1792
  static NSString* normalizeCursorTypeForDesktop(NSString *cursorType) {
2115
1793
  if (!cursorType || [cursorType length] == 0) {
2116
1794
  return @"default";
@@ -2208,23 +1886,11 @@ NSString* getCursorType() {
2208
1886
  int currentSeed = SafeCGSCurrentCursorSeed();
2209
1887
  g_lastCursorSeed = currentSeed; // Save for getCursorPosition()
2210
1888
 
2211
- // Önce NSCursor/CGS; köşe diagonal için sistem imleci güvenilir olmayabiliyor — AX ile pencere çerçevesi düzeltmesi
1889
+ // Use cursorTypeFromNSCursor for detection (pointer equality + image-based)
1890
+ // DO NOT use accessibility detection as it's unreliable and causes false positives
2212
1891
  NSString *systemCursorType = detectSystemCursorType();
2213
1892
  NSString *rawType = systemCursorType && [systemCursorType length] > 0 ? systemCursorType : @"default";
2214
1893
 
2215
- __block NSString *chromeResize = nil;
2216
- if ([NSThread isMainThread]) {
2217
- chromeResize = ResizeTypeFromWindowUnderCursor(cursorPos);
2218
- } else {
2219
- dispatch_sync(dispatch_get_main_queue(), ^{
2220
- chromeResize = ResizeTypeFromWindowUnderCursor(cursorPos);
2221
- });
2222
- }
2223
-
2224
- if (chromeResize) {
2225
- rawType = chromeResize;
2226
- }
2227
-
2228
1894
  // Desktop SVG'lerine uyumlu tipe normalize et
2229
1895
  NSString *finalType = normalizeCursorTypeForDesktop(rawType);
2230
1896
 
@@ -3,22 +3,6 @@
3
3
  #import <AppKit/AppKit.h>
4
4
  #import "../logging.h"
5
5
 
6
- static NSCursor *CursorFactoryNamed(NSString *name) {
7
- if (!name || [name length] == 0) return nil;
8
- SEL sel = NSSelectorFromString(name);
9
- if (!sel || ![NSCursor respondsToSelector:sel]) return nil;
10
- IMP imp = [NSCursor methodForSelector:sel];
11
- if (!imp) return nil;
12
- typedef NSCursor *(*CursorFactoryFunc)(id, SEL);
13
- CursorFactoryFunc fn = (CursorFactoryFunc)imp;
14
- return fn([NSCursor class], sel);
15
- }
16
-
17
- static BOOL CursorEqualsFactoryNamed(NSCursor *cursor, NSString *factoryName) {
18
- NSCursor *ref = CursorFactoryNamed(factoryName);
19
- return ref != nil && cursor == ref;
20
- }
21
-
22
6
  // Thread-safe cursor tracking for Electron
23
7
  static dispatch_queue_t g_cursorQueue = nil;
24
8
 
@@ -39,34 +23,18 @@ static NSString* MapCursorToType(NSCursor *cursor) {
39
23
  if (cursor == [NSCursor pointingHandCursor]) return @"pointer";
40
24
  if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)]) {
41
25
  if (cursor == [NSCursor resizeLeftRightCursor] ||
42
- ([NSCursor respondsToSelector:@selector(resizeLeftCursor)] && cursor == [NSCursor resizeLeftCursor]) ||
43
- ([NSCursor respondsToSelector:@selector(resizeRightCursor)] && cursor == [NSCursor resizeRightCursor])) {
26
+ [NSCursor instancesRespondToSelector:@selector(resizeLeftCursor)] && (cursor == [NSCursor resizeLeftCursor]) ||
27
+ [NSCursor instancesRespondToSelector:@selector(resizeRightCursor)] && (cursor == [NSCursor resizeRightCursor])) {
44
28
  return @"col-resize";
45
29
  }
46
30
  }
47
31
  if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)]) {
48
32
  if (cursor == [NSCursor resizeUpDownCursor] ||
49
- ([NSCursor respondsToSelector:@selector(resizeUpCursor)] && cursor == [NSCursor resizeUpCursor]) ||
50
- ([NSCursor respondsToSelector:@selector(resizeDownCursor)] && cursor == [NSCursor resizeDownCursor])) {
33
+ [NSCursor instancesRespondToSelector:@selector(resizeUpCursor)] && (cursor == [NSCursor resizeUpCursor]) ||
34
+ [NSCursor instancesRespondToSelector:@selector(resizeDownCursor)] && (cursor == [NSCursor resizeDownCursor])) {
51
35
  return @"ns-resize";
52
36
  }
53
37
  }
54
- if (CursorEqualsFactoryNamed(cursor, @"resizeNorthWestSouthEastCursor")) {
55
- return @"nwse-resize";
56
- }
57
- if (CursorEqualsFactoryNamed(cursor, @"resizeNorthEastSouthWestCursor")) {
58
- return @"nesw-resize";
59
- }
60
- if (@available(macOS 15.0, *)) {
61
- if ([NSCursor respondsToSelector:@selector(columnResizeCursor)] &&
62
- cursor == [NSCursor columnResizeCursor]) {
63
- return @"col-resize";
64
- }
65
- if ([NSCursor respondsToSelector:@selector(rowResizeCursor)] &&
66
- cursor == [NSCursor rowResizeCursor]) {
67
- return @"row-resize";
68
- }
69
- }
70
38
  if ([NSCursor respondsToSelector:@selector(openHandCursor)] && cursor == [NSCursor openHandCursor]) return @"grab";
71
39
  if ([NSCursor respondsToSelector:@selector(closedHandCursor)] && cursor == [NSCursor closedHandCursor]) return @"grabbing";
72
40
  if ([NSCursor respondsToSelector:@selector(crosshairCursor)] && cursor == [NSCursor crosshairCursor]) return @"crosshair";
@@ -61,15 +61,17 @@ Napi::Value GetWindowsElectronSafe(const Napi::CallbackInfo& info) {
61
61
  for (SCWindow *window in content.windows) {
62
62
  // Filter out system and small windows
63
63
  if (window.frame.size.width < 50 || window.frame.size.height < 50) continue;
64
- if (!window.title || window.title.length == 0) continue;
65
-
64
+
66
65
  NSString *appName = window.owningApplication.applicationName ?: @"Unknown";
67
-
66
+
68
67
  if (ShouldSkipWindowOwner(appName)) continue;
69
-
68
+
69
+ // Fallback: frameless Electron pencereleri kCGWindowName/title döndürmez
70
+ NSString *displayTitle = (window.title && window.title.length > 0) ? window.title : appName;
71
+
70
72
  NSDictionary *windowInfo = @{
71
73
  @"id": @(window.windowID),
72
- @"name": window.title,
74
+ @"name": displayTitle,
73
75
  @"appName": appName,
74
76
  @"bundleId": window.owningApplication.bundleIdentifier ?: @"",
75
77
  @"x": @((int)window.frame.origin.x),
@@ -116,7 +118,9 @@ Napi::Value GetWindowsElectronSafe(const Napi::CallbackInfo& info) {
116
118
 
117
119
  NSString *appName = (__bridge NSString*)ownerName;
118
120
  NSString *windowTitle = windowName ? (__bridge NSString*)windowName : @"";
119
-
121
+ // Fallback: frameless Electron pencereleri kCGWindowName döndürmez
122
+ if (windowTitle.length == 0) windowTitle = appName;
123
+
120
124
  if (ShouldSkipWindowOwner(appName)) continue;
121
125
 
122
126
  // Get window bounds
@@ -1042,6 +1042,12 @@ Napi::Value GetWindows(const Napi::CallbackInfo& info) {
1042
1042
  CGRectMakeWithDictionaryRepresentation(boundsRef, &bounds);
1043
1043
  }
1044
1044
 
1045
+ // Fallback: frameless Electron pencereleri kCGWindowName döndürmez,
1046
+ // bu durumda appName'i kullan ki pencere listede görünsün.
1047
+ if (windowName.empty() && !appName.empty()) {
1048
+ windowName = appName;
1049
+ }
1050
+
1045
1051
  // Skip windows without name or very small windows
1046
1052
  if (windowName.empty() || bounds.size.width < 50 || bounds.size.height < 50) {
1047
1053
  continue;
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const MacRecorder = require('../index.js');
4
-
5
- const INTERVAL_MS = 33;
6
-
7
- const recorder = new MacRecorder();
8
-
9
- process.stderr.write(
10
- `Canlı cursor tipi (${INTERVAL_MS}ms). Çıkmak: Ctrl+C\n`
11
- );
12
-
13
- function tick() {
14
- try {
15
- const p = recorder.getCursorPosition();
16
- const type = p.cursorType != null ? String(p.cursorType) : '?';
17
- const seed =
18
- typeof p.seed === 'number' && p.seed > 0 ? ` seed:${p.seed}` : '';
19
- process.stdout.write(`\r\x1b[KcursorType: ${type}${seed}`);
20
- } catch (e) {
21
- process.stdout.write(`\r\x1b[K${e.message || e}`);
22
- }
23
- }
24
-
25
- const id = setInterval(tick, INTERVAL_MS);
26
- tick();
27
-
28
- process.on('SIGINT', () => {
29
- clearInterval(id);
30
- process.stdout.write('\n');
31
- process.exit(0);
32
- });
@@ -1,122 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- const MacRecorder = require('../index.js');
5
-
6
- const OUT_DIR = path.join(__dirname, '..', 'test-output');
7
- const DURATION_SEC = 15;
8
-
9
- async function sleep(ms) {
10
- return new Promise((r) => setTimeout(r, ms));
11
- }
12
-
13
- function summarizeCursorTypes(cursorJsonPath) {
14
- if (!cursorJsonPath || !fs.existsSync(cursorJsonPath)) {
15
- return { counts: {}, ordered: [] };
16
- }
17
- const raw = fs.readFileSync(cursorJsonPath, 'utf8');
18
- let events;
19
- try {
20
- events = JSON.parse(raw);
21
- } catch {
22
- return { counts: {}, ordered: [] };
23
- }
24
- if (!Array.isArray(events)) {
25
- return { counts: {}, ordered: [] };
26
- }
27
- const counts = {};
28
- const order = [];
29
- for (const ev of events) {
30
- const t = ev && ev.cursorType ? String(ev.cursorType) : '?';
31
- if (counts[t] === undefined) {
32
- counts[t] = 0;
33
- order.push(t);
34
- }
35
- counts[t]++;
36
- }
37
- return { counts, ordered: order };
38
- }
39
-
40
- async function main() {
41
- fs.mkdirSync(OUT_DIR, { recursive: true });
42
-
43
- const stamp = Date.now();
44
- const screenPath = path.join(OUT_DIR, `cursor-types-test-${stamp}.mov`);
45
-
46
- const recorder = new MacRecorder();
47
-
48
- const displays = await recorder.getDisplays();
49
- const cameras = await recorder.getCameraDevices();
50
- const audioDevices = await recorder.getAudioDevices();
51
-
52
- if (!displays.length) {
53
- process.stderr.write('No displays.\n');
54
- process.exit(1);
55
- }
56
-
57
- const display = displays[0];
58
-
59
- let cursorPathFromStart = null;
60
- recorder.once('recordingStarted', (info) => {
61
- cursorPathFromStart = info.cursorOutputPath || null;
62
- });
63
-
64
- const options = {
65
- displayId: display.id,
66
- captureCamera: cameras.length > 0,
67
- cameraDeviceId: cameras.length > 0 ? cameras[0].id : undefined,
68
- includeMicrophone: audioDevices.length > 0,
69
- audioDeviceId: audioDevices.length > 0 ? audioDevices[0].id : undefined,
70
- includeSystemAudio: true,
71
- captureCursor: true,
72
- frameRate: 60,
73
- };
74
-
75
- process.stdout.write(
76
- `Kayıt ${DURATION_SEC}s: ekran + sistem sesi + mik ${
77
- options.includeMicrophone ? 'açık' : 'kapalı'
78
- } + kamera ${options.captureCamera ? 'açık' : 'kapalı'} + cursor JSON.\n` +
79
- `Kenarlarda resize, köşelerde çapraz, grab/grabbing ve crosshair deneyin.\n\n`
80
- );
81
-
82
- await recorder.startRecording(screenPath, options);
83
-
84
- const cursorPath = cursorPathFromStart;
85
-
86
- for (let i = 1; i <= DURATION_SEC; i++) {
87
- await sleep(1000);
88
- process.stdout.write(`${i}s `);
89
- }
90
- process.stdout.write('\n');
91
-
92
- await recorder.stopRecording();
93
-
94
- process.stdout.write('\nDosyalar:\n');
95
- process.stdout.write(` Ekran: ${screenPath}\n`);
96
- if (options.captureCamera && cameras.length) {
97
- process.stdout.write(
98
- ` Kamera: kayıt başladıktan sonra native temp_camera_* ile aynı oturum\n`
99
- );
100
- }
101
- if (options.includeMicrophone || options.includeSystemAudio) {
102
- process.stdout.write(
103
- ` Ses: kayıt başladıktan sonra native temp_audio_* ile aynı oturum\n`
104
- );
105
- }
106
- process.stdout.write(` Cursor JSON: ${cursorPath || 'bilinmiyor'}\n`);
107
-
108
- if (cursorPath && fs.existsSync(cursorPath)) {
109
- const { counts, ordered } = summarizeCursorTypes(cursorPath);
110
- process.stdout.write('\nCursor JSON — cursorType özeti (ilk görülme sırası):\n');
111
- for (const t of ordered) {
112
- process.stdout.write(` ${t}: ${counts[t]}\n`);
113
- }
114
- }
115
-
116
- recorder.removeAllListeners();
117
- }
118
-
119
- main().catch((e) => {
120
- process.stderr.write(String(e && e.message ? e.message : e) + '\n');
121
- process.exit(1);
122
- });