node-mac-recorder 2.18.3 → 2.18.5
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 +4 -1
- package/index.js +28 -25
- package/package.json +1 -1
- package/src/avfoundation_recorder.mm +113 -24
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"WebFetch(domain:stackoverflow.com)",
|
|
10
10
|
"Bash(timeout:*)",
|
|
11
11
|
"Bash(grep:*)",
|
|
12
|
-
"Bash(./test-cleanup.sh:*)"
|
|
12
|
+
"Bash(./test-cleanup.sh:*)",
|
|
13
|
+
"Bash(FORCE_AVFOUNDATION=1 node test-avfoundation-dpr.js)",
|
|
14
|
+
"Bash(FORCE_AVFOUNDATION=1 node test-cursor-fix.js)",
|
|
15
|
+
"Bash(pkill:*)"
|
|
13
16
|
],
|
|
14
17
|
"deny": [],
|
|
15
18
|
"ask": []
|
package/index.js
CHANGED
|
@@ -705,8 +705,18 @@ class MacRecorder extends EventEmitter {
|
|
|
705
705
|
// Calculate video offset based on recording type
|
|
706
706
|
let videoOffsetX = 0;
|
|
707
707
|
let videoOffsetY = 0;
|
|
708
|
-
|
|
709
|
-
|
|
708
|
+
// CRITICAL FIX: Use physical dimensions for video when scale factor > 1 (AVFoundation)
|
|
709
|
+
// AVFoundation records at physical resolution, ScreenCaptureKit at logical resolution
|
|
710
|
+
let videoWidth, videoHeight;
|
|
711
|
+
if (options.displayInfo.scaleFactor && options.displayInfo.scaleFactor > 1.0) {
|
|
712
|
+
// AVFoundation case: use physical dimensions to match actual video size
|
|
713
|
+
videoWidth = options.displayInfo.physicalWidth || options.displayInfo.width || options.displayInfo.logicalWidth;
|
|
714
|
+
videoHeight = options.displayInfo.physicalHeight || options.displayInfo.height || options.displayInfo.logicalHeight;
|
|
715
|
+
} else {
|
|
716
|
+
// ScreenCaptureKit case: use logical dimensions
|
|
717
|
+
videoWidth = options.displayInfo.width || options.displayInfo.logicalWidth;
|
|
718
|
+
videoHeight = options.displayInfo.height || options.displayInfo.logicalHeight;
|
|
719
|
+
}
|
|
710
720
|
|
|
711
721
|
if (options.recordingType === 'window' && options.windowId) {
|
|
712
722
|
// For window recording: offset = window position in display
|
|
@@ -759,8 +769,13 @@ class MacRecorder extends EventEmitter {
|
|
|
759
769
|
scaleFactor: this.recordingDisplayInfo.scaleFactor || 1.0,
|
|
760
770
|
videoOffsetX: 0,
|
|
761
771
|
videoOffsetY: 0,
|
|
762
|
-
|
|
763
|
-
|
|
772
|
+
// CRITICAL FIX: Use physical dimensions for video when scale factor > 1 (AVFoundation)
|
|
773
|
+
videoWidth: (this.recordingDisplayInfo.scaleFactor > 1.0) ?
|
|
774
|
+
(this.recordingDisplayInfo.physicalWidth || this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth) :
|
|
775
|
+
(this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth),
|
|
776
|
+
videoHeight: (this.recordingDisplayInfo.scaleFactor > 1.0) ?
|
|
777
|
+
(this.recordingDisplayInfo.physicalHeight || this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight) :
|
|
778
|
+
(this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight),
|
|
764
779
|
videoRelative: true,
|
|
765
780
|
recordingType: options.recordingType || 'display'
|
|
766
781
|
};
|
|
@@ -812,32 +827,20 @@ class MacRecorder extends EventEmitter {
|
|
|
812
827
|
const displayRelativeX = position.x - this.cursorDisplayInfo.displayX;
|
|
813
828
|
const displayRelativeY = position.y - this.cursorDisplayInfo.displayY;
|
|
814
829
|
|
|
815
|
-
// CRITICAL FIX: Apply physical scaling for AVFoundation recording
|
|
816
|
-
// AVFoundation records at physical resolution while cursor coordinates are logical
|
|
817
|
-
let scaledX = displayRelativeX;
|
|
818
|
-
let scaledY = displayRelativeY;
|
|
819
|
-
|
|
820
|
-
if (this.cursorDisplayInfo.scaleFactor && this.cursorDisplayInfo.scaleFactor > 1.0) {
|
|
821
|
-
// Scale logical cursor coordinates to match physical video dimensions
|
|
822
|
-
scaledX = displayRelativeX * this.cursorDisplayInfo.scaleFactor;
|
|
823
|
-
scaledY = displayRelativeY * this.cursorDisplayInfo.scaleFactor;
|
|
824
|
-
coordinateSystem = "video-relative-scaled";
|
|
825
|
-
}
|
|
826
|
-
|
|
827
830
|
// Step 2: Transform display-relative → video-relative coordinates
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// Bounds check for video area (use physical video dimensions for AVFoundation)
|
|
833
|
-
const videoWidth = this.cursorDisplayInfo.physicalWidth || this.cursorDisplayInfo.videoWidth;
|
|
834
|
-
const videoHeight = this.cursorDisplayInfo.physicalHeight || this.cursorDisplayInfo.videoHeight;
|
|
831
|
+
// videoWidth/Height already account for physical vs logical dimensions
|
|
832
|
+
x = displayRelativeX - this.cursorDisplayInfo.videoOffsetX;
|
|
833
|
+
y = displayRelativeY - this.cursorDisplayInfo.videoOffsetY;
|
|
834
|
+
coordinateSystem = "video-relative";
|
|
835
835
|
|
|
836
|
-
|
|
836
|
+
// Bounds check for video area using correct video dimensions
|
|
837
|
+
const outsideVideo = x < 0 || y < 0 ||
|
|
838
|
+
x >= this.cursorDisplayInfo.videoWidth ||
|
|
839
|
+
y >= this.cursorDisplayInfo.videoHeight;
|
|
837
840
|
|
|
838
841
|
// For debugging - add metadata if cursor is outside video area
|
|
839
842
|
if (outsideVideo) {
|
|
840
|
-
coordinateSystem =
|
|
843
|
+
coordinateSystem = "video-relative-outside";
|
|
841
844
|
}
|
|
842
845
|
}
|
|
843
846
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,83 @@
|
|
|
6
6
|
#import <AppKit/AppKit.h>
|
|
7
7
|
#include <string>
|
|
8
8
|
|
|
9
|
+
// Advanced display scaling detection (same algorithm as cursor_tracker.mm)
|
|
10
|
+
NSDictionary* getAdvancedDisplayScalingInfo(CGDirectDisplayID displayID) {
|
|
11
|
+
@try {
|
|
12
|
+
CGRect displayBounds = CGDisplayBounds(displayID);
|
|
13
|
+
CGSize logicalSize = displayBounds.size;
|
|
14
|
+
|
|
15
|
+
NSLog(@"🔍 Advanced scaling detection for display %u:", displayID);
|
|
16
|
+
NSLog(@" Logical bounds: %.0fx%.0f", logicalSize.width, logicalSize.height);
|
|
17
|
+
|
|
18
|
+
// CRITICAL FIX: Get REAL physical dimensions using multiple detection methods
|
|
19
|
+
// Method 1: CGDisplayCreateImage (may be scaled on some systems)
|
|
20
|
+
CGImageRef testImage = CGDisplayCreateImage(displayID);
|
|
21
|
+
CGSize imageSize = CGSizeMake(CGImageGetWidth(testImage), CGImageGetHeight(testImage));
|
|
22
|
+
CGImageRelease(testImage);
|
|
23
|
+
|
|
24
|
+
// Method 2: Native display mode detection for true physical resolution
|
|
25
|
+
CGSize actualPhysicalSize = imageSize;
|
|
26
|
+
CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(displayID, NULL);
|
|
27
|
+
if (displayModes) {
|
|
28
|
+
CFIndex modeCount = CFArrayGetCount(displayModes);
|
|
29
|
+
CGSize maxResolution = CGSizeMake(0, 0);
|
|
30
|
+
|
|
31
|
+
// Find the highest resolution mode (native resolution)
|
|
32
|
+
for (CFIndex i = 0; i < modeCount; i++) {
|
|
33
|
+
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i);
|
|
34
|
+
CGSize modeSize = CGSizeMake(CGDisplayModeGetWidth(mode), CGDisplayModeGetHeight(mode));
|
|
35
|
+
|
|
36
|
+
if (modeSize.width > maxResolution.width ||
|
|
37
|
+
(modeSize.width == maxResolution.width && modeSize.height > maxResolution.height)) {
|
|
38
|
+
maxResolution = modeSize;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Use the max resolution if it's significantly higher than image size
|
|
43
|
+
if (maxResolution.width > imageSize.width * 1.5 || maxResolution.height > imageSize.height * 1.5) {
|
|
44
|
+
actualPhysicalSize = maxResolution;
|
|
45
|
+
NSLog(@" Using display mode detection: %.0fx%.0f (was %.0fx%.0f)",
|
|
46
|
+
maxResolution.width, maxResolution.height, imageSize.width, imageSize.height);
|
|
47
|
+
} else {
|
|
48
|
+
actualPhysicalSize = imageSize;
|
|
49
|
+
NSLog(@" Using image size detection: %.0fx%.0f", imageSize.width, imageSize.height);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
CFRelease(displayModes);
|
|
53
|
+
} else {
|
|
54
|
+
actualPhysicalSize = imageSize;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
CGSize reportedPhysicalSize = CGSizeMake(CGDisplayPixelsWide(displayID), CGDisplayPixelsHigh(displayID));
|
|
58
|
+
|
|
59
|
+
NSLog(@"🔍 ADVANCED scaling info:");
|
|
60
|
+
NSLog(@" Logical: %.0fx%.0f", logicalSize.width, logicalSize.height);
|
|
61
|
+
NSLog(@" Reported physical: %.0fx%.0f", reportedPhysicalSize.width, reportedPhysicalSize.height);
|
|
62
|
+
NSLog(@" ACTUAL physical: %.0fx%.0f", actualPhysicalSize.width, actualPhysicalSize.height);
|
|
63
|
+
|
|
64
|
+
CGFloat scaleX = actualPhysicalSize.width / logicalSize.width;
|
|
65
|
+
CGFloat scaleY = actualPhysicalSize.height / logicalSize.height;
|
|
66
|
+
CGFloat scaleFactor = MAX(scaleX, scaleY);
|
|
67
|
+
|
|
68
|
+
NSLog(@"🔍 ADVANCED scale factors: X=%.2f, Y=%.2f, Final=%.2f", scaleX, scaleY, scaleFactor);
|
|
69
|
+
|
|
70
|
+
return @{
|
|
71
|
+
@"displayID": @(displayID),
|
|
72
|
+
@"logicalSize": [NSValue valueWithSize:NSMakeSize(logicalSize.width, logicalSize.height)],
|
|
73
|
+
@"physicalSize": [NSValue valueWithSize:NSMakeSize(actualPhysicalSize.width, actualPhysicalSize.height)],
|
|
74
|
+
@"reportedPhysicalSize": [NSValue valueWithSize:NSMakeSize(reportedPhysicalSize.width, reportedPhysicalSize.height)],
|
|
75
|
+
@"scaleFactor": @(scaleFactor),
|
|
76
|
+
@"scaleX": @(scaleX),
|
|
77
|
+
@"scaleY": @(scaleY),
|
|
78
|
+
@"displayBounds": [NSValue valueWithRect:NSMakeRect(displayBounds.origin.x, displayBounds.origin.y, displayBounds.size.width, displayBounds.size.height)]
|
|
79
|
+
};
|
|
80
|
+
} @catch (NSException *exception) {
|
|
81
|
+
NSLog(@"❌ Advanced scaling detection failed: %@", exception);
|
|
82
|
+
return nil;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
9
86
|
static AVAssetWriter *g_avWriter = nil;
|
|
10
87
|
static AVAssetWriterInput *g_avVideoInput = nil;
|
|
11
88
|
static AVAssetWriterInputPixelBufferAdaptor *g_avPixelBufferAdaptor = nil;
|
|
@@ -53,23 +130,35 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
53
130
|
return false;
|
|
54
131
|
}
|
|
55
132
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
CGSize
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
133
|
+
// CRITICAL FIX: Use advanced scaling detection (same as cursor_tracker.mm)
|
|
134
|
+
NSDictionary *advancedScalingInfo = getAdvancedDisplayScalingInfo(displayID);
|
|
135
|
+
|
|
136
|
+
CGSize logicalSize;
|
|
137
|
+
CGSize actualImageSize;
|
|
138
|
+
CGFloat scaleFactor;
|
|
139
|
+
|
|
140
|
+
if (advancedScalingInfo) {
|
|
141
|
+
// Use advanced detection results
|
|
142
|
+
NSSize logicalSizeValue = [[advancedScalingInfo objectForKey:@"logicalSize"] sizeValue];
|
|
143
|
+
NSSize physicalSizeValue = [[advancedScalingInfo objectForKey:@"physicalSize"] sizeValue];
|
|
144
|
+
|
|
145
|
+
logicalSize = CGSizeMake(logicalSizeValue.width, logicalSizeValue.height);
|
|
146
|
+
actualImageSize = CGSizeMake(physicalSizeValue.width, physicalSizeValue.height);
|
|
147
|
+
scaleFactor = [[advancedScalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
148
|
+
|
|
149
|
+
NSLog(@"✅ Using ADVANCED scaling detection for AVFoundation");
|
|
150
|
+
} else {
|
|
151
|
+
// Fallback to old method
|
|
152
|
+
NSLog(@"⚠️ Advanced scaling detection failed, using fallback");
|
|
153
|
+
CGRect displayBounds = CGDisplayBounds(displayID);
|
|
154
|
+
logicalSize = displayBounds.size;
|
|
155
|
+
|
|
156
|
+
CGImageRef testImage = CGDisplayCreateImage(displayID);
|
|
157
|
+
actualImageSize = CGSizeMake(CGImageGetWidth(testImage), CGImageGetHeight(testImage));
|
|
158
|
+
CGImageRelease(testImage);
|
|
159
|
+
|
|
160
|
+
scaleFactor = MAX(actualImageSize.width / logicalSize.width, actualImageSize.height / logicalSize.height);
|
|
161
|
+
}
|
|
73
162
|
|
|
74
163
|
// CRITICAL FIX: Use actual image dimensions to match what CGDisplayCreateImage returns
|
|
75
164
|
// This prevents the "1/4 recording area" bug on Retina displays
|
|
@@ -85,18 +174,18 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
85
174
|
recordingSize = actualImageSize;
|
|
86
175
|
}
|
|
87
176
|
|
|
88
|
-
NSLog(@"🎯
|
|
177
|
+
NSLog(@"🎯 ADVANCED: Logical %.0fx%.0f → Actual image %.0fx%.0f",
|
|
89
178
|
logicalSize.width, logicalSize.height, actualImageSize.width, actualImageSize.height);
|
|
90
|
-
|
|
179
|
+
|
|
91
180
|
NSLog(@"🖥️ Display bounds (logical): %.0fx%.0f", logicalSize.width, logicalSize.height);
|
|
92
|
-
NSLog(@"🖥️ Display
|
|
93
|
-
|
|
181
|
+
NSLog(@"🖥️ Display actual physical: %.0fx%.0f", actualImageSize.width, actualImageSize.height);
|
|
182
|
+
|
|
94
183
|
if (scaleFactor > 1.5) {
|
|
95
|
-
NSLog(@"🔍 Scale factor: %.
|
|
184
|
+
NSLog(@"🔍 ADVANCED Scale factor: %.2fx → Retina display detected", scaleFactor);
|
|
96
185
|
} else if (scaleFactor > 1.1) {
|
|
97
|
-
NSLog(@"🔍 Scale factor: %.
|
|
186
|
+
NSLog(@"🔍 ADVANCED Scale factor: %.2fx → Non-standard scaling detected", scaleFactor);
|
|
98
187
|
} else {
|
|
99
|
-
NSLog(@"🔍 Scale factor: %.
|
|
188
|
+
NSLog(@"🔍 ADVANCED Scale factor: %.2fx → Standard display", scaleFactor);
|
|
100
189
|
}
|
|
101
190
|
|
|
102
191
|
NSLog(@"🎯 Recording size: %.0fx%.0f (using actual physical dimensions for Retina fix)", recordingSize.width, recordingSize.height);
|