node-mac-recorder 2.14.0 โ 2.15.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/.claude/settings.local.json +3 -1
- package/package.json +1 -1
- package/src/screen_capture_kit.mm +108 -26
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"Bash(ffmpeg:*)",
|
|
32
32
|
"WebSearch",
|
|
33
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\")",
|
|
34
|
-
"Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''''๐ Debugging frame writing...'''');\nconst MacRecorder = require(''''./index'''');\nconst recorder = new MacRecorder();\n\nasync function debugFrameWriting() {\n try {\n const outputPath = ''''./test-output/frame-debug.mov'''';\n console.log(''''๐น Starting debug test...'''');\n \n const success = await recorder.startRecording(outputPath);\n \n if (success) {\n console.log(''''โฑ๏ธ Recording for 2 seconds...'''');\n await new Promise(resolve => setTimeout(resolve, 2000));\n \n console.log(''''๐ Stopping...'''');\n await recorder.stopRecording();\n \n // Wait for finalization\n await new Promise(resolve => setTimeout(resolve, 1000));\n \n } else {\n console.log(''''โ Failed to start'''');\n }\n } catch (error) {\n console.log(''''โ Error:'''', error);\n }\n}\n\ndebugFrameWriting();\n\")"
|
|
34
|
+
"Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''''๐ Debugging frame writing...'''');\nconst MacRecorder = require(''''./index'''');\nconst recorder = new MacRecorder();\n\nasync function debugFrameWriting() {\n try {\n const outputPath = ''''./test-output/frame-debug.mov'''';\n console.log(''''๐น Starting debug test...'''');\n \n const success = await recorder.startRecording(outputPath);\n \n if (success) {\n console.log(''''โฑ๏ธ Recording for 2 seconds...'''');\n await new Promise(resolve => setTimeout(resolve, 2000));\n \n console.log(''''๐ Stopping...'''');\n await recorder.stopRecording();\n \n // Wait for finalization\n await new Promise(resolve => setTimeout(resolve, 1000));\n \n } else {\n console.log(''''โ Failed to start'''');\n }\n } catch (error) {\n console.log(''''โ Error:'''', error);\n }\n}\n\ndebugFrameWriting();\n\")",
|
|
35
|
+
"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/crash-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 3 seconds'');\n await new Promise(resolve => setTimeout(resolve, 3000));\n console.log(''๐ Stopping recording...'');\n await recorder.stopRecording();\n console.log(''โ
Test completed without crash'');\n } else {\n console.log(''โ Recording start failed'');\n }\n } catch (error) {\n console.log(''โ Error:'', error.message);\n console.log(''Stack:'', error.stack);\n }\n}\n\ntest();\n\")",
|
|
36
|
+
"Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐ Debugging frame writing...'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function debugFrameWriting() {\n try {\n const outputPath = ''./test-output/frame-debug.mov'';\n console.log(''๐น Starting debug test...'');\n \n const success = await recorder.startRecording(outputPath);\n \n if (success) {\n console.log(''โฑ๏ธ Recording for 2 seconds...'');\n await new Promise(resolve => setTimeout(resolve, 2000));\n \n console.log(''๐ Stopping...'');\n await recorder.stopRecording();\n \n // Wait for finalization\n await new Promise(resolve => setTimeout(resolve, 1000));\n \n } else {\n console.log(''โ Failed to start'');\n }\n } catch (error) {\n console.log(''โ Error:'', error);\n }\n}\n\ndebugFrameWriting();\n\")"
|
|
35
37
|
],
|
|
36
38
|
"deny": []
|
|
37
39
|
}
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ static SCStream * API_AVAILABLE(macos(12.3)) g_stream = nil;
|
|
|
5
5
|
static SCRecordingOutput * API_AVAILABLE(macos(15.0)) g_recordingOutput = nil;
|
|
6
6
|
static id<SCStreamDelegate> API_AVAILABLE(macos(12.3)) g_streamDelegate = nil;
|
|
7
7
|
static BOOL g_isRecording = NO;
|
|
8
|
+
static BOOL g_isCleaningUp = NO; // Prevent recursive cleanup
|
|
8
9
|
static NSString *g_outputPath = nil;
|
|
9
10
|
|
|
10
11
|
@interface PureScreenCaptureDelegate : NSObject <SCStreamDelegate>
|
|
@@ -13,6 +14,13 @@ static NSString *g_outputPath = nil;
|
|
|
13
14
|
@implementation PureScreenCaptureDelegate
|
|
14
15
|
- (void)stream:(SCStream * API_AVAILABLE(macos(12.3)))stream didStopWithError:(NSError *)error API_AVAILABLE(macos(12.3)) {
|
|
15
16
|
NSLog(@"๐ Pure ScreenCapture stream stopped");
|
|
17
|
+
|
|
18
|
+
// Prevent recursive calls during cleanup
|
|
19
|
+
if (g_isCleaningUp) {
|
|
20
|
+
NSLog(@"โ ๏ธ Already cleaning up, ignoring delegate callback");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
g_isRecording = NO;
|
|
17
25
|
|
|
18
26
|
if (error) {
|
|
@@ -21,7 +29,12 @@ static NSString *g_outputPath = nil;
|
|
|
21
29
|
NSLog(@"โ
Stream stopped cleanly");
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
// Use dispatch_async to prevent potential deadlocks in Electron
|
|
33
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
34
|
+
if (!g_isCleaningUp) { // Double-check before finalizing
|
|
35
|
+
[ScreenCaptureKitRecorder finalizeRecording];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
25
38
|
}
|
|
26
39
|
@end
|
|
27
40
|
|
|
@@ -35,9 +48,14 @@ static NSString *g_outputPath = nil;
|
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
+ (BOOL)startRecordingWithConfiguration:(NSDictionary *)config delegate:(id)delegate error:(NSError **)error {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
52
|
+
if (g_isRecording || g_isCleaningUp) {
|
|
53
|
+
NSLog(@"โ ๏ธ Already recording or cleaning up (recording:%d cleaning:%d)", g_isRecording, g_isCleaningUp);
|
|
54
|
+
return NO;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Reset any stale state
|
|
58
|
+
g_isCleaningUp = NO;
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
g_outputPath = config[@"outputPath"];
|
|
@@ -45,14 +63,14 @@ static NSString *g_outputPath = nil;
|
|
|
45
63
|
// Extract configuration options
|
|
46
64
|
NSNumber *displayId = config[@"displayId"];
|
|
47
65
|
NSNumber *windowId = config[@"windowId"];
|
|
48
|
-
|
|
66
|
+
NSDictionary *captureRect = config[@"captureRect"];
|
|
49
67
|
NSNumber *captureCursor = config[@"captureCursor"];
|
|
50
68
|
NSNumber *includeMicrophone = config[@"includeMicrophone"];
|
|
51
69
|
NSNumber *includeSystemAudio = config[@"includeSystemAudio"];
|
|
52
70
|
|
|
53
71
|
NSLog(@"๐ฌ Starting PURE ScreenCaptureKit recording (NO AVFoundation)");
|
|
54
|
-
NSLog(@"๐ง Config: cursor=%@ mic=%@ system=%@ display=%@ window=%@",
|
|
55
|
-
captureCursor, includeMicrophone, includeSystemAudio, displayId, windowId);
|
|
72
|
+
NSLog(@"๐ง Config: cursor=%@ mic=%@ system=%@ display=%@ window=%@ crop=%@",
|
|
73
|
+
captureCursor, includeMicrophone, includeSystemAudio, displayId, windowId, captureRect);
|
|
56
74
|
|
|
57
75
|
// Get shareable content
|
|
58
76
|
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
|
|
@@ -121,6 +139,20 @@ static NSString *g_outputPath = nil;
|
|
|
121
139
|
recordingHeight = targetDisplay.height;
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
// CROP AREA SUPPORT - Adjust dimensions and source rect
|
|
143
|
+
if (captureRect && captureRect[@"width"] && captureRect[@"height"]) {
|
|
144
|
+
CGFloat cropWidth = [captureRect[@"width"] doubleValue];
|
|
145
|
+
CGFloat cropHeight = [captureRect[@"height"] doubleValue];
|
|
146
|
+
|
|
147
|
+
if (cropWidth > 0 && cropHeight > 0) {
|
|
148
|
+
NSLog(@"๐ฒ Crop area specified: %.0fx%.0f at (%.0f,%.0f)",
|
|
149
|
+
cropWidth, cropHeight,
|
|
150
|
+
[captureRect[@"x"] doubleValue], [captureRect[@"y"] doubleValue]);
|
|
151
|
+
recordingWidth = (NSInteger)cropWidth;
|
|
152
|
+
recordingHeight = (NSInteger)cropHeight;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
124
156
|
// Configure stream with extracted options
|
|
125
157
|
SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
|
|
126
158
|
streamConfig.width = recordingWidth;
|
|
@@ -129,6 +161,20 @@ static NSString *g_outputPath = nil;
|
|
|
129
161
|
streamConfig.pixelFormat = kCVPixelFormatType_32BGRA;
|
|
130
162
|
streamConfig.scalesToFit = NO;
|
|
131
163
|
|
|
164
|
+
// Apply crop area using sourceRect
|
|
165
|
+
if (captureRect && captureRect[@"x"] && captureRect[@"y"] && captureRect[@"width"] && captureRect[@"height"]) {
|
|
166
|
+
CGFloat cropX = [captureRect[@"x"] doubleValue];
|
|
167
|
+
CGFloat cropY = [captureRect[@"y"] doubleValue];
|
|
168
|
+
CGFloat cropWidth = [captureRect[@"width"] doubleValue];
|
|
169
|
+
CGFloat cropHeight = [captureRect[@"height"] doubleValue];
|
|
170
|
+
|
|
171
|
+
if (cropWidth > 0 && cropHeight > 0) {
|
|
172
|
+
CGRect sourceRect = CGRectMake(cropX, cropY, cropWidth, cropHeight);
|
|
173
|
+
streamConfig.sourceRect = sourceRect;
|
|
174
|
+
NSLog(@"โ๏ธ Crop sourceRect applied: (%.0f,%.0f) %.0fx%.0f", cropX, cropY, cropWidth, cropHeight);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
132
178
|
// CURSOR SUPPORT
|
|
133
179
|
BOOL shouldShowCursor = captureCursor ? [captureCursor boolValue] : YES;
|
|
134
180
|
streamConfig.showsCursor = shouldShowCursor;
|
|
@@ -219,18 +265,29 @@ static NSString *g_outputPath = nil;
|
|
|
219
265
|
}
|
|
220
266
|
|
|
221
267
|
+ (void)stopRecording {
|
|
222
|
-
if (!g_isRecording || !g_stream) {
|
|
268
|
+
if (!g_isRecording || !g_stream || g_isCleaningUp) {
|
|
269
|
+
NSLog(@"โ ๏ธ Cannot stop: recording=%d stream=%@ cleaning=%d", g_isRecording, g_stream, g_isCleaningUp);
|
|
223
270
|
return;
|
|
224
271
|
}
|
|
225
272
|
|
|
226
273
|
NSLog(@"๐ Stopping pure ScreenCaptureKit recording");
|
|
274
|
+
g_isCleaningUp = YES;
|
|
227
275
|
|
|
228
|
-
|
|
276
|
+
// Store stream reference to prevent it from being deallocated
|
|
277
|
+
SCStream *streamToStop = g_stream;
|
|
278
|
+
|
|
279
|
+
[streamToStop stopCaptureWithCompletionHandler:^(NSError *error) {
|
|
229
280
|
if (error) {
|
|
230
281
|
NSLog(@"โ Stop error: %@", error);
|
|
231
282
|
}
|
|
232
283
|
NSLog(@"โ
Pure stream stopped");
|
|
233
|
-
|
|
284
|
+
|
|
285
|
+
// Finalize on main queue to prevent threading issues
|
|
286
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
287
|
+
if (g_isCleaningUp) { // Only finalize if we initiated cleanup
|
|
288
|
+
[ScreenCaptureKitRecorder finalizeRecording];
|
|
289
|
+
}
|
|
290
|
+
});
|
|
234
291
|
}];
|
|
235
292
|
}
|
|
236
293
|
|
|
@@ -244,16 +301,23 @@ static NSString *g_outputPath = nil;
|
|
|
244
301
|
}
|
|
245
302
|
|
|
246
303
|
+ (void)finalizeRecording {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
NSLog(@"
|
|
304
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
305
|
+
if (g_isCleaningUp && g_isRecording == NO) {
|
|
306
|
+
NSLog(@"โ ๏ธ Already finalizing, skipping duplicate call");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
NSLog(@"๐ฌ Finalizing pure ScreenCaptureKit recording");
|
|
311
|
+
|
|
312
|
+
g_isRecording = NO;
|
|
313
|
+
|
|
314
|
+
if (g_recordingOutput) {
|
|
315
|
+
// SCRecordingOutput finalizes automatically
|
|
316
|
+
NSLog(@"โ
Pure recording output finalized");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
[ScreenCaptureKitRecorder cleanupVideoWriter];
|
|
254
320
|
}
|
|
255
|
-
|
|
256
|
-
[ScreenCaptureKitRecorder cleanupVideoWriter];
|
|
257
321
|
}
|
|
258
322
|
|
|
259
323
|
+ (void)finalizeVideoWriter {
|
|
@@ -262,13 +326,31 @@ static NSString *g_outputPath = nil;
|
|
|
262
326
|
}
|
|
263
327
|
|
|
264
328
|
+ (void)cleanupVideoWriter {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
329
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
330
|
+
NSLog(@"๐งน Starting ScreenCaptureKit cleanup");
|
|
331
|
+
|
|
332
|
+
// Clean up in proper order to prevent crashes
|
|
333
|
+
if (g_stream) {
|
|
334
|
+
g_stream = nil;
|
|
335
|
+
NSLog(@"โ
Stream reference cleared");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (g_recordingOutput) {
|
|
339
|
+
g_recordingOutput = nil;
|
|
340
|
+
NSLog(@"โ
Recording output reference cleared");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (g_streamDelegate) {
|
|
344
|
+
g_streamDelegate = nil;
|
|
345
|
+
NSLog(@"โ
Stream delegate reference cleared");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
g_isRecording = NO;
|
|
349
|
+
g_isCleaningUp = NO; // Reset cleanup flag
|
|
350
|
+
g_outputPath = nil;
|
|
351
|
+
|
|
352
|
+
NSLog(@"๐งน Pure ScreenCaptureKit cleanup complete");
|
|
353
|
+
}
|
|
272
354
|
}
|
|
273
355
|
|
|
274
356
|
@end
|