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.
@@ -15,6 +15,9 @@ class MultiWindowRecorder extends EventEmitter {
15
15
  this.recorders = [];
16
16
  this.windows = [];
17
17
  this.isRecording = false;
18
+ this.isPaused = false;
19
+ this.pauseStartedAt = null;
20
+ this.pausedDurationMs = 0;
18
21
  this.outputFiles = [];
19
22
  this.cursorFiles = [];
20
23
  this.cameraFile = null; // Camera output file (from first recorder)
@@ -51,6 +54,11 @@ class MultiWindowRecorder extends EventEmitter {
51
54
  */
52
55
  async addWindow(windowInfo) {
53
56
  const recorder = new MacRecorder();
57
+ // Child-process errors use EventEmitter's special "error" event.
58
+ // Listen before the worker can fail (including during addWindow).
59
+ recorder.on('error', (error) => {
60
+ this.emit('recorderError', { windowId: windowInfo.id, error: error.message });
61
+ });
54
62
 
55
63
  const recorderInfo = {
56
64
  recorder,
@@ -255,8 +263,9 @@ class MultiWindowRecorder extends EventEmitter {
255
263
  } catch (error) {
256
264
  console.error(` ❌ Failed to start recorder ${i + 1}:`, error.message);
257
265
 
258
- // Stop all previously started recorders and cursor tracking
259
- for (let j = 0; j < i; j++) {
266
+ // Cursor setup can fail AFTER the current video started.
267
+ // Reclaim that recorder too, not just previous windows.
268
+ for (let j = 0; j <= i; j++) {
260
269
  try {
261
270
  await this.recorders[j].recorder.stopRecording();
262
271
  } catch (stopError) {
@@ -282,7 +291,7 @@ class MultiWindowRecorder extends EventEmitter {
282
291
  // Start timeUpdate timer (emit every second)
283
292
  this.timeUpdateInterval = setInterval(() => {
284
293
  if (this.isRecording && this.metadata.startTime) {
285
- const elapsed = Math.floor((Date.now() - this.metadata.startTime) / 1000);
294
+ const elapsed = this._getRecordingTimeSeconds();
286
295
  this.emit('timeUpdate', elapsed);
287
296
  }
288
297
  }, 1000);
@@ -303,6 +312,66 @@ class MultiWindowRecorder extends EventEmitter {
303
312
  };
304
313
  }
305
314
 
315
+ _getPausedDurationMs(now = Date.now()) {
316
+ return this.pausedDurationMs +
317
+ (this.isPaused && this.pauseStartedAt ? Math.max(0, now - this.pauseStartedAt) : 0);
318
+ }
319
+
320
+ _getRecordingTimeSeconds(now = Date.now()) {
321
+ if (!this.metadata.startTime) return 0;
322
+ return Math.floor(Math.max(0, now - this.metadata.startTime - this._getPausedDurationMs(now)) / 1000);
323
+ }
324
+
325
+ async pauseRecording() {
326
+ if (!this.isRecording) throw new Error('No recording in progress');
327
+ if (this.isPaused) return this.getStatus();
328
+
329
+ const pauseResults = await Promise.allSettled(
330
+ this.recorders.map(recInfo => recInfo.recorder.pauseRecording())
331
+ );
332
+ const pauseFailure = pauseResults.find(result => result.status === 'rejected');
333
+ if (pauseFailure) {
334
+ const pausedRecorders = this.recorders
335
+ .filter((_, index) => pauseResults[index].status === 'fulfilled')
336
+ .map(recInfo => recInfo.recorder);
337
+ await Promise.allSettled(pausedRecorders.map(recorder => recorder.resumeRecording()));
338
+ throw pauseFailure.reason;
339
+ }
340
+
341
+ const pausedAt = Date.now();
342
+ this.cursorRecorder?.pauseCursorCapture?.(pausedAt);
343
+ this.isPaused = true;
344
+ this.pauseStartedAt = pausedAt;
345
+ const status = this.getStatus();
346
+ this.emit('paused', status);
347
+ return status;
348
+ }
349
+
350
+ async resumeRecording() {
351
+ if (!this.isRecording) throw new Error('No recording in progress');
352
+ if (!this.isPaused) return this.getStatus();
353
+
354
+ const resumeResults = await Promise.allSettled(
355
+ this.recorders.map(recInfo => recInfo.recorder.resumeRecording())
356
+ );
357
+ const resumeFailure = resumeResults.find(result => result.status === 'rejected');
358
+ if (resumeFailure) {
359
+ const resumedRecorders = this.recorders
360
+ .filter((_, index) => resumeResults[index].status === 'fulfilled')
361
+ .map(recInfo => recInfo.recorder);
362
+ await Promise.allSettled(resumedRecorders.map(recorder => recorder.pauseRecording()));
363
+ throw resumeFailure.reason;
364
+ }
365
+ const resumedAt = Date.now();
366
+ this.cursorRecorder?.resumeCursorCapture?.(resumedAt);
367
+ this.pausedDurationMs += Math.max(0, resumedAt - this.pauseStartedAt);
368
+ this.pauseStartedAt = null;
369
+ this.isPaused = false;
370
+ const status = this.getStatus();
371
+ this.emit('resumed', status);
372
+ return status;
373
+ }
374
+
306
375
  /**
307
376
  * Stop all recordings
308
377
  */
@@ -392,7 +461,8 @@ class MultiWindowRecorder extends EventEmitter {
392
461
  }
393
462
 
394
463
  // Calculate duration
395
- const duration = stopTimestamp - this.metadata.startTime;
464
+ const pausedDuration = this._getPausedDurationMs(stopTimestamp);
465
+ const duration = Math.max(0, stopTimestamp - this.metadata.startTime - pausedDuration);
396
466
 
397
467
  const result = {
398
468
  success: results.every(r => r.success),
@@ -402,6 +472,7 @@ class MultiWindowRecorder extends EventEmitter {
402
472
  cameraFile: this.cameraFile, // Camera output path (from first recorder)
403
473
  audioFile: this.audioFile, // Audio output path (from first recorder)
404
474
  duration: duration,
475
+ pausedDuration,
405
476
  metadata: {
406
477
  ...this.metadata,
407
478
  stopTime: stopTimestamp,
@@ -426,6 +497,10 @@ class MultiWindowRecorder extends EventEmitter {
426
497
 
427
498
  this.emit('allStopped', result);
428
499
 
500
+ this.isPaused = false;
501
+ this.pauseStartedAt = null;
502
+ this.pausedDurationMs = 0;
503
+
429
504
  return result;
430
505
  }
431
506
 
@@ -435,6 +510,9 @@ class MultiWindowRecorder extends EventEmitter {
435
510
  getStatus() {
436
511
  return {
437
512
  isRecording: this.isRecording,
513
+ isPaused: this.isPaused,
514
+ recordingTime: this._getRecordingTimeSeconds(),
515
+ pausedDuration: this._getPausedDurationMs() / 1000,
438
516
  windowCount: this.recorders.length,
439
517
  outputFiles: this.outputFiles,
440
518
  metadata: this.metadata,
@@ -538,6 +616,9 @@ class MultiWindowRecorder extends EventEmitter {
538
616
  this.outputFiles = [];
539
617
  this.cursorFiles = [];
540
618
  this.isRecording = false;
619
+ this.isPaused = false;
620
+ this.pauseStartedAt = null;
621
+ this.pausedDurationMs = 0;
541
622
 
542
623
  console.log('✅ Multi-window recorder cleaned up');
543
624
  }
package/README.md CHANGED
@@ -188,6 +188,21 @@ await recorder.startRecording("./recording.mov", {
188
188
  });
189
189
  ```
190
190
 
191
+ #### `pauseRecording()` / `resumeRecording()`
192
+
193
+ Pauses every active track without closing the output files, then resumes the
194
+ same recording session. Time spent paused is removed from the screen, camera,
195
+ audio, cursor, and keyboard timelines.
196
+
197
+ ```javascript
198
+ await recorder.pauseRecording();
199
+ // The output writers remain open while paused.
200
+ await recorder.resumeRecording();
201
+ ```
202
+
203
+ Both calls are idempotent and emit `paused` / `resumed` events with the current
204
+ status. `stopRecording()` may also be called while paused.
205
+
191
206
  #### `stopRecording()`
192
207
 
193
208
  Stops the current recording.
@@ -334,6 +349,7 @@ const status = recorder.getStatus();
334
349
  console.log(status);
335
350
  // {
336
351
  // isRecording: true,
352
+ // isPaused: false,
337
353
  // outputPath: "./recording.mov",
338
354
  // cameraOutputPath: "./temp_camera_1720000000000.webm",
339
355
  // audioOutputPath: "./temp_audio_1720000000000.webm",
@@ -341,7 +357,8 @@ console.log(status);
341
357
  // audioCapturing: true,
342
358
  // sessionTimestamp: 1720000000000,
343
359
  // options: { ... },
344
- // recordingTime: 15
360
+ // recordingTime: 15,
361
+ // pausedDuration: 3.5
345
362
  // }
346
363
  ```
347
364
 
package/binding.gyp CHANGED
@@ -44,7 +44,8 @@
44
44
  "-framework Carbon",
45
45
  "-framework Accessibility",
46
46
  "-framework CoreAudio",
47
- "-framework CoreMediaIO"
47
+ "-framework CoreMediaIO",
48
+ "-framework IOKit"
48
49
  ]
49
50
  },
50
51
  "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
@@ -13,6 +13,7 @@ class MacRecorderMultiProcess extends EventEmitter {
13
13
 
14
14
  this.worker = null;
15
15
  this.isRecording = false;
16
+ this.isPaused = false;
16
17
  this.outputPath = null;
17
18
  this.ready = false;
18
19
  this.pendingRequests = new Map();
@@ -36,6 +37,8 @@ class MacRecorderMultiProcess extends EventEmitter {
36
37
 
37
38
  this.worker.on('error', (error) => {
38
39
  console.error('❌ Worker error:', error);
40
+ for (const { reject } of this.pendingRequests.values()) reject(error);
41
+ this.pendingRequests.clear();
39
42
  this.emit('error', error);
40
43
  });
41
44
 
@@ -43,6 +46,7 @@ class MacRecorderMultiProcess extends EventEmitter {
43
46
  console.log(`🛑 Worker exited: code=${code}, signal=${signal}`);
44
47
  this.ready = false;
45
48
  this.isRecording = false;
49
+ this.isPaused = false;
46
50
 
47
51
  // Reject all pending requests
48
52
  for (const [id, { reject }] of this.pendingRequests) {
@@ -75,8 +79,13 @@ class MacRecorderMultiProcess extends EventEmitter {
75
79
  // Update local state based on events
76
80
  if (msg.event === 'recordingStarted') {
77
81
  this.isRecording = true;
82
+ } else if (msg.event === 'paused') {
83
+ this.isPaused = true;
84
+ } else if (msg.event === 'resumed') {
85
+ this.isPaused = false;
78
86
  } else if (msg.event === 'stopped') {
79
87
  this.isRecording = false;
88
+ this.isPaused = false;
80
89
  }
81
90
  return;
82
91
  }
@@ -84,7 +93,10 @@ class MacRecorderMultiProcess extends EventEmitter {
84
93
  // Handle errors
85
94
  if (msg.type === 'error') {
86
95
  console.error('❌ Worker error:', msg.message);
87
- this.emit('error', new Error(msg.message));
96
+ const error = new Error(msg.message);
97
+ for (const { reject } of this.pendingRequests.values()) reject(error);
98
+ this.pendingRequests.clear();
99
+ this.emit('error', error);
88
100
  return;
89
101
  }
90
102
 
@@ -110,7 +122,7 @@ class MacRecorderMultiProcess extends EventEmitter {
110
122
 
111
123
  _sendRequest(type, data = null, timeout = 30000) {
112
124
  return new Promise((resolve, reject) => {
113
- if (!this.worker) {
125
+ if (!this.worker || !this.worker.connected) {
114
126
  return reject(new Error('Worker not initialized'));
115
127
  }
116
128
 
@@ -131,9 +143,6 @@ class MacRecorderMultiProcess extends EventEmitter {
131
143
  }
132
144
  }, timeout);
133
145
 
134
- // Send message to worker
135
- this.worker.send({ type, data, id });
136
-
137
146
  // Clear timeout on completion
138
147
  const originalResolve = resolve;
139
148
  const originalReject = reject;
@@ -149,6 +158,16 @@ class MacRecorderMultiProcess extends EventEmitter {
149
158
  originalReject(error);
150
159
  }
151
160
  });
161
+
162
+ // A closed IPC pipe may fail asynchronously. Passing a callback
163
+ // contains that error and releases the pending request/timer.
164
+ const sendFailed = (error) => {
165
+ if (!error) return;
166
+ this.pendingRequests.get(id)?.reject(error);
167
+ this.pendingRequests.delete(id);
168
+ };
169
+ try { this.worker.send({ type, data, id }, sendFailed); }
170
+ catch (error) { sendFailed(error); }
152
171
  });
153
172
  }
154
173
 
@@ -186,16 +205,37 @@ class MacRecorderMultiProcess extends EventEmitter {
186
205
  options
187
206
  }, 60000); // Longer timeout for recording start
188
207
 
208
+ // The native first-frame event can arrive later than this response.
209
+ // Stop must already be available during that interval.
210
+ this.isRecording = true;
211
+ this.isPaused = false;
189
212
  return result.outputPath;
190
213
  }
191
214
 
215
+ async pauseRecording() {
216
+ if (!this.isRecording) throw new Error('No recording in progress');
217
+ if (this.isPaused) return this.getStatus();
218
+ const status = await this._sendRequest('pauseRecording');
219
+ this.isPaused = true;
220
+ return status;
221
+ }
222
+
223
+ async resumeRecording() {
224
+ if (!this.isRecording) throw new Error('No recording in progress');
225
+ if (!this.isPaused) return this.getStatus();
226
+ const status = await this._sendRequest('resumeRecording');
227
+ this.isPaused = false;
228
+ return status;
229
+ }
230
+
192
231
  async stopRecording() {
193
232
  if (!this.isRecording) {
194
233
  throw new Error('No recording in progress');
195
234
  }
196
235
 
197
- const result = await this._sendRequest('stopRecording', null, 10000);
236
+ const result = await this._sendRequest('stopRecording', null, 45000);
198
237
  this.isRecording = false;
238
+ this.isPaused = false;
199
239
 
200
240
  return result;
201
241
  }
@@ -231,6 +271,10 @@ class MacRecorderMultiProcess extends EventEmitter {
231
271
 
232
272
  this.ready = false;
233
273
  this.isRecording = false;
274
+ this.isPaused = false;
275
+ for (const { reject } of this.pendingRequests.values()) {
276
+ reject(new Error('Recorder worker destroyed'));
277
+ }
234
278
  this.pendingRequests.clear();
235
279
  }
236
280
  }