node-mac-recorder 2.2.1 → 2.4.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.
@@ -1,10 +1,7 @@
1
1
  #import <napi.h>
2
2
  #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
- <<<<<<< HEAD
4
- =======
5
3
  #import <AVFoundation/AVFoundation.h>
6
4
  #import <CoreMedia/CoreMedia.h>
7
- >>>>>>> screencapture
8
5
  #import <AppKit/AppKit.h>
9
6
  #import <Foundation/Foundation.h>
10
7
  #import <CoreGraphics/CoreGraphics.h>
@@ -21,13 +18,9 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
21
18
  // Window selector function declarations
22
19
  Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
23
20
 
24
- <<<<<<< HEAD
25
- @interface MacRecorderDelegate : NSObject
26
- =======
27
21
  // ScreenCaptureKit Recording Delegate
28
22
  API_AVAILABLE(macos(12.3))
29
23
  @interface SCKRecorderDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
30
- >>>>>>> screencapture
31
24
  @property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
32
25
  @property (nonatomic, copy) void (^startedHandler)(void);
33
26
  @property (nonatomic, strong) AVAssetWriter *assetWriter;
@@ -41,20 +34,6 @@ API_AVAILABLE(macos(12.3))
41
34
  @property (nonatomic, assign) BOOL startFailed;
42
35
  @end
43
36
 
