node-mac-recorder 2.13.10 โ 2.13.12
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/.claude/settings.local.json +3 -1
- package/package.json +1 -1
- package/src/mac_recorder.mm +3 -6
- package/src/screen_capture_kit.h +1 -1
- package/src/screen_capture_kit.mm +136 -308
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"Bash(ELECTRON_VERSION=25.0.0 node test-native-call.js)",
|
|
29
29
|
"Bash(chmod:*)",
|
|
30
30
|
"Bash(ffprobe:*)",
|
|
31
|
-
"Bash(ffmpeg:*)"
|
|
31
|
+
"Bash(ffmpeg:*)",
|
|
32
|
+
"WebSearch",
|
|
33
|
+
"Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐ Testing with proper permissions and Electron env'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function test() {\n try {\n const outputPath = ''./test-output/proper-test.mov'';\n console.log(''๐น Starting recording...'');\n const success = await recorder.startRecording(outputPath, {\n captureCursor: true,\n includeMicrophone: false,\n includeSystemAudio: false\n });\n \n if (success) {\n console.log(''โ
Recording started - waiting 2 seconds'');\n await new Promise(resolve => setTimeout(resolve, 2000));\n console.log(''๐ Stopping recording...'');\n await recorder.stopRecording();\n console.log(''โ
Test completed'');\n } else {\n console.log(''โ Recording start failed'');\n }\n } catch (error) {\n console.log(''โ Error:'', error.message);\n }\n}\n\ntest();\n\")"
|
|
32
34
|
],
|
|
33
35
|
"deny": []
|
|
34
36
|
}
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -165,7 +165,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
165
165
|
// Smart Recording Selection: ScreenCaptureKit vs Alternative
|
|
166
166
|
NSLog(@"๐ฏ Smart Recording Engine Selection");
|
|
167
167
|
|
|
168
|
-
//
|
|
168
|
+
// Electron environment detection (removed disable logic)
|
|
169
169
|
BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
|
|
170
170
|
[NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
|
|
171
171
|
(NSProcessInfo.processInfo.processName &&
|
|
@@ -175,11 +175,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
175
175
|
[NSBundle.mainBundle.bundlePath containsString:@"Electron"]);
|
|
176
176
|
|
|
177
177
|
if (isElectron) {
|
|
178
|
-
NSLog(@"โก Electron environment detected -
|
|
179
|
-
NSLog(@"
|
|
180
|
-
// Skip ScreenCaptureKit completely for Electron
|
|
181
|
-
NSLog(@"โ Recording disabled in Electron for stability - use Node.js environment instead");
|
|
182
|
-
return Napi::Boolean::New(env, false);
|
|
178
|
+
NSLog(@"โก Electron environment detected - continuing with ScreenCaptureKit");
|
|
179
|
+
NSLog(@"โ ๏ธ Warning: ScreenCaptureKit in Electron may require additional stability measures");
|
|
183
180
|
}
|
|
184
181
|
|
|
185
182
|
// Non-Electron: Use ScreenCaptureKit
|
package/src/screen_capture_kit.h
CHANGED
|
@@ -6,267 +6,100 @@ static id<SCStreamDelegate> g_streamDelegate = nil;
|
|
|
6
6
|
static id<SCStreamOutput> g_streamOutput = nil;
|
|
7
7
|
static BOOL g_isRecording = NO;
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Modern ScreenCaptureKit writer
|
|
10
10
|
static AVAssetWriter *g_assetWriter = nil;
|
|
11
11
|
static AVAssetWriterInput *g_assetWriterInput = nil;
|
|
12
12
|
static AVAssetWriterInputPixelBufferAdaptor *g_pixelBufferAdaptor = nil;
|
|
13
13
|
static NSString *g_outputPath = nil;
|
|
14
|
-
static CMTime g_startTime;
|
|
15
|
-
static CMTime g_currentTime;
|
|
16
14
|
static BOOL g_writerStarted = NO;
|
|
17
|
-
static int
|
|
15
|
+
static int g_frameCount = 0;
|
|
18
16
|
|
|
19
|
-
@interface
|
|
17
|
+
@interface ModernStreamDelegate : NSObject <SCStreamDelegate>
|
|
20
18
|
@end
|
|
21
19
|
|
|
22
|
-
@implementation
|
|
20
|
+
@implementation ModernStreamDelegate
|
|
23
21
|
- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
|
|
24
|
-
NSLog(@"๐
|
|
22
|
+
NSLog(@"๐ Stream stopped");
|
|
25
23
|
g_isRecording = NO;
|
|
26
24
|
|
|
27
25
|
if (error) {
|
|
28
|
-
NSLog(@"โ Stream
|
|
29
|
-
} else {
|
|
30
|
-
NSLog(@"โ
ScreenCaptureKit stream stopped successfully in delegate");
|
|
26
|
+
NSLog(@"โ Stream error: %@", error);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
// Finalize video writer
|
|
34
|
-
NSLog(@"๐ฌ Delegate calling finalizeVideoWriter...");
|
|
35
29
|
[ScreenCaptureKitRecorder finalizeVideoWriter];
|
|
36
|
-
NSLog(@"๐ฌ Delegate finished calling finalizeVideoWriter");
|
|
37
30
|
}
|
|
38
31
|
@end
|
|
39
32
|
|
|
40
|
-
@interface
|
|
41
|
-
- (void)processSampleBufferSafely:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type;
|
|
33
|
+
@interface ModernStreamOutput : NSObject <SCStreamOutput>
|
|
42
34
|
@end
|
|
43
35
|
|
|
44
|
-
@implementation
|
|
36
|
+
@implementation ModernStreamOutput
|
|
45
37
|
- (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- (void)processSampleBufferSafely:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
55
|
-
// ELECTRON CRASH PROTECTION: Multiple layers of safety
|
|
56
|
-
if (!g_isRecording || !g_assetWriterInput) {
|
|
57
|
-
NSLog(@"๐ ProcessSampleBuffer: isRecording=%d, type=%d, writerInput=%p", g_isRecording, (int)type, g_assetWriterInput);
|
|
38
|
+
if (!g_isRecording) return;
|
|
39
|
+
|
|
40
|
+
// Only process screen frames
|
|
41
|
+
if (type != SCStreamOutputTypeScreen) return;
|
|
42
|
+
|
|
43
|
+
// Validate sample buffer
|
|
44
|
+
if (!sampleBuffer || !CMSampleBufferIsValid(sampleBuffer)) {
|
|
45
|
+
NSLog(@"โ ๏ธ Invalid sample buffer");
|
|
58
46
|
return;
|
|
59
47
|
}
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
NSLog(@"๐ Received audio sample buffer - skipping for video-only recording");
|
|
49
|
+
// Get pixel buffer
|
|
50
|
+
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
|
51
|
+
if (!pixelBuffer) {
|
|
52
|
+
NSLog(@"โ ๏ธ No pixel buffer in sample");
|
|
66
53
|
return;
|
|
67
54
|
}
|
|
68
55
|
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
// Initialize writer on first frame
|
|
57
|
+
static dispatch_once_t onceToken;
|
|
58
|
+
dispatch_once(&onceToken, ^{
|
|
59
|
+
[self initializeWriterWithSampleBuffer:sampleBuffer];
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!g_writerStarted) {
|
|
71
63
|
return;
|
|
72
64
|
}
|
|
73
65
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
// Write frame
|
|
67
|
+
[self writePixelBuffer:pixelBuffer];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
- (void)initializeWriterWithSampleBuffer:(CMSampleBufferRef)sampleBuffer {
|
|
71
|
+
if (!g_assetWriter) return;
|
|
72
|
+
|
|
73
|
+
NSLog(@"๐ฌ Initializing writer with first sample");
|
|
74
|
+
|
|
75
|
+
CMTime startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
76
|
+
if (!CMTIME_IS_VALID(startTime)) {
|
|
77
|
+
startTime = CMTimeMakeWithSeconds(0, 600);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[g_assetWriter startWriting];
|
|
81
|
+
[g_assetWriter startSessionAtSourceTime:startTime];
|
|
82
|
+
g_writerStarted = YES;
|
|
83
|
+
|
|
84
|
+
NSLog(@"โ
Writer initialized");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
- (void)writePixelBuffer:(CVPixelBufferRef)pixelBuffer {
|
|
88
|
+
if (!g_assetWriterInput.isReadyForMoreMediaData) {
|
|
77
89
|
return;
|
|
78
90
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// SAFETY CHECK: Ensure valid time
|
|
92
|
-
if (CMTIME_IS_VALID(presentationTime) && CMTIME_IS_NUMERIC(presentationTime)) {
|
|
93
|
-
g_startTime = presentationTime;
|
|
94
|
-
g_currentTime = g_startTime;
|
|
95
|
-
|
|
96
|
-
// SAFETY LAYER 4: Writer state validation
|
|
97
|
-
if (g_assetWriter.status == AVAssetWriterStatusUnknown) {
|
|
98
|
-
[g_assetWriter startWriting];
|
|
99
|
-
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
100
|
-
g_writerStarted = YES;
|
|
101
|
-
NSLog(@"โ
Ultra-safe ScreenCaptureKit writer started");
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
// Use current time if sample buffer time is invalid
|
|
105
|
-
NSLog(@"โ ๏ธ Invalid sample buffer time, using current time");
|
|
106
|
-
g_startTime = CMTimeMakeWithSeconds(CACurrentMediaTime(), 600);
|
|
107
|
-
g_currentTime = g_startTime;
|
|
108
|
-
|
|
109
|
-
if (g_assetWriter.status == AVAssetWriterStatusUnknown) {
|
|
110
|
-
[g_assetWriter startWriting];
|
|
111
|
-
[g_assetWriter startSessionAtSourceTime:g_startTime];
|
|
112
|
-
g_writerStarted = YES;
|
|
113
|
-
NSLog(@"โ
Ultra-safe ScreenCaptureKit writer started with current time");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} @catch (NSException *writerException) {
|
|
117
|
-
NSLog(@"โ ๏ธ Writer initialization failed safely: %@", writerException.reason);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// SAFETY LAYER 5: Frame processing with isolation
|
|
123
|
-
if (!g_writerStarted || !g_assetWriterInput || !g_pixelBufferAdaptor) {
|
|
124
|
-
NSLog(@"โ LAYER 5 FAIL: writer=%d, input=%p, adaptor=%p", g_writerStarted, g_assetWriterInput, g_pixelBufferAdaptor);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
NSLog(@"โ
LAYER 5 PASS: Writer components ready");
|
|
128
|
-
|
|
129
|
-
// SAFETY LAYER 6: Higher frame rate for video
|
|
130
|
-
static NSTimeInterval lastProcessTime = 0;
|
|
131
|
-
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
|
|
132
|
-
if (currentTime - lastProcessTime < 0.033) { // Max 30 FPS
|
|
133
|
-
NSLog(@"โ LAYER 6 FAIL: Rate limited (%.3fs since last)", currentTime - lastProcessTime);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
lastProcessTime = currentTime;
|
|
137
|
-
NSLog(@"โ
LAYER 6 PASS: Rate limiting OK");
|
|
138
|
-
|
|
139
|
-
// SAFETY LAYER 7: Input readiness check
|
|
140
|
-
if (!g_assetWriterInput.isReadyForMoreMediaData) {
|
|
141
|
-
NSLog(@"โ LAYER 7 FAIL: Writer not ready for data");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
NSLog(@"โ
LAYER 7 PASS: Writer ready for data");
|
|
145
|
-
|
|
146
|
-
// SAFETY LAYER 8: Get pixel buffer from sample buffer
|
|
147
|
-
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
|
148
|
-
BOOL createdDummyBuffer = NO;
|
|
149
|
-
|
|
150
|
-
if (!pixelBuffer) {
|
|
151
|
-
// Try alternative methods to get pixel buffer
|
|
152
|
-
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
|
|
153
|
-
if (formatDesc) {
|
|
154
|
-
CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);
|
|
155
|
-
NSLog(@"๐ Sample buffer media type: %u (Video=%u)", (unsigned int)mediaType, (unsigned int)kCMMediaType_Video);
|
|
156
|
-
return; // Skip processing if no pixel buffer
|
|
157
|
-
} else {
|
|
158
|
-
NSLog(@"โ No pixel buffer and no format description - permissions issue");
|
|
159
|
-
|
|
160
|
-
// Create a dummy pixel buffer using the pool from adaptor
|
|
161
|
-
CVPixelBufferRef dummyBuffer = NULL;
|
|
162
|
-
|
|
163
|
-
// Try to get a pixel buffer from the adaptor's buffer pool
|
|
164
|
-
CVPixelBufferPoolRef bufferPool = g_pixelBufferAdaptor.pixelBufferPool;
|
|
165
|
-
if (bufferPool) {
|
|
166
|
-
CVReturn poolResult = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, bufferPool, &dummyBuffer);
|
|
167
|
-
if (poolResult == kCVReturnSuccess && dummyBuffer) {
|
|
168
|
-
pixelBuffer = dummyBuffer;
|
|
169
|
-
createdDummyBuffer = YES;
|
|
170
|
-
NSLog(@"โ
Created dummy buffer from adaptor pool");
|
|
171
|
-
|
|
172
|
-
// Fill buffer with black pixels
|
|
173
|
-
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
|
174
|
-
void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
175
|
-
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
|
176
|
-
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
177
|
-
if (baseAddress) {
|
|
178
|
-
memset(baseAddress, 0, bytesPerRow * height);
|
|
179
|
-
}
|
|
180
|
-
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
|
181
|
-
} else {
|
|
182
|
-
NSLog(@"โ Failed to create buffer from pool: %d", poolResult);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Fallback: create manual buffer if pool method failed
|
|
187
|
-
if (!dummyBuffer) {
|
|
188
|
-
CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault,
|
|
189
|
-
1920, 1080,
|
|
190
|
-
kCVPixelFormatType_32BGRA,
|
|
191
|
-
NULL, &dummyBuffer);
|
|
192
|
-
if (result == kCVReturnSuccess && dummyBuffer) {
|
|
193
|
-
pixelBuffer = dummyBuffer;
|
|
194
|
-
createdDummyBuffer = YES;
|
|
195
|
-
NSLog(@"โ
Created manual dummy buffer");
|
|
196
|
-
|
|
197
|
-
// Fill buffer with black pixels
|
|
198
|
-
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
|
199
|
-
void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
200
|
-
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
|
201
|
-
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
202
|
-
if (baseAddress) {
|
|
203
|
-
memset(baseAddress, 0, bytesPerRow * height);
|
|
204
|
-
}
|
|
205
|
-
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
|
206
|
-
} else {
|
|
207
|
-
NSLog(@"โ Failed to create dummy pixel buffer");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
NSLog(@"โ
LAYER 8 PASS: Pixel buffer ready (dummy=%d)", createdDummyBuffer);
|
|
214
|
-
|
|
215
|
-
// SAFETY LAYER 9: Dimension validation - flexible this time
|
|
216
|
-
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
|
217
|
-
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
218
|
-
if (width == 0 || height == 0 || width > 4096 || height > 4096) {
|
|
219
|
-
NSLog(@"โ LAYER 9 FAIL: Invalid dimensions %zux%zu", width, height);
|
|
220
|
-
return; // Skip only if clearly invalid
|
|
221
|
-
}
|
|
222
|
-
NSLog(@"โ
LAYER 9 PASS: Valid dimensions %zux%zu", width, height);
|
|
223
|
-
|
|
224
|
-
// SAFETY LAYER 10: Time validation - use sequential timing
|
|
225
|
-
g_frameNumber++;
|
|
226
|
-
|
|
227
|
-
// Create sequential time stamps
|
|
228
|
-
CMTime relativeTime = CMTimeMake(g_frameNumber, 30); // 30 FPS sequential
|
|
229
|
-
|
|
230
|
-
if (!CMTIME_IS_VALID(relativeTime)) {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
double seconds = CMTimeGetSeconds(relativeTime);
|
|
235
|
-
if (seconds > 30.0) { // Max 30 seconds
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// SAFETY LAYER 11: Append with complete exception handling
|
|
240
|
-
@try {
|
|
241
|
-
// Use pixel buffer directly - copy was causing errors
|
|
242
|
-
NSLog(@"๐ Attempting to append frame %d with time %.3fs", g_frameNumber, seconds);
|
|
243
|
-
BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
|
|
244
|
-
|
|
245
|
-
if (success) {
|
|
246
|
-
g_currentTime = relativeTime;
|
|
247
|
-
static int ultraSafeFrameCount = 0;
|
|
248
|
-
ultraSafeFrameCount++;
|
|
249
|
-
NSLog(@"โ
Frame %d appended successfully! (%.1fs)", ultraSafeFrameCount, seconds);
|
|
250
|
-
} else {
|
|
251
|
-
NSLog(@"โ Failed to append frame %d - adaptor rejected", g_frameNumber);
|
|
252
|
-
}
|
|
253
|
-
} @catch (NSException *appendException) {
|
|
254
|
-
NSLog(@"๐ก๏ธ Append exception handled safely: %@", appendException.reason);
|
|
255
|
-
// Continue gracefully - don't crash
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Cleanup dummy pixel buffer if we created one
|
|
259
|
-
if (pixelBuffer && createdDummyBuffer) {
|
|
260
|
-
CVPixelBufferRelease(pixelBuffer);
|
|
261
|
-
NSLog(@"๐งน Released dummy pixel buffer");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
} @catch (NSException *outerException) {
|
|
265
|
-
NSLog(@"๐ก๏ธ Outer exception handled: %@", outerException.reason);
|
|
266
|
-
// Ultimate safety - graceful continue
|
|
267
|
-
} @catch (...) {
|
|
268
|
-
NSLog(@"๐ก๏ธ Unknown exception caught and handled safely");
|
|
269
|
-
// Catch any C++ exceptions too
|
|
91
|
+
|
|
92
|
+
// Create time for this frame
|
|
93
|
+
CMTime frameTime = CMTimeMakeWithSeconds(g_frameCount / 30.0, 600);
|
|
94
|
+
g_frameCount++;
|
|
95
|
+
|
|
96
|
+
// Write the frame
|
|
97
|
+
BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
|
|
98
|
+
|
|
99
|
+
if (success) {
|
|
100
|
+
NSLog(@"โ
Frame %d written", g_frameCount);
|
|
101
|
+
} else {
|
|
102
|
+
NSLog(@"โ Failed to write frame %d", g_frameCount);
|
|
270
103
|
}
|
|
271
104
|
}
|
|
272
105
|
@end
|
|
@@ -282,62 +115,63 @@ static int g_frameNumber = 0;
|
|
|
282
115
|
|
|
283
116
|
+ (BOOL)startRecordingWithConfiguration:(NSDictionary *)config delegate:(id)delegate error:(NSError **)error {
|
|
284
117
|
if (g_isRecording) {
|
|
118
|
+
NSLog(@"โ ๏ธ Already recording");
|
|
285
119
|
return NO;
|
|
286
120
|
}
|
|
287
121
|
|
|
288
122
|
g_outputPath = config[@"outputPath"];
|
|
289
|
-
|
|
290
|
-
g_frameNumber = 0; // Reset frame counter for new recording
|
|
123
|
+
g_frameCount = 0;
|
|
291
124
|
|
|
292
|
-
|
|
293
|
-
[ScreenCaptureKitRecorder setupVideoWriter];
|
|
125
|
+
NSLog(@"๐ฌ Starting modern ScreenCaptureKit recording");
|
|
294
126
|
|
|
295
|
-
|
|
127
|
+
// Setup writer first
|
|
128
|
+
if (![self setupVideoWriter]) {
|
|
129
|
+
NSLog(@"โ Failed to setup video writer");
|
|
130
|
+
return NO;
|
|
131
|
+
}
|
|
296
132
|
|
|
133
|
+
// Get shareable content
|
|
297
134
|
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
|
|
298
135
|
if (contentError) {
|
|
299
|
-
NSLog(@"โ
|
|
136
|
+
NSLog(@"โ Content error: %@", contentError);
|
|
300
137
|
return;
|
|
301
138
|
}
|
|
302
139
|
|
|
303
|
-
NSLog(@"โ
Got
|
|
140
|
+
NSLog(@"โ
Got %lu displays", content.displays.count);
|
|
304
141
|
|
|
305
142
|
if (content.displays.count == 0) {
|
|
306
|
-
NSLog(@"โ No displays
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Get primary display
|
|
311
|
-
SCDisplay *targetDisplay = content.displays.firstObject;
|
|
312
|
-
if (!targetDisplay) {
|
|
313
|
-
NSLog(@"โ No target display found");
|
|
143
|
+
NSLog(@"โ No displays found");
|
|
314
144
|
return;
|
|
315
145
|
}
|
|
316
146
|
|
|
317
|
-
|
|
147
|
+
// Use first display
|
|
148
|
+
SCDisplay *display = content.displays.firstObject;
|
|
149
|
+
NSLog(@"๐ฅ๏ธ Using display %u (%dx%d)", display.displayID, (int)display.width, (int)display.height);
|
|
318
150
|
|
|
319
|
-
// Create
|
|
320
|
-
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:
|
|
321
|
-
NSLog(@"โ
Content filter created for display");
|
|
151
|
+
// Create filter for entire display
|
|
152
|
+
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display excludingWindows:@[]];
|
|
322
153
|
|
|
323
|
-
//
|
|
154
|
+
// Configure stream
|
|
324
155
|
SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
|
|
325
|
-
|
|
326
|
-
|
|
156
|
+
|
|
157
|
+
// Use display's actual dimensions but scale if too large
|
|
158
|
+
NSInteger targetWidth = MIN(display.width, 1920);
|
|
159
|
+
NSInteger targetHeight = MIN(display.height, 1080);
|
|
160
|
+
|
|
161
|
+
streamConfig.width = targetWidth;
|
|
162
|
+
streamConfig.height = targetHeight;
|
|
327
163
|
streamConfig.minimumFrameInterval = CMTimeMake(1, 30); // 30 FPS
|
|
328
164
|
streamConfig.pixelFormat = kCVPixelFormatType_32BGRA;
|
|
329
165
|
streamConfig.showsCursor = YES;
|
|
166
|
+
streamConfig.scalesToFit = YES;
|
|
330
167
|
|
|
331
|
-
NSLog(@"๐ง Stream
|
|
168
|
+
NSLog(@"๐ง Stream: %ldx%ld @ 30fps", targetWidth, targetHeight);
|
|
332
169
|
|
|
333
|
-
// Create
|
|
334
|
-
g_streamDelegate = [[
|
|
335
|
-
g_streamOutput = [[
|
|
170
|
+
// Create delegates
|
|
171
|
+
g_streamDelegate = [[ModernStreamDelegate alloc] init];
|
|
172
|
+
g_streamOutput = [[ModernStreamOutput alloc] init];
|
|
336
173
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// Create stream
|
|
340
|
-
NSError *streamError = nil;
|
|
174
|
+
// Create and start stream
|
|
341
175
|
g_stream = [[SCStream alloc] initWithFilter:filter configuration:streamConfig delegate:g_streamDelegate];
|
|
342
176
|
|
|
343
177
|
if (!g_stream) {
|
|
@@ -345,27 +179,27 @@ static int g_frameNumber = 0;
|
|
|
345
179
|
return;
|
|
346
180
|
}
|
|
347
181
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
error:&streamError];
|
|
182
|
+
// Add output
|
|
183
|
+
NSError *outputError = nil;
|
|
184
|
+
BOOL outputAdded = [g_stream addStreamOutput:g_streamOutput
|
|
185
|
+
type:SCStreamOutputTypeScreen
|
|
186
|
+
sampleHandlerQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
|
|
187
|
+
error:&outputError];
|
|
355
188
|
|
|
356
|
-
if (!
|
|
357
|
-
NSLog(@"โ
|
|
189
|
+
if (!outputAdded || outputError) {
|
|
190
|
+
NSLog(@"โ Output error: %@", outputError);
|
|
358
191
|
return;
|
|
359
192
|
}
|
|
360
193
|
|
|
361
|
-
NSLog(@"โ
|
|
194
|
+
NSLog(@"โ
Output added");
|
|
362
195
|
|
|
196
|
+
// Start capture
|
|
363
197
|
[g_stream startCaptureWithCompletionHandler:^(NSError *startError) {
|
|
364
198
|
if (startError) {
|
|
365
|
-
NSLog(@"โ
|
|
199
|
+
NSLog(@"โ Start error: %@", startError);
|
|
366
200
|
g_isRecording = NO;
|
|
367
201
|
} else {
|
|
368
|
-
NSLog(@"โ
|
|
202
|
+
NSLog(@"โ
Capture started successfully!");
|
|
369
203
|
g_isRecording = YES;
|
|
370
204
|
}
|
|
371
205
|
}];
|
|
@@ -379,19 +213,14 @@ static int g_frameNumber = 0;
|
|
|
379
213
|
return;
|
|
380
214
|
}
|
|
381
215
|
|
|
382
|
-
NSLog(@"๐ Stopping
|
|
216
|
+
NSLog(@"๐ Stopping recording");
|
|
383
217
|
|
|
384
|
-
[g_stream stopCaptureWithCompletionHandler:^(NSError *
|
|
385
|
-
if (
|
|
386
|
-
NSLog(@"โ Stop error: %@",
|
|
387
|
-
} else {
|
|
388
|
-
NSLog(@"โ
ScreenCaptureKit stream stopped in completion handler");
|
|
218
|
+
[g_stream stopCaptureWithCompletionHandler:^(NSError *error) {
|
|
219
|
+
if (error) {
|
|
220
|
+
NSLog(@"โ Stop error: %@", error);
|
|
389
221
|
}
|
|
390
|
-
|
|
391
|
-
// Finalize video since delegate might not be called
|
|
392
|
-
NSLog(@"๐ฌ Completion handler calling finalizeVideoWriter...");
|
|
222
|
+
NSLog(@"โ
Stream stopped");
|
|
393
223
|
[ScreenCaptureKitRecorder finalizeVideoWriter];
|
|
394
|
-
NSLog(@"๐ฌ Completion handler finished calling finalizeVideoWriter");
|
|
395
224
|
}];
|
|
396
225
|
}
|
|
397
226
|
|
|
@@ -399,12 +228,10 @@ static int g_frameNumber = 0;
|
|
|
399
228
|
return g_isRecording;
|
|
400
229
|
}
|
|
401
230
|
|
|
402
|
-
+ (
|
|
403
|
-
if (g_assetWriter)
|
|
404
|
-
return; // Already setup
|
|
405
|
-
}
|
|
231
|
+
+ (BOOL)setupVideoWriter {
|
|
232
|
+
if (g_assetWriter) return YES;
|
|
406
233
|
|
|
407
|
-
NSLog(@"๐ง Setting up
|
|
234
|
+
NSLog(@"๐ง Setting up video writer");
|
|
408
235
|
|
|
409
236
|
NSURL *outputURL = [NSURL fileURLWithPath:g_outputPath];
|
|
410
237
|
NSError *error = nil;
|
|
@@ -412,66 +239,67 @@ static int g_frameNumber = 0;
|
|
|
412
239
|
g_assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&error];
|
|
413
240
|
|
|
414
241
|
if (error || !g_assetWriter) {
|
|
415
|
-
NSLog(@"โ
|
|
416
|
-
return;
|
|
242
|
+
NSLog(@"โ Writer creation error: %@", error);
|
|
243
|
+
return NO;
|
|
417
244
|
}
|
|
418
245
|
|
|
419
|
-
//
|
|
246
|
+
// Video settings
|
|
420
247
|
NSDictionary *videoSettings = @{
|
|
421
248
|
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
422
249
|
AVVideoWidthKey: @1920,
|
|
423
250
|
AVVideoHeightKey: @1080,
|
|
424
251
|
AVVideoCompressionPropertiesKey: @{
|
|
425
|
-
AVVideoAverageBitRateKey: @(
|
|
426
|
-
AVVideoMaxKeyFrameIntervalKey: @30
|
|
427
|
-
AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel
|
|
252
|
+
AVVideoAverageBitRateKey: @(5000000), // 5 Mbps
|
|
253
|
+
AVVideoMaxKeyFrameIntervalKey: @30
|
|
428
254
|
}
|
|
429
255
|
};
|
|
430
256
|
|
|
431
257
|
g_assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
|
|
432
|
-
g_assetWriterInput.expectsMediaDataInRealTime =
|
|
258
|
+
g_assetWriterInput.expectsMediaDataInRealTime = YES;
|
|
433
259
|
|
|
434
|
-
// Pixel buffer
|
|
260
|
+
// Pixel buffer adaptor
|
|
435
261
|
NSDictionary *pixelBufferAttributes = @{
|
|
436
262
|
(NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
|
|
437
263
|
(NSString*)kCVPixelBufferWidthKey: @1920,
|
|
438
264
|
(NSString*)kCVPixelBufferHeightKey: @1080
|
|
439
265
|
};
|
|
440
266
|
|
|
441
|
-
g_pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor
|
|
267
|
+
g_pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor
|
|
268
|
+
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:g_assetWriterInput
|
|
269
|
+
sourcePixelBufferAttributes:pixelBufferAttributes];
|
|
442
270
|
|
|
443
271
|
if ([g_assetWriter canAddInput:g_assetWriterInput]) {
|
|
444
272
|
[g_assetWriter addInput:g_assetWriterInput];
|
|
445
|
-
NSLog(@"โ
|
|
273
|
+
NSLog(@"โ
Video writer ready");
|
|
274
|
+
return YES;
|
|
446
275
|
} else {
|
|
447
|
-
NSLog(@"โ
|
|
276
|
+
NSLog(@"โ Cannot add input to writer");
|
|
277
|
+
return NO;
|
|
448
278
|
}
|
|
449
279
|
}
|
|
450
280
|
|
|
451
281
|
+ (void)finalizeVideoWriter {
|
|
452
|
-
NSLog(@"๐ฌ Finalizing video
|
|
282
|
+
NSLog(@"๐ฌ Finalizing video");
|
|
283
|
+
|
|
284
|
+
g_isRecording = NO;
|
|
453
285
|
|
|
454
286
|
if (!g_assetWriter || !g_writerStarted) {
|
|
455
|
-
NSLog(@"โ ๏ธ
|
|
456
|
-
[
|
|
287
|
+
NSLog(@"โ ๏ธ Writer not ready for finalization");
|
|
288
|
+
[self cleanupVideoWriter];
|
|
457
289
|
return;
|
|
458
290
|
}
|
|
459
291
|
|
|
460
|
-
NSLog(@"๐ฌ Marking input as finished and finalizing...");
|
|
461
292
|
[g_assetWriterInput markAsFinished];
|
|
462
293
|
|
|
463
294
|
[g_assetWriter finishWritingWithCompletionHandler:^{
|
|
464
|
-
NSLog(@"๐ฌ Finalization completion handler called");
|
|
465
295
|
if (g_assetWriter.status == AVAssetWriterStatusCompleted) {
|
|
466
|
-
NSLog(@"โ
Video
|
|
296
|
+
NSLog(@"โ
Video saved: %@", g_outputPath);
|
|
467
297
|
} else {
|
|
468
|
-
NSLog(@"โ
|
|
298
|
+
NSLog(@"โ Write failed: %@", g_assetWriter.error);
|
|
469
299
|
}
|
|
470
300
|
|
|
471
301
|
[ScreenCaptureKitRecorder cleanupVideoWriter];
|
|
472
302
|
}];
|
|
473
|
-
|
|
474
|
-
NSLog(@"๐ฌ Finalization request submitted, waiting for completion...");
|
|
475
303
|
}
|
|
476
304
|
|
|
477
305
|
+ (void)cleanupVideoWriter {
|
|
@@ -479,12 +307,12 @@ static int g_frameNumber = 0;
|
|
|
479
307
|
g_assetWriterInput = nil;
|
|
480
308
|
g_pixelBufferAdaptor = nil;
|
|
481
309
|
g_writerStarted = NO;
|
|
482
|
-
|
|
310
|
+
g_frameCount = 0;
|
|
483
311
|
g_stream = nil;
|
|
484
312
|
g_streamDelegate = nil;
|
|
485
313
|
g_streamOutput = nil;
|
|
486
314
|
|
|
487
|
-
NSLog(@"๐งน
|
|
315
|
+
NSLog(@"๐งน Cleanup complete");
|
|
488
316
|
}
|
|
489
317
|
|
|
490
318
|
@end
|