node-mac-recorder 2.20.17 → 2.21.1

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.
@@ -21,6 +21,21 @@ extern "C" {
21
21
  NSString* audioDeviceId);
22
22
  bool stopAVFoundationRecording();
23
23
  bool isAVFoundationRecording();
24
+
25
+ NSArray<NSDictionary *> *listCameraDevices();
26
+ bool startCameraRecording(NSString *outputPath, NSString *deviceId, NSError **error);
27
+ bool stopCameraRecording();
28
+ bool isCameraRecording();
29
+ NSString *currentCameraRecordingPath();
30
+ NSString *currentStandaloneAudioRecordingPath();
31
+
32
+ NSArray<NSDictionary *> *listAudioCaptureDevices();
33
+ bool startStandaloneAudioRecording(NSString *outputPath, NSString *preferredDeviceId, NSError **error);
34
+ bool stopStandaloneAudioRecording();
35
+ bool isStandaloneAudioRecording();
36
+ bool hasAudioPermission();
37
+
38
+ NSString *ScreenCaptureKitCurrentAudioPath(void);
24
39
  }
25
40
 
26
41
  // Cursor tracker function declarations
@@ -42,6 +57,80 @@ extern "C" void showOverlays();
42
57
  // Global state for recording (ScreenCaptureKit only)
43
58
  static MacRecorderDelegate *g_delegate = nil;
44
59
  static bool g_isRecording = false;
60
+ static bool g_usingStandaloneAudio = false;
61
+
62
+ static bool startCameraIfRequested(bool captureCamera,
63
+ NSString **cameraOutputPathRef,
64
+ NSString *cameraDeviceId,
65
+ const std::string &screenOutputPath,
66
+ int64_t sessionTimestampMs) {
67
+ if (!captureCamera) {
68
+ return true;
69
+ }
70
+
71
+ NSString *resolvedOutputPath = cameraOutputPathRef ? *cameraOutputPathRef : nil;
72
+ if (!resolvedOutputPath || [resolvedOutputPath length] == 0) {
73
+ NSString *screenPath = [NSString stringWithUTF8String:screenOutputPath.c_str()];
74
+ NSString *directory = nil;
75
+ if (screenPath && [screenPath length] > 0) {
76
+ directory = [screenPath stringByDeletingLastPathComponent];
77
+ }
78
+ if (!directory || [directory length] == 0) {
79
+ directory = [[NSFileManager defaultManager] currentDirectoryPath];
80
+ }
81
+ int64_t timestampValue = sessionTimestampMs > 0 ? sessionTimestampMs
82
+ : (int64_t)([[NSDate date] timeIntervalSince1970] * 1000.0);
83
+ NSString *fileName = [NSString stringWithFormat:@"temp_camera_%lld.webm", timestampValue];
84
+ resolvedOutputPath = [directory stringByAppendingPathComponent:fileName];
85
+ MRLog(@"📁 Auto-generated camera path: %@", resolvedOutputPath);
86
+ }
87
+
88
+ NSError *cameraError = nil;
89
+ bool cameraStarted = startCameraRecording(resolvedOutputPath, cameraDeviceId, &cameraError);
90
+ if (!cameraStarted) {
91
+ if (cameraError) {
92
+ NSLog(@"❌ Failed to start camera recording: %@", cameraError.localizedDescription);
93
+ } else {
94
+ NSLog(@"❌ Failed to start camera recording: Unknown error");
95
+ }
96
+ return false;
97
+ }
98
+ NSString *actualPath = currentCameraRecordingPath();
99
+ if (actualPath && [actualPath length] > 0) {
100
+ resolvedOutputPath = actualPath;
101
+ if (cameraOutputPathRef) {
102
+ *cameraOutputPathRef = actualPath;
103
+ }
104
+ }
105
+
106
+ MRLog(@"🎥 Camera recording started (output: %@)", resolvedOutputPath);
107
+ return true;
108
+ }
109
+
110
+ static bool startAudioIfRequested(bool captureAudio,
111
+ NSString *audioOutputPath,
112
+ NSString *preferredDeviceId) {
113
+ if (!captureAudio) {
114
+ return true;
115
+ }
116
+ if (!audioOutputPath || [audioOutputPath length] == 0) {
117
+ NSLog(@"❌ Audio recording requested but no output path provided");
118
+ return false;
119
+ }
120
+ NSError *audioError = nil;
121
+ bool started = startStandaloneAudioRecording(audioOutputPath, preferredDeviceId, &audioError);
122
+ if (!started) {
123
+ if (audioError) {
124
+ NSLog(@"❌ Failed to start audio recording: %@", audioError.localizedDescription);
125
+ } else {
126
+ NSLog(@"❌ Failed to start audio recording (unknown error)");
127
+ }
128
+ return false;
129
+ }
130
+ g_usingStandaloneAudio = true;
131
+ MRLog(@"🎙️ Standalone audio recording started (output: %@)", audioOutputPath);
132
+ return true;
133
+ }
45
134
 
