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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.14.0",
3
+ "version": "2.15.1",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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
- [ScreenCaptureKitRecorder finalizeRecording];
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
- if (g_isRecording) {
39
- NSLog(@"โš ๏ธ Already recording");
40
- return NO;
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
- NSValue *captureAreaValue = config[@"captureArea"];
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
- [g_stream stopCaptureWithCompletionHandler:^(NSError *error) {
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
- [ScreenCaptureKitRecorder finalizeRecording];
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
- NSLog(@"๐ŸŽฌ Finalizing pure ScreenCaptureKit recording");
248
-
249
- g_isRecording = NO;
250
-
251
- if (g_recordingOutput) {
252
- // SCRecordingOutput finalizes automatically
253
- NSLog(@"โœ… Pure recording output finalized");
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
- g_stream = nil;
266
- g_recordingOutput = nil;
267
- g_streamDelegate = nil;
268
- g_isRecording = NO;
269
- g_outputPath = nil;
270
-
271
- NSLog(@"๐Ÿงน Pure ScreenCaptureKit cleanup complete");
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