44
- <<<<<<< HEAD
45
- @implementation MacRecorderDelegate
46
- - (void)recordingDidStart {
47
- NSLog(@"[mac_recorder] ScreenCaptureKit recording started");
48
- }
49
- - (void)recordingDidFinish:(NSURL *)outputURL error:(NSError *)error {
50
- if (error) {
51
- NSLog(@"[mac_recorder] ScreenCaptureKit recording finished with error: %@", error.localizedDescription);
52
- } else {
53
- NSLog(@"[mac_recorder] ScreenCaptureKit recording finished OK → %@", outputURL.path);
54
- }
55
- if (self.completionHandler) {
56
- self.completionHandler(outputURL, error);
57
- =======
58
37
  @implementation SCKRecorderDelegate
59
38
 
60
39
  // Standard SCStreamDelegate method - should be called automatically
@@ -67,7 +46,6 @@ API_AVAILABLE(macos(12.3))
67
46
  NSLog(@"🛑 Stream stopped with error: %@", error ? error.localizedDescription : @"none");
68
47
  if (self.completionHandler) {
69
48
  self.completionHandler(self.outputURL, error);
70
- >>>>>>> screencapture
71
49
  }
72
50
  }
73
51
 
@@ -142,42 +120,75 @@ API_AVAILABLE(macos(12.3))
142
120
 
143
121
  @end
144
122
 
145
- <<<<<<< HEAD
146
- // Global state for recording
147
- static MacRecorderDelegate *g_delegate = nil;
148
- static bool g_isRecording = false;
149
-
150
- // Helper function to cleanup recording resources
151
- void cleanupRecording() {
152
- g_delegate = nil;
153
- =======
154
123
  // Global state for ScreenCaptureKit recording
155
124
  static SCStream *g_scStream = nil;
156
125
  static SCKRecorderDelegate *g_scDelegate = nil;
157
126
  static bool g_isRecording = false;
127
+ static BOOL g_screenOutputAttached = NO;
128
+ static BOOL g_audioOutputAttached = NO;
129
+ static dispatch_queue_t g_outputQueue = NULL; // use a dedicated serial queue for sample handling
158
130
 
159
131
  // Helper function to cleanup ScreenCaptureKit recording resources
160
132
  void cleanupSCKRecording() {
161
133
  NSLog(@"🛑 Cleaning up ScreenCaptureKit recording");
162
-
134
+
135
+ // Detach outputs first to prevent further callbacks into the delegate
136
+ if (g_scStream && g_scDelegate) {
137
+ NSError *rmError = nil;
138
+ if (g_screenOutputAttached) {
139
+ [g_scStream removeStreamOutput:g_scDelegate type:SCStreamOutputTypeScreen error:&rmError];
140
+ g_screenOutputAttached = NO;
141
+ }
142
+ if (g_audioOutputAttached) {
143
+ rmError = nil;
144
+ [g_scStream removeStreamOutput:g_scDelegate type:SCStreamOutputTypeAudio error:&rmError];
145
+ g_audioOutputAttached = NO;
146
+ }
147
+ }
148
+
163
149
  if (g_scStream) {
164
150
  NSLog(@"🛑 Stopping SCStream");
165
- [g_scStream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
151
+ SCStream *streamToStop = g_scStream; // keep local until stop completes
152
+ [streamToStop stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
166
153
  if (error) {
167
154
  NSLog(@"❌ Error stopping SCStream: %@", error.localizedDescription);
168
155
  } else {
169
156
  NSLog(@"✅ SCStream stopped successfully");
170
157
  }
158
+
159
+ // Finish writer after stream has stopped to ensure no further buffers arrive
160
+ if (g_scDelegate && g_scDelegate.assetWriter && g_scDelegate.isWriting) {
161
+ NSLog(@"🛑 Finishing asset writer (status: %ld)", (long)g_scDelegate.assetWriter.status);
162
+ g_scDelegate.isWriting = NO;
163
+
164
+ if (g_scDelegate.assetWriter.status == AVAssetWriterStatusWriting) {
165
+ if (g_scDelegate.videoInput) {
166
+ [g_scDelegate.videoInput markAsFinished];
167
+ }
168
+ if (g_scDelegate.audioInput) {
169
+ [g_scDelegate.audioInput markAsFinished];
170
+ }
171
+
172
+ [g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{
173
+ NSLog(@"✅ Asset writer finished. Status: %ld", (long)g_scDelegate.assetWriter.status);
174
+ if (g_scDelegate.assetWriter.error) {
175
+ NSLog(@"❌ Asset writer error: %@", g_scDelegate.assetWriter.error.localizedDescription);
176
+ }
177
+ }];
178
+ } else if (g_scDelegate.assetWriter.status == AVAssetWriterStatusFailed) {
179
+ NSLog(@"❌ Asset writer failed: %@", g_scDelegate.assetWriter.error.localizedDescription);
180
+ }
181
+ }
182
+
183
+ g_isRecording = false;
184
+ g_scStream = nil;
185
+ g_scDelegate = nil;
171
186
  }];
172
- g_scStream = nil;
173
- }
174
-
175
- if (g_scDelegate) {
176
- if (g_scDelegate.assetWriter && g_scDelegate.isWriting) {
187
+ } else {
188
+ // No stream, just finalize writer if needed
189
+ if (g_scDelegate && g_scDelegate.assetWriter && g_scDelegate.isWriting) {
177
190
  NSLog(@"🛑 Finishing asset writer (status: %ld)", (long)g_scDelegate.assetWriter.status);
178
191
  g_scDelegate.isWriting = NO;
179
-
180
- // Only mark inputs as finished if asset writer is actually writing
181
192
  if (g_scDelegate.assetWriter.status == AVAssetWriterStatusWriting) {
182
193
  if (g_scDelegate.videoInput) {
183
194
  [g_scDelegate.videoInput markAsFinished];
@@ -185,24 +196,12 @@ void cleanupSCKRecording() {
185
196
  if (g_scDelegate.audioInput) {
186
197
  [g_scDelegate.audioInput markAsFinished];
187
198
  }
188
-
189
- [g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{
190
- NSLog(@"✅ Asset writer finished. Status: %ld", (long)g_scDelegate.assetWriter.status);
191
- if (g_scDelegate.assetWriter.error) {
192
- NSLog(@"❌ Asset writer error: %@", g_scDelegate.assetWriter.error.localizedDescription);
193
- }
194
- }];
195
- } else {
196
- NSLog(@"⚠️ Asset writer not in writing status, cannot finish normally");
197
- if (g_scDelegate.assetWriter.status == AVAssetWriterStatusFailed) {
198
- NSLog(@"❌ Asset writer failed: %@", g_scDelegate.assetWriter.error.localizedDescription);
199
- }
199
+ [g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{}];
200
200
  }
201
201
  }
202
+ g_isRecording = false;
202
203
  g_scDelegate = nil;
203
204
  }
204
- >>>>>>> screencapture
205
- g_isRecording = false;
206
205
  }
207
206
 
208
207
  // Check if ScreenCaptureKit is available
@@ -216,7 +215,7 @@ bool isScreenCaptureKitAvailable() {
216
215
  // NAPI Function: Start Recording with ScreenCaptureKit
217
216
  Napi::Value StartRecording(const Napi::CallbackInfo& info) {
218
217
  Napi::Env env = info.Env();
219
-
218
+ @autoreleasepool {
220
219
  if (!isScreenCaptureKitAvailable()) {
221
220
  NSLog(@"ScreenCaptureKit requires macOS 12.3 or later");
222
221
  return Napi::Boolean::New(env, false);
@@ -246,31 +245,13 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
246
245
  NSLog(@"✅ Screen recording permission verified");
247
246
 
248
247
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
249
- NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
250
248
 
251
- <<<<<<< HEAD
252
- // Options parsing (shared)
253
- CGRect captureRect = CGRectNull;
254
- bool captureCursor = false; // Default olarak cursor gizli
255
- bool includeMicrophone = false; // Default olarak mikrofon kapalı
256
- bool includeSystemAudio = true; // Default olarak sistem sesi açık
257
- CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
258
- NSString *audioDeviceId = nil; // Default audio device ID
259
- NSString *systemAudioDeviceId = nil; // System audio device ID
260
- bool forceUseSC = false;
261
- // Exclude options for ScreenCaptureKit (optional, backward compatible)
262
- NSMutableArray<NSString*> *excludedAppBundleIds = [NSMutableArray array];
263
- NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
264
- NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
265
- bool autoExcludeSelf = false;
266
- =======
267
249
  // Default options
268
250
  bool captureCursor = false;
269
251
  bool includeSystemAudio = true;
270
252
  CGDirectDisplayID displayID = 0; // Will be set to first available display
271
253
  uint32_t windowID = 0;
272
254
  CGRect captureRect = CGRectNull;
273
- >>>>>>> screencapture
274
255
 
275
256
  // Parse options
276
257
  if (info.Length() > 1 && info[1].IsObject()) {
@@ -285,53 +266,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
285
266
  includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
286
267
  }
287
268
 
288
- <<<<<<< HEAD
289
- // System audio device ID
290
- if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
291
- std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
292
- systemAudioDeviceId = [NSString stringWithUTF8String:sysDeviceId.c_str()];
293
- }
294
-
295
- // ScreenCaptureKit toggle (optional)
296
- if (options.Has("useScreenCaptureKit")) {
297
- forceUseSC = options.Get("useScreenCaptureKit").As<Napi::Boolean>();
298
- }
299
-
300
- // Exclusion lists (optional)
301
- if (options.Has("excludedAppBundleIds") && options.Get("excludedAppBundleIds").IsArray()) {
302
- Napi::Array arr = options.Get("excludedAppBundleIds").As<Napi::Array>();
303
- for (uint32_t i = 0; i < arr.Length(); i++) {
304
- if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
305
- std::string s = arr.Get(i).As<Napi::String>().Utf8Value();
306
- [excludedAppBundleIds addObject:[NSString stringWithUTF8String:s.c_str()]];
307
- }
308
- }
309
- }
310
- if (options.Has("excludedPIDs") && options.Get("excludedPIDs").IsArray()) {
311
- Napi::Array arr = options.Get("excludedPIDs").As<Napi::Array>();
312
- for (uint32_t i = 0; i < arr.Length(); i++) {
313
- if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
314
- double v = arr.Get(i).As<Napi::Number>().DoubleValue();
315
- [excludedPIDs addObject:@( (pid_t)v )];
316
- }
317
- }
318
- }
319
- if (options.Has("excludedWindowIds") && options.Get("excludedWindowIds").IsArray()) {
320
- Napi::Array arr = options.Get("excludedWindowIds").As<Napi::Array>();
321
- for (uint32_t i = 0; i < arr.Length(); i++) {
322
- if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
323
- double v = arr.Get(i).As<Napi::Number>().DoubleValue();
324
- [excludedWindowIds addObject:@( (uint32_t)v )];
325
- }
326
- }
327
- }
328
- if (options.Has("autoExcludeSelf")) {
329
- autoExcludeSelf = options.Get("autoExcludeSelf").As<Napi::Boolean>();
330
- }
331
-
332
- // Display ID
333
- =======
334
- >>>>>>> screencapture
335
269
  if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
336
270
  uint32_t tempDisplayID = options.Get("displayId").As<Napi::Number>().Uint32Value();
337
271
  if (tempDisplayID != 0) {
@@ -356,64 +290,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
356
290
  }
357
291
  }
358
292
 
359
- <<<<<<< HEAD
360
- @try {
361
- // Always prefer ScreenCaptureKit if available
362
- NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
363
- if (@available(macOS 12.3, *)) {
364
- if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
365
- NSMutableDictionary *scConfig = [@{} mutableCopy];
366
- scConfig[@"displayId"] = @(displayID);
367
- if (!CGRectIsNull(captureRect)) {
368
- scConfig[@"captureArea"] = @{ @"x": @(captureRect.origin.x),
369
- @"y": @(captureRect.origin.y),
370
- @"width": @(captureRect.size.width),
371
- @"height": @(captureRect.size.height) };
372
- }
373
- scConfig[@"captureCursor"] = @(captureCursor);
374
- scConfig[@"includeMicrophone"] = @(includeMicrophone);
375
- scConfig[@"includeSystemAudio"] = @(includeSystemAudio);
376
- if (excludedAppBundleIds.count) scConfig[@"excludedAppBundleIds"] = excludedAppBundleIds;
377
- if (excludedPIDs.count) scConfig[@"excludedPIDs"] = excludedPIDs;
378
- if (excludedWindowIds.count) scConfig[@"excludedWindowIds"] = excludedWindowIds;
379
- // Auto exclude current app by PID if requested
380
- if (autoExcludeSelf) {
381
- pid_t pid = getpid();
382
- NSMutableArray *arr = [NSMutableArray arrayWithArray:scConfig[@"excludedPIDs"] ?: @[]];
383
- [arr addObject:@(pid)];
384
- scConfig[@"excludedPIDs"] = arr;
385
- }
386
-
387
- // Output path for SC
388
- std::string outputPathStr = info[0].As<Napi::String>().Utf8Value();
389
- scConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPathStr.c_str()];
390
-
391
- NSError *scErr = nil;
392
- NSLog(@"[mac_recorder] Using ScreenCaptureKit path (displayId=%u)", displayID);
393
-
394
- // Create and set up delegate
395
- g_delegate = [[MacRecorderDelegate alloc] init];
396
-
397
- BOOL ok = [ScreenCaptureKitRecorder startRecordingWithConfiguration:scConfig delegate:g_delegate error:&scErr];
398
- if (ok) {
399
- g_isRecording = true;
400
- NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → OK");
401
- return Napi::Boolean::New(env, true);
402
- }
403
- NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → FAIL: %@", scErr.localizedDescription);
404
- cleanupRecording();
405
- return Napi::Boolean::New(env, false);
406
- }
407
- } else {
408
- NSLog(@"[mac_recorder] ScreenCaptureKit not available");
409
- cleanupRecording();
410
- return Napi::Boolean::New(env, false);
411
- }
412
-
413
- } @catch (NSException *exception) {
414
- cleanupRecording();
415
- return Napi::Boolean::New(env, false);
416
- =======
417
293
  // Create output URL
418
294
  NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
419
295
  NSLog(@"📁 Output URL: %@", outputURL.absoluteString);
@@ -543,13 +419,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
543
419
 
544
420
  contentFilter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:excluded];
545
421
  NSLog(@"✅ Content filter created for display recording");
546
- >>>>>>> screencapture
547
422
  }
548
423
 
549
- <<<<<<< HEAD
550
- if (!g_isRecording) {
551
- return Napi::Boolean::New(env, false);
552
- =======
553
424
  // Get actual display dimensions for proper video configuration
554
425
  CGRect displayBounds = CGDisplayBounds(displayID);
555
426
  NSSize videoSize = NSMakeSize(displayBounds.size.width, displayBounds.size.height);
@@ -571,27 +442,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
571
442
  NSLog(@"🔊 Audio configuration: capture=%@, excludeProcess=%@", includeSystemAudio ? @"YES" : @"NO", @"YES");
572
443
  } else {
573
444
  NSLog(@"⚠️ macOS 13.0+ features not available");
574
- >>>>>>> screencapture
575
445
  }
576
446
  config.showsCursor = captureCursor;
577
447
 
578
- <<<<<<< HEAD
579
- @try {
580
- NSLog(@"[mac_recorder] StopRecording called");
581
-
582
- // Stop ScreenCaptureKit recording
583
- NSLog(@"[mac_recorder] Stopping ScreenCaptureKit stream");
584
- if (@available(macOS 12.3, *)) {
585
- [ScreenCaptureKitRecorder stopRecording];
586
- }
587
- g_isRecording = false;
588
- cleanupRecording();
589
- NSLog(@"[mac_recorder] ScreenCaptureKit stopped");
590
- return Napi::Boolean::New(env, true);
591
-
592
- } @catch (NSException *exception) {
593
- cleanupRecording();
594
- =======
595
448
  if (!CGRectIsNull(captureRect)) {
596
449
  config.sourceRect = captureRect;
597
450
  // Update video size if capture rect is specified
@@ -611,7 +464,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
611
464
 
612
465
  if (writerError) {
613
466
  NSLog(@"❌ Failed to create asset writer: %@", writerError.localizedDescription);
614
- >>>>>>> screencapture
615
467
  return Napi::Boolean::New(env, false);
616
468
  }
617
469
 
@@ -635,117 +487,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
635
487
  NSLog(@"❌ Cannot add video input to asset writer");
636
488
  }
637
489
 
638
- <<<<<<< HEAD
639
- @try {
640
- NSMutableArray *devices = [NSMutableArray array];
641
-
642
- // Use CoreAudio to get audio devices since we're removing AVFoundation
643
- AudioObjectPropertyAddress propertyAddress = {
644
- kAudioHardwarePropertyDevices,
645
- kAudioObjectPropertyScopeGlobal,
646
- kAudioObjectPropertyElementMain
647
- };
648
-
649
- UInt32 dataSize = 0;
650
- OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
651
- if (status != noErr) {
652
- return Napi::Array::New(env, 0);
653
- }
654
-
655
- UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
656
- AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
657
-
658
- status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDevices);
659
- if (status != noErr) {
660
- free(audioDevices);
661
- return Napi::Array::New(env, 0);
662
- }
663
-
664
- for (UInt32 i = 0; i < deviceCount; i++) {
665
- AudioDeviceID deviceID = audioDevices[i];
666
-
667
- // Check if device has input streams
668
- AudioObjectPropertyAddress streamsAddress = {
669
- kAudioDevicePropertyStreams,
670
- kAudioDevicePropertyScopeInput,
671
- kAudioObjectPropertyElementMain
672
- };
673
-
674
- UInt32 streamsSize = 0;
675
- status = AudioObjectGetPropertyDataSize(deviceID, &streamsAddress, 0, NULL, &streamsSize);
676
- if (status != noErr || streamsSize == 0) {
677
- continue; // Skip output-only devices
678
- }
679
-
680
- // Get device name
681
- AudioObjectPropertyAddress nameAddress = {
682
- kAudioDevicePropertyDeviceNameCFString,
683
- kAudioObjectPropertyScopeGlobal,
684
- kAudioObjectPropertyElementMain
685
- };
686
-
687
- CFStringRef deviceNameRef = NULL;
688
- UInt32 nameSize = sizeof(CFStringRef);
689
- status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &nameSize, &deviceNameRef);
690
-
691
- NSString *deviceName = @"Unknown Device";
692
- if (status == noErr && deviceNameRef) {
693
- deviceName = (__bridge NSString *)deviceNameRef;
694
- }
695
-
696
- // Get device UID
697
- AudioObjectPropertyAddress uidAddress = {
698
- kAudioDevicePropertyDeviceUID,
699
- kAudioObjectPropertyScopeGlobal,
700
- kAudioObjectPropertyElementMain
701
- };
702
-
703
- CFStringRef deviceUIDRef = NULL;
704
- UInt32 uidSize = sizeof(CFStringRef);
705
- status = AudioObjectGetPropertyData(deviceID, &uidAddress, 0, NULL, &uidSize, &deviceUIDRef);
706
-
707
- NSString *deviceUID = [NSString stringWithFormat:@"%u", deviceID];
708
- if (status == noErr && deviceUIDRef) {
709
- deviceUID = (__bridge NSString *)deviceUIDRef;
710
- }
711
-
712
- // Check if this is the default input device
713
- AudioObjectPropertyAddress defaultAddress = {
714
- kAudioHardwarePropertyDefaultInputDevice,
715
- kAudioObjectPropertyScopeGlobal,
716
- kAudioObjectPropertyElementMain
717
- };
718
-
719
- AudioDeviceID defaultDeviceID = kAudioDeviceUnknown;
720
- UInt32 defaultSize = sizeof(AudioDeviceID);
721
- AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultAddress, 0, NULL, &defaultSize, &defaultDeviceID);
722
-
723
- BOOL isDefault = (deviceID == defaultDeviceID);
724
-
725
- [devices addObject:@{
726
- @"id": deviceUID,
727
- @"name": deviceName,
728
- @"manufacturer": @"Unknown",
729
- @"isDefault": @(isDefault)
730
- }];
731
-
732
- if (deviceNameRef) CFRelease(deviceNameRef);
733
- if (deviceUIDRef) CFRelease(deviceUIDRef);
734
- }
735
-
736
- free(audioDevices);
737
-
738
- // Convert to NAPI array
739
- Napi::Array result = Napi::Array::New(env, devices.count);
740
- for (NSUInteger i = 0; i < devices.count; i++) {
741
- NSDictionary *device = devices[i];
742
- Napi::Object deviceObj = Napi::Object::New(env);
743
- deviceObj.Set("id", Napi::String::New(env, [device[@"id"] UTF8String]));
744
- deviceObj.Set("name", Napi::String::New(env, [device[@"name"] UTF8String]));
745
- deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
746
- deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
747
- result[i] = deviceObj;
748
- =======
749
490
  // Audio input settings (if needed)
750
491
  if (includeSystemAudio) {
751
492
  NSDictionary *audioSettings = @{
@@ -762,19 +503,22 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
762
503
  }
763
504
  }
764
505
 
765
- // Create callback queue for the delegate
766
- dispatch_queue_t delegateQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
767
-
506
+ // Create a dedicated serial queue for output callbacks
507
+ if (g_outputQueue == NULL) {
508
+ g_outputQueue = dispatch_queue_create("com.node-mac-recorder.stream-output", DISPATCH_QUEUE_SERIAL);
509
+ }
510
+
768
511
  // Create and start stream first
769
512
  g_scStream = [[SCStream alloc] initWithFilter:contentFilter configuration:config delegate:g_scDelegate];
770
513
 
771
514
  // Attach outputs to actually receive sample buffers
772
515
  NSLog(@"✅ Setting up stream output callback for sample buffers");
773
- dispatch_queue_t outputQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
516
+ dispatch_queue_t outputQueue = g_outputQueue;
774
517
  NSError *outputError = nil;
775
518
  BOOL addedScreenOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeScreen sampleHandlerQueue:outputQueue error:&outputError];
