node-mac-recorder 2.20.17 → 2.21.1

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,17 +1,123 @@
1
1
  #import "screen_capture_kit.h"
2
2
  #import "logging.h"
3
+ #import <AVFoundation/AVFoundation.h>
4
+ #import <CoreVideo/CoreVideo.h>
5
+ #import <CoreMedia/CoreMedia.h>
6
+ #import <AudioToolbox/AudioToolbox.h>
3
7
 
4
8
  // Pure ScreenCaptureKit implementation - NO AVFoundation
5
9
  static SCStream * API_AVAILABLE(macos(12.3)) g_stream = nil;
6
- static SCRecordingOutput * API_AVAILABLE(macos(15.0)) g_recordingOutput = nil;
7
10
  static id<SCStreamDelegate> API_AVAILABLE(macos(12.3)) g_streamDelegate = nil;
8
11
  static BOOL g_isRecording = NO;
9
12
  static BOOL g_isCleaningUp = NO; // Prevent recursive cleanup
10
13
  static NSString *g_outputPath = nil;
11
14
 
15
+ static dispatch_queue_t g_videoQueue = nil;
16
+ static dispatch_queue_t g_audioQueue = nil;
17
+ static id g_videoStreamOutput = nil;
18
+ static id g_audioStreamOutput = nil;
19
+
20
+ static AVAssetWriter *g_videoWriter = nil;
21
+ static AVAssetWriterInput *g_videoInput = nil;
22
+ static CFTypeRef g_pixelBufferAdaptorRef = NULL;
23
+ static CMTime g_videoStartTime = kCMTimeInvalid;
24
+ static BOOL g_videoWriterStarted = NO;
25
+
26
+ static BOOL g_shouldCaptureAudio = NO;
27
+ static NSString *g_audioOutputPath = nil;
28
+ static AVAssetWriter *g_audioWriter = nil;
29
+ static AVAssetWriterInput *g_audioInput = nil;
30
+ static CMTime g_audioStartTime = kCMTimeInvalid;
31
+ static BOOL g_audioWriterStarted = NO;
32
+
33
+ static NSInteger g_configuredSampleRate = 48000;
34
+ static NSInteger g_configuredChannelCount = 2;
35
+
36
+ static void CleanupWriters(void);
37
+ static AVAssetWriterInputPixelBufferAdaptor * _Nullable CurrentPixelBufferAdaptor(void) {
38
+ if (!g_pixelBufferAdaptorRef) {
39
+ return nil;
40
+ }
41
+ return (__bridge AVAssetWriterInputPixelBufferAdaptor *)g_pixelBufferAdaptorRef;
42
+ }
43
+
44
+ static NSString *MRNormalizePath(id value) {
45
+ if (!value || value == (id)kCFNull) {
46
+ return nil;
47
+ }
48
+ if ([value isKindOfClass:[NSString class]]) {
49
+ return (NSString *)value;
50
+ }
51
+ if ([value isKindOfClass:[NSURL class]]) {
52
+ return [(NSURL *)value path];
53
+ }
54
+ if ([value isKindOfClass:[NSArray class]]) {
55
+ for (id entry in (NSArray *)value) {
56
+ NSString *candidate = MRNormalizePath(entry);
57
+ if (candidate.length > 0) {
58
+ return candidate;
59
+ }
60
+ }
61
+ }
62
+ return nil;
63
+ }
64
+
65
+ static void FinishWriter(AVAssetWriter *writer, AVAssetWriterInput *input) {
66
+ if (!writer) {
67
+ return;
68
+ }
69
+
70
+ if (input) {
71
+ [input markAsFinished];
72
+ }
73
+
74
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
75
+ [writer finishWritingWithCompletionHandler:^{
76
+ dispatch_semaphore_signal(semaphore);
77
+ }];
78
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
79
+ dispatch_semaphore_wait(semaphore, timeout);
80
+ }
81
+
82
+ static void CleanupWriters(void) {
83
+ if (g_videoWriter) {
84
+ FinishWriter(g_videoWriter, g_videoInput);
85
+ g_videoWriter = nil;
86
+ g_videoInput = nil;
87
+ if (g_pixelBufferAdaptorRef) {
88
+ CFRelease(g_pixelBufferAdaptorRef);
89
+ g_pixelBufferAdaptorRef = NULL;
90
+ }
91
+ g_videoWriterStarted = NO;
92
+ g_videoStartTime = kCMTimeInvalid;
93
+ }
94
+
95
+ if (g_audioWriter) {
96
+ FinishWriter(g_audioWriter, g_audioInput);
97
+ g_audioWriter = nil;
98
+ g_audioInput = nil;
99
+ g_audioWriterStarted = NO;
100
+ g_audioStartTime = kCMTimeInvalid;
101
+ }
102
+ }
103
+
12
104
  @interface PureScreenCaptureDelegate : NSObject <SCStreamDelegate>