46
135
  // Helper function to cleanup recording resources
47
136
  void cleanupRecording() {
@@ -56,6 +145,14 @@ void cleanupRecording() {
56
145
  if (isAVFoundationRecording()) {
57
146
  stopAVFoundationRecording();
58
147
  }
148
+
149
+ if (isCameraRecording()) {
150
+ stopCameraRecording();
151
+ }
152
+ if (isStandaloneAudioRecording()) {
153
+ stopStandaloneAudioRecording();
154
+ }
155
+ g_usingStandaloneAudio = false;
59
156
 
60
157
  g_isRecording = false;
61
158
  }
@@ -78,6 +175,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
78
175
  MRLog(@"⚠️ Still recording after cleanup - forcing stop");
79
176
  return Napi::Boolean::New(env, false);
80
177
  }
178
+ g_usingStandaloneAudio = false;
81
179
 
82
180
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
83
181
 
@@ -90,6 +188,11 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
90
188
  uint32_t windowID = 0; // Default no window selection
91
189
  NSString *audioDeviceId = nil; // Default audio device ID
92
190
  NSString *systemAudioDeviceId = nil; // System audio device ID
191
+ bool captureCamera = false;
192
+ NSString *cameraDeviceId = nil;
193
+ NSString *cameraOutputPath = nil;
194
+ int64_t sessionTimestamp = 0;
195
+ NSString *audioOutputPath = nil;
93
196
 
94
197
  if (info.Length() > 1 && info[1].IsObject()) {
95
198
  Napi::Object options = info[1].As<Napi::Object>();
@@ -133,6 +236,30 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
133
236
  std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
134
237
  systemAudioDeviceId = [NSString stringWithUTF8String:sysDeviceId.c_str()];
135
238
  }
239
+
240
+ // Camera capture options
241
+ if (options.Has("captureCamera")) {
242
+ captureCamera = options.Get("captureCamera").As<Napi::Boolean>();
243
+ }
244
+
245
+ if (options.Has("cameraDeviceId") && !options.Get("cameraDeviceId").IsNull()) {
246
+ std::string cameraDevice = options.Get("cameraDeviceId").As<Napi::String>().Utf8Value();
247
+ cameraDeviceId = [NSString stringWithUTF8String:cameraDevice.c_str()];
248
+ }
249
+
250
+ if (options.Has("cameraOutputPath") && !options.Get("cameraOutputPath").IsNull()) {
251
+ std::string cameraPath = options.Get("cameraOutputPath").As<Napi::String>().Utf8Value();
252
+ cameraOutputPath = [NSString stringWithUTF8String:cameraPath.c_str()];
253
+ }
254
+
255
+ if (options.Has("audioOutputPath") && !options.Get("audioOutputPath").IsNull()) {
256
+ std::string audioPath = options.Get("audioOutputPath").As<Napi::String>().Utf8Value();
257
+ audioOutputPath = [NSString stringWithUTF8String:audioPath.c_str()];
258
+ }
259
+
260
+ if (options.Has("sessionTimestamp") && options.Get("sessionTimestamp").IsNumber()) {
261
+ sessionTimestamp = options.Get("sessionTimestamp").As<Napi::Number>().Int64Value();
262
+ }
136
263
 
137
264
  // Display ID
138
265
  if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
@@ -172,6 +299,21 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
172
299
  MRLog(@"🪟 Window ID specified: %u", windowID);
173
300
  }
174
301
  }
