node-mac-recorder 2.24.9 → 2.24.11

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,6 +1,8 @@
1
1
  #import "screen_capture_kit.h"
2
2
  #import "logging.h"
3
3
  #import "sync_timeline.h"
4
+ #import "recording_writer_safety.h"
5
+ #include <atomic>
4
6
  #import <AVFoundation/AVFoundation.h>
5
7
  #import <CoreVideo/CoreVideo.h>
6
8
  #import <CoreMedia/CoreMedia.h>
@@ -132,9 +134,10 @@ static dispatch_queue_t g_sessionsQueue = nil;
132
134
  // Legacy global state for backward compatibility (points to first/default session)
133
135
  static SCStream * API_AVAILABLE(macos(12.3)) g_stream = nil;
134
136
  static id<SCStreamDelegate> API_AVAILABLE(macos(12.3)) g_streamDelegate = nil;
135
- static BOOL g_isRecording = NO;
136
- static BOOL g_isCleaningUp = NO;
137
- static BOOL g_isScheduling = NO;
137
+ static std::atomic<bool> g_isRecording{false};
138
+ static std::atomic<bool> g_isCleaningUp{false};
139
+ static std::atomic<bool> g_isScheduling{false};
140
+ static uint64_t g_schedulingGeneration = 0;
138
141
  static NSString *g_outputPath = nil;
139
142
  static BOOL g_firstFrameReceived = NO;
140
143
  static NSInteger g_frameCountSinceStart = 0;
@@ -156,6 +159,7 @@ static CMTime g_audioStartTime = kCMTimeInvalid;
156
159
  static BOOL g_audioWriterStarted = NO;
157
160
  static BOOL g_captureMicrophoneEnabled = NO;
158
161
  static BOOL g_captureSystemAudioEnabled = NO;
162
+ static BOOL g_captureCameraEnabled = NO;
159
163
  static BOOL g_mixAudioEnabled = YES;
160
164
  static float g_mixMicGain = 0.8f;
161
165
  static float g_mixSystemGain = 0.4f;
@@ -177,6 +181,16 @@ static NSInteger g_lastVideoFramesAppended = 0;
177
181
  static NSInteger g_lastVideoFramesDropped = 0;
178
182
  static NSInteger g_lastVideoTargetFPS = 0;
179
183
 
184
+ // This target uses manual reference counting. Async configuration copies no
185
+ // longer leak, so globals must explicitly own paths used after setup returns.
186
+ static void SCKSetOwnedString(NSString **slot, NSString *value) {
187
+ @synchronized([ScreenCaptureKitRecorder class]) {
188
+ NSString *owned = [value copy];
189
+ [*slot release];
190
+ *slot = owned;
191
+ }
192
+ }
193
+
180
194
  // ---- Prewarm edilmis SCShareableContent onbellegi ----
181
195
  // Kayit baslatilirken envanter cagrisini tamamen atlayabilmek icin saklanir.
182
196
  // ARC KAPALI: retain/release elle yonetiliyor.
@@ -331,7 +345,9 @@ static void SCKQualityBitrateForDimensions(NSString *preset,
331
345
  static dispatch_queue_t ScreenCaptureControlQueue(void);
332
346
  static void SCKMarkSchedulingComplete(void);
333
347
  static void SCKFailScheduling(void);
334
- static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *content) API_AVAILABLE(macos(12.3));
348
+ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *content, uint64_t generation) API_AVAILABLE(macos(12.3));
349
+ static void SCKCompleteStop(void);
350
+ static void SCKRequestStop(SCStream *expectedStream) API_AVAILABLE(macos(12.3));
335
351
 
336
352
  static void CleanupWriters(void);
337
353
 
@@ -437,25 +453,13 @@ static NSString *MRNormalizePath(id value) {
437
453
  }
438
454
 
439
455
  static void FinishWriter(AVAssetWriter *writer, AVAssetWriterInput *input) {
440
- if (!writer) {
441
- return;
442
- }
443
-
444
- if (input) {
445
- [input markAsFinished];
446
- }
447
-
448
- dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
449
- [writer finishWritingWithCompletionHandler:^{
450
- dispatch_semaphore_signal(semaphore);
451
- }];
452
- dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
453
- dispatch_semaphore_wait(semaphore, timeout);
456
+ MRFinishAssetWriterSafely(writer, 5.0);
454
457
  }
455
458
 
