node-mac-recorder 2.20.16 → 2.21.0

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
@@ -34,6 +34,11 @@ class MacRecorder extends EventEmitter {
34
34
  this.lastCapturedData = null;
35
35
  this.cursorDisplayInfo = null;
36
36
  this.recordingDisplayInfo = null;
37
+ this.cameraCaptureFile = null;
38
+ this.cameraCaptureActive = false;
39
+ this.sessionTimestamp = null;
40
+ this.audioCaptureFile = null;
41
+ this.audioCaptureActive = false;
37
42
 
38
43
  this.options = {
39
44
  includeMicrophone: false, // Default olarak mikrofon kapalı
@@ -45,6 +50,9 @@ class MacRecorder extends EventEmitter {
45
50
  showClicks: false,
46
51
  displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
47
52
  windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
53
+ captureCamera: false,
54
+ cameraDeviceId: null,
55
+ systemAudioDeviceId: null,
48
56
  };
49
57
 
50
58
  // Display cache için async initialization
@@ -63,9 +71,11 @@ class MacRecorder extends EventEmitter {
63
71
  try {
64
72
  const devices = nativeBinding.getAudioDevices();
65
73
  const formattedDevices = devices.map((device) => ({
66
- name: typeof device === "string" ? device : device.name || device,
67
- id: typeof device === "object" ? device.id : device,
68
- type: typeof device === "object" ? device.type : "Audio Device",
74
+ name: device?.name || "Unknown Audio Device",
75
+ id: device?.id || "",
76
+ manufacturer: device?.manufacturer || null,
77
+ isDefault: device?.isDefault === true,
78
+ transportType: device?.transportType ?? null,
69
79
  }));
70
80
  resolve(formattedDevices);
71
81
  } catch (error) {
@@ -74,6 +84,39 @@ class MacRecorder extends EventEmitter {
74
84
  });
75
85
  }
76
86
 
87
+ /**
88
+ * macOS kamera cihazlarını listeler
89
+ */
90
+ async getCameraDevices() {
91
+ return new Promise((resolve, reject) => {
92
+ try {
93
+ const devices = nativeBinding.getCameraDevices();
94
+ if (!Array.isArray(devices)) {
95
+ return resolve([]);
96
+ }
97
+
98
+ const formatted = devices.map((device) => ({
99
+ id: device?.id ?? "",
100
+ name: device?.name ?? "Unknown Camera",
101
+ model: device?.model ?? null,
102
+ manufacturer: device?.manufacturer ?? null,
103
+ position: device?.position ?? "unspecified",
104
+ transportType: device?.transportType ?? null,
105
+ isConnected: device?.isConnected ?? false,
106
+ hasFlash: device?.hasFlash ?? false,
107
+ supportsDepth: device?.supportsDepth ?? false,
108
+ deviceType: device?.deviceType ?? null,
109
+ requiresContinuityCameraPermission: device?.requiresContinuityCameraPermission ?? false,
110
+ maxResolution: device?.maxResolution ?? null,
111
+ }));
112
+
113
+ resolve(formatted);
114
+ } catch (error) {
115
+ reject(error);
116
+ }
117
+ });
118
+ }
119
+
77
120
  /**
78
121
  * macOS ekranlarını listeler
79
122
  */
@@ -118,6 +161,11 @@ class MacRecorder extends EventEmitter {
118
161
  audioDeviceId: options.audioDeviceId || null, // null = default device
119
162
  systemAudioDeviceId: options.systemAudioDeviceId || null, // null = auto-detect system audio device
120
163
  captureArea: options.captureArea || null,
164
+ captureCamera: options.captureCamera === true,
165
+ cameraDeviceId:
166
+ typeof options.cameraDeviceId === "string" && options.cameraDeviceId.length > 0
167
+ ? options.cameraDeviceId
168
+ : null,
121
169
  };
122
170
  }
123
171
 
@@ -129,6 +177,15 @@ class MacRecorder extends EventEmitter {
129
177
  return this.options.includeMicrophone;
130
178
  }
131
179
 
180
+ setAudioDevice(deviceId) {
181
+ if (typeof deviceId === "string" && deviceId.length > 0) {
182
+ this.options.audioDeviceId = deviceId;
183
+ } else {
184
+ this.options.audioDeviceId = null;
185
+ }
186
+ return this.options.audioDeviceId;
187
+ }
188
+
132
189
  /**
133
190
  * Sistem sesi kaydını açar/kapatır
134
191
  */
@@ -137,6 +194,38 @@ class MacRecorder extends EventEmitter {
137
194
  return this.options.includeSystemAudio;
138
195
  }
139
196
 
197
+ setSystemAudioDevice(deviceId) {
198
+ if (typeof deviceId === "string" && deviceId.length > 0) {
199
+ this.options.systemAudioDeviceId = deviceId;
200
+ } else {
201
+ this.options.systemAudioDeviceId = null;
202
+ }
203
+ return this.options.systemAudioDeviceId;
204
+ }
205
+
206
+ /**
207
+ * Kamera kaydını açar/kapatır
208
+ */
209
+ setCameraEnabled(enabled) {
210
+ this.options.captureCamera = enabled === true;
211
+ if (!this.options.captureCamera) {
212
+ this.cameraCaptureActive = false;
213
+ }
214
+ return this.options.captureCamera;
215
+ }
216
+
217
+ /**
218
+ * Kamera cihazını seçer
219
+ */
220
+ setCameraDevice(deviceId) {
221
+ if (typeof deviceId === "string" && deviceId.length > 0) {
222
+ this.options.cameraDeviceId = deviceId;
223
+ } else {
224
+ this.options.cameraDeviceId = null;
225
+ }
226
+ return this.options.cameraDeviceId;
227
+ }
228
+
140
229
  /**
141
230
  * Mikrofon durumunu döndürür
142
231
  */
@@ -151,6 +240,13 @@ class MacRecorder extends EventEmitter {
151
240
  return this.options.includeSystemAudio === true;
152
241
  }
153
242
 
243
+ /**
244
+ * Kamera durumunu döndürür
245
+ */
246
+ isCameraEnabled() {
247
+ return this.options.captureCamera === true;
248
+ }
249
+
154
250
  /**
155
251
  * Audio ayarlarını toplu olarak değiştirir
156
252
  */
@@ -325,9 +421,34 @@ class MacRecorder extends EventEmitter {
325
421
  return new Promise((resolve, reject) => {
326
422
  try {
327
423
  // Create cursor file path with timestamp in the same directory as video
328
- const timestamp = Date.now();
424
+ const sessionTimestamp = Date.now();
425
+ this.sessionTimestamp = sessionTimestamp;
329
426
  const outputDir = path.dirname(outputPath);
330
- const cursorFilePath = path.join(outputDir, `temp_cursor_${timestamp}.json`);
427
+ const cursorFilePath = path.join(outputDir, `temp_cursor_${sessionTimestamp}.json`);
428
+ const cameraFilePath =
429
+ this.options.captureCamera === true
430
+ ? path.join(outputDir, `temp_camera_${sessionTimestamp}.webm`)
431
+ : null;
432
+ const captureAudio = this.options.includeMicrophone === true || this.options.includeSystemAudio === true;
433
+ const audioFilePath = captureAudio
434
+ ? path.join(outputDir, `temp_audio_${sessionTimestamp}.webm`)
435
+ : null;
436
+
437
+ if (this.options.captureCamera === true) {
438
+ this.cameraCaptureFile = cameraFilePath;
439
+ this.cameraCaptureActive = false;
440
+ } else {
441
+ this.cameraCaptureFile = null;
442
+ this.cameraCaptureActive = false;
443
+ }
444
+
445
+ if (captureAudio) {
446
+ this.audioCaptureFile = audioFilePath;
447
+ this.audioCaptureActive = false;
448
+ } else {
449
+ this.audioCaptureFile = null;
450
+ this.audioCaptureActive = false;
451
+ }
331
452
 
332
453
  // Native kayıt başlat
333
454
  const recordingOptions = {
@@ -338,8 +459,19 @@ class MacRecorder extends EventEmitter {
338
459
  windowId: this.options.windowId || null, // null = tam ekran
339
460
  audioDeviceId: this.options.audioDeviceId || null, // null = default device
340
461
  systemAudioDeviceId: this.options.systemAudioDeviceId || null, // null = auto-detect system audio device
462
+ captureCamera: this.options.captureCamera === true,
463
+ cameraDeviceId: this.options.cameraDeviceId || null,
464
+ sessionTimestamp,
341
465
  };
342
466
 
467
+ if (cameraFilePath) {
468
+ recordingOptions.cameraOutputPath = cameraFilePath;
469
+ }
470
+
471
+ if (audioFilePath) {
472
+ recordingOptions.audioOutputPath = audioFilePath;
473
+ }
474
+
343
475
  // Manuel captureArea varsa onu kullan
344
476
  if (this.options.captureArea) {
345
477
  recordingOptions.captureArea = {
@@ -365,6 +497,29 @@ class MacRecorder extends EventEmitter {
365
497
  this.isRecording = true;
366
498
  this.recordingStartTime = Date.now();
367
499
 
500
+ if (this.options.captureCamera === true && cameraFilePath) {
501
+ this.cameraCaptureActive = true;
502
+ this.emit("cameraCaptureStarted", {
503
+ outputPath: cameraFilePath,
504
+ deviceId: this.options.cameraDeviceId || null,
505
+ timestamp: sessionTimestamp,
506
+ sessionTimestamp,
507
+ });
508
+ }
509
+
510
+ if (captureAudio && audioFilePath) {
511
+ this.audioCaptureActive = true;
512
+ this.emit("audioCaptureStarted", {
513
+ outputPath: audioFilePath,
514
+ deviceIds: {
515
+ microphone: this.options.audioDeviceId || null,
516
+ system: this.options.systemAudioDeviceId || null,
517
+ },
518
+ timestamp: sessionTimestamp,
519
+ sessionTimestamp,
520
+ });
521
+ }
522
+
368
523
  // Start unified cursor tracking with video-relative coordinates
369
524
  // This ensures cursor positions match exactly with video frames
370
525
  const standardCursorOptions = {
@@ -398,24 +553,32 @@ class MacRecorder extends EventEmitter {
398
553
  clearInterval(checkRecordingStatus);
399
554
 
400
555
  // Kayıt gerçekten başladığı anda event emit et
401
- this.emit("recordingStarted", {
402
- outputPath: this.outputPath,
403
- timestamp: Date.now(), // Gerçek başlangıç zamanı
404
- options: this.options,
405
- nativeConfirmed: true
406
- });
556
+ this.emit("recordingStarted", {
557
+ outputPath: this.outputPath,
558
+ timestamp: Date.now(), // Gerçek başlangıç zamanı
559
+ options: this.options,
560
+ nativeConfirmed: true,
561
+ cameraOutputPath: this.cameraCaptureFile || null,
562
+ audioOutputPath: this.audioCaptureFile || null,
563
+ cursorOutputPath: cursorFilePath,
564
+ sessionTimestamp: this.sessionTimestamp,
565
+ });
407
566
  }
408
567
  } catch (error) {
409
568
  // Native status check error - fallback
410
569
  if (!recordingStartedEmitted) {
411
570
  recordingStartedEmitted = true;
412
571
  clearInterval(checkRecordingStatus);
413
- this.emit("recordingStarted", {
414
- outputPath: this.outputPath,
415
- timestamp: this.recordingStartTime,
416
- options: this.options,
417
- nativeConfirmed: false
418
- });
572
+ this.emit("recordingStarted", {
573
+ outputPath: this.outputPath,
574
+ timestamp: this.recordingStartTime,
575
+ options: this.options,
576
+ nativeConfirmed: false,
577
+ cameraOutputPath: this.cameraCaptureFile || null,
578
+ audioOutputPath: this.audioCaptureFile || null,
579
+ cursorOutputPath: cursorFilePath,
580
+ sessionTimestamp: this.sessionTimestamp,
581
+ });
419
582
  }
420
583
  }
421
584
  }, 50); // Her 50ms kontrol et
@@ -425,18 +588,48 @@ class MacRecorder extends EventEmitter {
425
588
  if (!recordingStartedEmitted) {
426
589
  recordingStartedEmitted = true;
427
590
  clearInterval(checkRecordingStatus);
428
- this.emit("recordingStarted", {
429
- outputPath: this.outputPath,
430
- timestamp: this.recordingStartTime,
431
- options: this.options,
432
- nativeConfirmed: false
433
- });
591
+ this.emit("recordingStarted", {
592
+ outputPath: this.outputPath,
593
+ timestamp: this.recordingStartTime,
594
+ options: this.options,
595
+ nativeConfirmed: false,
596
+ cameraOutputPath: this.cameraCaptureFile || null,
597
+ audioOutputPath: this.audioCaptureFile || null,
598
+ cursorOutputPath: cursorFilePath,
599
+ sessionTimestamp: this.sessionTimestamp,
600
+ });
434
601
  }
435
602
  }, 5000);
436
603
 
437
604
  this.emit("started", this.outputPath);
438
605
  resolve(this.outputPath);
439
606
  } else {
607
+ this.cameraCaptureActive = false;
608
+ if (this.options.captureCamera === true) {
609
+ if (cameraFilePath && fs.existsSync(cameraFilePath)) {
610
+ try {
611
+ fs.unlinkSync(cameraFilePath);
612
+ } catch (cleanupError) {
613
+ console.warn("Camera temp file cleanup failed:", cleanupError.message);
614
+ }
615
+ }
616
+ this.cameraCaptureFile = null;
617
+ }
618
+
619
+ if (captureAudio) {
620
+ this.audioCaptureActive = false;
621
+ if (audioFilePath && fs.existsSync(audioFilePath)) {
622
+ try {
623
+ fs.unlinkSync(audioFilePath);
624
+ } catch (cleanupError) {
625
+ console.warn("Audio temp file cleanup failed:", cleanupError.message);
626
+ }
627
+ }
628
+ this.audioCaptureFile = null;
629
+ }
630
+
631
+ this.sessionTimestamp = null;
632
+
440
633
  reject(
441
634
  new Error(
442
635
  "Recording failed to start. Check permissions, output path, and system compatibility."
@@ -444,6 +637,7 @@ class MacRecorder extends EventEmitter {
444
637
  );
445
638
  }
446
639
  } catch (error) {
640
+ this.sessionTimestamp = null;
447
641
  reject(error);
448
642
  }
449
643
  });
@@ -470,6 +664,24 @@ class MacRecorder extends EventEmitter {
470
664
  success = true; // Assume success to avoid throwing
471
665
  }
472
666
 
667
+ if (this.cameraCaptureActive) {
668
+ this.cameraCaptureActive = false;
669
+ this.emit("cameraCaptureStopped", {
670
+ outputPath: this.cameraCaptureFile || null,
671
+ success: success === true,
672
+ sessionTimestamp: this.sessionTimestamp,
673
+ });
674
+ }
675
+
676
+ if (this.audioCaptureActive) {
677
+ this.audioCaptureActive = false;
678
+ this.emit("audioCaptureStopped", {
679
+ outputPath: this.audioCaptureFile || null,
680
+ success: success === true,
681
+ sessionTimestamp: this.sessionTimestamp,
682
+ });
683
+ }
684
+
473
685
  // Stop cursor tracking automatically
474
686
  if (this.cursorCaptureInterval) {
475
687
  this.stopCursorCapture().catch(cursorError => {
@@ -486,9 +698,13 @@ class MacRecorder extends EventEmitter {
486
698
  this.isRecording = false;
487
699
  this.recordingDisplayInfo = null;
488
700
 
701
+ const sessionId = this.sessionTimestamp;
489
702
  const result = {
490
703
  code: success ? 0 : 1,
491
704
  outputPath: this.outputPath,
705
+ cameraOutputPath: this.cameraCaptureFile || null,
706
+ audioOutputPath: this.audioCaptureFile || null,
707
+ sessionTimestamp: sessionId,
492
708
  };
493
709
 
494
710
  this.emit("stopped", result);
@@ -502,10 +718,15 @@ class MacRecorder extends EventEmitter {
502
718
  }, 1000);
503
719
  }
504
720
 
721
+ this.sessionTimestamp = null;
505
722
  resolve(result);
506
723
  } catch (error) {
507
724
  this.isRecording = false;
508
725
  this.recordingDisplayInfo = null;
726
+ this.cameraCaptureActive = false;
727
+ this.audioCaptureActive = false;
728
+ this.audioCaptureFile = null;
729
+ this.sessionTimestamp = null;
509
730
  if (this.recordingTimer) {
510
731
  clearInterval(this.recordingTimer);
511
732
  this.recordingTimer = null;
@@ -523,6 +744,11 @@ class MacRecorder extends EventEmitter {
523
744
  return {
524
745
  isRecording: this.isRecording && nativeStatus,
525
746
  outputPath: this.outputPath,
747
+ cameraOutputPath: this.cameraCaptureFile || null,
748
+ audioOutputPath: this.audioCaptureFile || null,
749
+ cameraCapturing: this.cameraCaptureActive,
750
+ audioCapturing: this.audioCaptureActive,
751
+ sessionTimestamp: this.sessionTimestamp,
526
752
  options: this.options,
527
753
  recordingTime: this.recordingStartTime
528
754
  ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
@@ -995,6 +1221,35 @@ class MacRecorder extends EventEmitter {
995
1221
  return this.getCursorPosition();
996
1222
  }
997
1223
 
1224
+ /**
1225
+ * Kamera capture durumunu döndürür
1226
+ */
1227
+ getCameraCaptureStatus() {
1228
+ return {
1229
+ isCapturing: this.cameraCaptureActive === true,
1230
+ outputFile: this.cameraCaptureFile || null,
1231
+ deviceId: this.options.cameraDeviceId || null,
1232
+ sessionTimestamp: this.sessionTimestamp,
1233
+ };
1234
+ }
1235
+
1236
+ /**
1237
+ * Audio capture durumunu döndürür
1238
+ */
1239
+ getAudioCaptureStatus() {
1240
+ return {
1241
+ isCapturing: this.audioCaptureActive === true,
1242
+ outputFile: this.audioCaptureFile || null,
1243
+ deviceIds: {
1244
+ microphone: this.options.audioDeviceId || null,
1245
+ system: this.options.systemAudioDeviceId || null,
1246
+ },
1247
+ includeMicrophone: this.options.includeMicrophone === true,
1248
+ includeSystemAudio: this.options.includeSystemAudio === true,
1249
+ sessionTimestamp: this.sessionTimestamp,
1250
+ };
1251
+ }
1252
+
998
1253
  /**
999
1254
  * Cursor capture durumunu döndürür
1000
1255
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.20.16",
3
+ "version": "2.21.0",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,103 @@
1
+ const MacRecorder = require("../index");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+
5
+ async function main() {
6
+ const recorder = new MacRecorder();
7
+
8
+ // Optional: list audio and camera devices for reference
9
+ const audioDevices = await recorder.getAudioDevices();
10
+ const cameraDevices = await recorder.getCameraDevices();
11
+
12
+ console.log("Audio devices:");
13
+ audioDevices.forEach((device, idx) => {
14
+ console.log(`${idx + 1}. ${device.name} (id: ${device.id})`);
15
+ });
16
+
17
+ console.log("\nCamera devices:");
18
+ cameraDevices.forEach((device, idx) => {
19
+ console.log(`${idx + 1}. ${device.name} (id: ${device.id})`);
20
+ });
21
+
22
+ // Pick the first available devices (customize as needed)
23
+ const preferredCamera = cameraDevices.find(device => !device.requiresContinuityCameraPermission);
24
+ const selectedCameraId = preferredCamera ? preferredCamera.id : null;
25
+ if (!selectedCameraId && cameraDevices.length > 0) {
26
+ console.warn("Skipping camera capture: only Continuity Camera devices detected. Add NSCameraUseContinuityCameraDeviceType to Info.plist or set ALLOW_CONTINUITY_CAMERA=1.");
27
+ }
28
+
29
+ if (selectedCameraId) {
30
+ console.log(`\nSelected camera: ${preferredCamera.name} (id: ${selectedCameraId})`);
31
+ } else {
32
+ console.log("\nSelected camera: none (camera capture disabled)");
33
+ }
34
+ const selectedMicId = audioDevices[0]?.id || null;
35
+
36
+ if (selectedCameraId) {
37
+ recorder.setCameraDevice(selectedCameraId);
38
+ recorder.setCameraEnabled(true);
39
+ }
40
+
41
+ recorder.setAudioSettings({
42
+ microphone: !!selectedMicId,
43
+ systemAudio: true,
44
+ });
45
+
46
+ if (selectedMicId) {
47
+ recorder.setAudioDevice(selectedMicId);
48
+ }
49
+
50
+ const outputDir = path.resolve(__dirname, "../tmp-tests");
51
+ if (!fs.existsSync(outputDir)) {
52
+ fs.mkdirSync(outputDir, { recursive: true });
53
+ }
54
+
55
+ const outputPath = path.join(outputDir, `test_capture_${Date.now()}.mov`);
56
+ console.log("\nStarting recording to:", outputPath);
57
+
58
+ recorder.on("recordingStarted", (payload) => {
59
+ console.log("recordingStarted", payload);
60
+ });
61
+ recorder.on("cameraCaptureStarted", (payload) => {
62
+ console.log("cameraCaptureStarted", payload);
63
+ });
64
+ recorder.on("audioCaptureStarted", (payload) => {
65
+ console.log("audioCaptureStarted", payload);
66
+ });
67
+ recorder.on("cameraCaptureStopped", (payload) => {
68
+ console.log("cameraCaptureStopped", payload);
69
+ });
70
+ recorder.on("audioCaptureStopped", (payload) => {
71
+ console.log("audioCaptureStopped", payload);
72
+ });
73
+ recorder.on("stopped", (payload) => {
74
+ console.log("stopped", payload);
75
+ });
76
+ recorder.on("completed", (filePath) => {
77
+ console.log("completed", filePath);
78
+ });
79
+
80
+ await recorder.startRecording(outputPath, {
81
+ includeMicrophone: !!selectedMicId,
82
+ includeSystemAudio: true,
83
+ captureCursor: true,
84
+ captureCamera: !!selectedCameraId,
85
+ });
86
+
87
+ console.log("Recording for 10 seconds...");
88
+ await new Promise((resolve) => setTimeout(resolve, 10_000));
89
+
90
+ const result = await recorder.stopRecording();
91
+ console.log("\nRecording finished:", result);
92
+
93
+ console.log("\nArtifacts:");
94
+ console.log("Video:", result.outputPath);
95
+ console.log("Camera:", result.cameraOutputPath);
96
+ console.log("Audio:", result.audioOutputPath);
97
+ console.log("Session timestamp:", result.sessionTimestamp);
98
+ }
99
+
100
+ main().catch((error) => {
101
+ console.error("Test capture failed:", error);
102
+ process.exit(1);
103
+ });