302
+
303
+ if (captureCamera && sessionTimestamp == 0) {
304
+ // Allow native side to auto-generate timestamp if not provided
305
+ sessionTimestamp = (int64_t)([[NSDate date] timeIntervalSince1970] * 1000.0);
306
+ }
307
+
308
+ bool captureMicrophone = includeMicrophone;
309
+ bool captureSystemAudio = includeSystemAudio;
310
+ bool captureAnyAudio = captureMicrophone || captureSystemAudio;
311
+ NSString *preferredAudioDeviceId = nil;
312
+ if (captureSystemAudio && systemAudioDeviceId && [systemAudioDeviceId length] > 0) {
313
+ preferredAudioDeviceId = systemAudioDeviceId;
314
+ } else if (captureMicrophone && audioDeviceId && [audioDeviceId length] > 0) {
315
+ preferredAudioDeviceId = audioDeviceId;
316
+ }
175
317
 
176
318
  @try {
177
319
  // Smart Recording Selection: ScreenCaptureKit vs Alternative
@@ -218,6 +360,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
218
360
 
219
361
  // Try ScreenCaptureKit with extensive safety measures
220
362
  @try {
363
+ if (@available(macOS 12.3, *)) {
221
364
  if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
222
365
  MRLog(@"✅ ScreenCaptureKit availability check passed");
223
366
  MRLog(@"🎯 Using ScreenCaptureKit - overlay windows will be automatically excluded");
@@ -231,6 +374,15 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
231
374
  sckConfig[@"includeMicrophone"] = @(includeMicrophone);
232
375
  sckConfig[@"audioDeviceId"] = audioDeviceId;
233
376
  sckConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPath.c_str()];
377
+ if (audioOutputPath) {
378
+ sckConfig[@"audioOutputPath"] = audioOutputPath;
379
+ }
380
+ if (audioDeviceId) {
381
+ sckConfig[@"microphoneDeviceId"] = audioDeviceId;
382
+ }
383
+ if (sessionTimestamp != 0) {
384
+ sckConfig[@"sessionTimestamp"] = @(sessionTimestamp);
385
+ }
234
386
 
235
387
  if (!CGRectIsNull(captureRect)) {
236
388
  sckConfig[@"captureRect"] = @{
@@ -245,17 +397,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
245
397
  NSError *sckError = nil;
246
398
 
247
399
  // Set timeout for ScreenCaptureKit initialization
248
- __block BOOL sckStarted = NO;
249
- __block BOOL sckTimedOut = NO;
250
-
251
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)),
252
- dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
253
- if (!sckStarted && !g_isRecording) {
254
- sckTimedOut = YES;
255
- MRLog(@"⏰ ScreenCaptureKit initialization timeout (3s)");
256
- }
257
- });
258
-
259
400
  // Attempt to start ScreenCaptureKit with safety wrapper
260
401
  @try {
261
402
  if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
@@ -263,9 +404,16 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
263
404
  error:&sckError]) {
264
405
 
265
406
  // ScreenCaptureKit başlatma başarılı - validation yapmıyoruz
266
- sckStarted = YES;
267
407
  MRLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
268
408
  MRLog(@"✅ ScreenCaptureKit recording started successfully");
409
+
410
+ if (!startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
411
+ MRLog(@"❌ Camera start failed - stopping ScreenCaptureKit session");
412
+ [ScreenCaptureKitRecorder stopRecording];
413
+ g_isRecording = false;
414
+ return Napi::Boolean::New(env, false);
415
+ }
416
+
269
417
  g_isRecording = true;
270
418
  return Napi::Boolean::New(env, true);
271
419
  } else {
@@ -280,6 +428,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
280
428
  } else {
281
429
  NSLog(@"❌ ScreenCaptureKit availability check failed - will fallback to AVFoundation");
282
430
  }
431
+ } else {
432
+ NSLog(@"❌ ScreenCaptureKit not available on this macOS version - falling back to AVFoundation");
433
+ }
283
434
  } @catch (NSException *availabilityException) {
284
435
  NSLog(@"❌ Exception during ScreenCaptureKit availability check - will fallback to AVFoundation: %@", availabilityException.reason);
285
436
  }
