node-mac-recorder 2.16.11 โ†’ 2.16.13

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,7 +1,7 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(FORCE_AVFOUNDATION=1 node -e \"\nconst MacRecorder = require(''./index.js'');\nconst recorder = new MacRecorder();\nrecorder.startRecording(''/tmp/test-final.mov'')\n .then(success => {\n console.log(''Recording start:'', success ? ''SUCCESS'' : ''FAILED'');\n if (success) {\n setTimeout(() => {\n recorder.stopRecording().then(() => {\n console.log(''Recording stopped'');\n const fs = require(''fs'');\n if (fs.existsSync(''/tmp/test-final.mov'')) {\n console.log(''File created:'', Math.round(fs.statSync(''/tmp/test-final.mov'').size/1024) + ''KB'');\n }\n });\n }, 2000);\n }\n })\n .catch(console.error);\n\")"
4
+ "Bash(FORCE_AVFOUNDATION=1 node -e \"\nconsole.log(''๐Ÿงช Testing Electron crash fix (macOS 14 simulation)...'');\nconst MacRecorder = require(''./index.js'');\nconst recorder = new MacRecorder();\n\nrecorder.on(''recordingStarted'', (details) => {\n console.log(''โœ… Recording started safely'');\n});\n\nrecorder.on(''stopped'', () => {\n console.log(''โœ… Recording stopped without crash'');\n});\n\nlet recordingStarted = false;\nrecorder.startRecording(''/tmp/crash-fix-test.mov'')\n .then(success => {\n console.log(''Start result:'', success ? ''โœ… SUCCESS'' : ''โŒ FAILED'');\n if (success) {\n recordingStarted = true;\n // Test quick start/stop cycles to stress test memory handling\n setTimeout(() => {\n console.log(''โน๏ธ Quick stop test...'');\n recorder.stopRecording().then(() => {\n console.log(''โœ… Quick stop completed'');\n \n // Test immediate restart\n setTimeout(() => {\n console.log(''๐Ÿ”„ Testing immediate restart...'');\n recorder.startRecording(''/tmp/crash-fix-test2.mov'').then(() => {\n setTimeout(() => {\n recorder.stopRecording().then(() => {\n console.log(''๐ŸŽ‰ Crash fix test completed successfully!'');\n const fs = require(''fs'');\n const files = [''/tmp/crash-fix-test.mov'', ''/tmp/crash-fix-test2.mov''];\n files.forEach(file => {\n if (fs.existsSync(file)) {\n const size = Math.round(fs.statSync(file).size/1024);\n console.log(''๐Ÿ“น '' + file + '':'', size + ''KB'');\n }\n });\n });\n }, 1000);\n });\n }, 500);\n });\n }, 2000);\n }\n })\n .catch(err => {\n console.error(''โŒ Error:'', err);\n if (recordingStarted) {\n recorder.stopRecording();\n }\n });\n\")"
5
5
  ],
6
6
  "deny": [],
7
7
  "ask": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.16.11",
3
+ "version": "2.16.13",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -123,7 +123,7 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
123
123
  g_avCaptureRect = captureRect;
124
124
  g_avFrameNumber = 0;
125
125
 
126
- // Start capture timer (15 FPS for compatibility)
126
+ // Start capture timer (10 FPS for Electron compatibility)
127
127
  dispatch_queue_t captureQueue = dispatch_queue_create("AVFoundationCaptureQueue", DISPATCH_QUEUE_SERIAL);
128
128
  g_avTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, captureQueue);
129
129
 
@@ -132,78 +132,118 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
132
132
  return false;
133
133
  }
134
134
 
135
- uint64_t interval = NSEC_PER_SEC / 15; // 15 FPS
135
+ uint64_t interval = NSEC_PER_SEC / 10; // 10 FPS for Electron stability
136
136
  dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, interval / 10);
137
137
 