456
459
  static void CleanupWriters(void) {
457
460
  if (g_videoWriter) {
458
461
  FinishWriter(g_videoWriter, g_videoInput);
462
+ [g_videoWriter release];
459
463
  g_videoWriter = nil;
460
464
  g_videoInput = nil;
461
465
  if (g_pixelBufferAdaptorRef) {
@@ -493,14 +497,11 @@ static void CleanupWriters(void) {
493
497
  }
494
498
 
495
499
  if (g_audioWriter) {
496
- if (g_systemAudioInput) {
497
- [g_systemAudioInput markAsFinished];
498
- }
499
- if (g_microphoneAudioInput) {
500
- [g_microphoneAudioInput markAsFinished];
501
- }
502
500
  FinishWriter(g_audioWriter, nil);
501
+ [g_audioWriter release];
503
502
  g_audioWriter = nil;
503
+ // Factory-created inputs are borrowed from the writer, just like the
504
+ // video input. Releasing them again would over-release on stop.
504
505
  g_systemAudioInput = nil;
505
506
  g_microphoneAudioInput = nil;
506
507
  g_audioWriterStarted = NO;
@@ -544,47 +545,62 @@ extern "C" void ScreenCaptureKitGetFrameStats(long *appendedOut,
544
545
  }
545
546
 
546
547
  extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
547
- if (!g_audioOutputPath) {
548
- return nil;
549
- }
550
- if ([g_audioOutputPath isKindOfClass:[NSArray class]]) {
551
- id first = [(NSArray *)g_audioOutputPath firstObject];
552
- if ([first isKindOfClass:[NSString class]]) {
553
- return first;
554
- }
555
- return nil;
548
+ @synchronized([ScreenCaptureKitRecorder class]) {
549
+ // Native stop may clear the owned path on another queue while N-API
550
+ // is converting it to a JS string. Return a caller-owned pool copy.
551
+ return [[MRNormalizePath(g_audioOutputPath) copy] autorelease];
556
552
  }
557
- return g_audioOutputPath;
558
553
  }
559
554
 
560
- @implementation PureScreenCaptureDelegate
561
- - (void)stream:(SCStream * API_AVAILABLE(macos(12.3)))stream didStopWithError:(NSError *)error API_AVAILABLE(macos(12.3)) {
562
- // ELECTRON FIX: Run cleanup on background thread to avoid blocking Electron
563
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
564
- MRLog(@"🛑 Pure ScreenCapture stream stopped");
565
-
566
- // Prevent recursive calls during cleanup
567
- if (g_isCleaningUp) {
568
- MRLog(@"⚠️ Already cleaning up, ignoring delegate callback");
569
- return;
570
- }
571
-
572
- @synchronized([ScreenCaptureKitRecorder class]) {
573
- g_isRecording = NO;
574
- }
555
+ // All finalization runs on the control queue. Stop publishing samples before
556
+ // draining both queues; only then may the pixel buffer adaptor be released.
557
+ static void SCKCompleteStop(void) {
558
+ g_isRecording = NO;
559
+ g_isCleaningUp = YES;
560
+ if (g_videoQueue) dispatch_sync(g_videoQueue, ^{});
561
+ if (g_audioQueue) dispatch_sync(g_audioQueue, ^{});
562
+ @try {
563
+ CleanupWriters();
564
+ } @catch (NSException *exception) {
565
+ NSLog(@"[Recorder] Capture finalization failed safely: %@", exception.reason);
566
+ } @finally {
567
+ [ScreenCaptureKitRecorder cleanupVideoWriter];
568
+ SCKMarkSchedulingComplete();
569
+ }
570
+ }
575
571
 
576
- if (error) {
577
- NSLog(@"❌ Stream error: %@", error);
578
- } else {
579
- MRLog(@"✅ Stream stopped cleanly");
580
- }
572
+ static void SCKRequestStop(SCStream *expectedStream) {
573
+ if ((expectedStream && expectedStream != g_stream) || g_isCleaningUp) return;
574
+ ++g_schedulingGeneration; // invalidate pending inventory/start callbacks
575
+ g_isCleaningUp = YES;
576
+ g_isRecording = NO;
577
+ g_isScheduling = NO;
578
+ SCStream *streamToStop = g_stream;
579
+ if (!streamToStop) {
580
+ SCKCompleteStop();
581
+ return;
582
+ }
583
+ @try {
584
+ [streamToStop stopCaptureWithCompletionHandler:^(NSError *error) {
585
+ dispatch_async(ScreenCaptureControlQueue(), ^{
586
+ if (streamToStop != g_stream) return;
587
+ if (error) NSLog(@"[Recorder] Capture stop error: %@", error);
588
+ SCKCompleteStop();
589
+ });
590
+ }];
591
+ } @catch (NSException *exception) {
592
+ NSLog(@"[Recorder] Capture stop failed safely: %@", exception.reason);
593
+ SCKCompleteStop();
594
+ }
595
+ }
581
596
 
582
- // Finalize on background thread with synchronization
583
- @synchronized([ScreenCaptureKitRecorder class]) {
584
- if (!g_isCleaningUp) {
585
- [ScreenCaptureKitRecorder finalizeRecording];
586
- }
587
- }
597
+ @implementation PureScreenCaptureDelegate
598
+ - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error API_AVAILABLE(macos(12.3)) {
599
+ dispatch_async(ScreenCaptureControlQueue(), ^{
600
+ if (stream != g_stream || g_isCleaningUp) return;
601
+ NSLog(@"[Recorder] Capture source stopped: %@", error);
602
+ ++g_schedulingGeneration;
603
+ SCKCompleteStop();
588
604
  });
589
605
  }
590
606
  @end
@@ -599,9 +615,15 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
599
615
 
600
616
  @implementation ScreenCaptureVideoOutput
601
617
  - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type API_AVAILABLE(macos(12.3)) {
618
+ if (g_isCleaningUp || stream != g_stream) return;
619
+ @try {
602
620
  if (!g_isRecording || type != SCStreamOutputTypeScreen) {
603
621
  return;
604
622
  }
623
+
624
+ if (MRSyncIsPaused()) {
625
+ return;
626
+ }
605
627
 
606
628
  if (!CMSampleBufferDataIsReady(sampleBuffer)) {
607
629
  return;
@@ -686,6 +708,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
686
708
  relativePresentation = kCMTimeZero;
687
709
  }
688
710
  }
711
+ relativePresentation = MRSyncAdjustForPauses(relativePresentation);
689
712
 
690
713
  double stopLimit = MRSyncGetStopLimitSeconds();
691
714
  if (stopLimit > 0) {
@@ -717,6 +740,11 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
717
740
  double actualFPS = g_frameCount / elapsed;
718
741
  MRLog(@"📊 Frame stats: %ld frames in %.1fs = %.1f FPS", (long)g_frameCount, elapsed, actualFPS);
719
742
  }
743
+ } @catch (NSException *exception) {
744
+ NSLog(@"[Recorder] Sample callback failed safely: %@", exception.reason);
745
+ dispatch_async(ScreenCaptureControlQueue(), ^{ SCKRequestStop(stream); });
746
+ }
747
+
720
748
  }
721
749
  @end
722
750
 
@@ -725,6 +753,8 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
725
753
 
726
754
  @implementation ScreenCaptureAudioOutput
727
755
  - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type API_AVAILABLE(macos(12.3)) {
756
+ if (g_isCleaningUp || stream != g_stream) return;
757
+ @try {
728
758
  static dispatch_once_t onceToken;
729
759
  dispatch_once(&onceToken, ^{
730
760
  MRLog(@"🎤 First audio sample callback received from ScreenCaptureKit");
@@ -734,6 +764,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
734
764
  return;
735
765
  }
736
766
 
767
+ if (MRSyncIsPaused()) {
768
+ return;
769
+ }
770
+
737
771
  BOOL isMicrophoneSample = NO;
738
772
  BOOL isSupportedSample = NO;
739
773
  if (@available(macOS 15.0, *)) {
@@ -783,6 +817,17 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
783
817
 
784
818
  CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
785
819
 
820
+ // When camera and microphone are both active, lip sync must be anchored to
821
+ // the microphone clock. System audio can arrive first on macOS 15; letting
822
+ // it start the shared writer makes camera alignment depend on callback
823
+ // ordering. Drop only the short system-audio lead-in until mic t=0 exists.
824
+ if (g_captureCameraEnabled &&
825
+ g_captureMicrophoneEnabled &&
826
+ !routeToMicrophoneTrack &&
827
+ CMTIME_IS_INVALID(MRSyncAudioFirstTimestamp())) {
828
+ return;
829
+ }
830
+
786
831
  // A/V SYNC: Hold audio samples until camera produces first frame
787
832
  if (MRSyncShouldHoldAudioSample(presentationTime)) {
788
833
  return;
@@ -838,7 +883,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
838
883
  if (CMTIME_COMPARE_INLINE(adjustedPTS, <, kCMTimeZero)) {
839
884
  adjustedPTS = kCMTimeZero;
840
885
  }
841
- timingInfo[i].presentationTimeStamp = adjustedPTS;
886
+ timingInfo[i].presentationTimeStamp = MRSyncAdjustForPauses(adjustedPTS);
842
887
  } else {
843
888
  timingInfo[i].presentationTimeStamp = kCMTimeZero;
844
889
  }
@@ -848,7 +893,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
848
893
  if (CMTIME_COMPARE_INLINE(adjustedDTS, <, kCMTimeZero)) {
849
894
  adjustedDTS = kCMTimeZero;
850
895
  }
851
- timingInfo[i].decodeTimeStamp = adjustedDTS;
896
+ timingInfo[i].decodeTimeStamp = MRSyncAdjustForPauses(adjustedDTS);
852
897
  }
853
898
  }
854
899
 
@@ -902,6 +947,11 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
902
947
  if (bufferToAppend != sampleBuffer) {
903
948
  CFRelease(bufferToAppend);
904
949
  }
950
+ } @catch (NSException *exception) {
951
+ NSLog(@"[Recorder] Sample callback failed safely: %@", exception.reason);
952
+ dispatch_async(ScreenCaptureControlQueue(), ^{ SCKRequestStop(stream); });
953
+ }
954
+
905
955
  }
906
956
  @end
907
957
 
@@ -936,8 +986,6 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
936
986
  SCKQualityBitrateForDimensions(normalizedQuality, width, height, encoderFPS,
937
987
  &bitrate, &bpp, &minBitrate, &maxBitrate);
938
988
 
939
- NSNumber *qualityHint = [normalizedQuality isEqualToString:@"high"] ? @1.0 : ([normalizedQuality isEqualToString:@"medium"] ? @0.9 : @0.85);
940
-
941
989
  MRLog(@"🎬 Screen encoder (%@): %ldx%ld@%ldfps, codec=H.264 High Profile, bitrate=%.2fMbps (%.2f bpp, min=%ldMbps max=%ldMbps)",
942
990
  normalizedQuality,
943
991
  (long)width,
@@ -957,7 +1005,14 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
957
1005
  // edilebilir. Kapatmak fansiz makinelerde kare dusmesini azaltir.
958
1006
  AVVideoAllowFrameReorderingKey: @NO,
959
1007
  AVVideoExpectedSourceFrameRateKey: @(encoderFPS),
960
- AVVideoQualityKey: qualityHint,
1008
+ // AVVideoQualityKey BILEREK YOK: Apple belgesine gore yalnizca JPEG ve
1009
+ // ProRes icin gecerli. H.264'te (avc1) donanim encoder'i onu sessizce
1010
+ // yok sayiyordu, ama encoder yazilim yoluna dustugunde AVFoundation
1011
+ // dogrulamasi sertlesiyor ve AVAssetWriterInput ISTISNA atiyor:
1012
+ // "Compression property Quality is not supported for video codec type
1013
+ // avc1". Istisna tum kayit kurulumunu iptal ettigi icin kayit "basladi"
1014
+ // gorunup dosya hic olusmuyordu (editorde bos canvas). Kaliteyi zaten
1015
+ // AVVideoAverageBitRateKey belirliyor.
961
1016
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
962
1017
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
963
1018
  AVVideoAverageNonDroppableFrameRateKey: @(encoderFPS),
@@ -982,7 +1037,75 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
982
1037
  AVVideoCompressionPropertiesKey: compressionProps
983
1038
  };
984
1039
 
985
- g_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
1040
+ // AVFoundation, avc1 icin hangi sikistirma ozelligini kabul ettigine
1041
+ // encoder yoluna gore karar veriyor ve reddettiginde NSError DEGIL ISTISNA
1042
+ // atiyor. Istisna tum kayit kurulumunu iptal ettigi icin kayit "basladi"
1043
+ // gorunup dosya hic olusmuyor; kullanici ancak editorde bos canvas gorunce
1044
+ // anliyor. OLCULDU (macOS 26, 1x harici ekran, iTerm penceresi): once
1045
+ // "Compression property Quality is not supported for video codec type avc1",
1046
+ // o kaldirilinca "AverageNonDroppableFrameRate is not supported" — yani tek
1047
+ // tek anahtar elemek guvenilir degil. Ayni ozelliklerle ayni ekrandaki baska
1048
+ // pencereler sorunsuz calisiyor.
1049
+ //
1050
+ // Bu yuzden ayarlar KADEMELI denenir: once tam set, sonra yalnizca her
1051
+ // encoder'in destekledigi cekirdek set, en sonda codec+boyut. Ilk kabul
1052
+ // edilen kazanir; kayit hicbir kosulda sessizce bos cikmaz.
1053
+ NSMutableArray<NSDictionary *> *settingsTiers = [NSMutableArray array];
1054
+ [settingsTiers addObject:videoSettings];
1055
+ [settingsTiers addObject:@{
1056
+ AVVideoCodecKey: AVVideoCodecTypeH264,
1057
+ AVVideoWidthKey: @(width),
1058
+ AVVideoHeightKey: @(height),
1059
+ AVVideoColorPropertiesKey: colorProps,
1060
+ AVVideoCompressionPropertiesKey: @{
1061
+ AVVideoAverageBitRateKey: @(bitrate),
1062
+ AVVideoMaxKeyFrameIntervalKey: @(encoderFPS),
1063
+ AVVideoAllowFrameReorderingKey: @NO,
1064
+ AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
1065
+ }
1066
+ }];
1067
+ [settingsTiers addObject:@{
1068
+ AVVideoCodecKey: AVVideoCodecTypeH264,
1069
+ AVVideoWidthKey: @(width),
1070
+ AVVideoHeightKey: @(height),
1071
+ AVVideoCompressionPropertiesKey: @{ AVVideoAverageBitRateKey: @(bitrate) }
1072
+ }];
1073
+ [settingsTiers addObject:@{
1074
+ AVVideoCodecKey: AVVideoCodecTypeH264,
1075
+ AVVideoWidthKey: @(width),
1076
+ AVVideoHeightKey: @(height)
1077
+ }];
1078
+
1079
+ g_videoInput = nil;
1080
+ NSString *lastRejection = nil;
1081
+ for (NSUInteger tier = 0; tier < settingsTiers.count; tier++) {
1082
+ @try {
1083
+ g_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
1084
+ outputSettings:settingsTiers[tier]];
1085
+ } @catch (NSException *exception) {
1086
+ g_videoInput = nil;
1087
+ lastRejection = exception.reason;
1088
+ NSLog(@"⚠️ Video encoder settings tier %lu rejected: %@", (unsigned long)tier, exception.reason);
1089
+ continue;
1090
+ }
1091
+ if (g_videoInput) {
1092
+ if (tier > 0) {
1093
+ NSLog(@"⚠️ Video encoder fell back to settings tier %lu (%ldx%ld); last rejection: %@",
1094
+ (unsigned long)tier, (long)width, (long)height, lastRejection);
1095
+ }
1096
+ break;
1097
+ }
1098
+ }
1099
+ if (!g_videoInput) {
1100
+ MRLog(@"❌ Video writer failed: no accepted encoder settings (%@)", lastRejection ?: @"unknown");
1101
+ if (error) {
1102
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-101 userInfo:@{
1103
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No accepted H.264 encoder settings: %@",
1104
+ lastRejection ?: @"unknown"]
1105
+ }];
1106
+ }
1107
+ return NO;
1108
+ }
986
1109
  g_videoInput.expectsMediaDataInRealTime = YES;
987
1110
 
988
1111
  AVAssetWriterInputPixelBufferAdaptor *pixelAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:g_videoInput sourcePixelBufferAttributes:@{
@@ -1050,7 +1173,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1050
1173
  if (![audioPath.pathExtension.lowercaseString isEqualToString:@"mov"]) {
1051
1174
  MRLog(@"⚠️ Audio path has wrong extension '%@', changing to .mov", audioPath.pathExtension);
1052
1175
  audioPath = [[audioPath stringByDeletingPathExtension] stringByAppendingPathExtension:@"mov"];
1053
- g_audioOutputPath = audioPath;
1176
+ SCKSetOwnedString(&g_audioOutputPath, audioPath);
1054
1177
  }
1055
1178
  audioURL = [NSURL fileURLWithPath:audioPath];
1056
1179
  [[NSFileManager defaultManager] removeItemAtURL:audioURL error:nil];
@@ -1207,6 +1330,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1207
1330
  // yapiliyordu. Artik sonucu saklayip kayitta yeniden kullaniyoruz,
1208
1331
  // yani bu adim tamamen atlanabiliyor.
1209
1332
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
1333
+ @try {
1210
1334
  CFAbsoluteTime fetchStart = CFAbsoluteTimeGetCurrent();
1211
1335
  [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
1212
1336
  if (contentError || !content) {
@@ -1219,6 +1343,9 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1219
1343
  (unsigned long)content.displays.count,
1220
1344
  (unsigned long)content.windows.count);
1221
1345
  }];
1346
+ } @catch (NSException *exception) {
1347
+ NSLog(@"[Recorder] Prewarm failed safely: %@", exception.reason);
1348
+ }
1222
1349
  });