@@ -333,6 +484,24 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
333
484
  if (avResult) {
334
485
  MRLog(@"🎥 RECORDING METHOD: AVFoundation");
335
486
  MRLog(@"✅ AVFoundation recording started successfully");
487
+
488
+ if (!startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
489
+ MRLog(@"❌ Camera start failed - stopping AVFoundation session");
490
+ stopAVFoundationRecording();
491
+ g_isRecording = false;
492
+ return Napi::Boolean::New(env, false);
493
+ }
494
+
495
+ if (!startAudioIfRequested(captureAnyAudio, audioOutputPath, preferredAudioDeviceId)) {
496
+ MRLog(@"❌ Audio start failed - stopping AVFoundation session");
497
+ if (captureCamera) {
498
+ stopCameraRecording();
499
+ }
500
+ stopAVFoundationRecording();
501
+ g_isRecording = false;
502
+ return Napi::Boolean::New(env, false);
503
+ }
504
+
336
505
  g_isRecording = true;
337
506
  return Napi::Boolean::New(env, true);
338
507
  } else {
@@ -365,7 +534,11 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
365
534
  if ([ScreenCaptureKitRecorder isRecording]) {
366
535
  MRLog(@"🛑 Stopping ScreenCaptureKit recording");
367
536
  [ScreenCaptureKitRecorder stopRecording];
537
+ if (isCameraRecording()) {
538
+ stopCameraRecording();
539
+ }
368
540
  g_isRecording = false;
541
+ g_usingStandaloneAudio = false;
369
542
  return Napi::Boolean::New(env, true);
370
543
  }
371
544
  }
@@ -378,11 +551,25 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
378
551
  if (isAVFoundationRecording()) {
379
552
  MRLog(@"🛑 Stopping AVFoundation recording");
380
553
  if (stopAVFoundationRecording()) {
554
+ if (g_usingStandaloneAudio && isStandaloneAudioRecording()) {
555
+ stopStandaloneAudioRecording();
556
+ }
557
+ if (isCameraRecording()) {
558
+ stopCameraRecording();
559
+ }
381
560
  g_isRecording = false;
561
+ g_usingStandaloneAudio = false;
382
562
  return Napi::Boolean::New(env, true);
383
563
  } else {
384
564
  NSLog(@"❌ Failed to stop AVFoundation recording");
565
+ if (g_usingStandaloneAudio && isStandaloneAudioRecording()) {
566
+ stopStandaloneAudioRecording();
567
+ }
568
+ if (isCameraRecording()) {
569
+ stopCameraRecording();
570
+ }
385
571
  g_isRecording = false;
572
+ g_usingStandaloneAudio = false;
386
573
  return Napi::Boolean::New(env, false);
387
574
  }
388
575
  }
@@ -393,6 +580,9 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
393
580
  }
394
581
 
395
582
  MRLog(@"⚠️ No active recording found to stop");
583
+ if (isCameraRecording()) {
584
+ stopCameraRecording();
585
+ }
396
586
  g_isRecording = false;
397
587
  return Napi::Boolean::New(env, true);
398
588
  }
@@ -503,65 +693,182 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
503
693
  Napi::Env env = info.Env();
504
694
 