13
105
  @end
14
106
 
107
+ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
108
+ if (!g_audioOutputPath) {
109
+ return nil;
110
+ }
111
+ if ([g_audioOutputPath isKindOfClass:[NSArray class]]) {
112
+ id first = [(NSArray *)g_audioOutputPath firstObject];
113
+ if ([first isKindOfClass:[NSString class]]) {
114
+ return first;
115
+ }
116
+ return nil;
117
+ }
118
+ return g_audioOutputPath;
119
+ }
120
+
15
121
  @implementation PureScreenCaptureDelegate
16
122
  - (void)stream:(SCStream * API_AVAILABLE(macos(12.3)))stream didStopWithError:(NSError *)error API_AVAILABLE(macos(12.3)) {
17
123
  MRLog(@"🛑 Pure ScreenCapture stream stopped");
@@ -39,8 +145,301 @@ static NSString *g_outputPath = nil;
39
145
  }
40
146
  @end
41
147
 
148
+ @interface ScreenCaptureKitRecorder (Private)
149
+ + (BOOL)prepareAudioWriterIfNeededWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;
150
+ @end
151
+
152
+ @interface ScreenCaptureVideoOutput : NSObject <SCStreamOutput>
153
+ @end
154
+
155
+ @implementation ScreenCaptureVideoOutput
156
+ - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type API_AVAILABLE(macos(12.3)) {
157
+ if (!g_isRecording || type != SCStreamOutputTypeScreen) {
158
+ return;
159
+ }
160
+
161
+ if (!CMSampleBufferDataIsReady(sampleBuffer)) {
162
+ return;
163
+ }
164
+
165
+ if (!g_videoWriter || !g_videoInput) {
166
+ return;
167
+ }
168
+
169
+ CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
170
+
171
+ if (!g_videoWriterStarted) {
172
+ if (![g_videoWriter startWriting]) {
173
+ NSLog(@"❌ ScreenCaptureKit video writer failed to start: %@", g_videoWriter.error);
174
+ return;
175
+ }
176
+ [g_videoWriter startSessionAtSourceTime:presentationTime];
177
+ g_videoStartTime = presentationTime;
178
+ g_videoWriterStarted = YES;
179
+ MRLog(@"🎞️ Video writer session started @ %.3f", CMTimeGetSeconds(presentationTime));
180
+ }
181
+
182
+ if (!g_videoInput.readyForMoreMediaData) {
183
+ return;
184
+ }
185
+
186
+ CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
187
+ if (!pixelBuffer) {
188
+ return;
189
+ }
190
+
191
+ AVAssetWriterInputPixelBufferAdaptor *adaptorCandidate = CurrentPixelBufferAdaptor();
192
+ if ([adaptorCandidate isKindOfClass:[NSArray class]]) {
193
+ id first = [(NSArray *)adaptorCandidate firstObject];
194
+ if ([first isKindOfClass:[AVAssetWriterInputPixelBufferAdaptor class]]) {
195
+ adaptorCandidate = first;
196
+ if (g_pixelBufferAdaptorRef) {
197
+ CFRelease(g_pixelBufferAdaptorRef);
198
+ }
199
+ g_pixelBufferAdaptorRef = CFBridgingRetain(adaptorCandidate);
200
+ }
201
+ }
202
+ if (![adaptorCandidate isKindOfClass:[AVAssetWriterInputPixelBufferAdaptor class]]) {
203
+ if (adaptorCandidate) {
204
+ MRLog(@"⚠️ Pixel buffer adaptor invalid (%@) – skipping frame", NSStringFromClass([adaptorCandidate class]));
205
+ }
206
+ NSLog(@"❌ Pixel buffer adaptor is nil – cannot append video frames");
207
+ return;
208
+ }
209
+
210
+ AVAssetWriterInputPixelBufferAdaptor *adaptor = adaptorCandidate;
211
+ BOOL appended = [adaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime];
212
+ if (!appended) {
213
+ NSLog(@"⚠️ Failed appending pixel buffer: %@", g_videoWriter.error);
214
+ }
215
+ }
216
+ @end
217
+
218
+ @interface ScreenCaptureAudioOutput : NSObject <SCStreamOutput>
219
+ @end
220
+
221
+ @implementation ScreenCaptureAudioOutput
222
+ - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type API_AVAILABLE(macos(12.3)) {
223
+ if (!g_isRecording || !g_shouldCaptureAudio) {
224
+ return;
225
+ }
226
+
227
+ if (@available(macOS 13.0, *)) {
228
+ if (type != SCStreamOutputTypeAudio) {
229
+ return;
230
+ }
231
+ } else {
232
+ return;
233
+ }
234
+
235
+ if (!CMSampleBufferDataIsReady(sampleBuffer)) {
236
+ return;
237
+ }
238
+
239
+ if (![ScreenCaptureKitRecorder prepareAudioWriterIfNeededWithSampleBuffer:sampleBuffer]) {
240
+ return;
241
+ }
242
+
243
+ if (!g_audioWriter || !g_audioInput) {
244
+ return;
245
+ }
246
+
247
+ CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
248
+
249
+ if (!g_audioWriterStarted) {
250
+ if (![g_audioWriter startWriting]) {
251
+ NSLog(@"❌ Audio writer failed to start: %@", g_audioWriter.error);
252
+ return;
253
+ }
254
+ [g_audioWriter startSessionAtSourceTime:presentationTime];
255
+ g_audioStartTime = presentationTime;
256
+ g_audioWriterStarted = YES;
257
+ MRLog(@"🔊 Audio writer session started @ %.3f", CMTimeGetSeconds(presentationTime));
258
+ }
259
+
260
+ if (!g_audioInput.readyForMoreMediaData) {
261
+ return;
262
+ }
263
+
264
+ if (![g_audioInput appendSampleBuffer:sampleBuffer]) {
265
+ NSLog(@"⚠️ Failed appending audio sample buffer: %@", g_audioWriter.error);
266
+ }
267
+ }
268
+ @end
269
+
42
270
  @implementation ScreenCaptureKitRecorder