1223
1350
  }
1224
1351
  }
@@ -1231,19 +1358,22 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1231
1358
  NSDictionary *configCopy = [config copy];
1232
1359
  dispatch_queue_t controlQueue = ScreenCaptureControlQueue();
1233
1360
  __block BOOL accepted = NO;
1361
+ __block uint64_t generation = 0;
1234
1362
 
1235
1363
  dispatch_sync(controlQueue, ^{
1236
1364
  if (g_isRecording || g_isCleaningUp || g_isScheduling) {
1237
- MRLog(@"⚠️ ScreenCaptureKit busy (recording:%d cleaning:%d scheduling:%d)", g_isRecording, g_isCleaningUp, g_isScheduling);
1365
+ MRLog(@"⚠️ ScreenCaptureKit busy (recording:%d cleaning:%d scheduling:%d)", g_isRecording.load(), g_isCleaningUp.load(), g_isScheduling.load());
1238
1366
  accepted = NO;
1239
1367
  return;
1240
1368
  }
1241
1369
  g_isCleaningUp = NO;
1242
1370
  g_isScheduling = YES;
1371
+ generation = ++g_schedulingGeneration;
1243
1372
  accepted = YES;
1244
1373
  });
1245
1374
 
1246
1375
  if (!accepted) {
1376
+ [configCopy release];
1247
1377
  return NO;
1248
1378
  }
