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.
- package/package.json +1 -1
- package/src/mac_recorder.mm +37 -8
- package/src/screen_capture_kit.h +3 -4
- package/src/screen_capture_kit.mm +173 -376
- 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
|
@@ -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 *
|
|
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
|
|
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
|
|
21
|
+
@implementation ElectronSafeDelegate
|
|
23
22
|
- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
|
|
24
|
-
NSLog(@"ScreenCaptureKit
|
|
23
|
+
NSLog(@"๐ ScreenCaptureKit stream stopped in delegate");
|
|
24
|
+
g_isRecording = NO;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
@
|
|
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 (!
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
86
|
+
+ (BOOL)startRecordingWithConfiguration:(NSDictionary *)config delegate:(id)delegate error:(NSError **)error {
|
|
87
|
+
if (g_isRecording) {
|
|
88
|
+
return NO;
|
|
89
|
+
}
|
|
128
90
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(@"
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
140
|
+
return YES;
|
|
328
141
|
}
|
|
329
142
|
|
|
330
143
|
+ (void)stopRecording {
|
|
331
|
-
if (
|
|
332
|
-
|
|
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
|
-
+ (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
164
|
+
+ (void)setupVideoWriter {
|
|
165
|
+
if (g_assetWriter) {
|
|
166
|
+
return; // Already setup
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
NSLog(@"๐ง Setting up Electron-safe video writer");
|
|
376
170
|
|
|
377
|
-
|
|
378
|
-
NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
|
|
171
|
+
NSURL *outputURL = [NSURL fileURLWithPath:g_outputPath];
|
|
379
172
|
NSError *error = nil;
|
|
380
|
-
|
|
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
|
|
178
|
+
return;
|
|
385
179
|
}
|
|
386
180
|
|
|
387
|
-
//
|
|
181
|
+
// Electron-safe video settings
|
|
388
182
|
NSDictionary *videoSettings = @{
|
|
389
183
|
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
390
|
-
AVVideoWidthKey: @
|
|
391
|
-
AVVideoHeightKey: @
|
|
184
|
+
AVVideoWidthKey: @1280,
|
|
185
|
+
AVVideoHeightKey: @720,
|
|
186
|
+
AVVideoCompressionPropertiesKey: @{
|
|
187
|
+
AVVideoAverageBitRateKey: @(1280 * 720 * 2),
|
|
188
|
+
AVVideoMaxKeyFrameIntervalKey: @30
|
|
189
|
+
}
|
|
392
190
|
};
|
|
393
191
|
|
|
394
|
-
|
|
395
|
-
|
|
192
|
+
g_assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
|
|
193
|
+
g_assetWriterInput.expectsMediaDataInRealTime = YES; // Important for live capture
|
|
396
194
|
|
|
397
|
-
|
|
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
|
|
406
|
-
(NSString
|
|
407
|
-
(NSString
|
|
408
|
-
(NSString *)kCVPixelBufferIOSurfacePropertiesKey: @{}
|
|
197
|
+
(NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
|
|
198
|
+
(NSString*)kCVPixelBufferWidthKey: @1280,
|
|
199
|
+
(NSString*)kCVPixelBufferHeightKey: @720
|
|
409
200
|
};
|
|
410
201
|
|
|
411
|
-
g_pixelBufferAdaptor = [
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
return
|
|
215
|
+
if (!g_assetWriter || !g_writerStarted) {
|
|
216
|
+
NSLog(@"โ ๏ธ Video writer not started, cleaning up");
|
|
217
|
+
[ScreenCaptureKitRecorder cleanupVideoWriter];
|
|
218
|
+
return;
|
|
441
219
|
}
|
|
442
220
|
|
|
443
|
-
|
|
444
|
-
NSLog(@"โ
ScreenCaptureKit video writer setup complete: %@", outputPath);
|
|
221
|
+
[g_assetWriterInput markAsFinished];
|
|
445
222
|
|
|
446
|
-
|
|
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
|