node-mac-recorder 2.12.5 → 2.13.0

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.
@@ -0,0 +1,507 @@
1
+ #import "screen_capture_kit.h"
2
+
3
+ // Global state
4
+ static SCStream *g_stream = nil;
5
+ static id<SCStreamDelegate> g_streamDelegate = nil;
6
+ static id<SCStreamOutput> g_streamOutput = nil;
7
+ static BOOL g_isRecording = NO;
8
+ // Simple image sequence approach for debugging
9
+ static NSMutableArray<NSString *> *g_imageFrames = nil;
10
+ static NSString *g_outputVideoPath = nil;
11
+ static NSInteger g_frameCount = 0;
12
+ static BOOL g_sessionStarted = NO;
13
+
14
+ @interface ScreenCaptureKitRecorderDelegate : NSObject <SCStreamDelegate>
15
+ @property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
16
+ @end
17
+
18
+ @interface ScreenCaptureKitStreamOutput : NSObject <SCStreamOutput>
19
+ @end
20
+
21
+ @implementation ScreenCaptureKitRecorderDelegate
22
+ - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
23
+ NSLog(@"ScreenCaptureKit recording stopped with error: %@", error);
24
+
25
+ // Finalize video file (delegate version)
26
+ if (g_assetWriter && g_assetWriter.status == AVAssetWriterStatusWriting) {
27
+ NSLog(@"🔄 Starting video finalization in delegate");
28
+ [g_videoWriterInput markAsFinished];
29
+ if (g_audioWriterInput) {
30
+ [g_audioWriterInput markAsFinished];
31
+ }
32
+
33
+ // Use asynchronous finishWriting with completion handler
34
+ [g_assetWriter finishWritingWithCompletionHandler:^{
35
+ if (g_assetWriter.status == AVAssetWriterStatusCompleted) {
36
+ NSLog(@"✅ ScreenCaptureKit video file finalized in delegate: %@", g_outputPath);
37
+ } else {
38
+ NSLog(@"❌ ScreenCaptureKit video finalization failed in delegate: %@", g_assetWriter.error);
39
+ }
40
+ }];
41
+
42
+ // Cleanup in delegate
43
+ g_assetWriter = nil;
44
+ g_videoWriterInput = nil;
45
+ g_audioWriterInput = nil;
46
+ g_outputPath = nil;
47
+ g_sessionStarted = NO;
48
+ }
49
+ }
50
+ @end
51
+
52
+ @implementation ScreenCaptureKitStreamOutput
53
+ - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
54
+ if (!g_assetWriter) {
55
+ return;
56
+ }
57
+
58
+ // Start session on first sample with proper validation
59
+ if (!g_sessionStarted && g_assetWriter.status == AVAssetWriterStatusWriting) {
60
+ CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
61
+ if (CMTIME_IS_VALID(presentationTime) && !CMTIME_IS_INDEFINITE(presentationTime)) {
62
+ [g_assetWriter startSessionAtSourceTime:presentationTime];
63
+ g_sessionStarted = YES;
64
+ NSLog(@"📽️ ScreenCaptureKit video session started at time: %lld/%d", presentationTime.value, presentationTime.timescale);
65
+ } else {
66
+ // Use zero time if presentation time is invalid
67
+ [g_assetWriter startSessionAtSourceTime:kCMTimeZero];
68
+ g_sessionStarted = YES;
69
+ NSLog(@"📽️ ScreenCaptureKit video session started at kCMTimeZero (invalid source time)");
70
+ }
71
+ }
72
+
73
+ if (g_assetWriter.status != AVAssetWriterStatusWriting) {
74
+ return;
75
+ }
76
+
77
+ switch (type) {
78
+ case SCStreamOutputTypeScreen:
79
+ if (g_videoWriterInput && g_videoWriterInput.isReadyForMoreMediaData) {
80
+ // Validate sample buffer and timing
81
+ CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
82
+ CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
83
+
84
+ if (formatDesc && CMTIME_IS_VALID(presentationTime)) {
85
+ CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);
86
+ if (mediaType == kCMMediaType_Video) {
87
+ // Log sample buffer format details
88
+ CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(formatDesc);
89
+ FourCharCode codecType = CMFormatDescriptionGetMediaSubType(formatDesc);
90
+ NSString *codecString = [NSString stringWithFormat:@"%c%c%c%c",
91
+ (codecType >> 24) & 0xFF,
92
+ (codecType >> 16) & 0xFF,
93
+ (codecType >> 8) & 0xFF,
94
+ codecType & 0xFF];
95
+ // Log first sample only to reduce noise
96
+ static BOOL firstSampleLogged = NO;
97
+ if (!firstSampleLogged) {
98
+ NSLog(@"📹 ScreenCaptureKit sample: %dx%d, codec: %@ (0x%x)",
99
+ dimensions.width, dimensions.height, codecString, (unsigned int)codecType);
100
+ firstSampleLogged = YES;
101
+ }
102
+
103
+ // Direct sample buffer appending with ultra-minimal settings
104
+ if (g_videoWriterInput.isReadyForMoreMediaData) {
105
+ BOOL success = [g_videoWriterInput appendSampleBuffer:sampleBuffer];
106
+ if (!success) {
107
+ NSLog(@"❌ Failed to append sample buffer: %@", g_assetWriter.error);
108
+ }
109
+ } else {
110
+ NSLog(@"❌ Video writer input not ready");
111
+ }
112
+ }
113
+ } else {
114
+ NSLog(@"❌ Invalid sample buffer - formatDesc:%@ presentationTime valid:%d",
115
+ formatDesc, CMTIME_IS_VALID(presentationTime));
116
+ }
117
+ }
118
+ break;
119
+ case SCStreamOutputTypeAudio:
120
+ if (g_audioWriterInput && g_audioWriterInput.isReadyForMoreMediaData) {
121
+ BOOL success = [g_audioWriterInput appendSampleBuffer:sampleBuffer];
122
+ if (!success) {
123
+ NSLog(@"❌ Failed to append audio sample: %@", g_assetWriter.error);
124
+ }
125
+ }
126
+ break;
127
+ case SCStreamOutputTypeMicrophone:
128
+ if (g_audioWriterInput && g_audioWriterInput.isReadyForMoreMediaData) {
129
+ BOOL success = [g_audioWriterInput appendSampleBuffer:sampleBuffer];
130
+ if (!success) {
131
+ NSLog(@"❌ Failed to append microphone sample: %@", g_assetWriter.error);
132
+ }
133
+ }
134
+ break;
135
+ }
136
+ }
137
+ @end
138
+
139
+ @implementation ScreenCaptureKitRecorder
140
+
141
+ + (BOOL)isScreenCaptureKitAvailable {
142
+ // ScreenCaptureKit etkinleştir - video dosyası sorunu çözülecek
143
+
144
+ if (@available(macOS 12.3, *)) {
145
+ NSLog(@"🔍 ScreenCaptureKit availability check - macOS 12.3+ confirmed");
146
+
147
+ // Try to access ScreenCaptureKit classes to verify they're actually available
148
+ @try {
149
+ Class scStreamClass = NSClassFromString(@"SCStream");
150
+ Class scContentFilterClass = NSClassFromString(@"SCContentFilter");
151
+ Class scShareableContentClass = NSClassFromString(@"SCShareableContent");
152
+
153
+ if (scStreamClass && scContentFilterClass && scShareableContentClass) {
154
+ NSLog(@"✅ ScreenCaptureKit classes are available");
155
+ return YES;
156
+ } else {
157
+ NSLog(@"❌ ScreenCaptureKit classes not found");
158
+ NSLog(@" SCStream: %@", scStreamClass ? @"✅" : @"❌");
159
+ NSLog(@" SCContentFilter: %@", scContentFilterClass ? @"✅" : @"❌");
160
+ NSLog(@" SCShareableContent: %@", scShareableContentClass ? @"✅" : @"❌");
161
+ return NO;
162
+ }
163
+ } @catch (NSException *exception) {
164
+ NSLog(@"❌ Exception checking ScreenCaptureKit classes: %@", exception.reason);
165
+ return NO;
166
+ }
167
+ }
168
+ NSLog(@"❌ macOS version < 12.3 - ScreenCaptureKit not available");
169
+ return NO;
170
+ }
171
+
172
+ + (BOOL)startRecordingWithConfiguration:(NSDictionary *)config
173
+ delegate:(id)delegate
174
+ error:(NSError **)error {
175
+
176
+ if (@available(macOS 12.3, *)) {
177
+ @try {
178
+ // Get current app PID to exclude overlay windows
179
+ NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
180
+ pid_t currentPID = currentApp.processIdentifier;
181
+
182
+ // Get all shareable content synchronously for immediate response
183
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
184
+ __block BOOL success = NO;
185
+ __block NSError *contentError = nil;
186
+
187
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *error) {
188
+ if (error) {
189
+ NSLog(@"Failed to get shareable content: %@", error);
190
+ contentError = error;
191
+ dispatch_semaphore_signal(semaphore);
192
+ return;
193
+ }
194
+
195
+ // Find display to record
196
+ SCDisplay *targetDisplay = content.displays.firstObject; // Default to first display
197
+ if (config[@"displayId"]) {
198
+ CGDirectDisplayID displayID = [config[@"displayId"] unsignedIntValue];
199
+ for (SCDisplay *display in content.displays) {
200
+ if (display.displayID == displayID) {
201
+ targetDisplay = display;
202
+ break;
203
+ }
204
+ }
205
+ }
206
+
207
+ // TEMPORARILY DISABLED: Window exclusion for testing
208
+ NSMutableArray *excludedWindows = [NSMutableArray array];
209
+ NSMutableArray *excludedApps = [NSMutableArray array];
210
+
211
+ NSLog(@"🎯 Window exclusion re-enabled with working video format");
212
+
213
+ // Exclude current Node.js process windows (overlay selectors)
214
+ for (SCWindow *window in content.windows) {
215
+ if (window.owningApplication.processID == currentPID) {
216
+ [excludedWindows addObject:window];
217
+ NSLog(@"🚫 Excluding Node.js overlay window: %@ (PID: %d)", window.title, currentPID);
218
+ }
219
+ }
220
+
221
+ // Also exclude Electron app and high-level overlay windows
222
+ for (SCWindow *window in content.windows) {
223
+ NSString *appName = window.owningApplication.applicationName;
224
+ NSString *windowTitle = window.title ? window.title : @"<No Title>";
225
+
226
+ // Comprehensive Electron window detection
227
+ BOOL shouldExclude = NO;
228
+
229
+ // Check app name patterns
230
+ if ([appName containsString:@"Electron"] ||
231
+ [appName isEqualToString:@"electron"] ||
232
+ [appName isEqualToString:@"Electron Helper"]) {
233
+ shouldExclude = YES;
234
+ }
235
+
236
+ // Check window title patterns
237
+ if ([windowTitle containsString:@"Electron"] ||
238
+ [windowTitle containsString:@"camera"] ||
239
+ [windowTitle containsString:@"Camera"] ||
240
+ [windowTitle containsString:@"overlay"] ||
241
+ [windowTitle containsString:@"Overlay"]) {
242
+ shouldExclude = YES;
243
+ }
244
+
245
+ // Check window properties (transparent, always on top windows)
246
+ if (window.windowLayer > 100) { // High window levels (like alwaysOnTop)
247
+ shouldExclude = YES;
248
+ NSLog(@"📋 High-level window detected: '%@' (Level: %ld)", windowTitle, (long)window.windowLayer);
249
+ }
250
+
251
+ if (shouldExclude) {
252
+ [excludedWindows addObject:window];
253
+ NSLog(@"🚫 Excluding window: '%@' from %@ (PID: %d, Level: %ld)",
254
+ windowTitle, appName, window.owningApplication.processID, (long)window.windowLayer);
255
+ }
256
+ }
257
+
258
+ NSLog(@"📊 Total windows to exclude: %lu", (unsigned long)excludedWindows.count);
259
+
260
+ // Create content filter - exclude overlay windows from recording
261
+ SCContentFilter *filter = [[SCContentFilter alloc]
262
+ initWithDisplay:targetDisplay
263
+ excludingWindows:excludedWindows];
264
+ NSLog(@"🎯 Using window-level exclusion for overlay prevention");
265
+
266
+ // Create stream configuration
267
+ SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
268
+
269
+ // Handle capture area if specified
270
+ if (config[@"captureRect"]) {
271
+ NSDictionary *rect = config[@"captureRect"];
272
+ streamConfig.width = [rect[@"width"] integerValue];
273
+ streamConfig.height = [rect[@"height"] integerValue];
274
+ // Note: ScreenCaptureKit crop rect would need additional handling
275
+ } else {
276
+ streamConfig.width = (NSInteger)targetDisplay.width;
277
+ streamConfig.height = (NSInteger)targetDisplay.height;
278
+ }
279
+
280
+ streamConfig.minimumFrameInterval = CMTimeMake(1, 60); // 60 FPS
281
+ streamConfig.queueDepth = 5;
282
+ streamConfig.showsCursor = [config[@"captureCursor"] boolValue];
283
+ streamConfig.capturesAudio = [config[@"includeSystemAudio"] boolValue];
284
+
285
+ // Setup video writer
286
+ g_outputPath = config[@"outputPath"];
287
+ if (![self setupVideoWriterWithWidth:streamConfig.width
288
+ height:streamConfig.height
289
+ outputPath:g_outputPath
290
+ includeAudio:[config[@"includeSystemAudio"] boolValue] || [config[@"includeMicrophone"] boolValue]]) {
291
+ NSLog(@"❌ Failed to setup video writer");
292
+ contentError = [NSError errorWithDomain:@"ScreenCaptureKitError" code:-3 userInfo:@{NSLocalizedDescriptionKey: @"Video writer setup failed"}];
293
+ dispatch_semaphore_signal(semaphore);
294
+ return;
295
+ }
296
+
297
+ // Create delegate and output
298
+ g_streamDelegate = [[ScreenCaptureKitRecorderDelegate alloc] init];
299
+ g_streamOutput = [[ScreenCaptureKitStreamOutput alloc] init];
300
+
301
+ // Create and start stream
302
+ g_stream = [[SCStream alloc] initWithFilter:filter
303
+ configuration:streamConfig
304
+ delegate:g_streamDelegate];
305
+
306
+ // Add stream output using correct API
307
+ NSError *outputError = nil;
308
+ BOOL outputAdded = [g_stream addStreamOutput:g_streamOutput
309
+ type:SCStreamOutputTypeScreen
310
+ sampleHandlerQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
311
+ error:&outputError];
312
+ if (!outputAdded) {
313
+ NSLog(@"❌ Failed to add screen output: %@", outputError);
314
+ }
315
+
316
+ if ([config[@"includeSystemAudio"] boolValue]) {
317
+ if (@available(macOS 13.0, *)) {
318
+ BOOL audioOutputAdded = [g_stream addStreamOutput:g_streamOutput
319
+ type:SCStreamOutputTypeAudio
320
+ sampleHandlerQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
321
+ error:&outputError];
322
+ if (!audioOutputAdded) {
323
+ NSLog(@"❌ Failed to add audio output: %@", outputError);
324
+ }
325
+ }
326
+ }
327
+
328
+ [g_stream startCaptureWithCompletionHandler:^(NSError *streamError) {
329
+ if (streamError) {
330
+ NSLog(@"❌ Failed to start ScreenCaptureKit recording: %@", streamError);
331
+ contentError = streamError;
332
+ g_isRecording = NO;
333
+ } else {
334
+ NSLog(@"✅ ScreenCaptureKit recording started successfully (excluding %lu overlay windows)", (unsigned long)excludedWindows.count);
335
+ g_isRecording = YES;
336
+ success = YES;
337
+ }
338
+ dispatch_semaphore_signal(semaphore);
339
+ }];
340
+ }];
341
+
342
+ // Wait for completion (with timeout)
343
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
344
+ if (dispatch_semaphore_wait(semaphore, timeout) == 0) {
345
+ if (contentError && error) {
346
+ *error = contentError;
347
+ }
348
+ return success;
349
+ } else {
350
+ NSLog(@"⏰ ScreenCaptureKit initialization timeout");
351
+ if (error) {
352
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitError"
353
+ code:-2
354
+ userInfo:@{NSLocalizedDescriptionKey: @"Initialization timeout"}];
355
+ }
356
+ return NO;
357
+ }
358
+
359
+ } @catch (NSException *exception) {
360
+ NSLog(@"ScreenCaptureKit recording exception: %@", exception);
361
+ if (error) {
362
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitError"
363
+ code:-1
364
+ userInfo:@{NSLocalizedDescriptionKey: exception.reason}];
365
+ }
366
+ return NO;
367
+ }
368
+ }
369
+
370
+ return NO;
371
+ }
372
+
373
+ + (void)stopRecording {
374
+ NSLog(@"🛑 stopRecording called");
375
+ if (@available(macOS 12.3, *)) {
376
+ if (g_stream && g_isRecording) {
377
+ NSLog(@"🛑 Calling stopCaptureWithCompletionHandler");
378
+ [g_stream stopCaptureWithCompletionHandler:^(NSError *error) {
379
+ NSLog(@"🛑 stopCaptureWithCompletionHandler callback invoked");
380
+ if (error) {
381
+ NSLog(@"Error stopping ScreenCaptureKit recording: %@", error);
382
+ } else {
383
+ NSLog(@"ScreenCaptureKit recording stopped successfully");
384
+ }
385
+
386
+ // Finalize video file immediately (sync)
387
+ NSLog(@"🔍 Checking asset writer status for finalization");
388
+ if (g_assetWriter) {
389
+ NSString *statusString = @"Unknown";
390
+ switch (g_assetWriter.status) {
391
+ case AVAssetWriterStatusUnknown: statusString = @"Unknown"; break;
392
+ case AVAssetWriterStatusWriting: statusString = @"Writing"; break;
393
+ case AVAssetWriterStatusCompleted: statusString = @"Completed"; break;
394
+ case AVAssetWriterStatusFailed: statusString = @"Failed"; break;
395
+ case AVAssetWriterStatusCancelled: statusString = @"Cancelled"; break;
396
+ }
397
+ NSLog(@"🔍 Asset writer status: %ld (%@)", (long)g_assetWriter.status, statusString);
398
+ if (g_assetWriter.status == AVAssetWriterStatusFailed) {
399
+ NSLog(@"❌ Asset writer failed with error: %@", g_assetWriter.error);
400
+ }
401
+
402
+ if (g_assetWriter.status == AVAssetWriterStatusWriting) {
403
+ NSLog(@"🔄 Starting video finalization process");
404
+ [g_videoWriterInput markAsFinished];
405
+ if (g_audioWriterInput) {
406
+ [g_audioWriterInput markAsFinished];
407
+ }
408
+
409
+ // Use asynchronous finishWriting with completion handler
410
+ [g_assetWriter finishWritingWithCompletionHandler:^{
411
+ if (g_assetWriter.status == AVAssetWriterStatusCompleted) {
412
+ NSLog(@"✅ ScreenCaptureKit video file finalized successfully: %@", g_outputPath);
413
+ } else {
414
+ NSLog(@"❌ ScreenCaptureKit video finalization failed: %@", g_assetWriter.error);
415
+ }
416
+ }];
417
+
418
+ // Cleanup
419
+ g_assetWriter = nil;
420
+ g_videoWriterInput = nil;
421
+ g_audioWriterInput = nil;
422
+ g_outputPath = nil;
423
+ g_sessionStarted = NO;
424
+ } else {
425
+ NSLog(@"⚠️ Asset writer not in writing status: %ld", (long)g_assetWriter.status);
426
+ }
427
+ } else {
428
+ NSLog(@"❌ No asset writer found for finalization");
429
+ }
430
+
431
+ g_isRecording = NO;
432
+ g_stream = nil;
433
+ g_streamDelegate = nil;
434
+ g_streamOutput = nil;
435
+ }];
436
+ }
437
+ }
438
+ }
439
+
440
+ + (BOOL)isRecording {
441
+ return g_isRecording;
442
+ }
443
+
444
+ + (BOOL)setupVideoWriterWithWidth:(NSInteger)width
445
+ height:(NSInteger)height
446
+ outputPath:(NSString *)outputPath
447
+ includeAudio:(BOOL)includeAudio {
448
+
449
+ // Create asset writer with QuickTime format like AVFoundation
450
+ NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
451
+ NSError *error = nil;
452
+ g_assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeMPEG4 error:&error];
453
+
454
+ if (error || !g_assetWriter) {
455
+ NSLog(@"❌ Failed to create asset writer: %@", error);
456
+ return NO;
457
+ }
458
+
459
+ // Ultra-minimal H.264 video settings - no compression properties at all
460
+ NSDictionary *videoSettings = @{
461
+ AVVideoCodecKey: AVVideoCodecTypeH264,
462
+ AVVideoWidthKey: @(width),
463
+ AVVideoHeightKey: @(height)
464
+ };
465
+
466
+ g_videoWriterInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
467
+ g_videoWriterInput.expectsMediaDataInRealTime = YES;
468
+
469
+ if (![g_assetWriter canAddInput:g_videoWriterInput]) {
470
+ NSLog(@"❌ Cannot add video input to asset writer");
471
+ return NO;
472
+ }
473
+ [g_assetWriter addInput:g_videoWriterInput];
474
+
475
+ // No pixel buffer adaptor - use direct sample buffer approach
476
+ NSLog(@"✅ Video input configured for direct sample buffer appending");
477
+
478
+ // Audio writer input (if needed)
479
+ if (includeAudio) {
480
+ NSDictionary *audioSettings = @{
481
+ AVFormatIDKey: @(kAudioFormatMPEG4AAC),
482
+ AVSampleRateKey: @(44100.0),
483
+ AVNumberOfChannelsKey: @(2),
484
+ AVEncoderBitRateKey: @(128000)
485
+ };
486
+
487
+ g_audioWriterInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
488
+ g_audioWriterInput.expectsMediaDataInRealTime = YES;
489
+
490
+ if ([g_assetWriter canAddInput:g_audioWriterInput]) {
491
+ [g_assetWriter addInput:g_audioWriterInput];
492
+ }
493
+ }
494
+
495
+ // Start writing (session will be started when first sample arrives)
496
+ if (![g_assetWriter startWriting]) {
497
+ NSLog(@"❌ Failed to start writing: %@", g_assetWriter.error);
498
+ return NO;
499
+ }
500
+
501
+ g_sessionStarted = NO; // Reset session flag
502
+ NSLog(@"✅ ScreenCaptureKit video writer setup complete: %@", outputPath);
503
+
504
+ return YES;
505
+ }
506
+
507
+ @end