node-mac-recorder 2.22.14 → 2.22.16

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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { EventEmitter } = require("events");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
+ const cursorCapturePolling = require("./lib/cursorCapture/polling");
4
5
 
5
6
  // Auto-switch to Electron-safe implementation when running under Electron and binary exists
6
7
  let USE_ELECTRON_SAFE = false;
@@ -1340,365 +1341,17 @@ class MacRecorder extends EventEmitter {
1340
1341
  });
1341
1342
  }
1342
1343
 
1343
- /**
1344
- * Event'in kaydedilip kaydedilmeyeceğini belirler
1345
- */
1346
- shouldCaptureEvent(currentData) {
1347
- if (!this.lastCapturedData) {
1348
- return true; // İlk event
1349
- }
1350
-
1351
- const last = this.lastCapturedData;
1352
-
1353
- // Event type değişmişse
1354
- if (currentData.type !== last.type) {
1355
- return true;
1356
- }
1357
-
1358
- // Pozisyon değişmişse (minimum 2 pixel tolerans)
1359
- if (
1360
- Math.abs(currentData.x - last.x) >= 2 ||
1361
- Math.abs(currentData.y - last.y) >= 2
1362
- ) {
1363
- return true;
1364
- }
1365
-
1366
- // Cursor type değişmişse
1367
- if (currentData.cursorType !== last.cursorType) {
1368
- return true;
1369
- }
1370
-
1371
- // Hiçbir değişiklik yoksa kaydetme
1372
- return false;
1373
- }
1374
-
1375
- /**
1376
- * Unified cursor capture for all recording types - uses video-relative coordinates
1377
- * @param {string|number} intervalOrFilepath - Cursor data JSON dosya yolu veya interval
1378
- * @param {Object} options - Cursor capture seçenekleri
1379
- * @param {boolean} options.videoRelative - Use video-relative coordinates (recommended)
1380
- * @param {Object} options.displayInfo - Display information for coordinate transformation
1381
- * @param {string} options.recordingType - Type of recording: 'display', 'window', 'area'
1382
- * @param {Object} options.captureArea - Capture area for area recording coordinate transformation
1383
- * @param {number} options.windowId - Window ID for window recording coordinate transformation
1384
- * @param {number} options.startTimestamp - Pre-defined start timestamp for synchronization (optional)
1385
- */
1386
1344
  async startCursorCapture(intervalOrFilepath = 100, options = {}) {
1387
- let filepath;
1388
- let interval = 20; // Default 50 FPS
1389
-
1390
- // Parameter parsing: number = interval, string = filepath
1391
- if (typeof intervalOrFilepath === "number") {
1392
- interval = Math.max(10, intervalOrFilepath); // Min 10ms
1393
- filepath = `cursor-data-${Date.now()}.json`;
1394
- } else if (typeof intervalOrFilepath === "string") {
1395
- filepath = intervalOrFilepath;
1396
- } else {
1397
- throw new Error(
1398
- "Parameter must be interval (number) or filepath (string)"
1399
- );
1400
- }
1401
-
1402
- if (this.cursorCaptureInterval) {
1403
- throw new Error("Cursor capture is already running");
1404
- }
1405
-
1406
- // SYNC FIX: Use pre-defined timestamp if provided for synchronization
1407
- const syncStartTime = options.startTimestamp || Date.now();
1408
-
1409
- // Fetch window bounds for multi-window recording
1410
- if (options.multiWindowBounds && options.multiWindowBounds.length > 0) {
1411
- try {
1412
- const allWindows = await this.getWindows();
1413
- // Match window IDs and populate bounds
1414
- for (const windowInfo of options.multiWindowBounds) {
1415
- const windowData = allWindows.find(w => w.id === windowInfo.windowId);
1416
- if (windowData) {
1417
- windowInfo.bounds = {
1418
- x: windowData.x || 0,
1419
- y: windowData.y || 0,
1420
- width: windowData.width || 0,
1421
- height: windowData.height || 0
1422
- };
1423
- }
1424
- }
1425
- } catch (error) {
1426
- console.warn('Failed to fetch window bounds for multi-window cursor tracking:', error.message);
1427
- }
1428
- }
1429
-
1430
- // Use video-relative coordinate system for all recording types
1431
- if (options.videoRelative && options.displayInfo) {
1432
- // Calculate video offset based on recording type
1433
- let videoOffsetX = 0;
1434
- let videoOffsetY = 0;
1435
- let videoWidth = options.displayInfo.width || options.displayInfo.logicalWidth;
1436
- let videoHeight = options.displayInfo.height || options.displayInfo.logicalHeight;
1437
-
1438
- if (options.recordingType === 'window' && options.windowId) {
1439
- // For window recording: offset = window position in display
1440
- if (options.captureArea) {
1441
- videoOffsetX = options.captureArea.x;
1442
- videoOffsetY = options.captureArea.y;
1443
- videoWidth = options.captureArea.width;
1444
- videoHeight = options.captureArea.height;
1445
- }
1446
- } else if (options.recordingType === 'area' && options.captureArea) {
1447
- // For area recording: offset = area position in display
1448
- videoOffsetX = options.captureArea.x;
1449
- videoOffsetY = options.captureArea.y;
1450
- videoWidth = options.captureArea.width;
1451
- videoHeight = options.captureArea.height;
1452
- }
1453
- // For display recording: offset remains 0,0
1454
-
1455
- this.cursorDisplayInfo = {
1456
- displayId: options.displayInfo.displayId || options.displayInfo.id,
1457
- displayX: options.displayInfo.x || 0,
1458
- displayY: options.displayInfo.y || 0,
1459
- displayWidth: options.displayInfo.width || options.displayInfo.logicalWidth,
1460
- displayHeight: options.displayInfo.height || options.displayInfo.logicalHeight,
1461
- videoOffsetX: videoOffsetX,
1462
- videoOffsetY: videoOffsetY,
1463
- videoWidth: videoWidth,
1464
- videoHeight: videoHeight,
1465
- videoRelative: true,
1466
- recordingType: options.recordingType || 'display',
1467
- // Store additional context for debugging
1468
- captureArea: options.captureArea,
1469
- windowId: options.windowId,
1470
- // Multi-window bounds for location detection
1471
- multiWindowBounds: options.multiWindowBounds || null
1472
- };
1473
- } else if (this.recordingDisplayInfo) {
1474
- // Fallback: Use recording display info if available
1475
- this.cursorDisplayInfo = {
1476
- ...this.recordingDisplayInfo,
1477
- displayX: this.recordingDisplayInfo.x || 0,
1478
- displayY: this.recordingDisplayInfo.y || 0,
1479
- displayWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
1480
- displayHeight: this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
1481
- videoOffsetX: 0,
1482
- videoOffsetY: 0,
1483
- videoWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
1484
- videoHeight: this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
1485
- videoRelative: true,
1486
- recordingType: options.recordingType || 'display',
1487
- multiWindowBounds: options.multiWindowBounds || null
1488
- };
1489
- } else {
1490
- // Final fallback: Main display global coordinates
1491
- try {
1492
- const displays = await this.getDisplays();
1493
- const mainDisplay = displays.find((d) => d.isPrimary) || displays[0];
1494
- if (mainDisplay) {
1495
- this.cursorDisplayInfo = {
1496
- displayId: mainDisplay.id,
1497
- x: mainDisplay.x,
1498
- y: mainDisplay.y,
1499
- width: parseInt(mainDisplay.resolution.split("x")[0]),
1500
- height: parseInt(mainDisplay.resolution.split("x")[1]),
1501
- multiWindowBounds: options.multiWindowBounds || null
1502
- };
1503
- }
1504
- } catch (error) {
1505
- console.warn("Main display bilgisi alınamadı:", error.message);
1506
- this.cursorDisplayInfo = null; // Fallback: global koordinatlar
1507
- }
1508
- }
1509
-
1510
- return new Promise((resolve, reject) => {
1511
- try {
1512
- // Dosyayı oluştur ve temizle
1513
- const fs = require("fs");
1514
- fs.writeFileSync(filepath, "[");
1515
-
1516
- this.cursorCaptureFile = filepath;
1517
- // SYNC FIX: Use synchronized start time for accurate timestamp calculation
1518
- this.cursorCaptureStartTime = syncStartTime;
1519
- this.cursorCaptureFirstWrite = true;
1520
- this.lastCapturedData = null;
1521
- // Store session timestamp for sync metadata
1522
- this.cursorCaptureSessionTimestamp = this.sessionTimestamp;
1523
-
1524
- // JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak için)
1525
- this.cursorCaptureInterval = setInterval(() => {
1526
- try {
1527
- const position = nativeBinding.getCursorPosition();
1528
- const timestamp = Date.now() - this.cursorCaptureStartTime;
1529
-
1530
- // Video-relative coordinate transformation for all recording types
1531
- let x = position.x;
1532
- let y = position.y;
1533
- let coordinateSystem = "global";
1534
-
1535
- // Apply video-relative transformation for all recording types
1536
- if (this.cursorDisplayInfo && this.cursorDisplayInfo.videoRelative) {
1537
- // Step 1: Transform global → display-relative coordinates
1538
- const displayRelativeX = position.x - this.cursorDisplayInfo.displayX;
1539
- const displayRelativeY = position.y - this.cursorDisplayInfo.displayY;
1540
-
1541
- // Step 2: Transform display-relative → video-relative coordinates
1542
- x = displayRelativeX - this.cursorDisplayInfo.videoOffsetX;
1543
- y = displayRelativeY - this.cursorDisplayInfo.videoOffsetY;
1544
- coordinateSystem = "video-relative";
1545
-
1546
- // Bounds check for video area (don't skip, just note if outside)
1547
- const outsideVideo = x < 0 || y < 0 ||
1548
- x >= this.cursorDisplayInfo.videoWidth ||
1549
- y >= this.cursorDisplayInfo.videoHeight;
1550
-
1551
- // For debugging - add metadata if cursor is outside video area
1552
- if (outsideVideo) {
1553
- coordinateSystem = "video-relative-outside";
1554
- }
1555
- }
1556
-
1557
- const cursorData = {
1558
- x: x,
1559
- y: y,
1560
- timestamp: timestamp,
1561
- unixTimeMs: Date.now(),
1562
- cursorType: position.cursorType,
1563
- type: position.eventType || "move",
1564
- coordinateSystem: coordinateSystem,
1565
- // Video-relative metadata for all recording types
1566
- recordingType: this.cursorDisplayInfo?.recordingType || "display",
1567
- videoInfo: this.cursorDisplayInfo ? {
1568
- width: this.cursorDisplayInfo.videoWidth,
1569
- height: this.cursorDisplayInfo.videoHeight,
1570
- offsetX: this.cursorDisplayInfo.videoOffsetX,
1571
- offsetY: this.cursorDisplayInfo.videoOffsetY
1572
- } : {},
1573
- displayInfo: this.cursorDisplayInfo ? {
1574
- displayId: this.cursorDisplayInfo.displayId,
1575
- width: this.cursorDisplayInfo.displayWidth,
1576
- height: this.cursorDisplayInfo.displayHeight
1577
- } : {}
1578
- };
1579
-
1580
- // Multi-window location detection with window-relative coordinates
1581
- if (this.cursorDisplayInfo?.multiWindowBounds && this.cursorDisplayInfo.multiWindowBounds.length > 0) {
1582
- const location = { hover: null, click: null };
1583
- let windowRelativeCoords = null;
1584
-
1585
- // Detect which window the cursor is over
1586
- for (const windowInfo of this.cursorDisplayInfo.multiWindowBounds) {
1587
- if (windowInfo.bounds) {
1588
- const { x: wx, y: wy, width: ww, height: wh } = windowInfo.bounds;
1589
- // Check if cursor is inside window bounds (using global coordinates)
1590
- if (position.x >= wx && position.x <= wx + ww &&
1591
- position.y >= wy && position.y <= wy + wh) {
1592
- location.hover = windowInfo.windowId;
1593
-
1594
- // Calculate window-relative coordinates
1595
- // These coords are relative to the window's top-left corner (0,0)
1596
- // This allows the desktop app to position cursor correctly
1597
- // regardless of where the window is placed on canvas
1598
- windowRelativeCoords = {
1599
- windowId: windowInfo.windowId,
1600
- x: position.x - wx,
1601
- y: position.y - wy,
1602
- // Also include window dimensions for reference
1603
- windowWidth: ww,
1604
- windowHeight: wh
1605
- };
1606
-
1607
- // If this is a click/drag event, mark click location
1608
- // Native eventType values: 'mousedown', 'mouseup', 'drag', 'rightmousedown', 'rightmouseup', 'rightdrag'
1609
- const eventType = position.eventType || '';
1610
- if (eventType === 'mousedown' ||
1611
- eventType === 'mouseup' ||
1612
- eventType === 'drag' ||
1613
- eventType === 'rightmousedown' ||
1614
- eventType === 'rightmouseup' ||
1615
- eventType === 'rightdrag') {
1616
- location.click = windowInfo.windowId;
1617
- }
1618
- break; // Found the window, stop searching
1619
- }
1620
- }
1621
- }
1622
-
1623
- // Add location info to cursor data
1624
- cursorData.location = location;
1625
-
1626
- // Add window-relative coordinates if cursor is over a window
1627
- if (windowRelativeCoords) {
1628
- cursorData.windowRelative = windowRelativeCoords;
1629
- }
1630
- }
1631
-
1632
- // Add sync metadata to first event only
1633
- if (this.cursorCaptureFirstWrite && this.cursorCaptureSessionTimestamp) {
1634
- cursorData._syncMetadata = {
1635
- videoStartTime: this.cursorCaptureSessionTimestamp,
1636
- cursorStartTime: this.cursorCaptureStartTime,
1637
- offsetMs: this.cursorCaptureStartTime - this.cursorCaptureSessionTimestamp
1638
- };
1639
- }
1640
-
1641
- // Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
1642
- if (this.shouldCaptureEvent(cursorData)) {
1643
- // Dosyaya ekle
1644
- const jsonString = JSON.stringify(cursorData);
1645
-
1646
- if (this.cursorCaptureFirstWrite) {
1647
- fs.appendFileSync(filepath, jsonString);
1648
- this.cursorCaptureFirstWrite = false;
1649
- } else {
1650
- fs.appendFileSync(filepath, "," + jsonString);
1651
- }
1652
-
1653
- // Son pozisyonu sakla
1654
- this.lastCapturedData = { ...cursorData };
1655
- }
1656
- } catch (error) {
1657
- console.error("Cursor capture error:", error);
1658
- }
1659
- }, interval); // Configurable FPS
1660
-
1661
- this.emit("cursorCaptureStarted", filepath);
1662
- resolve(true);
1663
- } catch (error) {
1664
- reject(error);
1665
- }
1666
- });
1345
+ return cursorCapturePolling.startCursorCapture(
1346
+ this,
1347
+ nativeBinding,
1348
+ intervalOrFilepath,
1349
+ options,
1350
+ );
1667
1351
  }
