node-mac-recorder 2.24.9 → 2.24.11

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