1249
1379
 
@@ -1255,8 +1385,9 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1255
1385
  if (prewarmed) {
1256
1386
  NSLog(@"⚡ Prewarmed shareable content kullanildi — envanter cagrisi atlandi");
1257
1387
  dispatch_async(controlQueue, ^{
1258
- SCKPerformRecordingSetup(configCopy, prewarmed);
1388
+ SCKPerformRecordingSetup(configCopy, prewarmed, generation);
1259
1389
  });
1390
+ [configCopy release];
1260
1391
  // NOT: Burada onbellegi tazelemek YOK.
1261
1392
  // Kayit setup'i calisirken yeni bir SCShareableContent istegi
1262
1393
  // baslatmak sistem envanterini kayitla ayni anda sorgular ve
@@ -1271,115 +1402,36 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1271
1402
  NSLog(@"🚀 Requesting shareable content...");
1272
1403
  CFAbsoluteTime contentFetchStart = CFAbsoluteTimeGetCurrent();
1273
1404
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
1274
- [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
1275
- if (contentError || !content) {
1276
- NSLog(@"❌ Content error: %@", contentError);
1277
- SCKFailScheduling();
1278
- return;
1279
- }
1280
- NSLog(@"✅ Got shareable content in %.0fms, starting recording setup...",
1281
- (CFAbsoluteTimeGetCurrent() - contentFetchStart) * 1000.0);
1405
+ @try {
1406
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
1407
+ dispatch_async(controlQueue, ^{
1408
+ if (generation != g_schedulingGeneration) return;
1409
+ if (contentError || !content) {
1410
+ NSLog(@"[Recorder] Content error: %@", contentError);
1411
+ SCKFailScheduling();
1412
+ return;
1413
+ }
1414
+ SCKPerformRecordingSetup(configCopy, content, generation);
1415
+ });
1416
+ }];
1417
+ } @catch (NSException *exception) {
1418
+ NSLog(@"[Recorder] Content discovery failed safely: %@", exception.reason);
1282
1419
  dispatch_async(controlQueue, ^{
1283
- SCKPerformRecordingSetup(configCopy, content);
1420
+ if (generation == g_schedulingGeneration) SCKFailScheduling();
1284
1421
  });
1285
- }];
1422
+ }
1286
1423
  });
