node-mac-recorder 2.21.21 → 2.21.22

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
@@ -103,6 +103,7 @@ class MacRecorder extends EventEmitter {
103
103
  position: device?.position ?? "unspecified",
104
104
  transportType: device?.transportType ?? null,
105
105
  isConnected: device?.isConnected ?? false,
106
+ isDefault: device?.isDefault === true,
106
107
  hasFlash: device?.hasFlash ?? false,
107
108
  supportsDepth: device?.supportsDepth ?? false,
108
109
  deviceType: device?.deviceType ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.21",
3
+ "version": "2.21.22",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -224,10 +224,30 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
224
224
  return YES;
225
225
  }
226
226
 
227
- [self.session stopRunning];
227
+ // CRITICAL FIX: For external devices (especially Continuity Microphone),
228
+ // stopRunning can hang if device is disconnected. Use async approach.
229
+ MRLog(@"🛑 AudioRecorder: Stopping session (external device safe)...");
230
+
231
+ // Stop session on background thread to avoid blocking
232
+ AVCaptureSession *sessionToStop = self.session;
233
+ AVCaptureAudioDataOutput *outputToStop = self.audioOutput;
234
+
235
+ // Clear references FIRST to prevent new samples
228
236
  self.session = nil;
229
237
  self.audioOutput = nil;
230
238
 
239
+ // Stop session asynchronously with timeout protection
240
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
241
+ @autoreleasepool {
242
+ if ([sessionToStop isRunning]) {
243
+ MRLog(@"🛑 Stopping AVCaptureSession...");
244
+ [sessionToStop stopRunning];
245
+ MRLog(@"✅ AVCaptureSession stopped");
246
+ }
247
+ // Release happens automatically when block completes
248
+ }
249
+ });
250
+
231
251
  // CRITICAL FIX: Check if writer exists before trying to finish it
232
252
  if (self.writer) {
233
253
  // Only mark as finished if writerInput exists
@@ -243,16 +263,16 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
243
263
  dispatch_semaphore_signal(semaphore);
244
264
  }];
245
265
 
246
- // Reduced timeout to 2 seconds for faster response
247
- dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
248
- dispatch_semaphore_wait(semaphore, timeout);
266
+ // Reduced timeout to 1 second for external devices
267
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
268
+ long result = dispatch_semaphore_wait(semaphore, timeout);
249
269
 
250
- if (!finished) {
251
- MRLog(@"⚠️ AudioRecorder: Timed out waiting for writer to finish");
270
+ if (result != 0 || !finished) {
271
+ MRLog(@"⚠️ AudioRecorder: Timed out waiting for writer (external device?) - forcing cancel");
252
272
  // Force cancel if timeout
253
273
  [self.writer cancelWriting];
254
274
  } else {
255
- MRLog(@"✅ AudioRecorder stopped successfully");
275
+ MRLog(@"✅ AudioRecorder writer finished successfully");
256
276
  }
257
277
  } else {
258
278
  MRLog(@"⚠️ AudioRecorder: No writer to finish (no audio captured)");
@@ -264,6 +284,7 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
264
284
  self.startTime = kCMTimeInvalid;
265
285
  self.outputPath = nil;
266
286
 
287
+ MRLog(@"✅ AudioRecorder stopped (safe for external devices)");
267
288
  return YES;
268
289
  }
269
290
 
@@ -334,11 +355,37 @@ NSArray<NSDictionary *> *listAudioCaptureDevices() {
334
355
  position:AVCaptureDevicePositionUnspecified];
335
356
 