138
+ // Retain objects before passing to block to prevent deallocation
139
+ AVAssetWriterInput *localVideoInput = g_avVideoInput;
140
+ AVAssetWriterInputPixelBufferAdaptor *localPixelBufferAdaptor = g_avPixelBufferAdaptor;
141
+
138
142
  dispatch_source_set_event_handler(g_avTimer, ^{
139
143
  if (!g_avIsRecording) return;
140
144
 
145
+ // Additional null checks for Electron safety
146
+ if (!localVideoInput || !localPixelBufferAdaptor) {
147
+ NSLog(@"โš ๏ธ Video input or pixel buffer adaptor is nil, stopping recording");
148
+ g_avIsRecording = false;
149
+ return;
150
+ }
151
+
141
152
  @autoreleasepool {
142
- // Capture screen
143
- CGImageRef screenImage = nil;
144
- if (CGRectIsEmpty(g_avCaptureRect)) {
145
- screenImage = CGDisplayCreateImage(g_avDisplayID);
146
- } else {
147
- CGImageRef fullScreen = CGDisplayCreateImage(g_avDisplayID);
148
- if (fullScreen) {
149
- screenImage = CGImageCreateWithImageInRect(fullScreen, g_avCaptureRect);
150
- CGImageRelease(fullScreen);
153
+ @try {
154
+ // Capture screen with Electron-safe error handling
155
+ CGImageRef screenImage = nil;
156
+ if (CGRectIsEmpty(g_avCaptureRect)) {
157
+ screenImage = CGDisplayCreateImage(g_avDisplayID);
158
+ } else {
159
+ CGImageRef fullScreen = CGDisplayCreateImage(g_avDisplayID);
160
+ if (fullScreen) {
161
+ screenImage = CGImageCreateWithImageInRect(fullScreen, g_avCaptureRect);
162
+ CGImageRelease(fullScreen);
163
+ }
151
164
  }
152
- }
153
-
154
- if (!screenImage) return;
155
-
156
- // Convert to pixel buffer
157
- CVPixelBufferRef pixelBuffer = nil;
158
- CVReturn cvRet = CVPixelBufferPoolCreatePixelBuffer(NULL, g_avPixelBufferAdaptor.pixelBufferPool, &pixelBuffer);
159
-
160
- if (cvRet == kCVReturnSuccess && pixelBuffer) {
161
- CVPixelBufferLockBaseAddress(pixelBuffer, 0);
162
-
163
- void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer);
164
- size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
165
-
166
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
167
165
 
168
- // Match bitmap info to pixel format for compatibility
169
- CGBitmapInfo bitmapInfo;
170
- OSType currentPixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
171
- if (currentPixelFormat == kCVPixelFormatType_32ARGB) {
172
- bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Big;
173
- } else { // kCVPixelFormatType_32BGRA
174
- bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little;
166
+ if (!screenImage) {
167
+ NSLog(@"โš ๏ธ Failed to capture screen image, skipping frame");
168
+ return;
175
169
  }
170
+
171
+ // Convert to pixel buffer with Electron-safe error handling
172
+ CVPixelBufferRef pixelBuffer = nil;
173
+ CVReturn cvRet = CVPixelBufferPoolCreatePixelBuffer(NULL, localPixelBufferAdaptor.pixelBufferPool, &pixelBuffer);
176
174
 
177
- CGContextRef context = CGBitmapContextCreate(pixelData,
178
- CVPixelBufferGetWidth(pixelBuffer),
179
- CVPixelBufferGetHeight(pixelBuffer),
180
- 8, bytesPerRow, colorSpace, bitmapInfo);
181
-
182
- if (context) {
183
- CGContextDrawImage(context, CGRectMake(0, 0, CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer)), screenImage);
184
- CGContextRelease(context);
185
- }
186
- CGColorSpaceRelease(colorSpace);
187
- CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
188
-
189
- // Write frame
190
- if (g_avVideoInput.readyForMoreMediaData) {
191
- CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(g_avFrameNumber / 15.0, 600));
192
- [g_avPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
193
- g_avFrameNumber++;
175
+ if (cvRet == kCVReturnSuccess && pixelBuffer) {
176
+ CVPixelBufferLockBaseAddress(pixelBuffer, 0);
177
+
178
+ void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer);
179
+ if (!pixelData) {
180
+ NSLog(@"โš ๏ธ Failed to get pixel buffer base address");
181
+ CVPixelBufferRelease(pixelBuffer);
182
+ CGImageRelease(screenImage);
183
+ return;
184
+ }
185
+
186
+ size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
187
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
188
+ if (!colorSpace) {
189
+ NSLog(@"โš ๏ธ Failed to create color space");
190
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
191
+ CVPixelBufferRelease(pixelBuffer);
192
+ CGImageRelease(screenImage);
193
+ return;
194
+ }
195
+
196
+ // Match bitmap info to pixel format for compatibility
197
+ CGBitmapInfo bitmapInfo;
198
+ OSType currentPixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
199
+ if (currentPixelFormat == kCVPixelFormatType_32ARGB) {
200
+ bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Big;
201
+ } else { // kCVPixelFormatType_32BGRA
202
+ bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little;
203
+ }
204
+
205
+ CGContextRef context = CGBitmapContextCreate(pixelData,
206
+ CVPixelBufferGetWidth(pixelBuffer),
207
+ CVPixelBufferGetHeight(pixelBuffer),
208
+ 8, bytesPerRow, colorSpace, bitmapInfo);
209
+
210
+ if (context) {
211
+ CGContextDrawImage(context, CGRectMake(0, 0, CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer)), screenImage);
212
+ CGContextRelease(context);
213
+
214
+ // Write frame only if input is ready
215
+ if (localVideoInput && localVideoInput.readyForMoreMediaData) {
216
+ CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(g_avFrameNumber / 10.0, 600));
217
+ BOOL appendSuccess = [localPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
218
+ if (appendSuccess) {
219
+ g_avFrameNumber++;
220
+ } else {
221
+ NSLog(@"โš ๏ธ Failed to append pixel buffer");
222
+ }
223
+ }
224
+ } else {
225
+ NSLog(@"โš ๏ธ Failed to create bitmap context");
226
+ }
227
+
228
+ CGColorSpaceRelease(colorSpace);
229
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
230
+ CVPixelBufferRelease(pixelBuffer);
231
+ } else {
232
+ NSLog(@"โš ๏ธ Failed to create pixel buffer: %d", cvRet);
194
233
  }
195
234
 
196
- CVPixelBufferRelease(pixelBuffer);
235
+ CGImageRelease(screenImage);
236
+ } @catch (NSException *exception) {
237
+ NSLog(@"โŒ Exception in AVFoundation capture loop: %@", exception.reason);
238
+ g_avIsRecording = false; // Stop recording on exception to prevent crash
197
239
  }
198
-
199
- CGImageRelease(screenImage);
200
240
  }
201
241
  });