505
695
  @try {
506
- NSMutableArray *devices = [NSMutableArray array];
507
-
508
- // Get all audio devices
509
- // Audio device enumeration removed - ScreenCaptureKit handles audio internally
510
- NSLog(@"🎵 Audio device enumeration disabled - using ScreenCaptureKit internal audio");
511
-
512
- // Add default system audio entry
513
- [devices addObject:@{
514
- @"id": @"default",
515
- @"name": @"Default Audio Device",
516
- @"isDefault": @YES
517
- }];
696
+ NSArray<NSDictionary *> *devices = listAudioCaptureDevices();
697
+ if (!devices) {
698
+ return Napi::Array::New(env, 0);
699
+ }
518
700
 
519
- // Convert to NAPI array
520
701
  Napi::Array result = Napi::Array::New(env, devices.count);
521
702
  for (NSUInteger i = 0; i < devices.count; i++) {
522
703
  NSDictionary *device = devices[i];
704
+ if (![device isKindOfClass:[NSDictionary class]]) {
705
+ continue;
706
+ }
523
707
  Napi::Object deviceObj = Napi::Object::New(env);
524
-
525
- // Safe string conversion with null checks
526
708
  NSString *deviceId = device[@"id"];
527
- NSString *deviceName = device[@"name"];
528
- NSString *deviceManufacturer = device[@"manufacturer"];
709
+ NSString *deviceName = device[@"name"];
710
+ NSString *manufacturer = device[@"manufacturer"];
529
711
  NSNumber *isDefault = device[@"isDefault"];
712
+ NSNumber *transportType = device[@"transportType"];
530
713
 
531
- if (deviceId && [deviceId isKindOfClass:[NSString class]]) {
532
- deviceObj.Set("id", Napi::String::New(env, [deviceId UTF8String]));
533
- } else {
534
- deviceObj.Set("id", Napi::String::New(env, "default"));
714
+ deviceObj.Set("id", Napi::String::New(env, deviceId ? [deviceId UTF8String] : ""));
715
+ deviceObj.Set("name", Napi::String::New(env, deviceName ? [deviceName UTF8String] : "Unknown Audio Device"));
716
+ if (manufacturer && [manufacturer isKindOfClass:[NSString class]]) {
717
+ deviceObj.Set("manufacturer", Napi::String::New(env, [manufacturer UTF8String]));
535
718
  }
536
-
537
- if (deviceName && [deviceName isKindOfClass:[NSString class]]) {
538
- deviceObj.Set("name", Napi::String::New(env, [deviceName UTF8String]));
539
- } else {
540
- deviceObj.Set("name", Napi::String::New(env, "Default Audio Device"));
541
- }
542
-
543
- if (deviceManufacturer && [deviceManufacturer isKindOfClass:[NSString class]]) {
544
- deviceObj.Set("manufacturer", Napi::String::New(env, [deviceManufacturer UTF8String]));
545
- } else {
546
- deviceObj.Set("manufacturer", Napi::String::New(env, "System"));
547
- }
548
-
549
719
  if (isDefault && [isDefault isKindOfClass:[NSNumber class]]) {
550
720
  deviceObj.Set("isDefault", Napi::Boolean::New(env, [isDefault boolValue]));
551
- } else {
552
- deviceObj.Set("isDefault", Napi::Boolean::New(env, true));
553
721
  }
554
-
722
+ if (transportType && [transportType isKindOfClass:[NSNumber class]]) {
723
+ deviceObj.Set("transportType", Napi::Number::New(env, [transportType integerValue]));
724
+ }
555
725
  result[i] = deviceObj;
556
726
  }
557
-
558
727
  return result;
559
-
560
728
  } @catch (NSException *exception) {
561
729
  return Napi::Array::New(env, 0);
562
730
  }
563
731
  }
564
732
 
