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.
- package/MultiWindowRecorder.js +85 -4
- package/README.md +18 -1
- package/index-multiprocess.js +50 -6
- package/index.js +153 -41
- package/package.json +1 -1
- package/recorder-worker.js +159 -69
- package/recorder_runtime_safety.cjs +101 -0
- package/src/audio_recorder.mm +43 -43
- package/src/avfoundation_recorder.mm +103 -69
- package/src/camera_recorder.mm +69 -78
- package/src/ios_device_recorder.mm +121 -20
- package/src/keyboard_tracker.mm +8 -1
- package/src/mac_recorder.mm +84 -13
- package/src/recording_writer_safety.h +66 -0
- package/src/screen_capture_kit.h +4 -2
- package/src/screen_capture_kit.mm +264 -199
- package/src/sync_timeline.h +9 -0
- package/src/sync_timeline.mm +54 -0
|
@@ -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
|
|
136
|
-
static
|
|
137
|
-
static
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1899
|
+
SCKSetOwnedString(&g_audioOutputPath, nil);
|
|
1829
1900
|
} else {
|
|
1830
|
-
g_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
|
-
|
|
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
|
-
|
|
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
|
}
|