336
357
  for (AVCaptureDevice *device in session.devices) {
358
+ // PRIORITY FIX: MacBook built-in devices should be default, not external devices
359
+ // Check if this is a built-in device (MacBook's own microphone)
360
+ NSString *deviceName = device.localizedName ?: @"";
361
+ BOOL isBuiltIn = NO;
362
+
363
+ // Built-in detection: Check for "MacBook", "iMac", "Mac Studio", "Mac mini", "Mac Pro" in name
364
+ if ([deviceName rangeOfString:@"MacBook" options:NSCaseInsensitiveSearch].location != NSNotFound ||
365
+ [deviceName rangeOfString:@"iMac" options:NSCaseInsensitiveSearch].location != NSNotFound ||
366
+ [deviceName rangeOfString:@"Mac Studio" options:NSCaseInsensitiveSearch].location != NSNotFound ||
367
+ [deviceName rangeOfString:@"Mac mini" options:NSCaseInsensitiveSearch].location != NSNotFound ||
368
+ [deviceName rangeOfString:@"Mac Pro" options:NSCaseInsensitiveSearch].location != NSNotFound) {
369
+ isBuiltIn = YES;
370
+ }
371
+
372
+ // Also check for generic "Built-in" in name
373
+ if ([deviceName rangeOfString:@"Built-in" options:NSCaseInsensitiveSearch].location != NSNotFound) {
374
+ isBuiltIn = YES;
375
+ }
376
+
377
+ // External devices (Continuity, USB, etc.) should NOT be default
378
+ if ([deviceName rangeOfString:@"Continuity" options:NSCaseInsensitiveSearch].location != NSNotFound ||
379
+ [deviceName rangeOfString:@"iPhone" options:NSCaseInsensitiveSearch].location != NSNotFound ||
380
+ [deviceName rangeOfString:@"iPad" options:NSCaseInsensitiveSearch].location != NSNotFound) {
381
+ isBuiltIn = NO;
382
+ }
383
+
337
384
  NSDictionary *info = @{
338
385
  @"id": device.uniqueID ?: @"",
339
- @"name": device.localizedName ?: @"Unknown Audio Device",
386
+ @"name": deviceName,
340
387
  @"manufacturer": device.manufacturer ?: @"",
341
- @"isDefault": @(device.connected),
388
+ @"isDefault": @(isBuiltIn), // Only built-in devices are default
342
389
  @"transportType": @(device.transportType)
343
390
  };
344
391
  [devicesInfo addObject:info];
@@ -189,18 +189,58 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
189
189
  position = @"unspecified";
190
190
  break;
191
191
  }
192
-
192
+
193
+ // PRIORITY FIX: MacBook built-in cameras should be default, not external cameras
194
+ // Check if this is a built-in camera (MacBook's own camera)
195
+ NSString *deviceName = device.localizedName ?: @"";
196
+ NSString *deviceType = device.deviceType ?: @"";
197
+ BOOL isBuiltIn = NO;
198
+
199
+ // Built-in detection: Check for common built-in camera names
200
+ if ([deviceName rangeOfString:@"FaceTime" options:NSCaseInsensitiveSearch].location != NSNotFound ||
201
+ [deviceName rangeOfString:@"iSight" options:NSCaseInsensitiveSearch].location != NSNotFound ||
202
+ [deviceName rangeOfString:@"Built-in" options:NSCaseInsensitiveSearch].location != NSNotFound) {
203
+ isBuiltIn = YES;
204
+ }
205
+
206
+ // Check device type for built-in wide angle camera
207
+ if (@available(macOS 10.15, *)) {
208
+ if ([deviceType isEqualToString:AVCaptureDeviceTypeBuiltInWideAngleCamera]) {
209
+ isBuiltIn = YES;
210
+ }
211
+ }
212
+
213
+ // External devices (Continuity Camera, iPhone, iPad, USB) should NOT be default
214
+ if (continuityCamera ||
215
+ [deviceName rangeOfString:@"iPhone" options:NSCaseInsensitiveSearch].location != NSNotFound ||
216
+ [deviceName rangeOfString:@"iPad" options:NSCaseInsensitiveSearch].location != NSNotFound ||
217
+ [deviceName rangeOfString:@"Continuity" options:NSCaseInsensitiveSearch].location != NSNotFound) {
218
+ isBuiltIn = NO;
219
+ }
220
+
221
+ // External device types should not be default
222
+ if (@available(macOS 14.0, *)) {
223
+ if ([deviceType isEqualToString:AVCaptureDeviceTypeExternal] ||
224
+ [deviceType isEqualToString:AVCaptureDeviceTypeContinuityCamera]) {
225
+ isBuiltIn = NO;
226
+ }
227
+ }
228
+ if ([deviceType isEqualToString:AVCaptureDeviceTypeExternalUnknown]) {
229
+ isBuiltIn = NO;
230
+ }
231
+
193
232
  NSDictionary *deviceInfo = @{
194
233
  @"id": device.uniqueID ?: @"",
195
- @"name": device.localizedName ?: @"Unknown Camera",
234
+ @"name": deviceName,
196
235
  @"model": device.modelID ?: @"",
197
236
  @"manufacturer": device.manufacturer ?: @"",
198
237
  @"position": position ?: @"unspecified",
199
238
  @"transportType": @(device.transportType),
200
239
  @"isConnected": @(device.isConnected),
240
+ @"isDefault": @(isBuiltIn), // Only built-in cameras are default
201
241
  @"hasFlash": @(device.hasFlash),
202
242
  @"supportsDepth": @NO,
203
- @"deviceType": device.deviceType ?: @"",
243
+ @"deviceType": deviceType,
204
244
  @"requiresContinuityCameraPermission": @(continuityCamera),
205
245
  @"maxResolution": @{
206
246
  @"width": @(bestDimensions.width),
@@ -678,17 +718,33 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
678
718
  return YES;
679
719
  }
680
720
 
721
+ // CRITICAL FIX: For external cameras (especially Continuity Camera/iPhone),
722
+ // stopRunning can hang if device is disconnected. Use async approach.
723
+ MRLog(@"🛑 CameraRecorder: Stopping session (external device safe)...");
724
+
681
725
  self.isShuttingDown = YES;
682
726
  self.isRecording = NO;
683
727
 
684
- @try {
685
- [self.session stopRunning];
686
- } @catch (NSException *exception) {
687
- MRLog(@"⚠️ CameraRecorder: Exception while stopping session: %@", exception.reason);
688
- }
689
-
728
+ // Stop delegate FIRST to prevent new frames
690
729
  [self.videoOutput setSampleBufferDelegate:nil queue:nil];
691
730
 
731
+ // Stop session on background thread to avoid blocking
732
+ AVCaptureSession *sessionToStop = self.session;
733
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
734
+ @autoreleasepool {
735
+ @try {
736
+ if ([sessionToStop isRunning]) {
737
+ MRLog(@"🛑 Stopping AVCaptureSession (camera)...");
738
+ [sessionToStop stopRunning];
739
+ MRLog(@"✅ AVCaptureSession stopped (camera)");
740
+ }
741
+ } @catch (NSException *exception) {
742
+ MRLog(@"⚠️ CameraRecorder: Exception while stopping session: %@", exception.reason);
743
+ }
744
+ // Release happens automatically when block completes
745
+ }
746
+ });
747
+
692
748
  // CRITICAL FIX: Check if assetWriter exists before trying to finish it