733
+ Napi::Value GetCameraDevices(const Napi::CallbackInfo& info) {
734
+ Napi::Env env = info.Env();
735
+ Napi::Array result = Napi::Array::New(env);
736
+
737
+ @try {
738
+ @autoreleasepool {
739
+ NSArray<NSDictionary *> *devices = listCameraDevices();
740
+ if (!devices) {
741
+ return result;
742
+ }
743
+
744
+ NSUInteger index = 0;
745
+ for (id entry in devices) {
746
+ if (![entry isKindOfClass:[NSDictionary class]]) {
747
+ continue;
748
+ }
749
+
750
+ NSDictionary *camera = (NSDictionary *)entry;
751
+ Napi::Object cameraObj = Napi::Object::New(env);
752
+
753
+ NSString *identifier = camera[@"id"];
754
+ NSString *name = camera[@"name"];
755
+ NSString *model = camera[@"model"];
756
+ NSString *manufacturer = camera[@"manufacturer"];
757
+ NSString *position = camera[@"position"];
758
+ NSNumber *transportType = camera[@"transportType"];
759
+ NSNumber *isConnected = camera[@"isConnected"];
760
+ NSNumber *hasFlash = camera[@"hasFlash"];
761
+ NSNumber *supportsDepth = camera[@"supportsDepth"];
762
+
763
+ if (identifier && [identifier isKindOfClass:[NSString class]]) {
764
+ cameraObj.Set("id", Napi::String::New(env, [identifier UTF8String]));
765
+ } else {
766
+ cameraObj.Set("id", Napi::String::New(env, ""));
767
+ }
768
+
769
+ if (name && [name isKindOfClass:[NSString class]]) {
770
+ cameraObj.Set("name", Napi::String::New(env, [name UTF8String]));
771
+ } else {
772
+ cameraObj.Set("name", Napi::String::New(env, "Unknown Camera"));
773
+ }
774
+
775
+ if (model && [model isKindOfClass:[NSString class]]) {
776
+ cameraObj.Set("model", Napi::String::New(env, [model UTF8String]));
777
+ }
778
+
779
+ if (manufacturer && [manufacturer isKindOfClass:[NSString class]]) {
780
+ cameraObj.Set("manufacturer", Napi::String::New(env, [manufacturer UTF8String]));
781
+ }
782
+
783
+ if (position && [position isKindOfClass:[NSString class]]) {
784
+ cameraObj.Set("position", Napi::String::New(env, [position UTF8String]));
785
+ }
786
+
787
+ if (transportType && [transportType isKindOfClass:[NSNumber class]]) {
788
+ cameraObj.Set("transportType", Napi::Number::New(env, [transportType integerValue]));
789
+ }
790
+
791
+ if (isConnected && [isConnected isKindOfClass:[NSNumber class]]) {
792
+ cameraObj.Set("isConnected", Napi::Boolean::New(env, [isConnected boolValue]));
793
+ }
794
+
795
+ if (hasFlash && [hasFlash isKindOfClass:[NSNumber class]]) {
796
+ cameraObj.Set("hasFlash", Napi::Boolean::New(env, [hasFlash boolValue]));
797
+ }
798
+
799
+ if (supportsDepth && [supportsDepth isKindOfClass:[NSNumber class]]) {
800
+ cameraObj.Set("supportsDepth", Napi::Boolean::New(env, [supportsDepth boolValue]));
801
+ }
802
+
803
+ NSDictionary *maxResolution = camera[@"maxResolution"];
804
+ if (maxResolution && [maxResolution isKindOfClass:[NSDictionary class]]) {
805
+ Napi::Object maxResObj = Napi::Object::New(env);
806
+
807
+ NSNumber *width = maxResolution[@"width"];
808
+ NSNumber *height = maxResolution[@"height"];
809
+ NSNumber *frameRate = maxResolution[@"maxFrameRate"];
810
+
811
+ if (width && [width isKindOfClass:[NSNumber class]]) {
812
+ maxResObj.Set("width", Napi::Number::New(env, [width integerValue]));
813
+ }
814
+ if (height && [height isKindOfClass:[NSNumber class]]) {
815
+ maxResObj.Set("height", Napi::Number::New(env, [height integerValue]));
816
+ }
817
+ if (frameRate && [frameRate isKindOfClass:[NSNumber class]]) {
818
+ maxResObj.Set("maxFrameRate", Napi::Number::New(env, [frameRate doubleValue]));
819
+ }
820
+
821
+ cameraObj.Set("maxResolution", maxResObj);
822
+ }
823
+
824
+ result[index++] = cameraObj;
825
+ }
826
+ }
827
+
828
+ return result;
829
+ } @catch (NSException *exception) {
830
+ NSLog(@"❌ Exception while listing camera devices: %@", exception.reason);
831
+ return result;
832
+ }
833
+ }
834
+
835
+ Napi::Value GetCameraRecordingPath(const Napi::CallbackInfo& info) {
836
+ Napi::Env env = info.Env();
837
+ @try {
838
+ NSString *path = currentCameraRecordingPath();
839
+ if (!path || [path length] == 0) {
840
+ return env.Null();
841
+ }
842
+ return Napi::String::New(env, [path UTF8String]);
843
+ } @catch (NSException *exception) {
844
+ NSLog(@"❌ Exception while reading camera output path: %@", exception.reason);
845
+ return env.Null();
846
+ }
847
+ }
848
+
849
+ Napi::Value GetAudioRecordingPath(const Napi::CallbackInfo& info) {
850
+ Napi::Env env = info.Env();
851
+ @try {
852
+ NSString *path = nil;
853
+ if (@available(macOS 12.3, *)) {
854
+ path = ScreenCaptureKitCurrentAudioPath();
855
+ }
856
+ if ([path isKindOfClass:[NSArray class]]) {
857
+ path = [(NSArray *)path firstObject];
858
+ }
859
+ if (!path || [path length] == 0) {
860
+ path = currentStandaloneAudioRecordingPath();
861
+ }
862
+ if (!path || [path length] == 0) {
863
+ return env.Null();
864
+ }
865
+ return Napi::String::New(env, [path UTF8String]);
866
+ } @catch (NSException *exception) {
867
+ NSLog(@"❌ Exception while reading audio output path: %@", exception.reason);
868
+ return env.Null();
869
+ }
870
+ }
871
+
565
872
  // NAPI Function: Get Displays
