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.
- package/package.json +1 -1
- package/src/mac_recorder.mm +36 -7
- package/src/screen_capture_kit.h +1 -4
- package/src/screen_capture_kit.mm +220 -380
- package/src/screen_capture_kit_backup.mm +507 -0
- package/src/screen_capture_kit_new.mm +240 -0
- package/src/screen_capture_kit_simple.mm +302 -0
- package/src/window_selector.mm +31 -1
|
@@ -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
|