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/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 // Align cursor timeline to actual start
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
- return cursorCapturePolling.startCursorCapture(
1346
- this,
1347
- nativeBinding,
1348
- intervalOrFilepath,
1349
- options,
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 cursorCapturePolling.stopCursorCapture(this);
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.24",
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
- AVAssetWriter *writerRef = g_avWriter;
506
- if (writerInput && writerRef && writerRef.status == AVAssetWriterStatusWriting) {
505
+ if (writerInput) {
507
506
  [writerInput markAsFinished];
508
507
  }
509
508