node-mac-recorder 2.24.10 → 2.24.12
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/binding.gyp +2 -1
- package/index-multiprocess.js +50 -6
- package/index.js +154 -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 +45 -62
- package/src/camera_recorder.mm +69 -78
- package/src/ios_device_recorder.mm +651 -78
- 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 +175 -195
- 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
|
|
|
@@ -1123,7 +1173,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
1123
1173
|
if (![audioPath.pathExtension.lowercaseString isEqualToString:@"mov"]) {
|
|
1124
1174
|
MRLog(@"⚠️ Audio path has wrong extension '%@', changing to .mov", audioPath.pathExtension);
|
|
1125
1175
|
audioPath = [[audioPath stringByDeletingPathExtension] stringByAppendingPathExtension:@"mov"];
|
|
1126
|
-
g_audioOutputPath
|
|
1176
|
+
SCKSetOwnedString(&g_audioOutputPath, audioPath);
|
|
1127
1177
|
}
|
|
1128
1178
|
audioURL = [NSURL fileURLWithPath:audioPath];
|
|
1129
1179
|
[[NSFileManager defaultManager] removeItemAtURL:audioURL error:nil];
|
|
@@ -1280,6 +1330,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
1280
1330
|
// yapiliyordu. Artik sonucu saklayip kayitta yeniden kullaniyoruz,
|
|
1281
1331
|
// yani bu adim tamamen atlanabiliyor.
|
|
1282
1332
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
|
1333
|
+
@try {
|
|
1283
1334
|
CFAbsoluteTime fetchStart = CFAbsoluteTimeGetCurrent();
|
|
1284
1335
|
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
|
|
1285
1336
|
if (contentError || !content) {
|
|
@@ -1292,6 +1343,9 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
1292
1343
|
(unsigned long)content.displays.count,
|
|
1293
1344
|
(unsigned long)content.windows.count);
|
|
1294
1345
|
}];
|
|
1346
|
+
} @catch (NSException *exception) {
|
|
1347
|
+
NSLog(@"[Recorder] Prewarm failed safely: %@", exception.reason);
|
|
1348
|
+
}
|
|
1295
1349
|
});
|
|
1296
1350
|
}
|
|
1297
1351
|
}
|
|
@@ -1304,19 +1358,22 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
1304
1358
|
NSDictionary *configCopy = [config copy];
|
|
1305
1359
|
dispatch_queue_t controlQueue = ScreenCaptureControlQueue();
|
|
1306
1360
|
__block BOOL accepted = NO;
|
|
1361
|
+
__block uint64_t generation = 0;
|
|
1307
1362
|
|
|
1308
1363
|
dispatch_sync(controlQueue, ^{
|
|
1309
1364
|
if (g_isRecording || g_isCleaningUp || g_isScheduling) {
|
|
1310
|
-
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());
|
|
1311
1366
|
accepted = NO;
|
|
1312
1367
|
return;
|
|
1313
1368
|
}
|
|
1314
1369
|
g_isCleaningUp = NO;
|
|
1315
1370
|
g_isScheduling = YES;
|
|
1371
|
+
generation = ++g_schedulingGeneration;
|
|
1316
1372
|
accepted = YES;
|
|
1317
1373
|
});
|
|
1318
1374
|
|
|
1319
1375
|
if (!accepted) {
|
|
1376
|
+
[configCopy release];
|
|
1320
1377
|
return NO;
|
|
1321
1378
|
}
|
|
1322
1379
|
|
|
@@ -1328,8 +1385,9 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
1328
1385
|
if (prewarmed) {
|
|
1329
1386
|
NSLog(@"⚡ Prewarmed shareable content kullanildi — envanter cagrisi atlandi");
|
|
1330
1387
|
dispatch_async(controlQueue, ^{
|
|
1331
|
-
SCKPerformRecordingSetup(configCopy, prewarmed);
|
|
1388
|
+
SCKPerformRecordingSetup(configCopy, prewarmed, generation);
|
|
1332
1389
|
});
|
|
1390
|
+
[configCopy release];
|
|
1333
1391
|
// NOT: Burada onbellegi tazelemek YOK.
|
|
1334
1392
|
// Kayit setup'i calisirken yeni bir SCShareableContent istegi
|
|
1335
1393
|
// baslatmak sistem envanterini kayitla ayni anda sorgular ve
|
|
@@ -1344,115 +1402,36 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
1344
1402
|
NSLog(@"🚀 Requesting shareable content...");
|
|
1345
1403
|
CFAbsoluteTime contentFetchStart = CFAbsoluteTimeGetCurrent();
|
|
1346
1404
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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);
|
|
1355
1419
|
dispatch_async(controlQueue, ^{
|
|
1356
|
-
|
|
1420
|
+
if (generation == g_schedulingGeneration) SCKFailScheduling();
|
|
1357
1421
|
});
|
|
1358
|
-
}
|
|
1422
|
+
}
|
|
1359
1423
|
});
|
|
1424
|
+
[configCopy release];
|
|
1360
1425
|
|
|
1361
1426
|
return YES;
|
|
1362
1427
|
}
|
|
1363
1428
|
|
|
1364
1429
|
+ (void)stopRecording {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
SCKMarkSchedulingComplete();
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
MRLog(@"🛑 Stopping pure ScreenCaptureKit recording");
|
|
1372
|
-
|
|
1373
|
-
// CRITICAL FIX: Set cleanup flag IMMEDIATELY to prevent race conditions
|
|
1374
|
-
// This prevents startRecording from being called while stop is in progress
|
|
1375
|
-
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
1376
|
-
g_isCleaningUp = YES;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// Store stream reference to prevent it from being deallocated
|
|
1380
|
-
SCStream *streamToStop = g_stream;
|
|
1381
|
-
|
|
1382
|
-
// ELECTRON FIX: Stop FULLY ASYNCHRONOUSLY - NO blocking, NO semaphores
|
|
1383
|
-
[streamToStop stopCaptureWithCompletionHandler:^(NSError *stopError) {
|
|
1384
|
-
@autoreleasepool {
|
|
1385
|
-
if (stopError) {
|
|
1386
|
-
NSLog(@"❌ Stop error: %@", stopError);
|
|
1387
|
-
} else {
|
|
1388
|
-
MRLog(@"✅ Pure stream stopped");
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
// Reset recording state to allow new recordings
|
|
1392
|
-
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
1393
|
-
g_isRecording = NO;
|
|
1394
|
-
g_isCleaningUp = NO; // CRITICAL: Reset cleanup flag when done
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// Cleanup after stop completes
|
|
1398
|
-
CleanupWriters();
|
|
1399
|
-
[ScreenCaptureKitRecorder cleanupVideoWriter];
|
|
1400
|
-
|
|
1401
|
-
// Post-process: mix (if enabled) then mux audio into video file
|
|
1402
|
-
if (g_shouldCaptureAudio && g_audioOutputPath) {
|
|
1403
|
-
NSString *primaryAudioPath = ScreenCaptureKitCurrentAudioPath();
|
|
1404
|
-
if ([primaryAudioPath isKindOfClass:[NSArray class]]) {
|
|
1405
|
-
id first = [(NSArray *)primaryAudioPath firstObject];
|
|
1406
|
-
if ([first isKindOfClass:[NSString class]]) {
|
|
1407
|
-
primaryAudioPath = (NSString *)first;
|
|
1408
|
-
} else {
|
|
1409
|
-
primaryAudioPath = nil;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
if (primaryAudioPath && [primaryAudioPath length] > 0) {
|
|
1413
|
-
BOOL preferInternal = NO;
|
|
1414
|
-
if (@available(macOS 15.0, *)) {
|
|
1415
|
-
preferInternal = (g_captureSystemAudioEnabled && g_captureMicrophoneEnabled);
|
|
1416
|
-
}
|
|
1417
|
-
NSString *externalMicPath = nil;
|
|
1418
|
-
if (currentStandaloneAudioRecordingPath) {
|
|
1419
|
-
externalMicPath = currentStandaloneAudioRecordingPath();
|
|
1420
|
-
}
|
|
1421
|
-
if (!externalMicPath || [externalMicPath length] == 0) {
|
|
1422
|
-
if (lastStandaloneAudioRecordingPath) {
|
|
1423
|
-
externalMicPath = lastStandaloneAudioRecordingPath();
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
|
1427
|
-
NSString *audioForMux = primaryAudioPath;
|
|
1428
|
-
if (g_mixAudioEnabled) {
|
|
1429
|
-
BOOL mixed = NO;
|
|
1430
|
-
// Try gain-aware mix first
|
|
1431
|
-
mixed = MRMixAudioToSingleTrackWithGains(primaryAudioPath, externalMicPath, preferInternal, g_mixMicGain, g_mixSystemGain);
|
|
1432
|
-
if (!mixed) {
|
|
1433
|
-
mixed = MRMixAudioToSingleTrack(primaryAudioPath, externalMicPath, preferInternal);
|
|
1434
|
-
}
|
|
1435
|
-
if (mixed) {
|
|
1436
|
-
MRLog(@"🎧 Post-mix completed: %@", primaryAudioPath);
|
|
1437
|
-
} else {
|
|
1438
|
-
MRLog(@"ℹ️ Post-mix skipped or failed; proceeding to mux");
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
if (g_outputPath && [g_outputPath length] > 0) {
|
|
1442
|
-
BOOL muxed = MRMuxAudioIntoVideo(g_outputPath, audioForMux);
|
|
1443
|
-
if (muxed) {
|
|
1444
|
-
MRLog(@"🔗 Muxed audio into video: %@", g_outputPath);
|
|
1445
|
-
} else {
|
|
1446
|
-
MRLog(@"⚠️ Failed to mux audio into video %@", g_outputPath);
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1430
|
+
dispatch_sync(ScreenCaptureControlQueue(), ^{ SCKRequestStop(nil); });
|
|
1431
|
+
}
|
|
1452
1432
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
}];
|
|
1433
|
+
+ (BOOL)isScheduling {
|
|
1434
|
+
return g_isScheduling;
|
|
1456
1435
|
}
|
|
1457
1436
|
|
|
1458
1437
|
+ (BOOL)isRecording {
|
|
@@ -1523,15 +1502,19 @@ BOOL isScreenCaptureKitCleaningUp() API_AVAILABLE(macos(12.3)) {
|
|
|
1523
1502
|
|
|
1524
1503
|
// Clean up in proper order to prevent crashes
|
|
1525
1504
|
if (g_stream) {
|
|
1505
|
+
[g_stream release];
|
|
1526
1506
|
g_stream = nil;
|
|
1527
1507
|
MRLog(@"✅ Stream reference cleared");
|
|
1528
1508
|
}
|
|
1529
1509
|
|
|
1530
1510
|
if (g_streamDelegate) {
|
|
1511
|
+
[(id)g_streamDelegate release];
|
|
1531
1512
|
g_streamDelegate = nil;
|
|
1532
1513
|
MRLog(@"✅ Stream delegate reference cleared");
|
|
1533
1514
|
}
|
|
1534
1515
|
|
|
1516
|
+
[g_videoStreamOutput release];
|
|
1517
|
+
[g_audioStreamOutput release];
|
|
1535
1518
|
g_videoStreamOutput = nil;
|
|
1536
1519
|
g_audioStreamOutput = nil;
|
|
1537
1520
|
g_videoQueue = nil;
|
|
@@ -1540,14 +1523,15 @@ BOOL isScreenCaptureKitCleaningUp() API_AVAILABLE(macos(12.3)) {
|
|
|
1540
1523
|
CFRelease(g_pixelBufferAdaptorRef);
|
|
1541
1524
|
g_pixelBufferAdaptorRef = NULL;
|
|
1542
1525
|
}
|
|
1543
|
-
g_audioOutputPath
|
|
1526
|
+
SCKSetOwnedString(&g_audioOutputPath, nil);
|
|
1544
1527
|
g_shouldCaptureAudio = NO;
|
|
1545
1528
|
g_captureMicrophoneEnabled = NO;
|
|
1546
1529
|
g_captureSystemAudioEnabled = NO;
|
|
1530
|
+
g_captureCameraEnabled = NO;
|
|
1547
1531
|
|
|
1548
1532
|
g_isRecording = NO;
|
|
1549
1533
|
g_isCleaningUp = NO; // Reset cleanup flag
|
|
1550
|
-
g_outputPath
|
|
1534
|
+
SCKSetOwnedString(&g_outputPath, nil);
|
|
1551
1535
|
|
|
1552
1536
|
// ELECTRON FIX: Reset frame tracking
|
|
1553
1537
|
g_firstFrameReceived = NO;
|
|
@@ -1572,12 +1556,13 @@ static void SCKMarkSchedulingComplete(void) {
|
|
|
1572
1556
|
}
|
|
1573
1557
|
|
|
1574
1558
|
static void SCKFailScheduling(void) {
|
|
1575
|
-
|
|
1576
|
-
g_isRecording = NO;
|
|
1559
|
+
SCKRequestStop(nil);
|
|
1577
1560
|
}
|
|
1578
1561
|
|
|
1579
|
-
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)) {
|
|
1580
1563
|
@autoreleasepool {
|
|
1564
|
+
@try {
|
|
1565
|
+
if (generation != g_schedulingGeneration) return;
|
|
1581
1566
|
if (!config || !content) {
|
|
1582
1567
|
SCKFailScheduling();
|
|
1583
1568
|
return;
|
|
@@ -1594,7 +1579,7 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
1594
1579
|
SCKFailScheduling();
|
|
1595
1580
|
return;
|
|
1596
1581
|
}
|
|
1597
|
-
g_outputPath
|
|
1582
|
+
SCKSetOwnedString(&g_outputPath, outputPath);
|
|
1598
1583
|
|
|
1599
1584
|
NSNumber *displayId = config[@"displayId"];
|
|
1600
1585
|
NSNumber *windowId = config[@"windowId"];
|
|
@@ -1620,7 +1605,7 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
1620
1605
|
if (g_mixSystemGain < 0.f) g_mixSystemGain = 0.f;
|
|
1621
1606
|
if (g_mixSystemGain > 2.f) g_mixSystemGain = 2.f;
|
|
1622
1607
|
}
|
|
1623
|
-
g_qualityPreset
|
|
1608
|
+
SCKSetOwnedString(&g_qualityPreset, SCKNormalizeQualityPreset(config[@"quality"]));
|
|
1624
1609
|
MRLog(@"🎚️ Requested quality preset: %@", g_qualityPreset);
|
|
1625
1610
|
NSNumber *captureCamera = config[@"captureCamera"];
|
|
1626
1611
|
|
|
@@ -1907,12 +1892,13 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
1907
1892
|
g_shouldCaptureAudio = shouldCaptureSystemAudio || shouldCaptureMic;
|
|
1908
1893
|
g_captureMicrophoneEnabled = shouldCaptureMic;
|
|
1909
1894
|
g_captureSystemAudioEnabled = shouldCaptureSystemAudio;
|
|
1895
|
+
g_captureCameraEnabled = captureCamera ? [captureCamera boolValue] : NO;
|
|
1910
1896
|
|
|
1911
1897
|
if (audioOutputPath && ![audioOutputPath isKindOfClass:[NSString class]]) {
|
|
1912
1898
|
MRLog(@"⚠️ audioOutputPath type mismatch: %@, converting...", NSStringFromClass([audioOutputPath class]));
|
|
1913
|
-
g_audioOutputPath
|
|
1899
|
+
SCKSetOwnedString(&g_audioOutputPath, nil);
|
|
1914
1900
|
} else {
|
|
1915
|
-
g_audioOutputPath
|
|
1901
|
+
SCKSetOwnedString(&g_audioOutputPath, audioOutputPath);
|
|
1916
1902
|
}
|
|
1917
1903
|
|
|
1918
1904
|
if (g_shouldCaptureAudio && (!g_audioOutputPath || [g_audioOutputPath length] == 0)) {
|
|
@@ -1995,7 +1981,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
1995
1981
|
if (![ScreenCaptureKitRecorder prepareVideoWriterWithWidth:recordingWidth height:recordingHeight error:&writerError]) {
|
|
1996
1982
|
NSLog(@"❌ Failed to prepare video writer: %@", writerError);
|
|
1997
1983
|
SCKFailScheduling();
|
|
1998
|
-
CleanupWriters();
|
|
1999
1984
|
return;
|
|
2000
1985
|
}
|
|
2001
1986
|
|
|
@@ -2012,7 +1997,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
2012
1997
|
g_stream = [[SCStream alloc] initWithFilter:filter configuration:streamConfig delegate:g_streamDelegate];
|
|
2013
1998
|
if (!g_stream) {
|
|
2014
1999
|
NSLog(@"❌ Failed to create pure stream");
|
|
2015
|
-
CleanupWriters();
|
|
2016
2000
|
SCKFailScheduling();
|
|
2017
2001
|
return;
|
|
2018
2002
|
}
|
|
@@ -2021,10 +2005,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
2021
2005
|
BOOL videoOutputAdded = [g_stream addStreamOutput:g_videoStreamOutput type:SCStreamOutputTypeScreen sampleHandlerQueue:g_videoQueue error:&outputError];
|
|
2022
2006
|
if (!videoOutputAdded || outputError) {
|
|
2023
2007
|
NSLog(@"❌ Failed to add video output: %@", outputError);
|
|
2024
|
-
CleanupWriters();
|
|
2025
|
-
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
2026
|
-
g_stream = nil;
|
|
2027
|
-
}
|
|
2028
2008
|
SCKFailScheduling();
|
|
2029
2009
|
return;
|
|
2030
2010
|
}
|
|
@@ -2044,8 +2024,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
2044
2024
|
error:&audioError];
|
|
2045
2025
|
if (!micAdded || audioError) {
|
|
2046
2026
|
NSLog(@"❌ Failed to add microphone output: %@", audioError);
|
|
2047
|
-
CleanupWriters();
|
|
2048
|
-
@synchronized([ScreenCaptureKitRecorder class]) { g_stream = nil; }
|
|
2049
2027
|
SCKFailScheduling();
|
|
2050
2028
|
return;
|
|
2051
2029
|
}
|
|
@@ -2061,8 +2039,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
2061
2039
|
error:&audioError];
|
|
2062
2040
|
if (!sysAdded || audioError) {
|
|
2063
2041
|
NSLog(@"❌ Failed to add system audio output: %@", audioError);
|
|
2064
|
-
CleanupWriters();
|
|
2065
|
-
@synchronized([ScreenCaptureKitRecorder class]) { g_stream = nil; }
|
|
2066
2042
|
SCKFailScheduling();
|
|
2067
2043
|
return;
|
|
2068
2044
|
}
|
|
@@ -2079,8 +2055,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
2079
2055
|
error:&audioError];
|
|
2080
2056
|
if (!audAdded || audioError) {
|
|
2081
2057
|
NSLog(@"❌ Failed to add audio output: %@", audioError);
|
|
2082
|
-
CleanupWriters();
|
|
2083
|
-
@synchronized([ScreenCaptureKitRecorder class]) { g_stream = nil; }
|
|
2084
2058
|
SCKFailScheduling();
|
|
2085
2059
|
return;
|
|
2086
2060
|
}
|
|
@@ -2090,8 +2064,6 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
2090
2064
|
|
|
2091
2065
|
if (!anyAudioAdded) {
|
|
2092
2066
|
NSLog(@"❌ No audio outputs added (unexpected configuration)");
|
|
2093
|
-
CleanupWriters();
|
|
2094
|
-
@synchronized([ScreenCaptureKitRecorder class]) { g_stream = nil; }
|
|
2095
2067
|
SCKFailScheduling();
|
|
2096
2068
|
return;
|
|
2097
2069
|
}
|
|
@@ -2109,18 +2081,22 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
2109
2081
|
}
|
|
2110
2082
|
|
|
2111
2083
|
NSLog(@"🚀 CALLING startCaptureWithCompletionHandler (async)...");
|
|
2112
|
-
|
|
2084
|
+
SCStream *startingStream = g_stream;
|
|
2085
|
+
[startingStream startCaptureWithCompletionHandler:^(NSError *startError) {
|
|
2113
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
|
+
}
|
|
2114
2095
|
if (startError) {
|
|
2115
2096
|
NSLog(@"❌ Failed to start pure capture: %@", startError);
|
|
2116
2097
|
NSLog(@"❌ Error domain: %@, code: %ld", startError.domain, (long)startError.code);
|
|
2117
2098
|
NSLog(@"❌ Error userInfo: %@", startError.userInfo);
|
|
2118
|
-
|
|
2119
|
-
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
2120
|
-
g_isRecording = NO;
|
|
2121
|
-
g_stream = nil;
|
|
2122
|
-
}
|
|
2123
|
-
SCKFailScheduling();
|
|
2099
|
+
SCKFailScheduling();
|
|
2124
2100
|
} else {
|
|
2125
2101
|
NSLog(@"🎉 PURE ScreenCaptureKit recording started successfully!");
|
|
2126
2102
|
NSLog(@"🎤 Audio capture enabled: %d (mic=%d, system=%d)", g_shouldCaptureAudio, g_captureMicrophoneEnabled, g_captureSystemAudioEnabled);
|
|
@@ -2131,5 +2107,9 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
2131
2107
|
}
|
|
2132
2108
|
});
|
|
2133
2109
|
}];
|
|
2110
|
+
} @catch (NSException *exception) {
|
|
2111
|
+
NSLog(@"[Recorder] Capture setup failed safely: %@", exception.reason);
|
|
2112
|
+
SCKFailScheduling();
|
|
2113
|
+
}
|
|
2134
2114
|
}
|
|
2135
2115
|
}
|