node-mac-recorder 2.24.10 → 2.24.12

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
@@ -8,13 +8,14 @@ const fs = require("fs");
8
8
  const VIDEO_START_TIMEOUT_MS = 12000;
9
9
  const VIDEO_START_POLL_MS = 10;
10
10
 
11
- // Auto-switch to Electron-safe implementation when running under Electron and binary exists
11
+ // The standard addon owns the complete writer/timeline feature set. Keep the
12
+ // legacy Electron-safe implementation opt-in; it cannot pause a writer without
13
+ // finalizing the current file.
12
14
  let USE_ELECTRON_SAFE = false;
13
15
  let ElectronSafeMacRecorder = null;
14
16
  try {
15
- const isElectron = !!(process && process.versions && process.versions.electron);
16
17
  const preferElectronSafe = process.env.PREFER_ELECTRON_SAFE === "1" || process.env.USE_ELECTRON_SAFE === "1";
17
- if (isElectron || preferElectronSafe) {
18
+ if (preferElectronSafe) {
18
19
  const rel = path.join(__dirname, "build", "Release", "mac_recorder_electron.node");
19
20
  const dbg = path.join(__dirname, "build", "Debug", "mac_recorder_electron.node");
20
21
  if (fs.existsSync(rel) || fs.existsSync(dbg) || preferElectronSafe) {
@@ -53,6 +54,9 @@ class MacRecorder extends EventEmitter {
53
54
  this.outputPath = null;
54
55
  this.recordingTimer = null;
55
56
  this.recordingStartTime = null;
57
+ this.isPaused = false;
58
+ this.pauseStartedAt = null;
59
+ this.pausedDurationMs = 0;
56
60
 
57
61
  // MULTI-SESSION: Unique session ID for this recorder instance
58
62
  this.nativeSessionId = null; // Will be generated when recording starts
@@ -61,6 +65,9 @@ class MacRecorder extends EventEmitter {
61
65
  this.cursorCaptureInterval = null;
62
66
  this.cursorCaptureFile = null;
63
67
  this.cursorCaptureStartTime = null;
68
+ this.cursorCapturePaused = false;
69
+ this.cursorPauseStartedAt = null;
70
+ this.cursorPausedDurationMs = 0;
64
71
  this.cursorCaptureFirstWrite = true;
65
72
  this.lastCapturedData = null;
66
73
  this.cursorDisplayInfo = null;
@@ -169,6 +176,7 @@ class MacRecorder extends EventEmitter {
169
176
  model: device?.model || null,
170
177
  connected: device?.connected !== false,
171
178
  suspended: device?.suspended === true,
179
+ captureReady: device?.captureReady !== false,
172
180
  width: Number(device?.width) || 0,
173
181
  height: Number(device?.height) || 0,
174
182
  hasAudio: device?.hasAudio !== false,
@@ -212,6 +220,7 @@ class MacRecorder extends EventEmitter {
212
220
  this.audioCaptureActive = options.includeMicrophone === true;
213
221
  this.sessionTimestamp = sessionTimestamp;
214
222
  this.recordingMode = "iphone";
223
+ this._resetPauseState();
215
224
 
216
225
  try {
217
226
  const success = nativeBinding.startIOSDeviceRecording(
@@ -233,10 +242,7 @@ class MacRecorder extends EventEmitter {
233
242
  this.timelineStartTimestamp = this.recordingStartTime;
234
243
  this.syncTimestamp = this.recordingStartTime;
235
244
  this.recordingTimer = setInterval(() => {
236
- this.emit(
237
- "timeUpdate",
238
- Math.floor((Date.now() - this.recordingStartTime) / 1000),
239
- );
245
+ this.emit("timeUpdate", this._getRecordingTimeSeconds());
240
246
  }, 1000);
241
247
 
242
248
  const event = {
@@ -272,8 +278,11 @@ class MacRecorder extends EventEmitter {
272
278
  throw new Error("No iPhone recording in progress");
273
279
  }
274
280
  let success = false;
281
+ const recordingTime = this._getRecordingTimeSeconds();
282
+ const pausedDuration = this._getPausedDurationMs() / 1000;
275
283
  try {
276
284
  success = nativeBinding.stopIOSDeviceRecording();
285
+ await require("./recorder_runtime_safety.cjs").waitForNativeIdle(nativeBinding);
277
286
  } finally {
278
287
  if (this.recordingTimer) clearInterval(this.recordingTimer);
279
288
  this.recordingTimer = null;
@@ -289,6 +298,8 @@ class MacRecorder extends EventEmitter {
289
298
  sessionTimestamp: this.sessionTimestamp,
290
299
  syncTimestamp: this.syncTimestamp,
291
300
  sourceType: "iphone",
301
+ recordingTime,
302
+ pausedDuration,
292
303
  };
293
304
  if (this.cameraCaptureActive) {
294
305
  this.emit("cameraCaptureStopped", {
@@ -314,6 +325,7 @@ class MacRecorder extends EventEmitter {
314
325
  }
315
326
  this.sessionTimestamp = null;
316
327
  this.syncTimestamp = null;
328
+ this._resetPauseState();
317
329
  return result;
318
330
  }
319
331
 
@@ -525,6 +537,7 @@ class MacRecorder extends EventEmitter {
525
537
  if (!outputPath) {
526
538
  throw new Error("Output path is required");
527
539
  }
540
+ this._resetPauseState();
528
541
 
529
542
  // Seçenekleri güncelle
530
543
  this.setOptions(options);
@@ -913,6 +926,8 @@ class MacRecorder extends EventEmitter {
913
926
  // MULTI-SESSION: Generate unique session ID for this recording
914
927
  // Use provided sessionTimestamp from options, or generate new one
915
928
  const sessionTimestamp = this.options.sessionTimestamp || Date.now();
929
+ const recordingGeneration = this._captureGeneration;
930
+ const isCurrentRecording = () => this._captureGeneration === recordingGeneration && !this._stopPromise;
916
931
  this.sessionTimestamp = sessionTimestamp;
917
932
  this.nativeSessionId = `rec_${sessionTimestamp}_${Math.random().toString(36).substr(2, 9)}`;
918
933
 
@@ -1088,7 +1103,7 @@ class MacRecorder extends EventEmitter {
1088
1103
  this._videoStartWatcherActive = true;
1089
1104
  this._videoStartWatcher = (async () => {
1090
1105
  while (
1091
- this._videoStartWatcherActive &&
1106
+ this._videoStartWatcherActive && isCurrentRecording() &&
1092
1107
  Date.now() - watchStart < VIDEO_START_TIMEOUT_MS
1093
1108
  ) {
1094
1109
  const value = readVideoStart();
@@ -1225,10 +1240,7 @@ class MacRecorder extends EventEmitter {
1225
1240
 
1226
1241
  // Timer başlat (progress tracking için)
1227
1242
  this.recordingTimer = setInterval(() => {
1228
- const elapsed = Math.floor(
1229
- (Date.now() - this.recordingStartTime) / 1000
1230
- );
1231
- this.emit("timeUpdate", elapsed);
1243
+ this.emit("timeUpdate", this._getRecordingTimeSeconds());
1232
1244
  }, 1000);
1233
1245
 
1234
1246
  // Native kayıt gerçekten başladığını kontrol etmek için polling başlat
@@ -1267,6 +1279,10 @@ class MacRecorder extends EventEmitter {
1267
1279
  }
1268
1280
  };
1269
1281
  const pollRecordingStatus = () => {
1282
+ if (!isCurrentRecording() || !this.isRecording) {
1283
+ clearInterval(checkRecordingStatus);
1284
+ return;
1285
+ }
1270
1286
  try {
1271
1287
  const nativeStatus = isNativeRecordingLive();
1272
1288
  if (nativeStatus && !recordingStartedEmitted) {
@@ -1314,10 +1330,11 @@ class MacRecorder extends EventEmitter {
1314
1330
  }
1315
1331
  };
1316
1332
 
1317
- checkRecordingStatus = setInterval(pollRecordingStatus, 50);
1333
+ checkRecordingStatus = this.recordingStatusInterval = setInterval(pollRecordingStatus, 50);
1318
1334
 
1319
1335
  // Timeout fallback - 5 saniye sonra hala başlamamışsa emit et
1320
- setTimeout(() => {
1336
+ this.recordingStartTimeout = setTimeout(() => {
1337
+ if (!isCurrentRecording() || !this.isRecording) return;
1321
1338
  if (!recordingStartedEmitted) {
1322
1339
  recordingStartedEmitted = true;
1323
1340
  clearInterval(checkRecordingStatus);
@@ -1344,25 +1361,13 @@ class MacRecorder extends EventEmitter {
1344
1361
  } else {
1345
1362
  this.cameraCaptureActive = false;
1346
1363
  if (this.options.captureCamera === true) {
1347
- if (cameraFilePath && fs.existsSync(cameraFilePath)) {
1348
- try {
1349
- fs.unlinkSync(cameraFilePath);
1350
- } catch (cleanupError) {
1351
- console.warn("Camera temp file cleanup failed:", cleanupError.message);
1352
- }
1353
- }
1364
+ // Native cleanup owns partially written files; never unlink an active writer.
1354
1365
  this.cameraCaptureFile = null;
1355
1366
  }
1356
1367
 
1357
1368
  if (captureAudio) {
1358
1369
  this.audioCaptureActive = false;
1359
- if (audioFilePath && fs.existsSync(audioFilePath)) {
1360
- try {
1361
- fs.unlinkSync(audioFilePath);
1362
- } catch (cleanupError) {
1363
- console.warn("Audio temp file cleanup failed:", cleanupError.message);
1364
- }
1365
- }
1370
+ // Native cleanup owns partially written files; never unlink an active writer.
1366
1371
  this.audioCaptureFile = null;
1367
1372
  }
1368
1373
 
@@ -1383,6 +1388,86 @@ class MacRecorder extends EventEmitter {
1383
1388
  });
1384
1389
  }
1385
1390
 
1391
+ _resetPauseState() {
1392
+ this.isPaused = false;
1393
+ this.pauseStartedAt = null;
1394
+ this.pausedDurationMs = 0;
1395
+ this.cursorCapturePaused = false;
1396
+ this.cursorPauseStartedAt = null;
1397
+ this.cursorPausedDurationMs = 0;
1398
+ }
1399
+
1400
+ _getPausedDurationMs(now = Date.now()) {
1401
+ return this.pausedDurationMs +
1402
+ (this.isPaused && this.pauseStartedAt ? Math.max(0, now - this.pauseStartedAt) : 0);
1403
+ }
1404
+
1405
+ _getRecordingTimeSeconds(now = Date.now()) {
1406
+ if (!this.recordingStartTime) return 0;
1407
+ return Math.floor(Math.max(0, now - this.recordingStartTime - this._getPausedDurationMs(now)) / 1000);
1408
+ }
1409
+
1410
+ pauseCursorCapture(pausedAt = Date.now()) {
1411
+ if (!this.cursorCaptureInterval || this.cursorCapturePaused) return false;
1412
+ this.cursorCapturePaused = true;
1413
+ this.cursorPauseStartedAt = pausedAt;
1414
+ return true;
1415
+ }
1416
+
1417
+ resumeCursorCapture(resumedAt = Date.now()) {
1418
+ if (!this.cursorCapturePaused) return false;
1419
+ if (this.cursorPauseStartedAt) {
1420
+ this.cursorPausedDurationMs += Math.max(0, resumedAt - this.cursorPauseStartedAt);
1421
+ }
1422
+ this.cursorCapturePaused = false;
1423
+ this.cursorPauseStartedAt = null;
1424
+ return true;
1425
+ }
1426
+
1427
+ async pauseRecording() {
1428
+ if (!this.isRecording) throw new Error("No recording in progress");
1429
+ if (this.isPaused) return this.getStatus();
1430
+
1431
+ const pauseMethod = this.recordingMode === "iphone"
1432
+ ? nativeBinding.pauseIOSDeviceRecording
1433
+ : nativeBinding.pauseRecording;
1434
+ if (typeof pauseMethod !== "function") {
1435
+ throw new Error("This recorder build does not support pausing");
1436
+ }
1437
+ const pausedAt = Date.now();
1438
+ if (pauseMethod.call(nativeBinding) !== true) {
1439
+ throw new Error("Recording could not be paused");
1440
+ }
1441
+ this.isPaused = true;
1442
+ this.pauseStartedAt = pausedAt;
1443
+ this.pauseCursorCapture(pausedAt);
1444
+ const status = this.getStatus();
1445
+ this.emit("paused", status);
1446
+ return status;
1447
+ }
1448
+
1449
+ async resumeRecording() {
1450
+ if (!this.isRecording) throw new Error("No recording in progress");
1451
+ if (!this.isPaused) return this.getStatus();
1452
+
1453
+ const resumeMethod = this.recordingMode === "iphone"
1454
+ ? nativeBinding.resumeIOSDeviceRecording
1455
+ : nativeBinding.resumeRecording;
1456
+ if (typeof resumeMethod !== "function") {
1457
+ throw new Error("This recorder build does not support resuming");
1458
+ }
1459
+ if (resumeMethod.call(nativeBinding) !== true) {
1460
+ throw new Error("Recording could not be resumed");
1461
+ }
1462
+ const resumedAt = Date.now();
1463
+ this.pausedDurationMs += Math.max(0, resumedAt - this.pauseStartedAt);
1464
+ this.pauseStartedAt = null;
1465
+ this.isPaused = false;
1466
+ this.resumeCursorCapture(resumedAt);
1467
+ const status = this.getStatus();
1468
+ this.emit("resumed", status);
1469
+ return status;
1470
+ }
1386
1471
 
1387
1472
  /**
1388
1473
  * Ekran kaydını durdurur - SYNCHRONIZED stop for all components
@@ -1400,14 +1485,23 @@ class MacRecorder extends EventEmitter {
1400
1485
  // Sure, VIDEONUN baslangicindan olculur. recordingStartTime (JS'in
1401
1486
  // hazir oldugunu fark ettigi an) videodan birkac on ms sonra oldugu
1402
1487
  // icin buradan hesaplanan stopLimit videoyu kuyrugundan kirpiyordu.
1488
+ // ScreenCaptureKit may publish its first frame hundreds of milliseconds
1489
+ // after cursor capture starts. Cursor metadata compensates that offset, but
1490
+ // the native camera/audio stop limit must use the screen's real t=0.
1491
+ // Otherwise camera files are extended by the startup delay and lip sync
1492
+ // changes from one recording session to the next.
1493
+ const nativeVideoStartTimestamp = Number(this.videoStartTimestamp);
1403
1494
  const durationReference =
1404
- this.timelineStartTimestamp && this.timelineStartTimestamp > 0
1405
- ? this.timelineStartTimestamp
1406
- : this.recordingStartTime;
1495
+ Number.isFinite(nativeVideoStartTimestamp) && nativeVideoStartTimestamp > 0
1496
+ ? nativeVideoStartTimestamp
1497
+ : this.timelineStartTimestamp && this.timelineStartTimestamp > 0
1498
+ ? this.timelineStartTimestamp
1499
+ : this.recordingStartTime;
1407
1500
  const elapsedSeconds =
1408
1501
  durationReference && durationReference > 0
1409
- ? (stopRequestedAt - durationReference) / 1000
1502
+ ? Math.max(0, stopRequestedAt - durationReference - this._getPausedDurationMs()) / 1000
1410
1503
  : -1;
1504
+ const pausedDuration = this._getPausedDurationMs() / 1000;
1411
1505
  try {
1412
1506
  console.log('🛑 SYNC: Stopping all recording components simultaneously');
1413
1507
 
@@ -1457,6 +1551,8 @@ class MacRecorder extends EventEmitter {
1457
1551
  success = true; // Assume success to avoid throwing
1458
1552
  }
1459
1553
 
1554
+ await require("./recorder_runtime_safety.cjs").waitForNativeIdle(nativeBinding);
1555
+
1460
1556
  if (this.options.captureCamera === true) {
1461
1557
  try {
1462
1558
  const nativeCameraPath = nativeBinding.getCameraRecordingPath
@@ -1527,6 +1623,8 @@ class MacRecorder extends EventEmitter {
1527
1623
  outputPath: this.outputPath,
1528
1624
  cameraOutputPath: this.cameraCaptureFile || null,
1529
1625
  audioOutputPath: this.audioCaptureFile || null,
1626
+ recordingTime: elapsedSeconds > 0 ? elapsedSeconds : this._getRecordingTimeSeconds(),
1627
+ pausedDuration,
1530
1628
  sessionTimestamp: sessionId,
1531
1629
  syncTimestamp: this.syncTimestamp,
1532
1630
  };
@@ -1535,15 +1633,18 @@ class MacRecorder extends EventEmitter {
1535
1633
 
1536
1634
  if (success) {
1537
1635
  // Dosyanın oluşturulmasını bekle
1636
+ const completedOutputPath = this.outputPath;
1637
+ const completedGeneration = this._captureGeneration;
1538
1638
  setTimeout(() => {
1539
- if (fs.existsSync(this.outputPath)) {
1540
- this.emit("completed", this.outputPath);
1639
+ if (completedGeneration === this._captureGeneration && fs.existsSync(completedOutputPath)) {
1640
+ this.emit("completed", completedOutputPath);
1541
1641
  }
1542
1642
  }, 1000);
1543
1643
  }
1544
1644
 
1545
1645
  this.sessionTimestamp = null;
1546
1646
  this.syncTimestamp = null;
1647
+ this._resetPauseState();
1547
1648
  resolve(result);
1548
1649
  } catch (error) {
1549
1650
  this.isRecording = false;
@@ -1553,6 +1654,7 @@ class MacRecorder extends EventEmitter {
1553
1654
  this.audioCaptureFile = null;
1554
1655
  this.sessionTimestamp = null;
1555
1656
  this.syncTimestamp = null;
1657
+ this._resetPauseState();
1556
1658
  if (this.recordingTimer) {
1557
1659
  clearInterval(this.recordingTimer);
1558
1660
  this.recordingTimer = null;
@@ -1566,13 +1668,17 @@ class MacRecorder extends EventEmitter {
1566
1668
  * Kayıt durumunu döndürür
1567
1669
  */
1568
1670
  getStatus() {
1569
- const nativeStatus =
1671
+ const iosStatus =
1570
1672
  this.recordingMode === "iphone" &&
1571
1673
  typeof nativeBinding.getIOSDeviceRecordingStatus === "function"
1572
- ? nativeBinding.getIOSDeviceRecordingStatus().isRecording === true
1573
- : nativeBinding.getRecordingStatus();
1674
+ ? nativeBinding.getIOSDeviceRecordingStatus()
1675
+ : null;
1676
+ const nativeStatus = iosStatus
1677
+ ? iosStatus.isRecording === true
1678
+ : nativeBinding.getRecordingStatus();
1574
1679
  return {
1575
1680
  isRecording: this.isRecording && nativeStatus,
1681
+ isPaused: this.isPaused || iosStatus?.isPaused === true,
1576
1682
  outputPath: this.outputPath,
1577
1683
  cameraOutputPath: this.cameraCaptureFile || null,
1578
1684
  audioOutputPath: this.audioCaptureFile || null,
@@ -1581,9 +1687,8 @@ class MacRecorder extends EventEmitter {
1581
1687
  sessionTimestamp: this.sessionTimestamp,
1582
1688
  syncTimestamp: this.syncTimestamp,
1583
1689
  options: this.options,
1584
- recordingTime: this.recordingStartTime
1585
- ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
1586
- : 0,
1690
+ recordingTime: this._getRecordingTimeSeconds(),
1691
+ pausedDuration: this._getPausedDurationMs() / 1000,
1587
1692
  };
1588
1693
  }
1589
1694
 
@@ -1890,6 +1995,9 @@ class MacRecorder extends EventEmitter {
1890
1995
  this.cursorCaptureFile = filepath;
1891
1996
  // SYNC FIX: Use synchronized start time for accurate timestamp calculation
1892
1997
  this.cursorCaptureStartTime = syncStartTime;
1998
+ this.cursorCapturePaused = false;
1999
+ this.cursorPauseStartedAt = null;
2000
+ this.cursorPausedDurationMs = 0;
1893
2001
  this.cursorCaptureFirstWrite = true;
1894
2002
  this.lastCapturedData = null;
1895
2003
  // Store session timestamp for sync metadata
@@ -1898,8 +2006,9 @@ class MacRecorder extends EventEmitter {
1898
2006
  // JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak için)
1899
2007
  this.cursorCaptureInterval = setInterval(() => {
1900
2008
  try {
2009
+ if (this.cursorCapturePaused) return;
1901
2010
  const position = nativeBinding.getCursorPosition();
1902
- const timestamp = Date.now() - this.cursorCaptureStartTime;
2011
+ const timestamp = Date.now() - this.cursorCaptureStartTime - this.cursorPausedDurationMs;
1903
2012
 
1904
2013
  // Video-relative coordinate transformation for all recording types
1905
2014
  let x = position.x;
@@ -2090,6 +2199,9 @@ class MacRecorder extends EventEmitter {
2090
2199
  // Değişkenleri temizle
2091
2200
  this.lastCapturedData = null;
2092
2201
  this.cursorCaptureStartTime = null;
2202
+ this.cursorCapturePaused = false;
2203
+ this.cursorPauseStartedAt = null;
2204
+ this.cursorPausedDurationMs = 0;
2093
2205
  this.cursorCaptureFirstWrite = true;
2094
2206
  this.cursorDisplayInfo = null;
2095
2207
 
@@ -2433,4 +2545,5 @@ class MacRecorder extends EventEmitter {
2433
2545
  // WindowSelector modülünü de export edelim
2434
2546
  MacRecorder.WindowSelector = require('./window-selector');
2435
2547
 
2548
+ if (!USE_ELECTRON_SAFE) require("./recorder_runtime_safety.cjs")(MacRecorder, nativeBinding);
2436
2549
  module.exports = USE_ELECTRON_SAFE ? ElectronSafeMacRecorder : MacRecorder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.24.10",
3
+ "version": "2.24.12",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [