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