node-mac-recorder 2.21.15 → 2.21.17
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/.claude/settings.local.json +2 -1
- package/check-devices.js +38 -0
- package/package.json +1 -1
- package/src/audio_recorder.mm +38 -19
- package/src/camera_recorder.mm +22 -11
- package/src/mac_recorder.mm +57 -22
package/check-devices.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const MacRecorder = require('./index.js');
|
|
4
|
+
const recorder = new MacRecorder();
|
|
5
|
+
|
|
6
|
+
async function checkDevices() {
|
|
7
|
+
console.log('\n🔍 TÜM CİHAZLARI KONTROL ET:\n');
|
|
8
|
+
|
|
9
|
+
const cameras = await recorder.getCameraDevices();
|
|
10
|
+
console.log(`📹 Kamera Cihazları (${cameras.length}):`);
|
|
11
|
+
cameras.forEach(cam => {
|
|
12
|
+
console.log(`\n - ${cam.name}`);
|
|
13
|
+
console.log(` ID: ${cam.id}`);
|
|
14
|
+
console.log(` Type: ${cam.deviceType || 'N/A'}`);
|
|
15
|
+
console.log(` Manufacturer: ${cam.manufacturer}`);
|
|
16
|
+
console.log(` Transport: ${cam.transportType}`);
|
|
17
|
+
console.log(` Continuity: ${cam.requiresContinuityCameraPermission ? 'YES' : 'NO'}`);
|
|
18
|
+
console.log(` Connected: ${cam.isConnected ? 'YES' : 'NO'}`);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const audio = await recorder.getAudioDevices();
|
|
22
|
+
console.log(`\n🎙️ Ses Cihazları (${audio.length}):`);
|
|
23
|
+
audio.forEach(aud => {
|
|
24
|
+
console.log(`\n - ${aud.name}`);
|
|
25
|
+
console.log(` ID: ${aud.id}`);
|
|
26
|
+
console.log(` Manufacturer: ${aud.manufacturer}`);
|
|
27
|
+
console.log(` Transport: ${aud.transportType}`);
|
|
28
|
+
console.log(` Default: ${aud.isDefault ? 'YES' : 'NO'}`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log('\n✅ İPUCU: iPhone bağlı değilse, Wi-Fi veya USB ile bağla\n');
|
|
32
|
+
console.log('Continuity Camera şartları:');
|
|
33
|
+
console.log(' 1. iPhone ve Mac aynı Apple ID ile giriş yapmış olmalı');
|
|
34
|
+
console.log(' 2. Her ikisinde de Bluetooth ve Wi-Fi açık olmalı');
|
|
35
|
+
console.log(' 3. iPhone Handoff açık olmalı\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
checkDevices().catch(err => console.error('Hata:', err));
|
package/package.json
CHANGED
package/src/audio_recorder.mm
CHANGED
|
@@ -110,16 +110,7 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
|
|
|
110
110
|
|
|
111
111
|
MRLog(@"🎤 Audio format: %.0f Hz, %lu channel(s)", sampleRate, (unsigned long)channels);
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
size_t layoutSize = 0;
|
|
115
|
-
if (channels == 1) {
|
|
116
|
-
layout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
|
|
117
|
-
layoutSize = sizeof(AudioChannelLayout);
|
|
118
|
-
} else if (channels == 2) {
|
|
119
|
-
layout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
|
|
120
|
-
layoutSize = sizeof(AudioChannelLayout);
|
|
121
|
-
}
|
|
122
|
-
|
|
113
|
+
// Create audio settings
|
|
123
114
|
NSMutableDictionary *audioSettings = [@{
|
|
124
115
|
AVFormatIDKey: @(kAudioFormatMPEG4AAC),
|
|
125
116
|
AVSampleRateKey: @(sampleRate),
|
|
@@ -128,9 +119,17 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
|
|
|
128
119
|
AVEncoderAudioQualityKey: @(AVAudioQualityHigh)
|
|
129
120
|
} mutableCopy];
|
|
130
121
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
122
|
+
// CRITICAL FIX: AVChannelLayoutKey is REQUIRED for ALL channel counts
|
|
123
|
+
// Force to stereo or mono for AAC compatibility
|
|
124
|
+
NSUInteger validChannels = (channels <= 1) ? 1 : 2; // Force to mono or stereo
|
|
125
|
+
audioSettings[AVNumberOfChannelsKey] = @(validChannels); // Update settings
|
|
126
|
+
|
|
127
|
+
AudioChannelLayout layout = {0};
|
|
128
|
+
layout.mChannelLayoutTag = (validChannels == 1) ? kAudioChannelLayoutTag_Mono : kAudioChannelLayoutTag_Stereo;
|
|
129
|
+
size_t layoutSize = sizeof(AudioChannelLayout);
|
|
130
|
+
audioSettings[AVChannelLayoutKey] = [NSData dataWithBytes:&layout length:layoutSize];
|
|
131
|
+
|
|
132
|
+
MRLog(@"🎤 Using %lu channel(s) for AAC encoding (original: %lu)", (unsigned long)validChannels, (unsigned long)channels);
|
|
134
133
|
|
|
135
134
|
self.writerInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
|
|
136
135
|
self.writerInput.expectsMediaDataInRealTime = YES;
|
|
@@ -224,27 +223,47 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
|
|
|
224
223
|
if (!self.session) {
|
|
225
224
|
return YES;
|
|
226
225
|
}
|
|
227
|
-
|
|
226
|
+
|
|
228
227
|
[self.session stopRunning];
|
|
229
228
|
self.session = nil;
|
|
230
229
|
self.audioOutput = nil;
|
|
231
|
-
|
|
230
|
+
|
|
231
|
+
// CRITICAL FIX: Check if writer exists before trying to finish it
|
|
232
232
|
if (self.writer) {
|
|
233
|
-
|
|
233
|
+
// Only mark as finished if writerInput exists
|
|
234
|
+
if (self.writerInput) {
|
|
235
|
+
[self.writerInput markAsFinished];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
__block BOOL finished = NO;
|
|
234
239
|
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
240
|
+
|
|
235
241
|
[self.writer finishWritingWithCompletionHandler:^{
|
|
242
|
+
finished = YES;
|
|
236
243
|
dispatch_semaphore_signal(semaphore);
|
|
237
244
|
}];
|
|
238
|
-
|
|
245
|
+
|
|
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));
|
|
239
248
|
dispatch_semaphore_wait(semaphore, timeout);
|
|
249
|
+
|
|
250
|
+
if (!finished) {
|
|
251
|
+
MRLog(@"⚠️ AudioRecorder: Timed out waiting for writer to finish");
|
|
252
|
+
// Force cancel if timeout
|
|
253
|
+
[self.writer cancelWriting];
|
|
254
|
+
} else {
|
|
255
|
+
MRLog(@"✅ AudioRecorder stopped successfully");
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
MRLog(@"⚠️ AudioRecorder: No writer to finish (no audio captured)");
|
|
240
259
|
}
|
|
241
|
-
|
|
260
|
+
|
|
242
261
|
self.writer = nil;
|
|
243
262
|
self.writerInput = nil;
|
|
244
263
|
self.writerStarted = NO;
|
|
245
264
|
self.startTime = kCMTimeInvalid;
|
|
246
265
|
self.outputPath = nil;
|
|
247
|
-
|
|
266
|
+
|
|
248
267
|
return YES;
|
|
249
268
|
}
|
|
250
269
|
|
package/src/camera_recorder.mm
CHANGED
|
@@ -677,44 +677,55 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
677
677
|
if (!self.isRecording) {
|
|
678
678
|
return YES;
|
|
679
679
|
}
|
|
680
|
-
|
|
680
|
+
|
|
681
681
|
self.isShuttingDown = YES;
|
|
682
682
|
self.isRecording = NO;
|
|
683
|
-
|
|
683
|
+
|
|
684
684
|
@try {
|
|
685
685
|
[self.session stopRunning];
|
|
686
686
|
} @catch (NSException *exception) {
|
|
687
687
|
MRLog(@"⚠️ CameraRecorder: Exception while stopping session: %@", exception.reason);
|
|
688
688
|
}
|
|
689
|
-
|
|
689
|
+
|
|
690
690
|
[self.videoOutput setSampleBufferDelegate:nil queue:nil];
|
|
691
|
-
|
|
691
|
+
|
|
692
|
+
// CRITICAL FIX: Check if assetWriter exists before trying to finish it
|
|
693
|
+
// If no frames were captured, assetWriter will be nil
|
|
694
|
+
if (!self.assetWriter) {
|
|
695
|
+
MRLog(@"⚠️ CameraRecorder: No writer to finish (no frames captured)");
|
|
696
|
+
[self resetState];
|
|
697
|
+
return YES; // Success - nothing to finish
|
|
698
|
+
}
|
|
699
|
+
|
|
692
700
|
if (self.assetWriterInput) {
|
|
693
701
|
[self.assetWriterInput markAsFinished];
|
|
694
702
|
}
|
|
695
|
-
|
|
703
|
+
|
|
696
704
|
__block BOOL finished = NO;
|
|
697
705
|
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
698
|
-
|
|
706
|
+
|
|
699
707
|
[self.assetWriter finishWritingWithCompletionHandler:^{
|
|
700
708
|
finished = YES;
|
|
701
709
|
dispatch_semaphore_signal(semaphore);
|
|
702
710
|
}];
|
|
703
|
-
|
|
704
|
-
|
|
711
|
+
|
|
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));
|
|
705
714
|
dispatch_semaphore_wait(semaphore, timeout);
|
|
706
|
-
|
|
715
|
+
|
|
707
716
|
if (!finished) {
|
|
708
717
|
MRLog(@"⚠️ CameraRecorder: Timed out waiting for writer to finish");
|
|
718
|
+
// Force cancel if timeout
|
|
719
|
+
[self.assetWriter cancelWriting];
|
|
709
720
|
}
|
|
710
|
-
|
|
721
|
+
|
|
711
722
|
BOOL success = (self.assetWriter.status == AVAssetWriterStatusCompleted);
|
|
712
723
|
if (!success) {
|
|
713
724
|
MRLog(@"⚠️ CameraRecorder: Writer finished with status %ld error %@", (long)self.assetWriter.status, self.assetWriter.error);
|
|
714
725
|
} else {
|
|
715
726
|
MRLog(@"✅ CameraRecorder stopped successfully");
|
|
716
727
|
}
|
|
717
|
-
|
|
728
|
+
|
|
718
729
|
[self resetState];
|
|
719
730
|
return success;
|
|
720
731
|
}
|
package/src/mac_recorder.mm
CHANGED
|
@@ -403,33 +403,50 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
403
403
|
|
|
404
404
|
// Use ScreenCaptureKit with window exclusion and timeout protection
|
|
405
405
|
NSError *sckError = nil;
|
|
406
|
-
|
|
406
|
+
|
|
407
407
|
// Set timeout for ScreenCaptureKit initialization
|
|
408
408
|
// Attempt to start ScreenCaptureKit with safety wrapper
|
|
409
409
|
@try {
|
|
410
|
-
|
|
411
|
-
|
|
410
|
+
// CRITICAL SYNC FIX: Start camera BEFORE ScreenCaptureKit for perfect sync
|
|
411
|
+
bool cameraStarted = true;
|
|
412
|
+
if (captureCamera) {
|
|
413
|
+
MRLog(@"🎯 SYNC: Starting camera recording first for parallel sync");
|
|
414
|
+
cameraStarted = startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp);
|
|
415
|
+
if (!cameraStarted) {
|
|
416
|
+
MRLog(@"❌ Camera start failed - aborting");
|
|
417
|
+
return Napi::Boolean::New(env, false);
|
|
418
|
+
}
|
|
419
|
+
MRLog(@"✅ SYNC: Camera recording started");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Now start ScreenCaptureKit immediately after camera
|
|
423
|
+
MRLog(@"🎯 SYNC: Starting ScreenCaptureKit recording immediately");
|
|
424
|
+
if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
|
|
425
|
+
delegate:g_delegate
|
|
412
426
|
error:&sckError]) {
|
|
413
|
-
|
|
427
|
+
|
|
414
428
|
// ScreenCaptureKit başlatma başarılı - validation yapmıyoruz
|
|
415
429
|
MRLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
|
|
416
|
-
MRLog(@"✅ ScreenCaptureKit recording started successfully");
|
|
417
|
-
|
|
418
|
-
if (!startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
|
|
419
|
-
MRLog(@"❌ Camera start failed - stopping ScreenCaptureKit session");
|
|
420
|
-
[ScreenCaptureKitRecorder stopRecording];
|
|
421
|
-
g_isRecording = false;
|
|
422
|
-
return Napi::Boolean::New(env, false);
|
|
423
|
-
}
|
|
424
|
-
|
|
430
|
+
MRLog(@"✅ SYNC: ScreenCaptureKit recording started successfully");
|
|
431
|
+
|
|
425
432
|
g_isRecording = true;
|
|
426
433
|
return Napi::Boolean::New(env, true);
|
|
427
434
|
} else {
|
|
428
435
|
NSLog(@"❌ ScreenCaptureKit failed to start");
|
|
429
436
|
NSLog(@"❌ Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
|
|
437
|
+
|
|
438
|
+
// Cleanup camera if ScreenCaptureKit failed
|
|
439
|
+
if (cameraStarted && isCameraRecording()) {
|
|
440
|
+
stopCameraRecording();
|
|
441
|
+
}
|
|
430
442
|
}
|
|
431
443
|
} @catch (NSException *sckException) {
|
|
432
444
|
NSLog(@"❌ Exception during ScreenCaptureKit startup: %@", sckException.reason);
|
|
445
|
+
|
|
446
|
+
// Cleanup camera on exception
|
|
447
|
+
if (isCameraRecording()) {
|
|
448
|
+
stopCameraRecording();
|
|
449
|
+
}
|
|
433
450
|
}
|
|
434
451
|
NSLog(@"❌ ScreenCaptureKit failed or unsafe - will fallback to AVFoundation");
|
|
435
452
|
|
|
@@ -487,19 +504,27 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
487
504
|
NSString* audioDeviceId,
|
|
488
505
|
NSString* audioOutputPath);
|
|
489
506
|
|
|
507
|
+
// CRITICAL SYNC FIX: Start camera BEFORE screen recording for perfect sync
|
|
508
|
+
// This ensures both capture their first frame at approximately the same time
|
|
509
|
+
bool cameraStarted = true;
|
|
510
|
+
if (captureCamera) {
|
|
511
|
+
MRLog(@"🎯 SYNC: Starting camera recording first for parallel sync");
|
|
512
|
+
cameraStarted = startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp);
|
|
513
|
+
if (!cameraStarted) {
|
|
514
|
+
MRLog(@"❌ Camera start failed - aborting");
|
|
515
|
+
return Napi::Boolean::New(env, false);
|
|
516
|
+
}
|
|
517
|
+
MRLog(@"✅ SYNC: Camera recording started");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Now start screen recording immediately after camera
|
|
521
|
+
MRLog(@"🎯 SYNC: Starting screen recording immediately");
|
|
490
522
|
bool avResult = startAVFoundationRecording(outputPath, displayID, windowID, captureRect,
|
|
491
523
|
captureCursor, includeMicrophone, includeSystemAudio, audioDeviceId, audioOutputPath);
|
|
492
|
-
|
|
524
|
+
|
|
493
525
|
if (avResult) {
|
|
494
526
|
MRLog(@"🎥 RECORDING METHOD: AVFoundation");
|
|
495
|
-
MRLog(@"✅
|
|
496
|
-
|
|
497
|
-
if (!startCameraIfRequested(captureCamera, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
|
|
498
|
-
MRLog(@"❌ Camera start failed - stopping AVFoundation session");
|
|
499
|
-
stopAVFoundationRecording();
|
|
500
|
-
g_isRecording = false;
|
|
501
|
-
return Napi::Boolean::New(env, false);
|
|
502
|
-
}
|
|
527
|
+
MRLog(@"✅ SYNC: Screen recording started successfully");
|
|
503
528
|
|
|
504
529
|
// NOTE: Audio is handled internally by AVFoundation, no need for standalone audio
|
|
505
530
|
// AVFoundation integrates audio recording directly
|
|
@@ -509,10 +534,20 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
509
534
|
} else {
|
|
510
535
|
NSLog(@"❌ AVFoundation recording failed to start");
|
|
511
536
|
NSLog(@"❌ Check permissions and output path validity");
|
|
537
|
+
|
|
538
|
+
// Cleanup camera if screen recording failed
|
|
539
|
+
if (cameraStarted && isCameraRecording()) {
|
|
540
|
+
stopCameraRecording();
|
|
541
|
+
}
|
|
512
542
|
}
|
|
513
543
|
} @catch (NSException *avException) {
|
|
514
544
|
NSLog(@"❌ Exception during AVFoundation startup: %@", avException.reason);
|
|
515
545
|
NSLog(@"❌ Stack trace: %@", [avException callStackSymbols]);
|
|
546
|
+
|
|
547
|
+
// Cleanup camera on exception
|
|
548
|
+
if (isCameraRecording()) {
|
|
549
|
+
stopCameraRecording();
|
|
550
|
+
}
|
|
516
551
|
}
|
|
517
552
|
|
|
518
553
|
// Both ScreenCaptureKit and AVFoundation failed
|