node-mac-recorder 2.18.2 → 2.18.4

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.
@@ -9,7 +9,8 @@
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)"
13
14
  ],
14
15
  "deny": [],
15
16
  "ask": []
package/index.js CHANGED
@@ -252,6 +252,12 @@ class MacRecorder extends EventEmitter {
252
252
 
253
253
  // Recording için display bilgisini sakla (cursor capture için)
254
254
  const targetDisplay = displays.find(d => d.id === targetDisplayId);
255
+
256
+ // CRITICAL FIX: Get actual physical dimensions for proper cursor scaling
257
+ // This fixes cursor coordinate issues on Retina displays during AVFoundation recording
258
+ const cursorPosition = nativeBinding.getCursorPosition();
259
+ const displayScalingInfo = cursorPosition.displayInfo;
260
+
255
261
  this.recordingDisplayInfo = {
256
262
  displayId: targetDisplayId,
257
263
  x: targetDisplay.x,
@@ -261,6 +267,10 @@ class MacRecorder extends EventEmitter {
261
267
  // Add scaling information for cursor coordinate transformation
262
268
  logicalWidth: parseInt(targetDisplay.resolution.split("x")[0]),
263
269
  logicalHeight: parseInt(targetDisplay.resolution.split("x")[1]),
270
+ // CRITICAL: Add physical dimensions for proper cursor scaling
271
+ physicalWidth: displayScalingInfo ? displayScalingInfo.physicalWidth : parseInt(targetDisplay.resolution.split("x")[0]),
272
+ physicalHeight: displayScalingInfo ? displayScalingInfo.physicalHeight : parseInt(targetDisplay.resolution.split("x")[1]),
273
+ scaleFactor: displayScalingInfo ? displayScalingInfo.scaleFactor : 1.0,
264
274
  };
265
275
  }
266
276
 
@@ -298,6 +308,11 @@ class MacRecorder extends EventEmitter {
298
308
  }
299
309
 
300
310
  if (targetDisplay) {
311
+ // CRITICAL FIX: Get actual physical dimensions for proper cursor scaling
312
+ // This fixes cursor coordinate issues on Retina displays during AVFoundation recording
313
+ const cursorPosition = nativeBinding.getCursorPosition();
314
+ const displayScalingInfo = cursorPosition.displayInfo;
315
+
301
316
  this.recordingDisplayInfo = {
302
317
  displayId: targetDisplay.id,
303
318
  x: targetDisplay.x || 0,
@@ -307,6 +322,10 @@ class MacRecorder extends EventEmitter {
307
322
  // Add scaling information for cursor coordinate transformation
308
323
  logicalWidth: parseInt(targetDisplay.resolution.split("x")[0]),
309
324
  logicalHeight: parseInt(targetDisplay.resolution.split("x")[1]),
325
+ // CRITICAL: Add physical dimensions for proper cursor scaling
326
+ physicalWidth: displayScalingInfo ? displayScalingInfo.physicalWidth : parseInt(targetDisplay.resolution.split("x")[0]),
327
+ physicalHeight: displayScalingInfo ? displayScalingInfo.physicalHeight : parseInt(targetDisplay.resolution.split("x")[1]),
328
+ scaleFactor: displayScalingInfo ? displayScalingInfo.scaleFactor : 1.0,
310
329
  };
311
330
  }
312
331
  } catch (error) {
@@ -712,6 +731,10 @@ class MacRecorder extends EventEmitter {
712
731
  displayY: options.displayInfo.y || 0,
713
732
  displayWidth: options.displayInfo.width || options.displayInfo.logicalWidth,
714
733
  displayHeight: options.displayInfo.height || options.displayInfo.logicalHeight,
734
+ // CRITICAL: Add physical dimensions for proper cursor scaling
735
+ physicalWidth: options.displayInfo.physicalWidth || options.displayInfo.width || options.displayInfo.logicalWidth,
736
+ physicalHeight: options.displayInfo.physicalHeight || options.displayInfo.height || options.displayInfo.logicalHeight,
737
+ scaleFactor: options.displayInfo.scaleFactor || 1.0,
715
738
  videoOffsetX: videoOffsetX,
716
739
  videoOffsetY: videoOffsetY,
717
740
  videoWidth: videoWidth,
@@ -730,6 +753,10 @@ class MacRecorder extends EventEmitter {
730
753
  displayY: this.recordingDisplayInfo.y || 0,
731
754
  displayWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
732
755
  displayHeight: this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
756
+ // CRITICAL: Include physical dimensions from recording display info
757
+ physicalWidth: this.recordingDisplayInfo.physicalWidth || this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
758
+ physicalHeight: this.recordingDisplayInfo.physicalHeight || this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
759
+ scaleFactor: this.recordingDisplayInfo.scaleFactor || 1.0,
733
760
  videoOffsetX: 0,
734
761
  videoOffsetY: 0,
735
762
  videoWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
@@ -785,19 +812,32 @@ class MacRecorder extends EventEmitter {
785
812
  const displayRelativeX = position.x - this.cursorDisplayInfo.displayX;
786
813
  const displayRelativeY = position.y - this.cursorDisplayInfo.displayY;
787
814
 
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
+
788
827
  // Step 2: Transform display-relative → video-relative coordinates
789
- x = displayRelativeX - this.cursorDisplayInfo.videoOffsetX;
790
- y = displayRelativeY - this.cursorDisplayInfo.videoOffsetY;
791
- coordinateSystem = "video-relative";
828
+ x = scaledX - this.cursorDisplayInfo.videoOffsetX;
829
+ y = scaledY - this.cursorDisplayInfo.videoOffsetY;
830
+ coordinateSystem = this.cursorDisplayInfo.scaleFactor > 1.0 ? "video-relative-scaled" : "video-relative";
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;
792
835
 
793
- // Bounds check for video area (don't skip, just note if outside)
794
- const outsideVideo = x < 0 || y < 0 ||
795
- x >= this.cursorDisplayInfo.videoWidth ||
796
- y >= this.cursorDisplayInfo.videoHeight;
836
+ const outsideVideo = x < 0 || y < 0 || x >= videoWidth || y >= videoHeight;
797
837
 
798
838
  // For debugging - add metadata if cursor is outside video area
799
839
  if (outsideVideo) {
800
- coordinateSystem = "video-relative-outside";
840
+ coordinateSystem = coordinateSystem + "-outside";
801
841
  }
802
842
  }
803
843
 
@@ -815,12 +855,19 @@ class MacRecorder extends EventEmitter {
815
855
  width: this.cursorDisplayInfo.videoWidth,
816
856
  height: this.cursorDisplayInfo.videoHeight,
817
857
  offsetX: this.cursorDisplayInfo.videoOffsetX,
818
- offsetY: this.cursorDisplayInfo.videoOffsetY
858
+ offsetY: this.cursorDisplayInfo.videoOffsetY,
859
+ // CRITICAL: Add physical dimensions for debugging
860
+ physicalWidth: this.cursorDisplayInfo.physicalWidth,
861
+ physicalHeight: this.cursorDisplayInfo.physicalHeight
819
862
  } : null,
820
863
  displayInfo: this.cursorDisplayInfo ? {
821
864
  displayId: this.cursorDisplayInfo.displayId,
822
865
  width: this.cursorDisplayInfo.displayWidth,
823
- height: this.cursorDisplayInfo.displayHeight
866
+ height: this.cursorDisplayInfo.displayHeight,
867
+ // CRITICAL: Add scaling info for debugging AVFoundation cursor issues
868
+ physicalWidth: this.cursorDisplayInfo.physicalWidth,
869
+ physicalHeight: this.cursorDisplayInfo.physicalHeight,
870
+ scaleFactor: this.cursorDisplayInfo.scaleFactor
824
871
  } : null
825
872
  };
826
873
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.18.2",
3
+ "version": "2.18.4",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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
- // Get display dimensions with proper scaling for macOS 14/13 compatibility
57
- CGRect displayBounds = CGDisplayBounds(displayID);
58
-
59
- // Get both logical (bounds) and physical (pixels) dimensions
60
- CGSize logicalSize = displayBounds.size;
61
- CGSize physicalSize = CGSizeMake(CGDisplayPixelsWide(displayID), CGDisplayPixelsHigh(displayID));
62
-
63
- // Calculate scale factor
64
- CGFloat scaleX = physicalSize.width / logicalSize.width;
65
- CGFloat scaleY = physicalSize.height / logicalSize.height;
66
- CGFloat scaleFactor = MAX(scaleX, scaleY); // Use max to handle non-uniform scaling
67
-
68
- // CRITICAL FIX: Use actual captured image dimensions for pixel buffer
69
- // CGDisplayCreateImage returns physical pixels on Retina displays
70
- CGImageRef testImage = CGDisplayCreateImage(displayID);
71
- CGSize actualImageSize = CGSizeMake(CGImageGetWidth(testImage), CGImageGetHeight(testImage));
72
- CGImageRelease(testImage);
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(@"🎯 CRITICAL: Logical %.0fx%.0f → Actual image %.0fx%.0f",
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 pixels (physical): %.0fx%.0f", physicalSize.width, physicalSize.height);
93
-
181
+ NSLog(@"🖥️ Display actual physical: %.0fx%.0f", actualImageSize.width, actualImageSize.height);
182
+
94
183
  if (scaleFactor > 1.5) {
95
- NSLog(@"🔍 Scale factor: %.1fx → Retina display detected (macOS 14/13 scaling fix applied)", scaleFactor);
184
+ NSLog(@"🔍 ADVANCED Scale factor: %.2fx → Retina display detected", scaleFactor);
96
185
  } else if (scaleFactor > 1.1) {
97
- NSLog(@"🔍 Scale factor: %.1fx → Non-standard scaling detected", scaleFactor);
186
+ NSLog(@"🔍 ADVANCED Scale factor: %.2fx → Non-standard scaling detected", scaleFactor);
98
187
  } else {
99
- NSLog(@"🔍 Scale factor: %.1fx → Standard display", scaleFactor);
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);