node-mac-recorder 2.12.5 โ†’ 2.13.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,90 +1,75 @@
1
1
  #import "screen_capture_kit.h"
2
+ #import <CoreImage/CoreImage.h>
2
3
 
3
- // Global state
4
4
  static SCStream *g_stream = nil;
5
5
  static id<SCStreamDelegate> g_streamDelegate = nil;
6
6
  static id<SCStreamOutput> g_streamOutput = nil;
7
7
  static BOOL g_isRecording = NO;
8
+
9
+ // Electron-safe direct writing approach
8
10
  static AVAssetWriter *g_assetWriter = nil;
9
- static AVAssetWriterInput *g_videoWriterInput = nil;
10
- static AVAssetWriterInput *g_audioWriterInput = nil;
11
- static NSString *g_outputPath = nil;
12
- static BOOL g_sessionStarted = NO;
11
+ static AVAssetWriterInput *g_assetWriterInput = nil;
13
12
  static AVAssetWriterInputPixelBufferAdaptor *g_pixelBufferAdaptor = nil;
13
+ static NSString *g_outputPath = nil;
14
+ static CMTime g_startTime;
15
+ static CMTime g_currentTime;
16
+ static BOOL g_writerStarted = NO;
14
17
 
15
- @interface ScreenCaptureKitRecorderDelegate : NSObject <SCStreamDelegate>
16
- @property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
17
- @end
18
-
19
- @interface ScreenCaptureKitStreamOutput : NSObject <SCStreamOutput>
18
+ @interface ElectronSafeDelegate : NSObject <SCStreamDelegate>
20
19
  @end
21
20
 
22
- @implementation ScreenCaptureKitRecorderDelegate
21
+ @implementation ElectronSafeDelegate
23
22
  - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
24
- NSLog(@"ScreenCaptureKit recording stopped with error: %@", error);
23
+ NSLog(@"๐Ÿ›‘ ScreenCaptureKit stream stopped in delegate");
24
+ g_isRecording = NO;
25
25
 
26
- // Finalize video file
27
- if (g_assetWriter && g_assetWriter.status == AVAssetWriterStatusWriting) {
28
- [g_videoWriterInput markAsFinished];
29
- if (g_audioWriterInput) {
30
- [g_audioWriterInput markAsFinished];
31
- }
32
- [g_assetWriter finishWritingWithCompletionHandler:^{
33
- NSLog(@"โœ… ScreenCaptureKit video file finalized: %@", g_outputPath);
34
- }];
26
+ if (error) {
27
+ NSLog(@"โŒ Stream stopped with error: %@", error);
28
+ } else {
29
+ NSLog(@"โœ… ScreenCaptureKit stream stopped successfully");
35
30
  }
31
+
32
+ // Finalize video writer
33
+ [ScreenCaptureKitRecorder finalizeVideoWriter];
36
34
  }
37
35
  @end
38
36
 
39
- @implementation ScreenCaptureKitStreamOutput
37
+ @interface ElectronSafeOutput : NSObject <SCStreamOutput>
38
+ @end
39
+
40
+ @implementation ElectronSafeOutput
40
41
  - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