1424
+ [configCopy release];
1287
1425
 
1288
1426
  return YES;
1289
1427
  }
1290
1428
 
1291
1429
  + (void)stopRecording {
1292
- if (!g_isRecording || !g_stream || g_isCleaningUp) {
1293
- NSLog(@"⚠️ Cannot stop: recording=%d stream=%@ cleaning=%d", g_isRecording, g_stream, g_isCleaningUp);
1294
- SCKMarkSchedulingComplete();
1295
- return;
1296
- }
1297
-
1298
- MRLog(@"🛑 Stopping pure ScreenCaptureKit recording");
1299
-
1300
- // CRITICAL FIX: Set cleanup flag IMMEDIATELY to prevent race conditions
1301
- // This prevents startRecording from being called while stop is in progress
1302
- @synchronized([ScreenCaptureKitRecorder class]) {
1303
- g_isCleaningUp = YES;
1304
- }
1305
-
1306
- // Store stream reference to prevent it from being deallocated
1307
- SCStream *streamToStop = g_stream;
1308
-
1309
- // ELECTRON FIX: Stop FULLY ASYNCHRONOUSLY - NO blocking, NO semaphores
1310
- [streamToStop stopCaptureWithCompletionHandler:^(NSError *stopError) {
1311
- @autoreleasepool {
1312
- if (stopError) {
1313
- NSLog(@"❌ Stop error: %@", stopError);
1314
- } else {
1315
- MRLog(@"✅ Pure stream stopped");
1316
- }
1317
-
1318
- // Reset recording state to allow new recordings
1319
- @synchronized([ScreenCaptureKitRecorder class]) {
1320
- g_isRecording = NO;
1321
- g_isCleaningUp = NO; // CRITICAL: Reset cleanup flag when done
1322
- }
1323
-
1324
- // Cleanup after stop completes
1325
- CleanupWriters();
1326
- [ScreenCaptureKitRecorder cleanupVideoWriter];
1327
-
1328
- // Post-process: mix (if enabled) then mux audio into video file
1329
- if (g_shouldCaptureAudio && g_audioOutputPath) {
1330
- NSString *primaryAudioPath = ScreenCaptureKitCurrentAudioPath();
1331
- if ([primaryAudioPath isKindOfClass:[NSArray class]]) {
1332
- id first = [(NSArray *)primaryAudioPath firstObject];
1333
- if ([first isKindOfClass:[NSString class]]) {
1334
- primaryAudioPath = (NSString *)first;
1335
- } else {
1336
- primaryAudioPath = nil;
1337
- }
1338
- }
1339
- if (primaryAudioPath && [primaryAudioPath length] > 0) {
1340
- BOOL preferInternal = NO;
1341
- if (@available(macOS 15.0, *)) {
1342
- preferInternal = (g_captureSystemAudioEnabled && g_captureMicrophoneEnabled);
1343
- }
1344
- NSString *externalMicPath = nil;
1345
- if (currentStandaloneAudioRecordingPath) {
1346
- externalMicPath = currentStandaloneAudioRecordingPath();
1347
- }
1348
- if (!externalMicPath || [externalMicPath length] == 0) {
1349
- if (lastStandaloneAudioRecordingPath) {
1350
- externalMicPath = lastStandaloneAudioRecordingPath();
1351
- }
1352
- }
1353
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
1354
- NSString *audioForMux = primaryAudioPath;
1355
- if (g_mixAudioEnabled) {
1356
- BOOL mixed = NO;
1357
- // Try gain-aware mix first
1358
- mixed = MRMixAudioToSingleTrackWithGains(primaryAudioPath, externalMicPath, preferInternal, g_mixMicGain, g_mixSystemGain);
1359
- if (!mixed) {
1360
- mixed = MRMixAudioToSingleTrack(primaryAudioPath, externalMicPath, preferInternal);
1361
- }
1362
- if (mixed) {
1363
- MRLog(@"🎧 Post-mix completed: %@", primaryAudioPath);
1364
- } else {
1365
- MRLog(@"ℹ️ Post-mix skipped or failed; proceeding to mux");
1366
- }
1367
- }
1368
- if (g_outputPath && [g_outputPath length] > 0) {
1369
- BOOL muxed = MRMuxAudioIntoVideo(g_outputPath, audioForMux);
1370
- if (muxed) {
1371
- MRLog(@"🔗 Muxed audio into video: %@", g_outputPath);
1372
- } else {
1373
- MRLog(@"⚠️ Failed to mux audio into video %@", g_outputPath);
1374
- }
1375
- }
1376
- });
1377
- }
1378
- }
1430
+ dispatch_sync(ScreenCaptureControlQueue(), ^{ SCKRequestStop(nil); });
1431
+ }
1379
1432
 
1380
- SCKMarkSchedulingComplete();
1381
- }
1382
- }];
1433
+ + (BOOL)isScheduling {
1434
+ return g_isScheduling;
1383
1435
  }
