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.
- package/.claude/settings.local.json +1 -1
- package/package.json +1 -1
- package/src/avfoundation_recorder.mm +116 -61
|
@@ -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
|
|
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
|
@@ -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 (
|
|
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 /
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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 @
|
|
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
|
-
|
|
233
|
-
|
|
282
|
+
// Finish writing with null checks
|
|
283
|
+
AVAssetWriterInput *writerInput = g_avVideoInput;
|
|
284
|
+
if (writerInput) {
|
|
285
|
+
[writerInput markAsFinished];
|
|
234
286
|
}
|
|
235
287
|
|
|
236
|
-
|
|
288
|
+
AVAssetWriter *writer = g_avWriter;
|
|
289
|
+
if (writer && writer.status == AVAssetWriterStatusWriting) {
|
|
237
290
|
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
238
|
-
[
|
|
291
|
+
[writer finishWritingWithCompletionHandler:^{
|
|
239
292
|
dispatch_semaphore_signal(semaphore);
|
|
240
293
|
}];
|
|
241
|
-
|
|
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
|