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.
@@ -3,7 +3,8 @@
3
3
  "allow": [
4
4
  "Bash(ffmpeg:*)",
5
5
  "Bash(chmod:*)",
6
- "Bash(node test-sync.js:*)"
6
+ "Bash(node test-sync.js:*)",
7
+ "Bash(node:*)"
7
8
  ],
8
9
  "deny": [],
9
10
  "ask": []
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.15",
3
+ "version": "2.21.17",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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
- AudioChannelLayout layout = {0};
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
- if (layoutSize > 0) {
132
- audioSettings[AVChannelLayoutKey] = [NSData dataWithBytes:&layout length:layoutSize];
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
- [self.writerInput markAsFinished];
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
- dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
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
 
@@ -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
- dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
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
  }
@@ -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
- if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
411
- delegate:g_delegate
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(@"✅ AVFoundation recording started successfully");
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