1668
1352
 
1669
- /**
1670
- * Cursor capture durdurur - dosya yazma işlemini sonlandırır
1671
- */
1672
1353
  async stopCursorCapture() {
1673
- return new Promise((resolve, reject) => {
1674
- try {
1675
- if (!this.cursorCaptureInterval) {
1676
- return resolve(false);
1677
- }
1678
-
1679
- // Interval'ı durdur
1680
- clearInterval(this.cursorCaptureInterval);
1681
- this.cursorCaptureInterval = null;
1682
-
1683
- // Dosyayı kapat
1684
- if (this.cursorCaptureFile) {
1685
- const fs = require("fs");
1686
- fs.appendFileSync(this.cursorCaptureFile, "]");
1687
- this.cursorCaptureFile = null;
1688
- }
1689
-
1690
- // Değişkenleri temizle
1691
- this.lastCapturedData = null;
1692
- this.cursorCaptureStartTime = null;
1693
- this.cursorCaptureFirstWrite = true;
1694
- this.cursorDisplayInfo = null;
1695
-
1696
- this.emit("cursorCaptureStopped");
1697
- resolve(true);
1698
- } catch (error) {
1699
- reject(error);
1700
- }
1701
- });
1354
+ return cursorCapturePolling.stopCursorCapture(this);
1702
1355
  }
