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 +1 -0
- package/package.json +1 -1
- package/src/audio_recorder.mm +56 -9
- package/src/camera_recorder.mm +72 -15
- package/src/mac_recorder.mm +6 -1
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
package/src/audio_recorder.mm
CHANGED
|
@@ -224,10 +224,30 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
|
|
|
224
224
|
return YES;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
|
|
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
|
|
247
|
-
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(
|
|
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
|
|
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
|
|
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":
|
|
386
|
+
@"name": deviceName,
|
|
340
387
|
@"manufacturer": device.manufacturer ?: @"",
|
|
341
|
-
@"isDefault": @(
|
|
388
|
+
@"isDefault": @(isBuiltIn), // Only built-in devices are default
|
|
342
389
|
@"transportType": @(device.transportType)
|
|
343
390
|
};
|
|
344
391
|
[devicesInfo addObject:info];
|
package/src/camera_recorder.mm
CHANGED
|
@@ -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":
|
|
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":
|
|
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
|
-
|
|
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
|
|
713
|
-
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(
|
|
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
|
|
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
|
|
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
|
|
package/src/mac_recorder.mm
CHANGED
|
@@ -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
|
}
|