776
519
  if (addedScreenOutput) {
777
520
  NSLog(@"✅ Screen output attached to SCStream");
521
+ g_screenOutputAttached = YES;
778
522
  } else {
779
523
  NSLog(@"❌ Failed to attach screen output to SCStream: %@", outputError.localizedDescription);
780
524
  }
@@ -783,9 +527,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
783
527
  BOOL addedAudioOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeAudio sampleHandlerQueue:outputQueue error:&outputError];
784
528
  if (addedAudioOutput) {
785
529
  NSLog(@"✅ Audio output attached to SCStream");
530
+ g_audioOutputAttached = YES;
786
531
  } else {
787
532
  NSLog(@"⚠️ Failed to attach audio output to SCStream (audio may be disabled): %@", outputError.localizedDescription);
788
- >>>>>>> screencapture
789
533
  }
790
534
  }
791
535
 
@@ -837,6 +581,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
837
581
 
838
582
  NSLog(@"🎬 Recording initialized successfully");
839
583
  return Napi::Boolean::New(env, true);
584
+ }
840
585
  }
841
586
 
842
587
  // NAPI Function: Stop Recording
@@ -851,6 +596,12 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
851
596
  return Napi::Boolean::New(env, true);
852
597
  }
853
598
 
599
+ // NAPI Function: Get Recording Status (for JS compatibility)
600
+ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
601
+ Napi::Env env = info.Env();
602
+ return Napi::Boolean::New(env, g_isRecording);
603
+ }
604
+
854
605
  // NAPI Function: Get Recording Status
