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.
- package/AUDIO_CAPTURE.md +84 -0
- package/CAMERA_CAPTURE.md +77 -0
- package/MACOS_PERMISSIONS.md +58 -0
- package/README.md +76 -3
- package/binding.gyp +3 -2
- package/index.js +342 -28
- package/package.json +1 -1
- package/scripts/test-multi-capture.js +103 -0
- package/src/audio_recorder.mm +372 -0
- package/src/camera_recorder.mm +754 -0
- package/src/cursor_tracker.mm +12 -63
- package/src/mac_recorder.mm +367 -72
- package/src/screen_capture_kit.h +2 -2
- package/src/screen_capture_kit.mm +472 -92
- package/src/audio_capture.mm +0 -41
package/src/mac_recorder.mm
CHANGED
|
@@ -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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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 *
|
|
709
|
+
NSString *deviceName = device[@"name"];
|
|
710
|
+
NSString *manufacturer = device[@"manufacturer"];
|
|
529
711
|
NSNumber *isDefault = device[@"isDefault"];
|
|
712
|
+
NSNumber *transportType = device[@"transportType"];
|
|
530
713
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
deviceObj.Set("
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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",
|
|
1258
|
+
NSLog(@" Audio: %s", audioPermissionGranted ? "READY" : "NOT READY");
|
|
967
1259
|
|
|
968
|
-
return Napi::Boolean::New(env, hasScreenPermission &&
|
|
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));
|
package/src/screen_capture_kit.h
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
3
3
|
// NO AVFoundation - Pure ScreenCaptureKit implementation
|
|
4
4
|
|
|
5
|
-
API_AVAILABLE(macos(
|
|
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
|