693
749
  // If no frames were captured, assetWriter will be nil
694
750
  if (!self.assetWriter) {
@@ -709,12 +765,12 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
709
765
  dispatch_semaphore_signal(semaphore);
710
766
  }];
711
767
 
712
- // Reduced timeout to 2 seconds for faster response
713
- dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
714
- dispatch_semaphore_wait(semaphore, timeout);
768
+ // Reduced timeout to 1 second for external devices
769
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
770
+ long result = dispatch_semaphore_wait(semaphore, timeout);
715
771
 
716
- if (!finished) {
717
- MRLog(@"⚠️ CameraRecorder: Timed out waiting for writer to finish");
772
+ if (result != 0 || !finished) {
773
+ MRLog(@"⚠️ CameraRecorder: Timed out waiting for writer (external device?) - forcing cancel");
718
774
  // Force cancel if timeout
719
775
  [self.assetWriter cancelWriting];
720
776
  }
@@ -723,10 +779,11 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
723
779
  if (!success) {
724
780
  MRLog(@"⚠️ CameraRecorder: Writer finished with status %ld error %@", (long)self.assetWriter.status, self.assetWriter.error);
725
781
  } else {
726
- MRLog(@"✅ CameraRecorder stopped successfully");
782
+ MRLog(@"✅ CameraRecorder writer finished successfully");
727
783
  }
728
784
 
729
785
  [self resetState];
786
+ MRLog(@"✅ CameraRecorder stopped (safe for external devices)");
730
787
  return success;
731
788
  }
732
789
 
@@ -827,6 +827,7 @@ Napi::Value GetCameraDevices(const Napi::CallbackInfo& info) {
827
827
  NSString *position = camera[@"position"];
828
828
  NSNumber *transportType = camera[@"transportType"];
829
829
  NSNumber *isConnected = camera[@"isConnected"];
830
+ NSNumber *isDefault = camera[@"isDefault"];
830
831
  NSNumber *hasFlash = camera[@"hasFlash"];
831
832
  NSNumber *supportsDepth = camera[@"supportsDepth"];
832
833
 
@@ -861,7 +862,11 @@ Napi::Value GetCameraDevices(const Napi::CallbackInfo& info) {
861
862
  if (isConnected && [isConnected isKindOfClass:[NSNumber class]]) {
862
863
  cameraObj.Set("isConnected", Napi::Boolean::New(env, [isConnected boolValue]));
863
864
  }
864
-
865
+
866
+ if (isDefault && [isDefault isKindOfClass:[NSNumber class]]) {
867
+ cameraObj.Set("isDefault", Napi::Boolean::New(env, [isDefault boolValue]));
868
+ }
869
+
865
870
  if (hasFlash && [hasFlash isKindOfClass:[NSNumber class]]) {
866
871
  cameraObj.Set("hasFlash", Napi::Boolean::New(env, [hasFlash boolValue]));
867
872
  }