43
271
 
272
+ + (BOOL)prepareVideoWriterWithWidth:(NSInteger)width height:(NSInteger)height error:(NSError **)error {
273
+ MRLog(@"🎬 Preparing video writer %ldx%ld", (long)width, (long)height);
274
+ if (!g_outputPath) {
275
+ MRLog(@"❌ Video writer failed: missing output path");
276
+ return NO;
277
+ }
278
+ if (width <= 0 || height <= 0) {
279
+ MRLog(@"❌ Video writer invalid dimensions %ldx%ld", (long)width, (long)height);
280
+ return NO;
281
+ }
282
+
283
+ NSURL *outputURL = [NSURL fileURLWithPath:g_outputPath];
284
+ [[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
285
+
286
+ g_videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:error];
287
+ if (!g_videoWriter || (error && *error)) {
288
+ MRLog(@"❌ Failed creating video writer: %@", error && *error ? (*error).localizedDescription : @"unknown");
289
+ return NO;
290
+ }
291
+
292
+ NSDictionary *compressionProps = @{
293
+ AVVideoAverageBitRateKey: @(width * height * 6),
294
+ AVVideoMaxKeyFrameIntervalKey: @30
295
+ };
296
+
297
+ NSDictionary *videoSettings = @{
298
+ AVVideoCodecKey: AVVideoCodecTypeH264,
299
+ AVVideoWidthKey: @(width),
300
+ AVVideoHeightKey: @(height),
301
+ AVVideoCompressionPropertiesKey: compressionProps
302
+ };
303
+
304
+ g_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
305
+ g_videoInput.expectsMediaDataInRealTime = YES;
306
+
307
+ AVAssetWriterInputPixelBufferAdaptor *pixelAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:g_videoInput sourcePixelBufferAttributes:@{
308
+ (NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
309
+ (NSString *)kCVPixelBufferWidthKey: @(width),
310
+ (NSString *)kCVPixelBufferHeightKey: @(height),
311
+ (NSString *)kCVPixelBufferCGImageCompatibilityKey: @YES,
312
+ (NSString *)kCVPixelBufferCGBitmapContextCompatibilityKey: @YES
313
+ }];
314
+
315
+ if (![g_videoWriter canAddInput:g_videoInput]) {
316
+ MRLog(@"❌ Cannot add video input to writer");
317
+ if (error) {
318
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-100 userInfo:@{NSLocalizedDescriptionKey: @"Cannot add video input to writer"}];
319
+ }
320
+ return NO;
321
+ }
322
+
323
+ [g_videoWriter addInput:g_videoInput];
324
+ if (g_pixelBufferAdaptorRef) {
325
+ CFRelease(g_pixelBufferAdaptorRef);
326
+ g_pixelBufferAdaptorRef = NULL;
327
+ }
328
+ if (pixelAdaptor) {
329
+ g_pixelBufferAdaptorRef = CFBridgingRetain(pixelAdaptor);
330
+ }
331
+ g_videoWriterStarted = NO;
332
+ g_videoStartTime = kCMTimeInvalid;
333
+ MRLog(@"✅ Video writer ready %ldx%ld", (long)width, (long)height);
334
+
335
+ return YES;
336
+ }
337
+
338
+ + (BOOL)prepareAudioWriterIfNeededWithSampleBuffer:(CMSampleBufferRef)sampleBuffer {
339
+ if (!g_shouldCaptureAudio || g_audioWriter || !g_audioOutputPath) {
340
+ return g_audioWriter != nil || !g_shouldCaptureAudio;
341
+ }
342
+
343
+ CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
344
+ if (!formatDescription) {
345
+ NSLog(@"⚠️ Missing audio format description");
346
+ return NO;
347
+ }
348
+
349
+ const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription);
350
+ if (!asbd) {
351
+ NSLog(@"⚠️ Unsupported audio format description");
352
+ return NO;
353
+ }
354
+
355
+ g_configuredSampleRate = (NSInteger)asbd->mSampleRate;
356
+ g_configuredChannelCount = asbd->mChannelsPerFrame;
357
+
358
+ NSString *originalPath = g_audioOutputPath ?: @"";
359
+ NSURL *audioURL = [NSURL fileURLWithPath:originalPath];
360
+ [[NSFileManager defaultManager] removeItemAtURL:audioURL error:nil];
361
+
362
+ NSError *writerError = nil;
363
+ AVFileType requestedFileType = AVFileTypeQuickTimeMovie;
364
+ BOOL requestedWebM = NO;
365
+ if (@available(macOS 15.0, *)) {
366
+ requestedFileType = @"public.webm";
367
+ requestedWebM = YES;
368
+ }
369
+
370
+ @try {
371
+ g_audioWriter = [[AVAssetWriter alloc] initWithURL:audioURL fileType:requestedFileType error:&writerError];
372
+ } @catch (NSException *exception) {
373
+ NSDictionary *info = @{
374
+ NSLocalizedDescriptionKey: exception.reason ?: @"Failed to initialize audio writer"
375
+ };
376
+ writerError = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-201 userInfo:info];
377
+ g_audioWriter = nil;
378
+ }
379
+
380
+ if ((!g_audioWriter || writerError) && requestedWebM) {
381
+ MRLog(@"⚠️ ScreenCaptureKit audio writer unavailable (%@) – falling back to QuickTime container", writerError.localizedDescription);
382
+ NSString *fallbackPath = [[originalPath stringByDeletingPathExtension] stringByAppendingPathExtension:@"mov"];
383
+ if (!fallbackPath || [fallbackPath length] == 0) {
384
+ fallbackPath = [originalPath stringByAppendingString:@".mov"];
385
+ }
386
+ [[NSFileManager defaultManager] removeItemAtPath:fallbackPath error:nil];
387
+ NSURL *fallbackURL = [NSURL fileURLWithPath:fallbackPath];
388
+ g_audioOutputPath = fallbackPath;
389
+ writerError = nil;
390
+ @try {
391
+ g_audioWriter = [[AVAssetWriter alloc] initWithURL:fallbackURL fileType:AVFileTypeQuickTimeMovie error:&writerError];
392
+ } @catch (NSException *exception) {
393
+ NSDictionary *info = @{
394
+ NSLocalizedDescriptionKey: exception.reason ?: @"Failed to initialize audio writer"
395
+ };
396
+ writerError = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-202 userInfo:info];
397
+ g_audioWriter = nil;
398
+ }
399
+ audioURL = fallbackURL;
400
+ }
401
+
402
+ if (!g_audioWriter || writerError) {
403
+ NSLog(@"❌ Failed to create audio writer: %@", writerError);
404
+ return NO;
405
+ }
406
+
407
+ NSInteger channelCount = MAX(1, g_configuredChannelCount);
408
+ AudioChannelLayout layout = {0};
409
+ size_t layoutSize = 0;
410
+ if (channelCount == 1) {
411
+ layout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
412
+ layoutSize = sizeof(AudioChannelLayout);
413
+ } else if (channelCount == 2) {
414
+ layout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
415
+ layoutSize = sizeof(AudioChannelLayout);
416
+ }
417
+
418
+ NSMutableDictionary *audioSettings = [@{
419
+ AVFormatIDKey: @(kAudioFormatMPEG4AAC),
420
+ AVSampleRateKey: @(g_configuredSampleRate),
421
+ AVNumberOfChannelsKey: @(channelCount),
422
+ AVEncoderBitRateKey: @(192000)
423
+ } mutableCopy];
424
+
425
+ if (layoutSize > 0) {
426
+ audioSettings[AVChannelLayoutKey] = [NSData dataWithBytes:&layout length:layoutSize];
427
+ }
428
+
429
+ g_audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
430
+ g_audioInput.expectsMediaDataInRealTime = YES;
431
+
432
+ if (![g_audioWriter canAddInput:g_audioInput]) {
433
+ NSLog(@"❌ Audio writer cannot add input");
434
+ return NO;
435
+ }
436
+ [g_audioWriter addInput:g_audioInput];
437
+ g_audioWriterStarted = NO;
438
+ g_audioStartTime = kCMTimeInvalid;
439
+
440
+ return YES;
441
+ }
442
+
44
443
  + (BOOL)isScreenCaptureKitAvailable {
45
444
  if (@available(macOS 15.0, *)) {
46
445
  return [SCShareableContent class] != nil && [SCStream class] != nil && [SCRecordingOutput class] != nil;
@@ -73,6 +472,9 @@ static NSString *g_outputPath = nil;
73
472
  NSNumber *captureCursor = config[@"captureCursor"];
74
473
  NSNumber *includeMicrophone = config[@"includeMicrophone"];
75
474
  NSNumber *includeSystemAudio = config[@"includeSystemAudio"];
475
+ NSString *microphoneDeviceId = config[@"microphoneDeviceId"];
476
+ NSString *audioOutputPath = MRNormalizePath(config[@"audioOutputPath"]);
477
+ NSNumber *sessionTimestampNumber = config[@"sessionTimestamp"];
76
478
 
77
479
  MRLog(@"🎬 Starting PURE ScreenCaptureKit recording (NO AVFoundation)");
78
480
  MRLog(@"🔧 Config: cursor=%@ mic=%@ system=%@ display=%@ window=%@ crop=%@",
@@ -188,6 +590,29 @@ static NSString *g_outputPath = nil;
188
590
  streamConfig.pixelFormat = kCVPixelFormatType_32BGRA;
189
591
  streamConfig.scalesToFit = NO;
190
592
 
593
+ BOOL shouldCaptureMic = includeMicrophone ? [includeMicrophone boolValue] : NO;
594
+ BOOL shouldCaptureSystemAudio = includeSystemAudio ? [includeSystemAudio boolValue] : NO;
595
+ g_shouldCaptureAudio = shouldCaptureMic || shouldCaptureSystemAudio;
596
+ g_audioOutputPath = audioOutputPath;
597
+ if (g_shouldCaptureAudio && (!g_audioOutputPath || [g_audioOutputPath length] == 0)) {
598
+ NSLog(@"⚠️ Audio capture requested but no audio output path supplied – audio will be disabled");
599
+ g_shouldCaptureAudio = NO;
600
+ }
601
+
602
+ if (@available(macos 13.0, *)) {
603
+ streamConfig.capturesAudio = g_shouldCaptureAudio;
604
+ streamConfig.sampleRate = g_configuredSampleRate;
605
+ streamConfig.channelCount = g_configuredChannelCount;
606
+ streamConfig.excludesCurrentProcessAudio = !shouldCaptureSystemAudio;
607
+ }
608
+
609
+ if (@available(macos 15.0, *)) {
610
+ streamConfig.captureMicrophone = shouldCaptureMic;
611
+ if (microphoneDeviceId && microphoneDeviceId.length > 0) {
612
+ streamConfig.microphoneCaptureDeviceID = microphoneDeviceId;
613
+ }
614
+ }
615
+
191
616
  // Apply crop area using sourceRect - CONVERT GLOBAL TO DISPLAY-RELATIVE COORDINATES
192
617
  if (captureRect && captureRect[@"x"] && captureRect[@"y"] && captureRect[@"width"] && captureRect[@"height"]) {
193
618
  CGFloat globalX = [captureRect[@"x"] doubleValue];
@@ -232,110 +657,63 @@ static NSString *g_outputPath = nil;
232
657
  MRLog(@"🎥 Pure ScreenCapture config: %ldx%ld @ 30fps, cursor=%d",
233
658
  recordingWidth, recordingHeight, shouldShowCursor);
234
659
 
235
- // AUDIO SUPPORT - Enable both microphone and system audio
236
- MRLog(@"🔍 AUDIO PROCESSING: includeMicrophone=%@ includeSystemAudio=%@", includeMicrophone, includeSystemAudio);
237
- BOOL shouldCaptureMic = includeMicrophone ? [includeMicrophone boolValue] : NO;
238
- BOOL shouldCaptureSystemAudio = includeSystemAudio ? [includeSystemAudio boolValue] : NO;
239
- MRLog(@"🔍 AUDIO COMPUTED: shouldCaptureMic=%d shouldCaptureSystemAudio=%d", shouldCaptureMic, shouldCaptureSystemAudio);
240
-
241
- // Enable audio if either microphone or system audio is requested
242
- if (@available(macOS 13.0, *)) {
243
- if (shouldCaptureMic || shouldCaptureSystemAudio) {
244
- streamConfig.capturesAudio = YES;
245
- streamConfig.sampleRate = 44100;
246
- streamConfig.channelCount = 2;
247
-
248
- if (shouldCaptureMic && shouldCaptureSystemAudio) {
249
- MRLog(@"🎵 Both microphone and system audio enabled");
250
- } else if (shouldCaptureMic) {
251
- MRLog(@"🎤 Microphone audio enabled");
252
- } else {
253
- MRLog(@"🔊 System audio enabled");
254
- }
255
- } else {
256
- streamConfig.capturesAudio = NO;
257
- MRLog(@"🔇 Audio disabled");
258
- }
259
- } else {
260
- streamConfig.capturesAudio = NO;
261
- MRLog(@"🔇 Audio disabled (macOS < 13.0)");
262
- }
263
-
264
- // Create pure ScreenCaptureKit recording output
265
- // Use local copy to prevent race conditions
266
- NSString *safeOutputPath = outputPath; // Local variable from outer scope
267
- if (!safeOutputPath || [safeOutputPath length] == 0) {
268
- NSLog(@"❌ Output path is nil or empty");
660
+ NSError *writerError = nil;
661
+ if (![ScreenCaptureKitRecorder prepareVideoWriterWithWidth:recordingWidth height:recordingHeight error:&writerError]) {
662
+ NSLog(@"❌ Failed to prepare video writer: %@", writerError);
269
663
  return;
270
664
  }
271
665
 
272
- NSURL *outputURL = [NSURL fileURLWithPath:safeOutputPath];
273
- if (!outputURL) {
274
- NSLog(@"❌ Failed to create output URL from path: %@", safeOutputPath);
275
- return;
276
- }
277
-
278
- if (@available(macOS 15.0, *)) {
279
- // Create recording output configuration
280
- SCRecordingOutputConfiguration *recordingConfig = [[SCRecordingOutputConfiguration alloc] init];
281
- recordingConfig.outputURL = outputURL;
282
- recordingConfig.videoCodecType = AVVideoCodecTypeH264;
283
-
284
- // Audio configuration - using available properties
285
- // Note: Specific audio routing handled by ScreenCaptureKit automatically
286
-
287
- // Create recording output with correct initializer
288
- g_recordingOutput = [[SCRecordingOutput alloc] initWithConfiguration:recordingConfig
289
- delegate:nil];
290
- if (shouldCaptureMic && shouldCaptureSystemAudio) {
291
- NSLog(@"🔧 Created SCRecordingOutput with microphone and system audio");
292
- } else if (shouldCaptureMic) {
293
- NSLog(@"🔧 Created SCRecordingOutput with microphone audio");
294
- } else if (shouldCaptureSystemAudio) {
295
- NSLog(@"🔧 Created SCRecordingOutput with system audio");
296
- } else {
297
- NSLog(@"🔧 Created SCRecordingOutput (audio disabled)");
298
- }
299
- }
300
-
301
- if (!g_recordingOutput) {
302
- NSLog(@"❌ Failed to create SCRecordingOutput");
303
- return;
666
+ g_videoQueue = dispatch_queue_create("screen_capture_video_queue", DISPATCH_QUEUE_SERIAL);
667
+ g_audioQueue = dispatch_queue_create("screen_capture_audio_queue", DISPATCH_QUEUE_SERIAL);
668
+ g_videoStreamOutput = [[ScreenCaptureVideoOutput alloc] init];
669
+ if (g_shouldCaptureAudio) {
670
+ g_audioStreamOutput = [[ScreenCaptureAudioOutput alloc] init];
671
+ } else {
672
+ g_audioStreamOutput = nil;
304
673
  }
305
674
 
306
- NSLog(@"✅ Pure ScreenCaptureKit recording output created");
307
-
308
- // Create delegate
309
675
  g_streamDelegate = [[PureScreenCaptureDelegate alloc] init];
310
-
311
- // Create and configure stream
312
676
  g_stream = [[SCStream alloc] initWithFilter:filter configuration:streamConfig delegate:g_streamDelegate];
313
677
 
314
678
  if (!g_stream) {
315
679
  NSLog(@"❌ Failed to create pure stream");
680
+ CleanupWriters();
316
681
  return;
317
682
  }
318
683
 
319
- // Add recording output directly to stream
320
684
  NSError *outputError = nil;
321
- BOOL outputAdded = NO;
322
-
323
- if (@available(macOS 15.0, *)) {
324
- outputAdded = [g_stream addRecordingOutput:g_recordingOutput error:&outputError];
685
+ BOOL videoOutputAdded = [g_stream addStreamOutput:g_videoStreamOutput type:SCStreamOutputTypeScreen sampleHandlerQueue:g_videoQueue error:&outputError];
686
+ if (!videoOutputAdded || outputError) {
687
+ NSLog(@"❌ Failed to add video output: %@", outputError);
688
+ CleanupWriters();
689
+ return;
325
690
  }
326
691
 
327
- if (!outputAdded || outputError) {
328
- NSLog(@"❌ Failed to add recording output: %@", outputError);
329
- return;
692
+ if (g_shouldCaptureAudio) {
693
+ if (@available(macOS 13.0, *)) {
694
+ NSError *audioError = nil;
695
+ BOOL audioOutputAdded = [g_stream addStreamOutput:g_audioStreamOutput type:SCStreamOutputTypeAudio sampleHandlerQueue:g_audioQueue error:&audioError];
696
+ if (!audioOutputAdded || audioError) {
697
+ NSLog(@"❌ Failed to add audio output: %@", audioError);
698
+ CleanupWriters();
699
+ return;
700
+ }
701
+ } else {
702
+ NSLog(@"⚠️ Audio capture requested but requires macOS 13.0+");
703
+ g_shouldCaptureAudio = NO;
704
+ }
330
705
  }
331
706
 
332
- MRLog(@"✅ Pure recording output added to stream");
707
+ MRLog(@"✅ Stream outputs configured (audio=%d)", g_shouldCaptureAudio);
708
+ if (sessionTimestampNumber) {
709
+ MRLog(@"🕒 Session timestamp: %@", sessionTimestampNumber);
710
+ }
333
711
 
334
- // Start capture with recording
335
712
  [g_stream startCaptureWithCompletionHandler:^(NSError *startError) {
336
713
  if (startError) {
337
714
  NSLog(@"❌ Failed to start pure capture: %@", startError);
338
715
  g_isRecording = NO;
716
+ CleanupWriters();
339
717
  } else {
340
718
  MRLog(@"🎉 PURE ScreenCaptureKit recording started successfully!");
341
719
  g_isRecording = YES;
@@ -368,6 +746,7 @@ static NSString *g_outputPath = nil;
368
746
 
369
747
  // Finalize on main queue to prevent threading issues
370
748
  dispatch_async(dispatch_get_main_queue(), ^{
749
+ CleanupWriters();
371
750
  [ScreenCaptureKitRecorder cleanupVideoWriter];
372
751
  });
373
752
  }];
@@ -390,11 +769,6 @@ static NSString *g_outputPath = nil;
390
769
  g_isCleaningUp = YES;
391
770
  g_isRecording = NO;
392
771
 
393
- if (g_recordingOutput) {
394
- // SCRecordingOutput finalizes automatically
395
- MRLog(@"✅ Pure recording output finalized");
396
- }
397
-
398
772
  [ScreenCaptureKitRecorder cleanupVideoWriter];
399
773
  }
400
774
  }
@@ -414,16 +788,22 @@ static NSString *g_outputPath = nil;
414
788
  MRLog(@"✅ Stream reference cleared");
415
789
  }
416
790
 
417
- if (g_recordingOutput) {
418
- g_recordingOutput = nil;
419
- MRLog(@"✅ Recording output reference cleared");
420
- }
421
-
422
791
  if (g_streamDelegate) {
423
792
  g_streamDelegate = nil;
424
793
  MRLog(@"✅ Stream delegate reference cleared");
425
794
  }
426
795
 
796
+ g_videoStreamOutput = nil;
797
+ g_audioStreamOutput = nil;
798
+ g_videoQueue = nil;
799
+ g_audioQueue = nil;
800
+ if (g_pixelBufferAdaptorRef) {
801
+ CFRelease(g_pixelBufferAdaptorRef);
802
+ g_pixelBufferAdaptorRef = NULL;
803
+ }
804
+ g_audioOutputPath = nil;
805
+ g_shouldCaptureAudio = NO;
806
+
427
807
  g_isRecording = NO;
428
808
  g_isCleaningUp = NO; // Reset cleanup flag
429
809
  g_outputPath = nil;
@@ -1,41 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
-
3
- @interface AudioCapture : NSObject
4
-
5
- + (NSArray *)getAudioDevices;
6
- + (BOOL)hasAudioPermission;
7
- + (void)requestAudioPermission:(void(^)(BOOL granted))completion;
8
-
9
- @end
10
-
11
- @implementation AudioCapture
12
-
13
- + (NSArray *)getAudioDevices {
14
- NSMutableArray *devices = [NSMutableArray array];
15
-
16
- // ScreenCaptureKit handles audio internally - return default device
17
- NSDictionary *deviceInfo = @{
18
- @"id": @"default",
19
- @"name": @"Default Audio Device",
20
- @"manufacturer": @"System",
21
- @"isDefault": @YES
22
- };
23
-
24
- [devices addObject:deviceInfo];
25
-
26
- return devices;
27
- }
28
-
29
- + (BOOL)hasAudioPermission {
30
- // ScreenCaptureKit handles audio permissions internally
31
- return YES;
32
- }
33
-
34
- + (void)requestAudioPermission:(void(^)(BOOL granted))completion {
35
- // ScreenCaptureKit handles audio permissions internally
36
- if (completion) {
37
- completion(YES);
38
- }
39
- }
40
-
41
- @end