202
242
 
203
243
  dispatch_resume(g_avTimer);
204
244
  g_avIsRecording = true;
205
245
 
206
- NSLog(@"๐ŸŽฅ AVFoundation recording started: %dx%d @ 15fps",
246
+ NSLog(@"๐ŸŽฅ AVFoundation recording started: %dx%d @ 10fps",
207
247
  (int)recordingSize.width, (int)recordingSize.height);
208
248
 
209
249
  return true;
@@ -222,23 +262,38 @@ extern "C" bool stopAVFoundationRecording() {
222
262
  g_avIsRecording = false;
223
263
 
224
264
  @try {
225
- // Stop timer
265
+ // Stop timer with Electron-safe cleanup
226
266
  if (g_avTimer) {
267
+ // Mark as not recording FIRST to stop timer callbacks
268
+ g_avIsRecording = false;
269
+
270
+ // Cancel timer and wait a brief moment for completion
227
271
  dispatch_source_cancel(g_avTimer);
272
+
273
+ // Use async to avoid deadlock in Electron
274
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
275
+ // Timer should be fully cancelled by now
276
+ });
277
+
228
278
  g_avTimer = nil;
279
+ NSLog(@"โœ… AVFoundation timer stopped safely");
229
280
  }
230
281
 
231
- // Finish writing
232
- if (g_avVideoInput) {
233
- [g_avVideoInput markAsFinished];
282
+ // Finish writing with null checks
283
+ AVAssetWriterInput *writerInput = g_avVideoInput;
284
+ if (writerInput) {
285
+ [writerInput markAsFinished];
234
286
  }
235
287
 
236
- if (g_avWriter && g_avWriter.status == AVAssetWriterStatusWriting) {
288
+ AVAssetWriter *writer = g_avWriter;
289
+ if (writer && writer.status == AVAssetWriterStatusWriting) {
237
290
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
238
- [g_avWriter finishWritingWithCompletionHandler:^{
291
+ [writer finishWritingWithCompletionHandler:^{
239
292
  dispatch_semaphore_signal(semaphore);
240
293
  }];
241
- dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
294
+ // Add timeout to prevent infinite wait in Electron
295
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
296
+ dispatch_semaphore_wait(semaphore, timeout);
242
297
  }
243
298
 
244
299
  // Cleanup