1384
1436
 
1385
1437
  + (BOOL)isRecording {
@@ -1450,15 +1502,19 @@ BOOL isScreenCaptureKitCleaningUp() API_AVAILABLE(macos(12.3)) {
1450
1502
 
1451
1503
  // Clean up in proper order to prevent crashes
1452
1504
  if (g_stream) {
1505
+ [g_stream release];
1453
1506
  g_stream = nil;
1454
1507
  MRLog(@"✅ Stream reference cleared");
1455
1508
  }
1456
1509
 
1457
1510
  if (g_streamDelegate) {
1511
+ [(id)g_streamDelegate release];
1458
1512
  g_streamDelegate = nil;
1459
1513
  MRLog(@"✅ Stream delegate reference cleared");
1460
1514
  }
1461
1515
 
1516
+ [g_videoStreamOutput release];
1517
+ [g_audioStreamOutput release];
1462
1518
  g_videoStreamOutput = nil;
1463
1519
  g_audioStreamOutput = nil;
1464
1520
  g_videoQueue = nil;
@@ -1467,14 +1523,15 @@ BOOL isScreenCaptureKitCleaningUp() API_AVAILABLE(macos(12.3)) {
1467
1523
  CFRelease(g_pixelBufferAdaptorRef);
1468
1524
  g_pixelBufferAdaptorRef = NULL;
1469
1525
  }
1470
- g_audioOutputPath = nil;
1526
+ SCKSetOwnedString(&g_audioOutputPath, nil);
1471
1527
  g_shouldCaptureAudio = NO;
1472
1528
  g_captureMicrophoneEnabled = NO;
1473
1529
  g_captureSystemAudioEnabled = NO;
1530
+ g_captureCameraEnabled = NO;
1474
1531
 
1475
1532
  g_isRecording = NO;
1476
1533
  g_isCleaningUp = NO; // Reset cleanup flag
1477
- g_outputPath = nil;
1534
+ SCKSetOwnedString(&g_outputPath, nil);
1478
1535
 
1479
1536
  // ELECTRON FIX: Reset frame tracking
1480
1537
  g_firstFrameReceived = NO;
@@ -1499,12 +1556,13 @@ static void SCKMarkSchedulingComplete(void) {
1499
1556
  }
1500
1557
 
1501
1558
  static void SCKFailScheduling(void) {
1502
- g_isScheduling = NO;
1503
- g_isRecording = NO;
1559
+ SCKRequestStop(nil);
1504
1560
  }
1505
1561
 
1506
- static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *content) API_AVAILABLE(macos(12.3)) {
1562
+ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *content, uint64_t generation) API_AVAILABLE(macos(12.3)) {
1507
1563
  @autoreleasepool {
1564
+ @try {
1565
+ if (generation != g_schedulingGeneration) return;
1508
1566
  if (!config || !content) {
1509
1567
  SCKFailScheduling();
1510
1568
  return;
@@ -1521,7 +1579,7 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1521
1579
  SCKFailScheduling();
1522
1580
  return;
1523
1581
  }
1524
- g_outputPath = outputPath;
1582
+ SCKSetOwnedString(&g_outputPath, outputPath);
1525
1583
 
1526
1584
  NSNumber *displayId = config[@"displayId"];
1527
1585
  NSNumber *windowId = config[@"windowId"];
@@ -1547,7 +1605,7 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1547
1605
  if (g_mixSystemGain < 0.f) g_mixSystemGain = 0.f;
1548
1606
  if (g_mixSystemGain > 2.f) g_mixSystemGain = 2.f;
1549
1607
  }
1550
- g_qualityPreset = SCKNormalizeQualityPreset(config[@"quality"]);
1608
+ SCKSetOwnedString(&g_qualityPreset, SCKNormalizeQualityPreset(config[@"quality"]));
1551
1609
  MRLog(@"🎚️ Requested quality preset: %@", g_qualityPreset);
1552
1610
  NSNumber *captureCamera = config[@"captureCamera"];
1553
1611
 
@@ -1786,6 +1844,18 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1786
1844
  (long)recordingHeight);
1787
1845
  }
1788
1846
 
1847
+ // H.264 4:2:0 luma duzlemi CIFT boyut ister; tek sayili genislik/yukseklik
1848
+ // encoder'i gereksiz yere yazilim yoluna itebiliyor. 1x harici ekranda
1849
+ // pencere boyutlari dogrudan piksele esitlendigi icin (Retina'da 2x ile
1850
+ // her zaman cift olur) tek sayilar yalnizca orada ortaya cikiyor.
1851
+ // En fazla 1 piksel kirpilir; olcek/konum degismez.
1852
+ // NOT: bu tek basina "bos kayit" hatasini cozmez — asil koruma asagidaki
1853
+ // kademeli encoder ayari geri dususudur.
1854
+ if (recordingWidth % 2 != 0) recordingWidth -= 1;
1855
+ if (recordingHeight % 2 != 0) recordingHeight -= 1;
1856
+ recordingWidth = MAX(2, recordingWidth);
1857
+ recordingHeight = MAX(2, recordingHeight);
1858
+
1789
1859
  SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
1790
1860
  streamConfig.width = recordingWidth;
1791
1861
  streamConfig.height = recordingHeight;
@@ -1822,12 +1892,13 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1822
1892
  g_shouldCaptureAudio = shouldCaptureSystemAudio || shouldCaptureMic;
1823
1893
  g_captureMicrophoneEnabled = shouldCaptureMic;
1824
1894
  g_captureSystemAudioEnabled = shouldCaptureSystemAudio;
1895
+ g_captureCameraEnabled = captureCamera ? [captureCamera boolValue] : NO;
1825
1896
 
1826
1897
  if (audioOutputPath && ![audioOutputPath isKindOfClass:[NSString class]]) {
1827
1898
  MRLog(@"⚠️ audioOutputPath type mismatch: %@, converting...", NSStringFromClass([audioOutputPath class]));
1828
- g_audioOutputPath = nil;
1899
+ SCKSetOwnedString(&g_audioOutputPath, nil);
1829
1900
  } else {
1830
- g_audioOutputPath = audioOutputPath;
1901
+ SCKSetOwnedString(&g_audioOutputPath, audioOutputPath);
1831
1902
  }
1832
1903
 
1833
1904
  if (g_shouldCaptureAudio && (!g_audioOutputPath || [g_audioOutputPath length] == 0)) {
@@ -1910,7 +1981,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1910
1981
  if (![ScreenCaptureKitRecorder prepareVideoWriterWithWidth:recordingWidth height:recordingHeight error:&writerError]) {
1911
1982
  NSLog(@"❌ Failed to prepare video writer: %@", writerError);
1912
1983
  SCKFailScheduling();
1913
- CleanupWriters();
1914
1984
  return;
1915
1985
  }
1916
1986
 
@@ -1927,7 +1997,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1927
1997
  g_stream = [[SCStream alloc] initWithFilter:filter configuration:streamConfig delegate:g_streamDelegate];
1928
1998
  if (!g_stream) {
1929
1999
  NSLog(@"❌ Failed to create pure stream");
1930
- CleanupWriters();
1931
2000
  SCKFailScheduling();
1932
2001
  return;
1933
2002
  }
@@ -1936,10 +2005,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1936
2005
  BOOL videoOutputAdded = [g_stream addStreamOutput:g_videoStreamOutput type:SCStreamOutputTypeScreen sampleHandlerQueue:g_videoQueue error:&outputError];
1937
2006
  if (!videoOutputAdded || outputError) {
1938
2007
  NSLog(@"❌ Failed to add video output: %@", outputError);
1939
- CleanupWriters();
1940
- @synchronized([ScreenCaptureKitRecorder class]) {
1941
- g_stream = nil;
1942
- }
1943
2008
  SCKFailScheduling();
1944
2009
  return;
1945
2010
  }
@@ -1959,8 +2024,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1959
2024
  error:&audioError];
1960
2025
  if (!micAdded || audioError) {
1961
2026
  NSLog(@"❌ Failed to add microphone output: %@", audioError);
1962
- CleanupWriters();
1963
- @synchronized([ScreenCaptureKitRecorder class]) { g_stream = nil; }
1964
2027
  SCKFailScheduling();
1965
2028
  return;
1966
2029
  }
@@ -1976,8 +2039,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1976
2039
  error:&audioError];