855
606
  Napi::Value IsRecording(const Napi::CallbackInfo& info) {
856
607
  Napi::Env env = info.Env();
@@ -950,19 +701,6 @@ Napi::Value GetWindows(const Napi::CallbackInfo& info) {
950
701
  Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
951
702
  Napi::Env env = info.Env();
952
703
 
953
- <<<<<<< HEAD
954
- @try {
955
- // Check screen recording permission using ScreenCaptureKit
956
- bool hasScreenPermission = true;
957
-
958
- if (@available(macOS 12.3, *)) {
959
- // Try to get shareable content to test ScreenCaptureKit permissions
960
- @try {
961
- SCShareableContent *content = [SCShareableContent currentShareableContent];
962
- hasScreenPermission = (content != nil && content.displays.count > 0);
963
- } @catch (NSException *exception) {
964
- hasScreenPermission = false;
965
- =======
966
704
  // Check screen recording permission
967
705
  bool hasPermission = CGPreflightScreenCaptureAccess();
968
706
 
@@ -1032,41 +770,8 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
1032
770
 
1033
771
  devices.Set(index++, deviceObj);
1034
772
  CFRelease(deviceName);
1035
- >>>>>>> screencapture
1036
- }
1037
- } else {
1038
- // Fallback for older macOS versions
1039
- if (@available(macOS 10.15, *)) {
1040
- // Try to create a display stream to test permissions
1041
- CGDisplayStreamRef stream = CGDisplayStreamCreate(
1042
- CGMainDisplayID(),
1043
- 1, 1,
1044
- kCVPixelFormatType_32BGRA,
1045
- nil,
1046
- ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
1047
- // Empty handler
1048
- }
1049
- );
1050
-
1051
- if (stream) {
1052
- CFRelease(stream);
1053
- hasScreenPermission = true;
1054
- } else {
1055
- hasScreenPermission = false;
1056
- }
1057
773
  }
1058
774
  }
1059
- <<<<<<< HEAD
1060
-
1061
- // For audio permission, we'll use a simpler check since we're using CoreAudio
1062
- bool hasAudioPermission = true;
1063
-
1064
- return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
1065
-
1066
- } @catch (NSException *exception) {
1067
- return Napi::Boolean::New(env, false);
1068
- =======
1069
- >>>>>>> screencapture
1070
775
  }
1071
776
 
1072
777
  free(audioDevices);
@@ -1075,37 +780,14 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
1075
780
 
1076
781
  // Initialize the addon
1077
782
  Napi::Object Init(Napi::Env env, Napi::Object exports) {
1078
- <<<<<<< HEAD
1079
- exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
1080
- exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
1081
-
1082
- exports.Set(Napi::String::New(env, "getAudioDevices"), Napi::Function::New(env, GetAudioDevices));
1083
- exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
1084
- exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
1085
- exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
1086
- exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
1087
- // ScreenCaptureKit availability (optional for clients)
1088
- exports.Set(Napi::String::New(env, "isScreenCaptureKitAvailable"), Napi::Function::New(env, [](const Napi::CallbackInfo& info){
1089
- Napi::Env env = info.Env();
1090
- if (@available(macOS 12.3, *)) {
1091
- bool available = [ScreenCaptureKitRecorder isScreenCaptureKitAvailable];
1092
- return Napi::Boolean::New(env, available);
1093
- }
1094
- return Napi::Boolean::New(env, false);
1095
- }));
1096
-
1097
- // Thumbnail functions
1098
- exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
1099
- exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
1100
- =======
1101
783
  exports.Set("startRecording", Napi::Function::New(env, StartRecording));
1102
784
  exports.Set("stopRecording", Napi::Function::New(env, StopRecording));
1103
785
  exports.Set("isRecording", Napi::Function::New(env, IsRecording));
786
+ exports.Set("getRecordingStatus", Napi::Function::New(env, GetRecordingStatus));
1104
787
  exports.Set("getDisplays", Napi::Function::New(env, GetDisplays));
1105
788
  exports.Set("getWindows", Napi::Function::New(env, GetWindows));
1106
789
  exports.Set("checkPermissions", Napi::Function::New(env, CheckPermissions));
1107
790
  exports.Set("getAudioDevices", Napi::Function::New(env, GetAudioDevices));
1108
- >>>>>>> screencapture
1109
791
 
1110
792
  // Initialize cursor tracker
1111
793
  InitCursorTracker(env, exports);