566
873
  Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
567
874
  Napi::Env env = info.Env();
@@ -892,12 +1199,6 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
892
1199
  BOOL forceAVFoundation = (getenv("FORCE_AVFOUNDATION") != NULL);
893
1200
 
894
1201
  // Electron detection
895
- BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
896
- [NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
897
- (NSProcessInfo.processInfo.processName &&
898
- [NSProcessInfo.processInfo.processName containsString:@"Electron"]) ||
899
- (NSProcessInfo.processInfo.environment[@"ELECTRON_RUN_AS_NODE"] != nil);
900
-
901
1202
  NSLog(@"🔒 Permission check for macOS %ld.%ld.%ld",
902
1203
  (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
903
1204
 
@@ -945,27 +1246,18 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
945
1246
  NSLog(@"🔒 Screen recording: No permission system (macOS < 10.15)");
946
1247
  }
947
1248
 
948
- // Check audio permission based on framework
949
- bool hasAudioPermission = true;
950
-
951
- if (willUseScreenCaptureKit) {
952
- // ScreenCaptureKit handles audio permissions internally
953
- hasAudioPermission = true;
954
- NSLog(@"🔒 Audio permission: Handled internally by ScreenCaptureKit");
955
-
956
- } else if (willUseAVFoundation) {
957
- // For AVFoundation, we don't enforce audio permissions
958
- // Recording can continue without audio if needed
959
- hasAudioPermission = true;
960
- NSLog(@"🔒 Audio permission: Will be requested during recording by AVFoundation");
1249
+ bool audioPermissionGranted = hasAudioPermission();
1250
+ NSLog(@"🔒 Audio permission: %s", audioPermissionGranted ? "GRANTED" : "DENIED");
1251
+ if (!audioPermissionGranted) {
1252
+ NSLog(@"📝 Grant microphone access in System Settings > Privacy & Security > Microphone");
961
1253
  }
962
1254
 
963
1255
  NSLog(@"🔒 Final permission status:");
964
1256
  NSLog(@" Framework: %s", willUseScreenCaptureKit ? "ScreenCaptureKit" : "AVFoundation");
965
1257
  NSLog(@" Screen Recording: %s", hasScreenPermission ? "GRANTED" : "DENIED");
966
- NSLog(@" Audio: %s", hasAudioPermission ? "READY" : "NOT READY");
1258
+ NSLog(@" Audio: %s", audioPermissionGranted ? "READY" : "NOT READY");
967
1259
 
968
- return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
1260
+ return Napi::Boolean::New(env, hasScreenPermission && audioPermissionGranted);
969
1261
 
970
1262
  } @catch (NSException *exception) {
971
1263
  NSLog(@"❌ Exception in permission check: %@", exception.reason);
@@ -979,6 +1271,9 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
979
1271
  exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
980
1272
 
981
1273
  exports.Set(Napi::String::New(env, "getAudioDevices"), Napi::Function::New(env, GetAudioDevices));
1274
+ exports.Set(Napi::String::New(env, "getCameraDevices"), Napi::Function::New(env, GetCameraDevices));
1275
+ exports.Set(Napi::String::New(env, "getCameraRecordingPath"), Napi::Function::New(env, GetCameraRecordingPath));
1276
+ exports.Set(Napi::String::New(env, "getAudioRecordingPath"), Napi::Function::New(env, GetAudioRecordingPath));
982
1277
  exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
983
1278
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
984
1279
  exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
@@ -2,7 +2,7 @@
2
2
  #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
3
  // NO AVFoundation - Pure ScreenCaptureKit implementation
4
4
 
5
- API_AVAILABLE(macos(15.0))
5
+ API_AVAILABLE(macos(12.3))
6
6
  @interface ScreenCaptureKitRecorder : NSObject
7
7
 
8
8
  + (BOOL)isScreenCaptureKitAvailable;
@@ -16,4 +16,4 @@ API_AVAILABLE(macos(15.0))
16
16
  + (void)finalizeVideoWriter;
17
17
  + (void)cleanupVideoWriter;
18
18
 
19
- @end
19
+ @end