node-mac-recorder 2.16.12 โ 2.16.14
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 +51 -11
- package/src/window_selector.mm +17 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(FORCE_AVFOUNDATION=1 node -e \"\nconsole.log(''๐งช Testing
|
|
4
|
+
"Bash(FORCE_AVFOUNDATION=1 node -e \"\nconsole.log(''๐งช Testing AVFoundation area recording...'');\nconst MacRecorder = require(''./index.js'');\nconst recorder = new MacRecorder();\n\n// Test specific area recording (top-left 500x500)\nconst options = {\n captureArea: {\n x: 100,\n y: 100, \n width: 500,\n height: 500\n }\n};\n\nrecorder.startRecording(''/tmp/area-test.mov'', options)\n .then(success => {\n console.log(''Area recording:'', success ? ''โ
SUCCESS'' : ''โ FAILED'');\n if (success) {\n setTimeout(() => {\n recorder.stopRecording().then(() => {\n console.log(''โ
Area recording complete'');\n const fs = require(''fs'');\n if (fs.existsSync(''/tmp/area-test.mov'')) {\n const size = Math.round(fs.statSync(''/tmp/area-test.mov'').size/1024);\n console.log(''๐น File size:'', size + ''KB'');\n }\n });\n }, 2000);\n }\n })\n .catch(console.error);\n\")"
|
|
5
5
|
],
|
|
6
6
|
"deny": [],
|
|
7
7
|
"ask": []
|
package/package.json
CHANGED
|
@@ -53,9 +53,14 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// Get display dimensions
|
|
56
|
+
// Get display dimensions - use width/height directly to avoid coordinate issues
|
|
57
57
|
CGRect displayBounds = CGDisplayBounds(displayID);
|
|
58
|
-
CGSize
|
|
58
|
+
CGSize displaySize = CGSizeMake(CGDisplayPixelsWide(displayID), CGDisplayPixelsHigh(displayID));
|
|
59
|
+
CGSize recordingSize = captureRect.size.width > 0 ? captureRect.size : displaySize;
|
|
60
|
+
|
|
61
|
+
NSLog(@"๐ฅ๏ธ Display bounds: %.0f,%.0f %.0fx%.0f", displayBounds.origin.x, displayBounds.origin.y, displayBounds.size.width, displayBounds.size.height);
|
|
62
|
+
NSLog(@"๐ฅ๏ธ Display pixels: %.0fx%.0f", displaySize.width, displaySize.height);
|
|
63
|
+
NSLog(@"๐ฏ Recording size: %.0fx%.0f", recordingSize.width, recordingSize.height);
|
|
59
64
|
|
|
60
65
|
// Video settings with macOS compatibility
|
|
61
66
|
NSString *codecKey;
|
|
@@ -135,9 +140,20 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
135
140
|
uint64_t interval = NSEC_PER_SEC / 10; // 10 FPS for Electron stability
|
|
136
141
|
dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, interval / 10);
|
|
137
142
|
|
|
143
|
+
// Retain objects before passing to block to prevent deallocation
|
|
144
|
+
AVAssetWriterInput *localVideoInput = g_avVideoInput;
|
|
145
|
+
AVAssetWriterInputPixelBufferAdaptor *localPixelBufferAdaptor = g_avPixelBufferAdaptor;
|
|
146
|
+
|
|
138
147
|
dispatch_source_set_event_handler(g_avTimer, ^{
|
|
139
148
|
if (!g_avIsRecording) return;
|
|
140
149
|
|
|
150
|
+
// Additional null checks for Electron safety
|
|
151
|
+
if (!localVideoInput || !localPixelBufferAdaptor) {
|
|
152
|
+
NSLog(@"โ ๏ธ Video input or pixel buffer adaptor is nil, stopping recording");
|
|
153
|
+
g_avIsRecording = false;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
@autoreleasepool {
|
|
142
158
|
@try {
|
|
143
159
|
// Capture screen with Electron-safe error handling
|
|
@@ -159,9 +175,19 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
159
175
|
|
|
160
176
|
// Convert to pixel buffer with Electron-safe error handling
|
|
161
177
|
CVPixelBufferRef pixelBuffer = nil;
|
|
162
|
-
CVReturn cvRet = CVPixelBufferPoolCreatePixelBuffer(NULL,
|
|
178
|
+
CVReturn cvRet = CVPixelBufferPoolCreatePixelBuffer(NULL, localPixelBufferAdaptor.pixelBufferPool, &pixelBuffer);
|
|
163
179
|
|
|
164
180
|
if (cvRet == kCVReturnSuccess && pixelBuffer) {
|
|
181
|
+
// Check pixel buffer dimensions match screen image
|
|
182
|
+
size_t bufferWidth = CVPixelBufferGetWidth(pixelBuffer);
|
|
183
|
+
size_t bufferHeight = CVPixelBufferGetHeight(pixelBuffer);
|
|
184
|
+
size_t imageWidth = CGImageGetWidth(screenImage);
|
|
185
|
+
size_t imageHeight = CGImageGetHeight(screenImage);
|
|
186
|
+
|
|
187
|
+
if (bufferWidth != imageWidth || bufferHeight != imageHeight) {
|
|
188
|
+
NSLog(@"โ ๏ธ Size mismatch! Buffer %zux%zu vs Image %zux%zu", bufferWidth, bufferHeight, imageWidth, imageHeight);
|
|
189
|
+
}
|
|
190
|
+
|
|
165
191
|
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
|
166
192
|
|
|
167
193
|
void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
@@ -201,9 +227,9 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
201
227
|
CGContextRelease(context);
|
|
202
228
|
|
|
203
229
|
// Write frame only if input is ready
|
|
204
|
-
if (
|
|
230
|
+
if (localVideoInput && localVideoInput.readyForMoreMediaData) {
|
|
205
231
|
CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(g_avFrameNumber / 10.0, 600));
|
|
206
|
-
BOOL appendSuccess = [
|
|
232
|
+
BOOL appendSuccess = [localPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
|
|
207
233
|
if (appendSuccess) {
|
|
208
234
|
g_avFrameNumber++;
|
|
209
235
|
} else {
|
|
@@ -253,22 +279,36 @@ extern "C" bool stopAVFoundationRecording() {
|
|
|
253
279
|
@try {
|
|
254
280
|
// Stop timer with Electron-safe cleanup
|
|
255
281
|
if (g_avTimer) {
|
|
282
|
+
// Mark as not recording FIRST to stop timer callbacks
|
|
283
|
+
g_avIsRecording = false;
|
|
284
|
+
|
|
285
|
+
// Cancel timer and wait a brief moment for completion
|
|
256
286
|
dispatch_source_cancel(g_avTimer);
|
|
287
|
+
|
|
288
|
+
// Use async to avoid deadlock in Electron
|
|
289
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
|
|
290
|
+
// Timer should be fully cancelled by now
|
|
291
|
+
});
|
|
292
|
+
|
|
257
293
|
g_avTimer = nil;
|
|
258
294
|
NSLog(@"โ
AVFoundation timer stopped safely");
|
|
259
295
|
}
|
|
260
296
|
|
|
261
|
-
// Finish writing
|
|
262
|
-
|
|
263
|
-
|
|
297
|
+
// Finish writing with null checks
|
|
298
|
+
AVAssetWriterInput *writerInput = g_avVideoInput;
|
|
299
|
+
if (writerInput) {
|
|
300
|
+
[writerInput markAsFinished];
|
|
264
301
|
}
|
|
265
302
|
|
|
266
|
-
|
|
303
|
+
AVAssetWriter *writer = g_avWriter;
|
|
304
|
+
if (writer && writer.status == AVAssetWriterStatusWriting) {
|
|
267
305
|
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
268
|
-
[
|
|
306
|
+
[writer finishWritingWithCompletionHandler:^{
|
|
269
307
|
dispatch_semaphore_signal(semaphore);
|
|
270
308
|
}];
|
|
271
|
-
|
|
309
|
+
// Add timeout to prevent infinite wait in Electron
|
|
310
|
+
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
|
|
311
|
+
dispatch_semaphore_wait(semaphore, timeout);
|
|
272
312
|
}
|
|
273
313
|
|
|
274
314
|
// Cleanup
|
package/src/window_selector.mm
CHANGED
|
@@ -875,25 +875,32 @@ void updateOverlay() {
|
|
|
875
875
|
}
|
|
876
876
|
|
|
877
877
|
// Determine if this is a primary display window
|
|
878
|
-
|
|
878
|
+
NSArray *allScreens = [NSScreen screens];
|
|
879
|
+
NSScreen *primaryScreen = [allScreens objectAtIndex:0]; // Primary screen
|
|
880
|
+
NSRect primaryFrame = [primaryScreen frame];
|
|
881
|
+
BOOL isPrimaryDisplayWindow = (x >= primaryFrame.origin.x &&
|
|
882
|
+
x <= primaryFrame.origin.x + primaryFrame.size.width &&
|
|
883
|
+
y >= primaryFrame.origin.y &&
|
|
884
|
+
y <= primaryFrame.origin.y + primaryFrame.size.height);
|
|
879
885
|
|
|
880
886
|
CGFloat localX, localY;
|
|
881
887
|
if (isPrimaryDisplayWindow) {
|
|
882
|
-
// Primary display windows:
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
888
|
+
// Primary display windows: Calculate dynamic offset from combined frame
|
|
889
|
+
NSRect combinedFrame = [g_overlayWindow frame];
|
|
890
|
+
|
|
891
|
+
// Calculate primary screen offset within combined frame
|
|
892
|
+
CGFloat primaryOffsetX = primaryFrame.origin.x - combinedFrame.origin.x;
|
|
893
|
+
CGFloat primaryOffsetY = primaryFrame.origin.y - combinedFrame.origin.y;
|
|
894
|
+
|
|
895
|
+
localX = x + primaryOffsetX;
|
|
896
|
+
localY = ([g_overlayView frame].size.height - (y + primaryOffsetY)) - height;
|
|
897
|
+
|
|
886
898
|
} else {
|
|
887
899
|
// Secondary display windows: Apply standard coordinate transformation
|
|
888
900
|
localX = x - globalOffset.x;
|
|
889
901
|
localY = ([g_overlayView frame].size.height - (y - globalOffset.y)) - height;
|
|
890
902
|
}
|
|
891
903
|
|
|
892
|
-
NSLog(@"๐ง COORDINATE DEBUG: Window (%d, %d) %dx%d [%@]", (int)x, (int)y, (int)width, (int)height, isPrimaryDisplayWindow ? @"PRIMARY" : @"SECONDARY");
|
|
893
|
-
NSLog(@" GlobalOffset: (%.0f, %.0f)", globalOffset.x, globalOffset.y);
|
|
894
|
-
NSLog(@" LocalCoords: (%.0f, %.0f)", localX, localY);
|
|
895
|
-
NSLog(@" ViewFrame: %.0fx%.0f", [g_overlayView frame].size.width, [g_overlayView frame].size.height);
|
|
896
|
-
|
|
897
904
|
// Update overlay view window info for highlighting
|
|
898
905
|
[overlayView setWindowInfo:targetWindow];
|
|
899
906
|
[overlayView setHighlightFrame:NSMakeRect(localX, localY, width, height)];
|