1977
2040
  if (!sysAdded || audioError) {
1978
2041
  NSLog(@"❌ Failed to add system audio output: %@", audioError);
1979
- CleanupWriters();
1980
- @synchronized([ScreenCaptureKitRecorder class]) { g_stream = nil; }
1981
2042
  SCKFailScheduling();
1982
2043
  return;
1983
2044
  }
@@ -1994,8 +2055,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1994
2055
  error:&audioError];
1995
2056
  if (!audAdded || audioError) {
1996
2057
  NSLog(@"❌ Failed to add audio output: %@", audioError);
1997
- CleanupWriters();
1998
- @synchronized([ScreenCaptureKitRecorder class]) { g_stream = nil; }
1999
2058
  SCKFailScheduling();
2000
2059
  return;
2001
2060
  }
@@ -2005,8 +2064,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
2005
2064
 
2006
2065
  if (!anyAudioAdded) {
2007
2066
  NSLog(@"❌ No audio outputs added (unexpected configuration)");
2008
- CleanupWriters();
2009
- @synchronized([ScreenCaptureKitRecorder class]) { g_stream = nil; }
2010
2067
  SCKFailScheduling();
2011
2068
  return;
2012
2069
  }
@@ -2024,18 +2081,22 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
2024
2081
  }
2025
2082
 