1703
1356
 
1704
1357
  /**
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ async function resolveCursorDisplayInfo(recorder, options) {
4
+ if (options.videoRelative && options.displayInfo) {
5
+ let videoOffsetX = 0;
6
+ let videoOffsetY = 0;
7
+ let videoWidth =
8
+ options.displayInfo.width || options.displayInfo.logicalWidth;
9
+ let videoHeight =
10
+ options.displayInfo.height || options.displayInfo.logicalHeight;
11
+
12
+ if (options.recordingType === "window" && options.windowId) {
13
+ if (options.captureArea) {
14
+ videoOffsetX = options.captureArea.x;
15
+ videoOffsetY = options.captureArea.y;
16
+ videoWidth = options.captureArea.width;
17
+ videoHeight = options.captureArea.height;
18
+ }
19
+ } else if (options.recordingType === "area" && options.captureArea) {
20
+ videoOffsetX = options.captureArea.x;
21
+ videoOffsetY = options.captureArea.y;
22
+ videoWidth = options.captureArea.width;
23
+ videoHeight = options.captureArea.height;
24
+ }
25
+
26
+ recorder.cursorDisplayInfo = {
27
+ displayId: options.displayInfo.displayId || options.displayInfo.id,
28
+ displayX: options.displayInfo.x || 0,
29
+ displayY: options.displayInfo.y || 0,
30
+ displayWidth:
31
+ options.displayInfo.width || options.displayInfo.logicalWidth,
32
+ displayHeight:
33
+ options.displayInfo.height || options.displayInfo.logicalHeight,
34
+ videoOffsetX,
35
+ videoOffsetY,
36
+ videoWidth,
37
+ videoHeight,
38
+ videoRelative: true,
39
+ recordingType: options.recordingType || "display",
40
+ captureArea: options.captureArea,
41
+ windowId: options.windowId,
42
+ multiWindowBounds: options.multiWindowBounds || null,
43
+ };
44
+ return;
45
+ }
46
+
47
+ if (recorder.recordingDisplayInfo) {
48
+ recorder.cursorDisplayInfo = {
49
+ ...recorder.recordingDisplayInfo,
50
+ displayX: recorder.recordingDisplayInfo.x || 0,
51
+ displayY: recorder.recordingDisplayInfo.y || 0,
52
+ displayWidth:
53
+ recorder.recordingDisplayInfo.width ||
54
+ recorder.recordingDisplayInfo.logicalWidth,
55
+ displayHeight:
56
+ recorder.recordingDisplayInfo.height ||
57
+ recorder.recordingDisplayInfo.logicalHeight,
58
+ videoOffsetX: 0,
59
+ videoOffsetY: 0,
60
+ videoWidth:
61
+ recorder.recordingDisplayInfo.width ||
62
+ recorder.recordingDisplayInfo.logicalWidth,
63
+ videoHeight:
64
+ recorder.recordingDisplayInfo.height ||
65
+ recorder.recordingDisplayInfo.logicalHeight,
66
+ videoRelative: true,
67
+ recordingType: options.recordingType || "display",
68
+ multiWindowBounds: options.multiWindowBounds || null,
69
+ };
70
+ return;
71
+ }
72
+
73
+ try {
74
+ const displays = await recorder.getDisplays();
75
+ const mainDisplay =
76
+ displays.find((d) => d.isPrimary) || displays[0];
77
+ if (mainDisplay) {
78
+ let w = mainDisplay.width;
79
+ let h = mainDisplay.height;
80
+ const res = mainDisplay.resolution;
81
+ if ((w == null || h == null) && res) {
82
+ const parts = String(res).split("x");
83
+ if (w == null) {
84
+ w = parseInt(parts[0], 10);
85
+ }
86
+ if (h == null) {
87
+ h = parseInt(parts[1], 10);
88
+ }
89
+ }
90
+ if (!Number.isFinite(w) || w <= 0) {
91
+ w = 1920;
92
+ }
93
+ if (!Number.isFinite(h) || h <= 0) {
94
+ h = 1080;
95
+ }
96
+ recorder.cursorDisplayInfo = {
97
+ displayId: mainDisplay.id,
98
+ x: mainDisplay.x,
99
+ y: mainDisplay.y,
100
+ width: w,
101
+ height: h,
102
+ multiWindowBounds: options.multiWindowBounds || null,
103
+ };
104
+ }
105
+ } catch {
106
+ recorder.cursorDisplayInfo = null;
107
+ }
108
+ }
109
+
110
+ module.exports = { resolveCursorDisplayInfo };