node-mac-recorder 2.16.15 โ 2.16.17
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 +37 -8
- package/src/window_selector.mm +112 -60
|
@@ -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 per-screen window selector overlay system...'');\nconst WindowSelector = require(''./window-selector.js'');\nconst selector = new WindowSelector();\n\n// Test permission check first\nselector.checkPermissions().then(perms => {\n console.log(''Permissions:'', perms);\n \n // Start window selection to test per-screen overlays\n return selector.startSelection();\n}).then(() => {\n console.log(''โ
Window selection started - testing per-screen overlays'');\n console.log(''โก๏ธ Move cursor over windows on different displays to test overlay positioning'');\n \n // Auto-stop after 10 seconds for testing\n setTimeout(() => {\n selector.stopSelection().then(() => {\n console.log(''โ
Window selection stopped - test complete'');\n });\n }, 10000);\n}).catch(console.error);\n\")"
|
|
5
5
|
],
|
|
6
6
|
"deny": [],
|
|
7
7
|
"ask": []
|
package/package.json
CHANGED
|
@@ -65,8 +65,17 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
65
65
|
CGFloat scaleY = physicalSize.height / logicalSize.height;
|
|
66
66
|
CGFloat scaleFactor = MAX(scaleX, scaleY); // Use max to handle non-uniform scaling
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
|
|
68
|
+
// CRITICAL FIX: Use actual captured image dimensions, not logical size
|
|
69
|
+
// CGDisplayCreateImage may return higher resolution on Retina displays
|
|
70
|
+
CGImageRef testImage = CGDisplayCreateImage(displayID);
|
|
71
|
+
CGSize actualImageSize = CGSizeMake(CGImageGetWidth(testImage), CGImageGetHeight(testImage));
|
|
72
|
+
CGImageRelease(testImage);
|
|
73
|
+
|
|
74
|
+
// For AVFoundation, use actual image size that CGDisplayCreateImage returns
|
|
75
|
+
CGSize recordingSize = captureRect.size.width > 0 ? captureRect.size : actualImageSize;
|
|
76
|
+
|
|
77
|
+
NSLog(@"๐ฏ CRITICAL: Logical %.0fx%.0f โ Actual image %.0fx%.0f",
|
|
78
|
+
logicalSize.width, logicalSize.height, actualImageSize.width, actualImageSize.height);
|
|
70
79
|
|
|
71
80
|
NSLog(@"๐ฅ๏ธ Display bounds (logical): %.0fx%.0f", logicalSize.width, logicalSize.height);
|
|
72
81
|
NSLog(@"๐ฅ๏ธ Display pixels (physical): %.0fx%.0f", physicalSize.width, physicalSize.height);
|
|
@@ -147,13 +156,30 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
147
156
|
|
|
148
157
|
// Apply scaling to capture rect if provided (for macOS 14/13 compatibility)
|
|
149
158
|
if (!CGRectIsEmpty(captureRect)) {
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
// Scale capture rect to match actual image dimensions if needed
|
|
160
|
+
CGFloat imageScaleX = actualImageSize.width / logicalSize.width;
|
|
161
|
+
CGFloat imageScaleY = actualImageSize.height / logicalSize.height;
|
|
162
|
+
|
|
163
|
+
if (imageScaleX > 1.1 || imageScaleY > 1.1) {
|
|
164
|
+
// Scale up capture rect for high-DPI displays
|
|
165
|
+
CGRect scaledRect = CGRectMake(
|
|
166
|
+
captureRect.origin.x * imageScaleX,
|
|
167
|
+
captureRect.origin.y * imageScaleY,
|
|
168
|
+
captureRect.size.width * imageScaleX,
|
|
169
|
+
captureRect.size.height * imageScaleY
|
|
170
|
+
);
|
|
171
|
+
g_avCaptureRect = scaledRect;
|
|
172
|
+
NSLog(@"๐ฒ Capture area scaled: %.0f,%.0f %.0fx%.0f (scale %.1fx%.1fx)",
|
|
173
|
+
scaledRect.origin.x, scaledRect.origin.y, scaledRect.size.width, scaledRect.size.height,
|
|
174
|
+
imageScaleX, imageScaleY);
|
|
175
|
+
} else {
|
|
176
|
+
g_avCaptureRect = captureRect;
|
|
177
|
+
NSLog(@"๐ฒ Capture area (no scaling): %.0f,%.0f %.0fx%.0f",
|
|
178
|
+
captureRect.origin.x, captureRect.origin.y, captureRect.size.width, captureRect.size.height);
|
|
179
|
+
}
|
|
154
180
|
} else {
|
|
155
181
|
g_avCaptureRect = CGRectZero; // Full screen
|
|
156
|
-
NSLog(@"๐ฅ๏ธ Full screen capture (
|
|
182
|
+
NSLog(@"๐ฅ๏ธ Full screen capture (actual image dimensions)");
|
|
157
183
|
}
|
|
158
184
|
|
|
159
185
|
g_avFrameNumber = 0;
|
|
@@ -214,8 +240,11 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
214
240
|
size_t imageWidth = CGImageGetWidth(screenImage);
|
|
215
241
|
size_t imageHeight = CGImageGetHeight(screenImage);
|
|
216
242
|
|
|
243
|
+
NSLog(@"๐ Debug: Buffer %zux%zu, Image %zux%zu", bufferWidth, bufferHeight, imageWidth, imageHeight);
|
|
244
|
+
|
|
217
245
|
if (bufferWidth != imageWidth || bufferHeight != imageHeight) {
|
|
218
|
-
NSLog(@"โ ๏ธ
|
|
246
|
+
NSLog(@"โ ๏ธ CRITICAL SIZE MISMATCH! Buffer %zux%zu vs Image %zux%zu", bufferWidth, bufferHeight, imageWidth, imageHeight);
|
|
247
|
+
NSLog(@" This indicates Retina scaling issue on macOS 14/13");
|
|
219
248
|
}
|
|
220
249
|
|
|
221
250
|
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
package/src/window_selector.mm
CHANGED
|
@@ -16,6 +16,11 @@ static NSTimer *g_trackingTimer = nil;
|
|
|
16
16
|
static NSDictionary *g_selectedWindowInfo = nil;
|
|
17
17
|
static NSMutableArray *g_allWindows = nil;
|
|
18
18
|
|
|
19
|
+
// Per-screen overlay system
|
|
20
|
+
static NSMutableArray *g_perScreenOverlays = nil;
|
|
21
|
+
static NSMutableArray *g_perScreenOverlayViews = nil;
|
|
22
|
+
static NSInteger g_activeScreenIndex = -1;
|
|
23
|
+
|
|
19
24
|
// Functions to hide/show main overlay window during recording
|
|
20
25
|
void hideAllOverlayWindows() {
|
|
21
26
|
if (g_overlayWindow && [g_overlayWindow isVisible]) {
|
|
@@ -866,44 +871,58 @@ void updateOverlay() {
|
|
|
866
871
|
}
|
|
867
872
|
}
|
|
868
873
|
|
|
869
|
-
//
|
|
870
|
-
// Account for multi-display offset from global origin
|
|
871
|
-
WindowSelectorOverlayView *overlayView = (WindowSelectorOverlayView *)g_overlayView;
|
|
872
|
-
NSPoint globalOffset = NSZeroPoint;
|
|
873
|
-
if (overlayView.globalOriginOffset) {
|
|
874
|
-
globalOffset = [overlayView.globalOriginOffset pointValue];
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// Determine if this is a primary display window
|
|
874
|
+
// NEW PER-SCREEN SYSTEM: Find which screen this window belongs to
|
|
878
875
|
NSArray *allScreens = [NSScreen screens];
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
CGFloat localX, localY;
|
|
887
|
-
if (isPrimaryDisplayWindow) {
|
|
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;
|
|
876
|
+
NSInteger targetScreenIndex = -1;
|
|
877
|
+
NSScreen *targetScreen = nil;
|
|
878
|
+
|
|
879
|
+
// Find screen containing this window
|
|
880
|
+
for (NSInteger i = 0; i < [allScreens count]; i++) {
|
|
881
|
+
NSScreen *screen = [allScreens objectAtIndex:i];
|
|
882
|
+
NSRect screenFrame = [screen frame];
|
|
894
883
|
|
|
895
|
-
|
|
896
|
-
|
|
884
|
+
// Check if window center is within this screen bounds
|
|
885
|
+
CGFloat windowCenterX = x + width/2;
|
|
886
|
+
CGFloat windowCenterY = y + height/2;
|
|
897
887
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
888
|
+
if (windowCenterX >= screenFrame.origin.x &&
|
|
889
|
+
windowCenterX <= screenFrame.origin.x + screenFrame.size.width &&
|
|
890
|
+
windowCenterY >= screenFrame.origin.y &&
|
|
891
|
+
windowCenterY <= screenFrame.origin.y + screenFrame.size.height) {
|
|
892
|
+
targetScreenIndex = i;
|
|
893
|
+
targetScreen = screen;
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (targetScreenIndex == -1) {
|
|
899
|
+
NSLog(@"โ ๏ธ Window not found on any screen: (%.0f,%.0f)", (CGFloat)x, (CGFloat)y);
|
|
900
|
+
return;
|
|
902
901
|
}
|
|
903
902
|
|
|
904
|
-
//
|
|
905
|
-
[
|
|
906
|
-
|
|
903
|
+
// Get the overlay for target screen
|
|
904
|
+
NSWindow *targetOverlay = [g_perScreenOverlays objectAtIndex:targetScreenIndex];
|
|
905
|
+
WindowSelectorOverlayView *targetOverlayView = [g_perScreenOverlayViews objectAtIndex:targetScreenIndex];
|
|
906
|
+
NSRect targetScreenFrame = [targetScreen frame];
|
|
907
|
+
|
|
908
|
+
// Calculate LOCAL coordinates within target screen (much simpler!)
|
|
909
|
+
CGFloat localX = x - targetScreenFrame.origin.x;
|
|
910
|
+
CGFloat localY = (targetScreenFrame.size.height - (y - targetScreenFrame.origin.y)) - height;
|
|
911
|
+
|
|
912
|
+
NSLog(@"๐ฏ Window on Screen %ld: Global(%.0f,%.0f) โ Local(%.0f,%.0f)",
|
|
913
|
+
targetScreenIndex, (CGFloat)x, (CGFloat)y, localX, localY);
|
|
914
|
+
|
|
915
|
+
// Update active screen if changed
|
|
916
|
+
if (g_activeScreenIndex != targetScreenIndex) {
|
|
917
|
+
NSLog(@"๐ Switching to Screen %ld overlay", targetScreenIndex);
|
|
918
|
+
g_activeScreenIndex = targetScreenIndex;
|
|
919
|
+
g_overlayWindow = targetOverlay;
|
|
920
|
+
g_overlayView = targetOverlayView;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Update TARGET overlay view window info for highlighting
|
|
924
|
+
[targetOverlayView setWindowInfo:targetWindow];
|
|
925
|
+
[targetOverlayView setHighlightFrame:NSMakeRect(localX, localY, width, height)];
|
|
907
926
|
|
|
908
927
|
// Only reset toggle state when switching to different window (not for position updates)
|
|
909
928
|
if (!g_hasToggledWindow) {
|
|
@@ -1123,7 +1142,27 @@ void cleanupWindowSelector() {
|
|
|
1123
1142
|
g_windowKeyEventMonitor = nil;
|
|
1124
1143
|
}
|
|
1125
1144
|
|
|
1126
|
-
// Close
|
|
1145
|
+
// NEW PER-SCREEN CLEANUP: Close all per-screen overlays
|
|
1146
|
+
if (g_perScreenOverlays) {
|
|
1147
|
+
NSLog(@"๐งน Cleaning up %lu per-screen overlays", [g_perScreenOverlays count]);
|
|
1148
|
+
for (NSWindow *overlay in g_perScreenOverlays) {
|
|
1149
|
+
if (overlay) {
|
|
1150
|
+
[overlay close];
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
[g_perScreenOverlays release];
|
|
1154
|
+
g_perScreenOverlays = nil;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (g_perScreenOverlayViews) {
|
|
1158
|
+
[g_perScreenOverlayViews release];
|
|
1159
|
+
g_perScreenOverlayViews = nil;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Reset per-screen state
|
|
1163
|
+
g_activeScreenIndex = -1;
|
|
1164
|
+
|
|
1165
|
+
// Close legacy overlay window (fallback cleanup)
|
|
1127
1166
|
if (g_overlayWindow) {
|
|
1128
1167
|
[g_overlayWindow close];
|
|
1129
1168
|
g_overlayWindow = nil;
|
|
@@ -1836,32 +1875,55 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
|
1836
1875
|
}
|
|
1837
1876
|
|
|
1838
1877
|
// Get all windows
|
|
1839
|
-
g_allWindows = [getAllSelectableWindows()
|
|
1878
|
+
g_allWindows = [[NSMutableArray alloc] initWithArray:getAllSelectableWindows()];
|
|
1840
1879
|
|
|
1841
1880
|
if (!g_allWindows || [g_allWindows count] == 0) {
|
|
1842
1881
|
Napi::Error::New(env, "No selectable windows found").ThrowAsJavaScriptException();
|
|
1843
1882
|
return env.Null();
|
|
1844
1883
|
}
|
|
1845
1884
|
|
|
1846
|
-
// Create
|
|
1885
|
+
// Create PER-SCREEN overlay windows (much simpler than combined overlay)
|
|
1847
1886
|
NSArray *allScreens = [NSScreen screens];
|
|
1848
|
-
|
|
1887
|
+
g_perScreenOverlays = [[NSMutableArray alloc] init];
|
|
1888
|
+
g_perScreenOverlayViews = [[NSMutableArray alloc] init];
|
|
1889
|
+
|
|
1890
|
+
NSLog(@"๐ฅ๏ธ Creating per-screen overlays for %lu displays", [allScreens count]);
|
|
1849
1891
|
|
|
1850
|
-
//
|
|
1851
|
-
for (
|
|
1892
|
+
// Create overlay for each screen
|
|
1893
|
+
for (NSInteger i = 0; i < [allScreens count]; i++) {
|
|
1894
|
+
NSScreen *screen = [allScreens objectAtIndex:i];
|
|
1852
1895
|
NSRect screenFrame = [screen frame];
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1896
|
+
|
|
1897
|
+
// Create per-screen overlay window
|
|
1898
|
+
NSWindow *screenOverlay = [[NoFocusWindow alloc] initWithContentRect:screenFrame
|
|
1899
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
1900
|
+
backing:NSBackingStoreBuffered
|
|
1901
|
+
defer:NO
|
|
1902
|
+
screen:screen];
|
|
1903
|
+
|
|
1904
|
+
// Create per-screen overlay view
|
|
1905
|
+
WindowSelectorOverlayView *overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:NSMakeRect(0, 0, screenFrame.size.width, screenFrame.size.height)];
|
|
1906
|
+
[screenOverlay setContentView:overlayView];
|
|
1907
|
+
|
|
1908
|
+
// Configure overlay window
|
|
1909
|
+
[screenOverlay setStyleMask:NSWindowStyleMaskBorderless];
|
|
1910
|
+
[screenOverlay setBackgroundColor:[NSColor clearColor]];
|
|
1911
|
+
[screenOverlay setOpaque:NO];
|
|
1912
|
+
[screenOverlay setIgnoresMouseEvents:NO];
|
|
1913
|
+
[screenOverlay setAcceptsMouseMovedEvents:YES];
|
|
1914
|
+
[screenOverlay setLevel:NSScreenSaverWindowLevel];
|
|
1915
|
+
[screenOverlay setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary];
|
|
1916
|
+
|
|
1917
|
+
NSLog(@" Screen %ld: (%.0f,%.0f) %.0fx%.0f โ Overlay created", i, screenFrame.origin.x, screenFrame.origin.y, screenFrame.size.width, screenFrame.size.height);
|
|
1918
|
+
|
|
1919
|
+
[g_perScreenOverlays addObject:screenOverlay];
|
|
1920
|
+
[g_perScreenOverlayViews addObject:overlayView];
|
|
1858
1921
|
}
|
|
1859
1922
|
|
|
1860
|
-
|
|
1861
|
-
g_overlayWindow = [
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
defer:NO];
|
|
1923
|
+
// For compatibility, keep first screen as main overlay
|
|
1924
|
+
g_overlayWindow = [g_perScreenOverlays objectAtIndex:0];
|
|
1925
|
+
g_overlayView = [g_perScreenOverlayViews objectAtIndex:0];
|
|
1926
|
+
g_activeScreenIndex = 0;
|
|
1865
1927
|
|
|
1866
1928
|
// Force completely borderless appearance
|
|
1867
1929
|
[g_overlayWindow setStyleMask:NSWindowStyleMaskBorderless];
|
|
@@ -1886,17 +1948,7 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
|
1886
1948
|
[g_overlayWindow setOpaque:NO];
|
|
1887
1949
|
[g_overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1888
1950
|
|
|
1889
|
-
|
|
1890
|
-
// Convert global coordinates to local view coordinates (offset by origin)
|
|
1891
|
-
NSRect localViewFrame = NSMakeRect(0, 0, fullScreenFrame.size.width, fullScreenFrame.size.height);
|
|
1892
|
-
g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:localViewFrame];
|
|
1893
|
-
|
|
1894
|
-
// Store the global origin offset for coordinate conversion
|
|
1895
|
-
WindowSelectorOverlayView *overlayView = (WindowSelectorOverlayView *)g_overlayView;
|
|
1896
|
-
overlayView.globalOriginOffset = [NSValue valueWithPoint:fullScreenFrame.origin];
|
|
1897
|
-
|
|
1898
|
-
NSLog(@"๐ง SET GlobalOriginOffset: (%.0f, %.0f)", fullScreenFrame.origin.x, fullScreenFrame.origin.y);
|
|
1899
|
-
[g_overlayWindow setContentView:g_overlayView];
|
|
1951
|
+
NSLog(@"โ
Per-screen overlay system initialized with %lu screens", [allScreens count]);
|
|
1900
1952
|
|
|
1901
1953
|
// Note: NSWindow doesn't have setWantsLayer method, only NSView does
|
|
1902
1954
|
|