41
- if (!g_assetWriter) {
42
- return;
43
- }
44
-
45
- // Start session on first sample
46
- if (!g_sessionStarted && g_assetWriter.status == AVAssetWriterStatusWriting) {
47
- CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
48
- [g_assetWriter startSessionAtSourceTime:presentationTime];
49
- g_sessionStarted = YES;
50
- NSLog(@"๐Ÿ“ฝ๏ธ ScreenCaptureKit video session started at time: %lld/%d", presentationTime.value, presentationTime.timescale);
51
- }
52
-
53
- if (g_assetWriter.status != AVAssetWriterStatusWriting) {
42
+ if (!g_isRecording || type != SCStreamOutputTypeScreen || !g_assetWriterInput) {
54
43
  return;
55
44
  }
56
45
 
57
- switch (type) {
58
- case SCStreamOutputTypeScreen:
59
- if (g_pixelBufferAdaptor && g_pixelBufferAdaptor.assetWriterInput.isReadyForMoreMediaData) {
60
- // Convert sample buffer to pixel buffer
61
- CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
62
- if (pixelBuffer) {
63
- CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
64
- BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime];
65
- if (!success) {
66
- NSLog(@"โŒ Failed to append pixel buffer: %@", g_assetWriter.error);
67
- }
68
- }
69
- }
70
- break;
71
- case SCStreamOutputTypeAudio:
72
- if (g_audioWriterInput && g_audioWriterInput.isReadyForMoreMediaData) {
73
- BOOL success = [g_audioWriterInput appendSampleBuffer:sampleBuffer];
74
- if (!success) {
75
- NSLog(@"โŒ Failed to append audio sample: %@", g_assetWriter.error);
76
- }
77
- }
78
- break;
79
- case SCStreamOutputTypeMicrophone:
80
- // Handle microphone input (if needed in future)
81
- if (g_audioWriterInput && g_audioWriterInput.isReadyForMoreMediaData) {
82
- BOOL success = [g_audioWriterInput appendSampleBuffer:sampleBuffer];
83
- if (!success) {
84
- NSLog(@"โŒ Failed to append microphone sample: %@", g_assetWriter.error);
46
+ @autoreleasepool {
47
+ // Initialize video writer on first frame
48
+ if (!g_writerStarted && g_assetWriter && g_assetWriterInput) {
49
+ g_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
50
+ g_currentTime = g_startTime;
51
+
52
+ [g_assetWriter startWriting];
53
+ [g_assetWriter startSessionAtSourceTime:g_startTime];
54
+ g_writerStarted = YES;
55
+ NSLog(@"โœ… Electron-safe video writer started");
56
+ }
57
+
58
+ // Write sample buffer directly (Electron-safe approach)
59
+ if (g_writerStarted && g_assetWriterInput.isReadyForMoreMediaData) {
60
+ CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
61
+ if (pixelBuffer && g_pixelBufferAdaptor) {
62
+ CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
63
+ CMTime relativeTime = CMTimeSubtract(presentationTime, g_startTime);
64
+
65
+ BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
66
+ if (success) {
67
+ g_currentTime = relativeTime;
68
+ } else {
69
+ NSLog(@"โš ๏ธ Failed to append pixel buffer");
85
70
  }
86
71
  }
87
- break;
72
+ }
88
73
  }
89
74
  }
90
75
  @end
@@ -92,358 +77,170 @@ static AVAssetWriterInputPixelBufferAdaptor *g_pixelBufferAdaptor = nil;
92
77
  @implementation ScreenCaptureKitRecorder
93
78
 
94
79
  + (BOOL)isScreenCaptureKitAvailable {
95
- // ScreenCaptureKit'i tekrar etkinleลŸtir - video writer ile
96
-
97
80
  if (@available(macOS 12.3, *)) {
98
- NSLog(@"๐Ÿ” ScreenCaptureKit availability check - macOS 12.3+ confirmed");
99
-
100
- // Try to access ScreenCaptureKit classes to verify they're actually available
101
- @try {
102
- Class scStreamClass = NSClassFromString(@"SCStream");
103
- Class scContentFilterClass = NSClassFromString(@"SCContentFilter");
104
- Class scShareableContentClass = NSClassFromString(@"SCShareableContent");
105
-
106
- if (scStreamClass && scContentFilterClass && scShareableContentClass) {
107
- NSLog(@"โœ… ScreenCaptureKit classes are available");
108
- return YES;
109
- } else {
110
- NSLog(@"โŒ ScreenCaptureKit classes not found");
111
- NSLog(@" SCStream: %@", scStreamClass ? @"โœ…" : @"โŒ");
112
- NSLog(@" SCContentFilter: %@", scContentFilterClass ? @"โœ…" : @"โŒ");
113
- NSLog(@" SCShareableContent: %@", scShareableContentClass ? @"โœ…" : @"โŒ");
114
- return NO;
115
- }
116
- } @catch (NSException *exception) {
117
- NSLog(@"โŒ Exception checking ScreenCaptureKit classes: %@", exception.reason);
118
- return NO;
119
- }
81
+ return [SCShareableContent class] != nil && [SCStream class] != nil;
120
82
  }
121
- NSLog(@"โŒ macOS version < 12.3 - ScreenCaptureKit not available");
122
83
  return NO;
123
84
  }
124
85
 
125
- + (BOOL)startRecordingWithConfiguration:(NSDictionary *)config
126
- delegate:(id)delegate
127
- error:(NSError **)error {
86
+ + (BOOL)startRecordingWithConfiguration:(NSDictionary *)config delegate:(id)delegate error:(NSError **)error {
87
+ if (g_isRecording) {
88
+ return NO;
89
+ }
128
90
 
129
- if (@available(macOS 12.3, *)) {
130
- @try {
131
- // Get current app PID to exclude overlay windows
132
- NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
133
- pid_t currentPID = currentApp.processIdentifier;
134
-
135
- // Get all shareable content synchronously for immediate response
136
- dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
137
- __block BOOL success = NO;
138
- __block NSError *contentError = nil;
139
-
140
- [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *error) {
141
- if (error) {
142
- NSLog(@"Failed to get shareable content: %@", error);
143
- contentError = error;
144
- dispatch_semaphore_signal(semaphore);
145
- return;
146
- }
147
-
148
- // Find display to record
149
- SCDisplay *targetDisplay = content.displays.firstObject; // Default to first display
150
- if (config[@"displayId"]) {
151
- CGDirectDisplayID displayID = [config[@"displayId"] unsignedIntValue];
152
- for (SCDisplay *display in content.displays) {
153
- if (display.displayID == displayID) {
154
- targetDisplay = display;
155
- break;
156
- }
157
- }
158
- }
159
-
160
- // Get current process and Electron app windows to exclude
161
- NSMutableArray *excludedWindows = [NSMutableArray array];
162
- NSMutableArray *excludedApps = [NSMutableArray array];
163
-
164
- // Exclude current Node.js process windows (overlay selectors)
165
- for (SCWindow *window in content.windows) {
166
- if (window.owningApplication.processID == currentPID) {
167
- [excludedWindows addObject:window];
168
- NSLog(@"๐Ÿšซ Excluding Node.js overlay window: %@ (PID: %d)", window.title, currentPID);
169
- }
170
- }
171
-
172
- // Also try to exclude Electron app if running (common overlay use case)
173
- for (SCWindow *window in content.windows) {
174
- NSString *appName = window.owningApplication.applicationName;
175
- NSString *windowTitle = window.title ? window.title : @"<No Title>";
176
-
177
- // Debug: Log all windows to see what we're dealing with (only for small subset)
178
- if ([appName containsString:@"Electron"] || [windowTitle containsString:@"camera"]) {
179
- NSLog(@"๐Ÿ“‹ Found potential exclude window: '%@' from app: '%@' (PID: %d, Level: %ld)",
180
- windowTitle, appName, window.owningApplication.processID, (long)window.windowLayer);
181
- }
182
-
183
- // Comprehensive Electron window detection
184
- BOOL shouldExclude = NO;
185
-
186
- // Check app name patterns
187
- if ([appName containsString:@"Electron"] ||
188
- [appName isEqualToString:@"electron"] ||
189
- [appName isEqualToString:@"Electron Helper"]) {
190
- shouldExclude = YES;
191
- }
192
-
193
- // Check window title patterns
194
- if ([windowTitle containsString:@"Electron"] ||
195
- [windowTitle containsString:@"camera"] ||
196
- [windowTitle containsString:@"Camera"] ||
197
- [windowTitle containsString:@"overlay"] ||
198
- [windowTitle containsString:@"Overlay"]) {
199
- shouldExclude = YES;
200
- }
201
-
202
- // Check window properties (transparent, always on top windows)
203
- if (window.windowLayer > 100) { // High window levels (like alwaysOnTop)
204
- shouldExclude = YES;
205
- NSLog(@"๐Ÿ“‹ High-level window detected: '%@' (Level: %ld)", windowTitle, (long)window.windowLayer);
206
- }
207
-
208
- if (shouldExclude) {
209
- [excludedWindows addObject:window];
210
- NSLog(@"๐Ÿšซ Excluding window: '%@' from %@ (PID: %d, Level: %ld)",
211
- windowTitle, appName, window.owningApplication.processID, (long)window.windowLayer);
212
- }
213
- }
214
-
215
- NSLog(@"๐Ÿ“Š Total windows to exclude: %lu", (unsigned long)excludedWindows.count);
216
-
217
- // Create content filter - exclude overlay windows from recording
218
- SCContentFilter *filter = [[SCContentFilter alloc]
219
- initWithDisplay:targetDisplay
220
- excludingWindows:excludedWindows];
221
- NSLog(@"๐ŸŽฏ Using window-level exclusion for overlay prevention");
222
-
223
- // Create stream configuration
224
- SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
225
-
226
- // Handle capture area if specified
227
- if (config[@"captureRect"]) {
228
- NSDictionary *rect = config[@"captureRect"];
229
- streamConfig.width = [rect[@"width"] integerValue];
230
- streamConfig.height = [rect[@"height"] integerValue];
231
- // Note: ScreenCaptureKit crop rect would need additional handling
232
- } else {
233
- streamConfig.width = (NSInteger)targetDisplay.width;
234
- streamConfig.height = (NSInteger)targetDisplay.height;
235
- }
236
-
237
- streamConfig.minimumFrameInterval = CMTimeMake(1, 60); // 60 FPS
238
- streamConfig.queueDepth = 5;
239
- streamConfig.showsCursor = [config[@"captureCursor"] boolValue];
240
- streamConfig.capturesAudio = [config[@"includeSystemAudio"] boolValue];
241
-
242
- // Setup video writer
243
- g_outputPath = config[@"outputPath"];
244
- if (![self setupVideoWriterWithWidth:streamConfig.width
245
- height:streamConfig.height
246
- outputPath:g_outputPath
247
- includeAudio:[config[@"includeSystemAudio"] boolValue] || [config[@"includeMicrophone"] boolValue]]) {
248
- NSLog(@"โŒ Failed to setup video writer");
249
- contentError = [NSError errorWithDomain:@"ScreenCaptureKitError" code:-3 userInfo:@{NSLocalizedDescriptionKey: @"Video writer setup failed"}];
250
- dispatch_semaphore_signal(semaphore);
251
- return;
252
- }
253
-
254
- // Create delegate and output
255
- g_streamDelegate = [[ScreenCaptureKitRecorderDelegate alloc] init];
256
- g_streamOutput = [[ScreenCaptureKitStreamOutput alloc] init];
257
-
258
- // Create and start stream
259
- g_stream = [[SCStream alloc] initWithFilter:filter
260
- configuration:streamConfig
261
- delegate:g_streamDelegate];
262
-
263
- // Add stream output using correct API
264
- NSError *outputError = nil;
265
- BOOL outputAdded = [g_stream addStreamOutput:g_streamOutput
266
- type:SCStreamOutputTypeScreen
267
- sampleHandlerQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
268
- error:&outputError];
269
- if (!outputAdded) {
270
- NSLog(@"โŒ Failed to add screen output: %@", outputError);
271
- }
272
-
273
- if ([config[@"includeSystemAudio"] boolValue]) {
274
- if (@available(macOS 13.0, *)) {
275
- BOOL audioOutputAdded = [g_stream addStreamOutput:g_streamOutput
276
- type:SCStreamOutputTypeAudio
277
- sampleHandlerQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
278
- error:&outputError];
279
- if (!audioOutputAdded) {
280
- NSLog(@"โŒ Failed to add audio output: %@", outputError);
281
- }
282
- }
283
- }
284
-
285
- [g_stream startCaptureWithCompletionHandler:^(NSError *streamError) {
286
- if (streamError) {
287
- NSLog(@"โŒ Failed to start ScreenCaptureKit recording: %@", streamError);
288
- contentError = streamError;
289
- g_isRecording = NO;
290
- } else {
291
- NSLog(@"โœ… ScreenCaptureKit recording started successfully (excluding %lu overlay windows)", (unsigned long)excludedWindows.count);
292
- g_isRecording = YES;
293
- success = YES;
294
- }
295
- dispatch_semaphore_signal(semaphore);
296
- }];
297
- }];
298
-
299
- // Wait for completion (with timeout)
300
- dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
301
- if (dispatch_semaphore_wait(semaphore, timeout) == 0) {
302
- if (contentError && error) {
303
- *error = contentError;
304
- }
305
- return success;
91
+ g_outputPath = config[@"outputPath"];
92
+ g_writerStarted = NO;
93
+
94
+ // Setup Electron-safe video writer
95
+ [ScreenCaptureKitRecorder setupVideoWriter];
96
+
97
+ NSLog(@"๐ŸŽฌ Starting Electron-safe ScreenCaptureKit recording");
98
+
99
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
100
+ if (contentError) {
101
+ NSLog(@"โŒ Failed to get content: %@", contentError);
102
+ return;
103
+ }
104
+
105
+ // Get primary display
106
+ SCDisplay *targetDisplay = content.displays.firstObject;
107
+
108
+ // Simple content filter - no exclusions for now
109
+ SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
110
+
111
+ // Stream configuration
112
+ SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
113
+ streamConfig.width = 1280;
114
+ streamConfig.height = 720;
115
+ streamConfig.minimumFrameInterval = CMTimeMake(1, 30);
116
+ streamConfig.pixelFormat = kCVPixelFormatType_32BGRA;
117
+
118
+ // Create Electron-safe delegates
119
+ g_streamDelegate = [[ElectronSafeDelegate alloc] init];
120
+ g_streamOutput = [[ElectronSafeOutput alloc] init];
121
+
122
+ // Create stream
123
+ g_stream = [[SCStream alloc] initWithFilter:filter configuration:streamConfig delegate:g_streamDelegate];
124
+
125
+ [g_stream addStreamOutput:g_streamOutput
126
+ type:SCStreamOutputTypeScreen
127
+ sampleHandlerQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
128
+ error:nil];
129
+
130
+ [g_stream startCaptureWithCompletionHandler:^(NSError *startError) {
131
+ if (startError) {
132
+ NSLog(@"โŒ Failed to start capture: %@", startError);
306
133
  } else {
307
- NSLog(@"โฐ ScreenCaptureKit initialization timeout");
308
- if (error) {
309
- *error = [NSError errorWithDomain:@"ScreenCaptureKitError"
310
- code:-2
311
- userInfo:@{NSLocalizedDescriptionKey: @"Initialization timeout"}];
312
- }
313
- return NO;
134
+ NSLog(@"โœ… Frame capture started");
135
+ g_isRecording = YES;
314
136
  }
315
-
316
- } @catch (NSException *exception) {
317
- NSLog(@"ScreenCaptureKit recording exception: %@", exception);
318
- if (error) {
319
- *error = [NSError errorWithDomain:@"ScreenCaptureKitError"
320
- code:-1
321
- userInfo:@{NSLocalizedDescriptionKey: exception.reason}];
322
- }
323
- return NO;
324
- }
325
- }
137
+ }];
138
+ }];
326
139
 
327
- return NO;
140
+ return YES;
328
141
  }
329
142
 
330
143
  + (void)stopRecording {
331
- if (@available(macOS 12.3, *)) {
332
- if (g_stream && g_isRecording) {
333
- [g_stream stopCaptureWithCompletionHandler:^(NSError *error) {
334
- if (error) {
335
- NSLog(@"Error stopping ScreenCaptureKit recording: %@", error);
336
- } else {
337
- NSLog(@"ScreenCaptureKit recording stopped successfully");
338
- }
339
-
340
- // Finalize video file
341
- if (g_assetWriter && g_assetWriter.status == AVAssetWriterStatusWriting) {
342
- [g_videoWriterInput markAsFinished];
343
- if (g_audioWriterInput) {
344
- [g_audioWriterInput markAsFinished];
345
- }
346
- [g_assetWriter finishWritingWithCompletionHandler:^{
347
- NSLog(@"โœ… ScreenCaptureKit video file finalized: %@", g_outputPath);
348
-
349
- // Cleanup
350
- g_assetWriter = nil;
351
- g_videoWriterInput = nil;
352
- g_audioWriterInput = nil;
353
- g_pixelBufferAdaptor = nil;
354
- g_outputPath = nil;
355
- g_sessionStarted = NO;
356
- }];
357
- }
358
-
359
- g_isRecording = NO;
360
- g_stream = nil;
361
- g_streamDelegate = nil;
362
- g_streamOutput = nil;
363
- }];
364
- }
144
+ if (!g_isRecording || !g_stream) {
145
+ return;
365
146
  }
147
+
148
+ NSLog(@"๐Ÿ›‘ Stopping Electron-safe ScreenCaptureKit recording");
149
+
150
+ [g_stream stopCaptureWithCompletionHandler:^(NSError *stopError) {
151
+ if (stopError) {
152
+ NSLog(@"โŒ Stop error: %@", stopError);
153
+ } else {
154
+ NSLog(@"โœ… ScreenCaptureKit stream stopped in completion handler");
155
+ }
156
+ // Video finalization happens in delegate
157
+ }];
366
158
  }
367
159
 
368
160
  + (BOOL)isRecording {
369
161
  return g_isRecording;
370
162
  }
371
163
 
372
- + (BOOL)setupVideoWriterWithWidth:(NSInteger)width
373
- height:(NSInteger)height
374
- outputPath:(NSString *)outputPath
375
- includeAudio:(BOOL)includeAudio {
164
+ + (void)setupVideoWriter {
165
+ if (g_assetWriter) {
166
+ return; // Already setup
167
+ }
168
+
169
+ NSLog(@"๐Ÿ”ง Setting up Electron-safe video writer");
376
170
 
377
- // Create asset writer with MP4 format for better compatibility
378
- NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
171
+ NSURL *outputURL = [NSURL fileURLWithPath:g_outputPath];
379
172
  NSError *error = nil;
380
- g_assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeMPEG4 error:&error];
173
+
174
+ g_assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&error];
381
175
 
382
176
  if (error || !g_assetWriter) {
383
177
  NSLog(@"โŒ Failed to create asset writer: %@", error);
384
- return NO;
178
+ return;
385
179
  }
386
180
 
387
- // Video writer input - basic settings for maximum compatibility
181
+ // Electron-safe video settings
388
182
  NSDictionary *videoSettings = @{
389
183
  AVVideoCodecKey: AVVideoCodecTypeH264,
390
- AVVideoWidthKey: @(width),
391
- AVVideoHeightKey: @(height)
184
+ AVVideoWidthKey: @1280,
185
+ AVVideoHeightKey: @720,
186
+ AVVideoCompressionPropertiesKey: @{
187
+ AVVideoAverageBitRateKey: @(1280 * 720 * 2),
188
+ AVVideoMaxKeyFrameIntervalKey: @30
189
+ }
392
190
  };
393
191
 
394
- g_videoWriterInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
395
- g_videoWriterInput.expectsMediaDataInRealTime = YES;
192
+ g_assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
193
+ g_assetWriterInput.expectsMediaDataInRealTime = YES; // Important for live capture
396
194
 
397
- if (![g_assetWriter canAddInput:g_videoWriterInput]) {
398
- NSLog(@"โŒ Cannot add video input to asset writer");
399
- return NO;
400
- }
401
- [g_assetWriter addInput:g_videoWriterInput];
402
-
403
- // Create pixel buffer adaptor for ScreenCaptureKit compatibility
195
+ // Pixel buffer attributes matching ScreenCaptureKit format
404
196
  NSDictionary *pixelBufferAttributes = @{
405
- (NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
406
- (NSString *)kCVPixelBufferWidthKey: @(width),
407
- (NSString *)kCVPixelBufferHeightKey: @(height),
408
- (NSString *)kCVPixelBufferIOSurfacePropertiesKey: @{}
197
+ (NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
198
+ (NSString*)kCVPixelBufferWidthKey: @1280,
199
+ (NSString*)kCVPixelBufferHeightKey: @720
409
200
  };
410
201
 
411
- g_pixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc]
412
- initWithAssetWriterInput:g_videoWriterInput
413
- sourcePixelBufferAttributes:pixelBufferAttributes];
414
-
415
- if (!g_pixelBufferAdaptor) {
416
- NSLog(@"โŒ Cannot create pixel buffer adaptor");
417
- return NO;
418
- }
202
+ g_pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:g_assetWriterInput sourcePixelBufferAttributes:pixelBufferAttributes];
419
203
 
420
- // Audio writer input (if needed)
421
- if (includeAudio) {
422
- NSDictionary *audioSettings = @{
423
- AVFormatIDKey: @(kAudioFormatMPEG4AAC),
424
- AVSampleRateKey: @(44100.0),
425
- AVNumberOfChannelsKey: @(2),
426
- AVEncoderBitRateKey: @(128000)
427
- };
428
-
429
- g_audioWriterInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
430
- g_audioWriterInput.expectsMediaDataInRealTime = YES;
431
-
432
- if ([g_assetWriter canAddInput:g_audioWriterInput]) {
433
- [g_assetWriter addInput:g_audioWriterInput];
434
- }
204
+ if ([g_assetWriter canAddInput:g_assetWriterInput]) {
205
+ [g_assetWriter addInput:g_assetWriterInput];
206
+ NSLog(@"โœ… Electron-safe video writer setup complete");
207
+ } else {
208
+ NSLog(@"โŒ Failed to add input to asset writer");
435
209
  }
210
+ }
211
+
212
+ + (void)finalizeVideoWriter {
213
+ NSLog(@"๐ŸŽฌ Finalizing Electron-safe video writer");
436
214
 
437
- // Start writing (session will be started when first sample arrives)
438
- if (![g_assetWriter startWriting]) {
439
- NSLog(@"โŒ Failed to start writing: %@", g_assetWriter.error);
440
- return NO;
215
+ if (!g_assetWriter || !g_writerStarted) {
216
+ NSLog(@"โš ๏ธ Video writer not started, cleaning up");
217
+ [ScreenCaptureKitRecorder cleanupVideoWriter];
218
+ return;
441
219
  }
442
220
 
443
- g_sessionStarted = NO; // Reset session flag
444
- NSLog(@"โœ… ScreenCaptureKit video writer setup complete: %@", outputPath);
221
+ [g_assetWriterInput markAsFinished];
445
222
 
446
- return YES;
223
+ [g_assetWriter finishWritingWithCompletionHandler:^{
224
+ if (g_assetWriter.status == AVAssetWriterStatusCompleted) {
225
+ NSLog(@"โœ… Electron-safe video created successfully: %@", g_outputPath);
226
+ } else {
227
+ NSLog(@"โŒ Video creation failed: %@", g_assetWriter.error);
228
+ }
229
+
230
+ [ScreenCaptureKitRecorder cleanupVideoWriter];
231
+ }];
232
+ }
233
+
234
+ + (void)cleanupVideoWriter {
235
+ g_assetWriter = nil;
236
+ g_assetWriterInput = nil;
237
+ g_pixelBufferAdaptor = nil;
238
+ g_writerStarted = NO;
239
+ g_stream = nil;
240
+ g_streamDelegate = nil;
241
+ g_streamOutput = nil;
242
+
243
+ NSLog(@"๐Ÿงน Video writer cleanup complete");
447
244
  }
448
245
 
449
246
  @end