node-mac-recorder 2.21.39 โ†’ 2.21.41

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.
@@ -0,0 +1,546 @@
1
+ /**
2
+ * MultiWindowRecorder - Creavit Desktop Integration
3
+ * Manages multiple simultaneous window recordings
4
+ */
5
+
6
+ const MacRecorder = require('./index-multiprocess');
7
+ const MacRecorderSync = require('./index'); // For cursor tracking
8
+ const path = require('path');
9
+ const { EventEmitter } = require('events');
10
+
11
+ class MultiWindowRecorder extends EventEmitter {
12
+ constructor(options = {}) {
13
+ super();
14
+
15
+ this.recorders = [];
16
+ this.windows = [];
17
+ this.isRecording = false;
18
+ this.outputFiles = [];
19
+ this.cursorFiles = [];
20
+ this.cameraFile = null; // Camera output file (from first recorder)
21
+ this.audioFile = null; // Audio output file (from first recorder)
22
+ this.cursorRecorder = null; // Separate recorder for cursor tracking
23
+ this.timeUpdateInterval = null; // Timer for timeUpdate events
24
+ this.metadata = {
25
+ startTime: null,
26
+ syncTimestamps: [],
27
+ windowCount: 0
28
+ };
29
+
30
+ this.options = {
31
+ frameRate: options.frameRate || 30,
32
+ captureCursor: false, // Don't show system cursor in window recording
33
+ preferScreenCaptureKit: options.preferScreenCaptureKit !== false,
34
+ // Audio options
35
+ enableMicrophone: options.enableMicrophone || false,
36
+ microphoneDeviceId: options.microphoneDeviceId || null,
37
+ captureSystemAudio: options.captureSystemAudio || false,
38
+ // Camera options
39
+ enableCamera: options.enableCamera || false,
40
+ cameraDeviceId: options.cameraDeviceId || null,
41
+ // Cursor tracking
42
+ trackCursor: options.trackCursor !== false, // Default: true
43
+ ...options
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Add a window to be recorded
49
+ * @param {Object} windowInfo - Window information from getWindows()
50
+ * @returns {number} Index of the added window
51
+ */
52
+ async addWindow(windowInfo) {
53
+ const recorder = new MacRecorder();
54
+
55
+ const recorderInfo = {
56
+ recorder,
57
+ windowId: windowInfo.id,
58
+ windowInfo: {
59
+ id: windowInfo.id,
60
+ appName: windowInfo.appName,
61
+ title: windowInfo.title,
62
+ width: windowInfo.width,
63
+ height: windowInfo.height
64
+ },
65
+ outputPath: null,
66
+ cursorFilePath: null,
67
+ syncTimestamp: null,
68
+ index: this.recorders.length
69
+ };
70
+
71
+ this.recorders.push(recorderInfo);
72
+ this.windows.push(windowInfo);
73
+
74
+ // Wait for worker to be ready
75
+ await new Promise(r => setTimeout(r, 500));
76
+
77
+ console.log(`โœ… Window added: ${windowInfo.appName} (index: ${recorderInfo.index})`);
78
+
79
+ return recorderInfo.index;
80
+ }
81
+
82
+ /**
83
+ * Remove a window by index
84
+ * @param {number} index - Window index
85
+ */
86
+ removeWindow(index) {
87
+ if (index < 0 || index >= this.recorders.length) {
88
+ throw new Error(`Invalid window index: ${index}`);
89
+ }
90
+
91
+ const recorderInfo = this.recorders[index];
92
+
93
+ if (recorderInfo && recorderInfo.recorder) {
94
+ recorderInfo.recorder.destroy();
95
+ console.log(`๐Ÿ—‘๏ธ Window removed: ${recorderInfo.windowInfo.appName}`);
96
+ }
97
+
98
+ this.recorders.splice(index, 1);
99
+ this.windows.splice(index, 1);
100
+
101
+ // Update indices
102
+ this.recorders.forEach((rec, i) => {
103
+ rec.index = i;
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Get current window count
109
+ */
110
+ getWindowCount() {
111
+ return this.recorders.length;
112
+ }
113
+
114
+ /**
115
+ * Start recording all windows
116
+ * @param {string} outputDir - Output directory path
117
+ * @param {Object} options - Recording options
118
+ */
119
+ async startRecording(outputDir, options = {}) {
120
+ if (this.isRecording) {
121
+ throw new Error('Recording already in progress');
122
+ }
123
+
124
+ if (this.recorders.length === 0) {
125
+ throw new Error('No windows added. Call addWindow() first.');
126
+ }
127
+
128
+ const timestamp = Date.now();
129
+ this.metadata.startTime = timestamp;
130
+ this.metadata.windowCount = this.recorders.length;
131
+ this.outputFiles = [];
132
+
133
+ console.log(`๐ŸŽฌ Starting ${this.recorders.length} window recordings...`);
134
+ console.log(`๐Ÿ“ Output directory: ${outputDir}`);
135
+
136
+ // Start all recorders sequentially with 1s delay between each
137
+ for (let i = 0; i < this.recorders.length; i++) {
138
+ const recInfo = this.recorders[i];
139
+ const appName = recInfo.windowInfo.appName.replace(/[^a-zA-Z0-9]/g, '_');
140
+ const outputPath = path.join(outputDir, `temp_window_${i}_${appName}_${timestamp}.mov`);
141
+
142
+ console.log(`\nโ–ถ๏ธ Starting recorder ${i + 1}/${this.recorders.length}: ${recInfo.windowInfo.appName}`);
143
+
144
+ const recordingOptions = {
145
+ windowId: recInfo.windowId,
146
+ frameRate: this.options.frameRate,
147
+ captureCursor: this.options.captureCursor,
148
+ preferScreenCaptureKit: this.options.preferScreenCaptureKit,
149
+ // Use the MAIN timestamp for ALL files to keep them synchronized
150
+ sessionTimestamp: timestamp,
151
+ // Audio options - ONLY record audio on first window to avoid duplicates
152
+ includeMicrophone: (i === 0 && this.options.enableMicrophone) || false,
153
+ audioDeviceId: (i === 0 && this.options.microphoneDeviceId) || null,
154
+ includeSystemAudio: (i === 0 && this.options.captureSystemAudio) || false,
155
+ systemAudioDeviceId: null,
156
+ // Camera options - record on first window only
157
+ captureCamera: (i === 0 && this.options.enableCamera) || false,
158
+ cameraDeviceId: (i === 0 && this.options.cameraDeviceId) || null,
159
+ ...options
160
+ };
161
+
162
+ try {
163
+ const startTimestamp = Date.now();
164
+
165
+ // For first recorder, pre-calculate camera and audio paths
166
+ // IMPORTANT: Use the MAIN timestamp, not startTimestamp, to match window files!
167
+ if (i === 0) {
168
+ if (this.options.enableCamera) {
169
+ this.cameraFile = path.join(outputDir, `temp_camera_${timestamp}.mov`);
170
+ console.log(` ๐Ÿ“ท Camera will be saved to: ${path.basename(this.cameraFile)}`);
171
+ }
172
+
173
+ if (this.options.enableMicrophone || this.options.captureSystemAudio) {
174
+ this.audioFile = path.join(outputDir, `temp_audio_${timestamp}.mov`);
175
+ console.log(` ๐ŸŽต Audio will be saved to: ${path.basename(this.audioFile)}`);
176
+ }
177
+ }
178
+
179
+ await recInfo.recorder.startRecording(outputPath, recordingOptions);
180
+
181
+ recInfo.outputPath = outputPath;
182
+ recInfo.syncTimestamp = startTimestamp;
183
+ this.metadata.syncTimestamps.push(startTimestamp);
184
+ this.outputFiles.push(outputPath);
185
+
186
+ console.log(` โœ… Recorder ${i + 1} started`);
187
+ console.log(` ๐Ÿ“„ Output: ${path.basename(outputPath)}`);
188
+
189
+ // Start cursor tracking if enabled (only once for all windows)
190
+ if (this.options.trackCursor && i === 0) {
191
+ const cursorPath = path.join(outputDir, `temp_cursor_${timestamp}.json`);
192
+
193
+ // Create cursor recorder on first use
194
+ if (!this.cursorRecorder) {
195
+ this.cursorRecorder = new MacRecorderSync();
196
+ }
197
+
198
+ // Get main display info for global cursor tracking
199
+ const displays = await this.cursorRecorder.getDisplays();
200
+ const mainDisplay = displays.find(d => d.isPrimary) || displays[0];
201
+
202
+ // Collect all window bounds for cursor location detection
203
+ const windowBounds = this.recorders.map(rec => ({
204
+ windowId: rec.windowId,
205
+ appName: rec.windowInfo.appName,
206
+ title: rec.windowInfo.title,
207
+ // Window bounds will be retrieved from native API
208
+ bounds: null // Will be filled by cursor tracker
209
+ }));
210
+
211
+ // Track cursor with global coordinates (for multi-window setup)
212
+ // Use main display as reference for coordinate system
213
+ const cursorOptions = {
214
+ videoRelative: false, // Global screen coordinates (not video-relative)
215
+ displayInfo: mainDisplay ? {
216
+ displayId: mainDisplay.id,
217
+ x: mainDisplay.x || 0,
218
+ y: mainDisplay.y || 0,
219
+ width: parseInt(mainDisplay.resolution.split('x')[0]),
220
+ height: parseInt(mainDisplay.resolution.split('x')[1]),
221
+ logicalWidth: parseInt(mainDisplay.resolution.split('x')[0]),
222
+ logicalHeight: parseInt(mainDisplay.resolution.split('x')[1])
223
+ } : null,
224
+ recordingType: 'multi-window', // Multi-window recording type
225
+ startTimestamp: startTimestamp, // Use same timestamp as video
226
+ // Pass window information for location detection
227
+ multiWindowBounds: windowBounds
228
+ };
229
+
230
+ await this.cursorRecorder.startCursorCapture(cursorPath, cursorOptions);
231
+
232
+ // Store cursor file path for all windows
233
+ this.recorders.forEach(rec => {
234
+ rec.cursorFilePath = cursorPath;
235
+ });
236
+ this.cursorFiles.push(cursorPath);
237
+
238
+ console.log(` ๐Ÿ–ฑ๏ธ Cursor tracking started (multi-window mode)`);
239
+ console.log(` ๐Ÿ“„ Cursor file: ${path.basename(cursorPath)}`);
240
+ }
241
+
242
+ this.emit('recorderStarted', {
243
+ index: i,
244
+ windowInfo: recInfo.windowInfo,
245
+ outputPath: outputPath,
246
+ cursorFilePath: recInfo.cursorFilePath,
247
+ timestamp: startTimestamp
248
+ });
249
+
250
+ // Wait for ScreenCaptureKit initialization (except for last recorder)
251
+ if (i < this.recorders.length - 1) {
252
+ console.log(` โณ Waiting 1s for ScreenCaptureKit init...`);
253
+ await new Promise(r => setTimeout(r, 1000));
254
+ }
255
+ } catch (error) {
256
+ console.error(` โŒ Failed to start recorder ${i + 1}:`, error.message);
257
+
258
+ // Stop all previously started recorders and cursor tracking
259
+ for (let j = 0; j < i; j++) {
260
+ try {
261
+ await this.recorders[j].recorder.stopRecording();
262
+ } catch (stopError) {
263
+ console.error(`Failed to stop recorder ${j}:`, stopError.message);
264
+ }
265
+ }
266
+
267
+ // Stop cursor tracking if it was started
268
+ if (this.options.trackCursor && this.cursorRecorder) {
269
+ try {
270
+ await this.cursorRecorder.stopCursorCapture();
271
+ } catch (cursorError) {
272
+ console.error(`Failed to stop cursor tracking:`, cursorError.message);
273
+ }
274
+ }
275
+
276
+ throw new Error(`Failed to start recorder ${i + 1}: ${error.message}`);
277
+ }
278
+ }
279
+
280
+ this.isRecording = true;
281
+
282
+ // Start timeUpdate timer (emit every second)
283
+ this.timeUpdateInterval = setInterval(() => {
284
+ if (this.isRecording && this.metadata.startTime) {
285
+ const elapsed = Math.floor((Date.now() - this.metadata.startTime) / 1000);
286
+ this.emit('timeUpdate', elapsed);
287
+ }
288
+ }, 1000);
289
+
290
+ console.log(`\nโœ… All ${this.recorders.length} recordings started successfully!`);
291
+ console.log(`๐Ÿ”ด Multi-window recording in progress...`);
292
+
293
+ this.emit('allStarted', {
294
+ windowCount: this.recorders.length,
295
+ outputFiles: this.outputFiles,
296
+ metadata: this.metadata
297
+ });
298
+
299
+ return {
300
+ windowCount: this.recorders.length,
301
+ outputFiles: this.outputFiles,
302
+ metadata: this.metadata
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Stop all recordings
308
+ */
309
+ async stopRecording() {
310
+ if (!this.isRecording) {
311
+ throw new Error('No recording in progress');
312
+ }
313
+
314
+ console.log(`\n๐Ÿ›‘ Stopping ${this.recorders.length} recordings...`);
315
+
316
+ const stopTimestamp = Date.now();
317
+
318
+ // Stop timeUpdate timer
319
+ if (this.timeUpdateInterval) {
320
+ clearInterval(this.timeUpdateInterval);
321
+ this.timeUpdateInterval = null;
322
+ console.log(` โฑ๏ธ Timer stopped`);
323
+ }
324
+
325
+ // Stop cursor tracking first (before stopping video recordings)
326
+ if (this.options.trackCursor && this.cursorRecorder) {
327
+ try {
328
+ console.log(` ๐Ÿ–ฑ๏ธ Stopping cursor tracking...`);
329
+ await this.cursorRecorder.stopCursorCapture();
330
+ console.log(` โœ… Cursor tracking stopped`);
331
+ } catch (error) {
332
+ console.error(` โŒ Failed to stop cursor tracking:`, error.message);
333
+ }
334
+ }
335
+
336
+ // Stop all recorders in parallel
337
+ const stopPromises = this.recorders.map(async (recInfo, index) => {
338
+ try {
339
+ console.log(` Stopping recorder ${index + 1}: ${recInfo.windowInfo.appName}...`);
340
+
341
+ await recInfo.recorder.stopRecording();
342
+ console.log(` โœ… Recorder ${index + 1} stopped`);
343
+
344
+ this.emit('recorderStopped', {
345
+ index,
346
+ windowInfo: recInfo.windowInfo,
347
+ outputPath: recInfo.outputPath,
348
+ cursorFilePath: recInfo.cursorFilePath
349
+ });
350
+
351
+ return {
352
+ index,
353
+ success: true,
354
+ outputPath: recInfo.outputPath,
355
+ cursorFilePath: recInfo.cursorFilePath
356
+ };
357
+ } catch (error) {
358
+ console.error(` โŒ Failed to stop recorder ${index + 1}:`, error.message);
359
+
360
+ this.emit('recorderError', {
361
+ index,
362
+ error: error.message
363
+ });
364
+
365
+ return {
366
+ index,
367
+ success: false,
368
+ error: error.message
369
+ };
370
+ }
371
+ });
372
+
373
+ const results = await Promise.all(stopPromises);
374
+
375
+ this.isRecording = false;
376
+
377
+ // Get camera and audio paths from first recorder's status
378
+ if (this.recorders.length > 0) {
379
+ try {
380
+ const firstRecorderStatus = this.recorders[0].recorder.getStatus();
381
+ if (firstRecorderStatus.cameraOutputPath) {
382
+ this.cameraFile = firstRecorderStatus.cameraOutputPath;
383
+ console.log(` ๐Ÿ“ท Camera file from recorder: ${path.basename(this.cameraFile)}`);
384
+ }
385
+ if (firstRecorderStatus.audioOutputPath) {
386
+ this.audioFile = firstRecorderStatus.audioOutputPath;
387
+ console.log(` ๐ŸŽต Audio file from recorder: ${path.basename(this.audioFile)}`);
388
+ }
389
+ } catch (error) {
390
+ console.error(` โš ๏ธ Could not get camera/audio paths from first recorder:`, error.message);
391
+ }
392
+ }
393
+
394
+ // Calculate duration
395
+ const duration = stopTimestamp - this.metadata.startTime;
396
+
397
+ const result = {
398
+ success: results.every(r => r.success),
399
+ windowCount: this.recorders.length,
400
+ outputFiles: this.outputFiles,
401
+ cursorFiles: this.cursorFiles,
402
+ cameraFile: this.cameraFile, // Camera output path (from first recorder)
403
+ audioFile: this.audioFile, // Audio output path (from first recorder)
404
+ duration: duration,
405
+ metadata: {
406
+ ...this.metadata,
407
+ stopTime: stopTimestamp,
408
+ duration: duration,
409
+ windows: this.recorders.map((recInfo, i) => ({
410
+ index: i,
411
+ windowInfo: recInfo.windowInfo,
412
+ outputPath: recInfo.outputPath,
413
+ cursorFilePath: recInfo.cursorFilePath,
414
+ syncTimestamp: recInfo.syncTimestamp,
415
+ syncOffset: recInfo.syncTimestamp - this.metadata.startTime
416
+ }))
417
+ }
418
+ };
419
+
420
+ console.log(`\nโœ… All recordings stopped successfully!`);
421
+ console.log(`๐Ÿ“Š Duration: ${(duration / 1000).toFixed(2)}s`);
422
+ console.log(`๐Ÿ“ Output files: ${this.outputFiles.length}`);
423
+ if (this.options.trackCursor) {
424
+ console.log(`๐Ÿ–ฑ๏ธ Cursor files: ${this.cursorFiles.length}`);
425
+ }
426
+
427
+ this.emit('allStopped', result);
428
+
429
+ return result;
430
+ }
431
+
432
+ /**
433
+ * Get recording status
434
+ */
435
+ getStatus() {
436
+ return {
437
+ isRecording: this.isRecording,
438
+ windowCount: this.recorders.length,
439
+ outputFiles: this.outputFiles,
440
+ metadata: this.metadata,
441
+ windows: this.recorders.map(rec => ({
442
+ index: rec.index,
443
+ windowInfo: rec.windowInfo,
444
+ outputPath: rec.outputPath
445
+ }))
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Get metadata for CRVT file creation
451
+ * @param {Object} options - Options for metadata generation
452
+ * @param {string} options.clipId - Clip ID for media paths (e.g., 'clip_1762961230017')
453
+ */
454
+ getMetadataForCRVT(options = {}) {
455
+ const { clipId } = options;
456
+
457
+ // Helper function to convert full path to media path
458
+ const toMediaPath = (fullPath) => {
459
+ if (!fullPath || !clipId) return fullPath;
460
+ const filename = path.basename(fullPath);
461
+ return `media/${clipId}/${filename}`;
462
+ };
463
+
464
+ return {
465
+ version: '2.0',
466
+ timestamp: this.metadata.startTime,
467
+ duration: this.metadata.duration || 0,
468
+ multiWindow: {
469
+ enabled: true,
470
+ windowCount: this.recorders.length,
471
+ windows: this.recorders.map((recInfo, i) => ({
472
+ index: i,
473
+ windowInfo: {
474
+ id: recInfo.windowId,
475
+ appName: recInfo.windowInfo.appName,
476
+ title: recInfo.windowInfo.title,
477
+ width: recInfo.windowInfo.width,
478
+ height: recInfo.windowInfo.height
479
+ },
480
+ outputPath: toMediaPath(recInfo.outputPath), // media/clip_xxx/filename.mov
481
+ cursorFilePath: toMediaPath(recInfo.cursorFilePath), // media/clip_xxx/filename.json
482
+ syncTimestamp: recInfo.syncTimestamp,
483
+ syncOffset: recInfo.syncTimestamp - this.metadata.startTime
484
+ })),
485
+ syncTimestamps: this.metadata.syncTimestamps
486
+ },
487
+ // Recording options
488
+ options: {
489
+ enableCamera: this.options.enableCamera,
490
+ cameraDeviceId: this.options.cameraDeviceId,
491
+ enableMicrophone: this.options.enableMicrophone,
492
+ microphoneDeviceId: this.options.microphoneDeviceId,
493
+ captureSystemAudio: this.options.captureSystemAudio,
494
+ trackCursor: this.options.trackCursor
495
+ }
496
+ };
497
+ }
498
+
499
+ /**
500
+ * Destroy all recorders and cleanup
501
+ */
502
+ destroy() {
503
+ console.log('๐Ÿงน Cleaning up multi-window recorder...');
504
+
505
+ // Stop timeUpdate timer
506
+ if (this.timeUpdateInterval) {
507
+ clearInterval(this.timeUpdateInterval);
508
+ this.timeUpdateInterval = null;
509
+ }
510
+
511
+ this.recorders.forEach((recInfo, index) => {
512
+ try {
513
+ recInfo.recorder.destroy();
514
+ console.log(` โœ“ Recorder ${index + 1} destroyed`);
515
+ } catch (error) {
516
+ console.error(` โœ— Failed to destroy recorder ${index + 1}:`, error.message);
517
+ }
518
+ });
519
+
520
+ // Destroy cursor recorder if exists
521
+ if (this.cursorRecorder) {
522
+ try {
523
+ // MacRecorderSync uses cleanup() instead of destroy()
524
+ if (typeof this.cursorRecorder.cleanup === 'function') {
525
+ this.cursorRecorder.cleanup();
526
+ } else if (typeof this.cursorRecorder.destroy === 'function') {
527
+ this.cursorRecorder.destroy();
528
+ }
529
+ console.log(` โœ“ Cursor recorder destroyed`);
530
+ } catch (error) {
531
+ console.error(` โœ— Failed to destroy cursor recorder:`, error.message);
532
+ }
533
+ this.cursorRecorder = null;
534
+ }
535
+
536
+ this.recorders = [];
537
+ this.windows = [];
538
+ this.outputFiles = [];
539
+ this.cursorFiles = [];
540
+ this.isRecording = false;
541
+
542
+ console.log('โœ… Multi-window recorder cleaned up');
543
+ }
544
+ }
545
+
546
+ module.exports = MultiWindowRecorder;
package/README.md CHANGED
@@ -18,6 +18,7 @@ A powerful native macOS screen recording Node.js package with advanced window se
18
18
  - ๐Ÿ”Š **Audio Capture** - Record microphone/system audio into synchronized companion files
19
19
  - ๐Ÿšซ **Automatic Overlay Exclusion** - Overlay windows automatically excluded from recordings
20
20
  - โšก **Electron Compatible** - Enhanced crash protection for Electron applications
21
+ - ๐ŸŽฌ **Multi-Window Recording** - โœจ NEW! Record multiple windows/displays simultaneously
21
22
 
22
23
  ๐ŸŽต **Granular Audio Controls**
23
24
 
@@ -93,6 +94,56 @@ await new Promise((resolve) => setTimeout(resolve, 5000)); // Record for 5 secon
93
94
  await recorder.stopRecording();
94
95
  ```
95
96
 
97
+ ## Multi-Window Recording โœจ NEW!
98
+
99
+ Record multiple windows or displays simultaneously using child processes:
100
+
101
+ ```javascript
102
+ const MacRecorder = require("node-mac-recorder/index-multiprocess");
103
+
104
+ // Create separate recorders for each window
105
+ const recorder1 = new MacRecorder();
106
+ const recorder2 = new MacRecorder();
107
+
108
+ // Get available windows
109
+ const windows = await recorder1.getWindows();
110
+
111
+ // Record first window (e.g., Finder)
112
+ await recorder1.startRecording("window1.mov", {
113
+ windowId: windows[0].id,
114
+ frameRate: 30
115
+ });
116
+
117
+ // Wait for ScreenCaptureKit initialization
118
+ await new Promise(r => setTimeout(r, 1000));
119
+
120
+ // Record second window (e.g., Chrome)
121
+ await recorder2.startRecording("window2.mov", {
122
+ windowId: windows[1].id,
123
+ frameRate: 30
124
+ });
125
+
126
+ // Both recordings are now running in parallel! ๐ŸŽ‰
127
+
128
+ // Stop both after 10 seconds
129
+ await new Promise(r => setTimeout(r, 10000));
130
+ await recorder1.stopRecording();
131
+ await recorder2.stopRecording();
132
+
133
+ // Cleanup
134
+ recorder1.destroy();
135
+ recorder2.destroy();
136
+ ```
137
+
138
+ **Key Benefits:**
139
+ - โœ… No native code changes required
140
+ - โœ… Each recorder runs in its own process
141
+ - โœ… True parallel recording
142
+ - โœ… Separate output files
143
+ - โœ… Independent control
144
+
145
+ **See:** `MULTI_RECORDING.md` for detailed documentation and examples.
146
+
96
147
  ## API Reference
97
148
 
98
149
  ### Constructor
package/binding.gyp CHANGED
@@ -9,6 +9,7 @@
9
9
  "src/camera_recorder.mm",
10
10
  "src/sync_timeline.mm",
11
11
  "src/audio_recorder.mm",
12
+ "src/audio_mixer.mm",
12
13
  "src/cursor_tracker.mm",
13
14
  "src/window_selector.mm"
14
15
  ],