2026
2083
  NSLog(@"🚀 CALLING startCaptureWithCompletionHandler (async)...");
2027
- [g_stream startCaptureWithCompletionHandler:^(NSError *startError) {
2084
+ SCStream *startingStream = g_stream;
2085
+ [startingStream startCaptureWithCompletionHandler:^(NSError *startError) {
2028
2086
  dispatch_async(ScreenCaptureControlQueue(), ^{
2087
+ if (generation != g_schedulingGeneration || startingStream != g_stream) {
2088
+ // Cancellation can precede the asynchronous start reply.
2089
+ if (!startError) {
2090
+ @try { [startingStream stopCaptureWithCompletionHandler:^(NSError *error) {}]; }
2091
+ @catch (NSException *exception) { NSLog(@"[Recorder] Stale stream stop: %@", exception.reason); }
2092
+ }
2093
+ return;
2094
+ }
2029
2095
  if (startError) {
2030
2096
  NSLog(@"❌ Failed to start pure capture: %@", startError);
2031
2097
  NSLog(@"❌ Error domain: %@, code: %ld", startError.domain, (long)startError.code);
2032
2098
  NSLog(@"❌ Error userInfo: %@", startError.userInfo);
2033
- CleanupWriters();
2034
- @synchronized([ScreenCaptureKitRecorder class]) {
2035
- g_isRecording = NO;
2036
- g_stream = nil;
2037
- }
2038
- SCKFailScheduling();
2099
+ SCKFailScheduling();
2039
2100
  } else {
2040
2101
  NSLog(@"🎉 PURE ScreenCaptureKit recording started successfully!");
2041
2102
  NSLog(@"🎤 Audio capture enabled: %d (mic=%d, system=%d)", g_shouldCaptureAudio, g_captureMicrophoneEnabled, g_captureSystemAudioEnabled);
@@ -2046,5 +2107,9 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
2046
2107
  }
2047
2108
  });
2048
2109
  }];
2110
+ } @catch (NSException *exception) {
2111
+ NSLog(@"[Recorder] Capture setup failed safely: %@", exception.reason);
2112
+ SCKFailScheduling();
2113
+ }
2049
2114
  }
2050
2115
  }