node-mac-recorder 2.16.29 โ†’ 2.16.31

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.
@@ -1,9 +1,7 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(FORCE_AVFOUNDATION=1 node test-sequential.js)",
5
- "Bash(grep:*)",
6
- "Bash(FORCE_AVFOUNDATION=1 node -e \"\nconsole.log(''๐Ÿงช Testing macOS 14/13 coordinate scaling fix...'');\nconst MacRecorder = require(''./index.js'');\nconst recorder = new MacRecorder();\n\nrecorder.startRecording(''/tmp/coordinate-fix-test.mov'')\n .then(success => {\n console.log(''Start result:'', success ? ''โœ… SUCCESS'' : ''โŒ FAILED'');\n if (success) {\n setTimeout(() => {\n recorder.stopRecording().then(() => {\n console.log(''โœ… Recording stopped'');\n const fs = require(''fs'');\n if (fs.existsSync(''/tmp/coordinate-fix-test.mov'')) {\n const size = Math.round(fs.statSync(''/tmp/coordinate-fix-test.mov'').size/1024);\n console.log(''๐Ÿ“น File size:'', size + ''KB'');\n if (size > 100) {\n console.log(''๐ŸŽ‰ macOS 14/13 coordinate fix test SUCCESS!'');\n } else {\n console.log(''โš ๏ธ File too small, may have issues'');\n }\n } else {\n console.log(''โŒ No output file created'');\n }\n });\n }, 3000); // 3 seconds recording\n }\n })\n .catch(console.error);\n\")"
4
+ "Bash(node:*)"
7
5
  ],
8
6
  "deny": [],
9
7
  "ask": []
@@ -0,0 +1,73 @@
1
+ const MacRecorder = require('./index.js');
2
+
3
+ console.log('๐Ÿงช Testing cursor DPR scaling fixes...\n');
4
+
5
+ const recorder = new MacRecorder();
6
+
7
+ // Test basic cursor position
8
+ console.log('1. Testing basic cursor position:');
9
+ try {
10
+ const position = recorder.getCursorPosition();
11
+ console.log(` Position: (${position.x}, ${position.y})`);
12
+
13
+ // If we have scaling debug info, show it
14
+ if (position.scaleFactor) {
15
+ console.log(` Scale factor: ${position.scaleFactor}x`);
16
+ console.log(` Raw position: (${position.rawX}, ${position.rawY})`);
17
+ console.log(` Logical position: (${position.x}, ${position.y})`);
18
+ }
19
+ } catch (error) {
20
+ console.error(' Error:', error.message);
21
+ }
22
+
23
+ console.log('\n2. Testing display information:');
24
+ recorder.getDisplays().then(displays => {
25
+ displays.forEach((display, index) => {
26
+ console.log(` Display ${index}: ${display.resolution} at (${display.x}, ${display.y})`);
27
+ console.log(` Primary: ${display.isPrimary}, ID: ${display.id}`);
28
+ });
29
+
30
+ console.log('\n3. Testing cursor capture with DPR fix:');
31
+ const outputFile = 'cursor-dpr-test.json';
32
+
33
+ recorder.startCursorCapture(outputFile, 50).then(() => {
34
+ console.log(` โœ… Cursor capture started, saving to ${outputFile}`);
35
+ console.log(' Move your mouse around for 5 seconds...');
36
+
37
+ setTimeout(() => {
38
+ recorder.stopCursorCapture().then(() => {
39
+ console.log(' โœ… Cursor capture stopped');
40
+
41
+ // Read and analyze the captured data
42
+ const fs = require('fs');
43
+ try {
44
+ const data = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
45
+ if (data.length > 0) {
46
+ const first = data[0];
47
+ const last = data[data.length - 1];
48
+
49
+ console.log(` ๐Ÿ“Š Captured ${data.length} cursor events`);
50
+ console.log(` ๐Ÿ“ First: (${first.x}, ${first.y}) ${first.coordinateSystem || 'unknown'}`);
51
+ console.log(` ๐Ÿ“ Last: (${last.x}, ${last.y}) ${last.coordinateSystem || 'unknown'}`);
52
+
53
+ // Check coordinate system
54
+ const hasCoordinateSystem = data.some(d => d.coordinateSystem);
55
+ console.log(` ๐ŸŽฏ Coordinate system info: ${hasCoordinateSystem ? 'Present' : 'Missing'}`);
56
+ } else {
57
+ console.log(' โš ๏ธ No cursor events captured');
58
+ }
59
+ } catch (readError) {
60
+ console.error(' โŒ Error reading capture file:', readError.message);
61
+ }
62
+
63
+ process.exit(0);
64
+ });
65
+ }, 5000);
66
+ }).catch(error => {
67
+ console.error(' โŒ Cursor capture failed:', error.message);
68
+ process.exit(1);
69
+ });
70
+ }).catch(error => {
71
+ console.error(' Error getting displays:', error.message);
72
+ process.exit(1);
73
+ });
@@ -0,0 +1 @@
1
+ []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.16.29",
3
+ "version": "2.16.31",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -65,14 +65,25 @@ 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
- // CRITICAL FIX: Use actual captured image dimensions, not logical size
69
- // CGDisplayCreateImage may return higher resolution on Retina displays
68
+ // CRITICAL FIX: Use actual captured image dimensions for pixel buffer
69
+ // CGDisplayCreateImage returns physical pixels on Retina displays
70
70
  CGImageRef testImage = CGDisplayCreateImage(displayID);
71
71
  CGSize actualImageSize = CGSizeMake(CGImageGetWidth(testImage), CGImageGetHeight(testImage));
72
72
  CGImageRelease(testImage);
73
73
 
74
- // CRITICAL FIX: Use logical size for consistency, actual image size only for validation
75
- CGSize recordingSize = captureRect.size.width > 0 ? captureRect.size : logicalSize;
74
+ // CRITICAL FIX: Use actual image dimensions to match what CGDisplayCreateImage returns
75
+ // This prevents the "1/4 recording area" bug on Retina displays
76
+ CGSize recordingSize;
77
+ if (!CGRectIsEmpty(captureRect)) {
78
+ // Scale capture rect to match actual image dimensions
79
+ recordingSize = CGSizeMake(
80
+ captureRect.size.width * (actualImageSize.width / logicalSize.width),
81
+ captureRect.size.height * (actualImageSize.height / logicalSize.height)
82
+ );
83
+ } else {
84
+ // Full screen: use actual image size
85
+ recordingSize = actualImageSize;
86
+ }
76
87
 
77
88
  NSLog(@"๐ŸŽฏ CRITICAL: Logical %.0fx%.0f โ†’ Actual image %.0fx%.0f",
78
89
  logicalSize.width, logicalSize.height, actualImageSize.width, actualImageSize.height);
@@ -88,7 +99,7 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
88
99
  NSLog(@"๐Ÿ” Scale factor: %.1fx โ†’ Standard display", scaleFactor);
89
100
  }
90
101
 
91
- NSLog(@"๐ŸŽฏ Recording size: %.0fx%.0f (using logical dimensions for AVFoundation)", recordingSize.width, recordingSize.height);
102
+ NSLog(@"๐ŸŽฏ Recording size: %.0fx%.0f (using actual physical dimensions for Retina fix)", recordingSize.width, recordingSize.height);
92
103
 
93
104
  // Video settings with macOS compatibility
94
105
  NSString *codecKey;
@@ -154,25 +165,34 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
154
165
  // Store recording parameters with scaling correction
155
166
  g_avDisplayID = displayID;
156
167
 
157
- // CRITICAL FIX: Use coordinates as-is from JS layer (already display-relative)
168
+ // CRITICAL FIX: Scale capture coordinates to match physical pixels
158
169
  if (!CGRectIsEmpty(captureRect)) {
159
- // Use coordinates directly - JS layer already converted global to display-relative
160
- g_avCaptureRect = captureRect;
170
+ // Scale coordinates from logical to physical (for CGDisplayCreateImage)
171
+ CGFloat scaleFactorX = actualImageSize.width / logicalSize.width;
172
+ CGFloat scaleFactorY = actualImageSize.height / logicalSize.height;
173
+
174
+ g_avCaptureRect = CGRectMake(
175
+ captureRect.origin.x * scaleFactorX,
176
+ captureRect.origin.y * scaleFactorY,
177
+ captureRect.size.width * scaleFactorX,
178
+ captureRect.size.height * scaleFactorY
179
+ );
161
180
 
162
- NSLog(@"๐Ÿ”ฒ macOS 14/13 FIXED: Display-relative capture area: %.0f,%.0f %.0fx%.0f",
163
- captureRect.origin.x, captureRect.origin.y, captureRect.size.width, captureRect.size.height);
181
+ NSLog(@"๐Ÿ”ฒ RETINA FIX: Logical (%.0f,%.0f %.0fx%.0f) โ†’ Physical (%.0f,%.0f %.0fx%.0f)",
182
+ captureRect.origin.x, captureRect.origin.y, captureRect.size.width, captureRect.size.height,
183
+ g_avCaptureRect.origin.x, g_avCaptureRect.origin.y, g_avCaptureRect.size.width, g_avCaptureRect.size.height);
164
184
 
165
- // Validate coordinates are within logical display bounds
166
- if (captureRect.origin.x >= 0 && captureRect.origin.y >= 0 &&
167
- captureRect.origin.x + captureRect.size.width <= logicalSize.width &&
168
- captureRect.origin.y + captureRect.size.height <= logicalSize.height) {
169
- NSLog(@"โœ… Coordinates validated within logical display bounds %.0fx%.0f", logicalSize.width, logicalSize.height);
185
+ // Validate coordinates are within physical display bounds
186
+ if (g_avCaptureRect.origin.x >= 0 && g_avCaptureRect.origin.y >= 0 &&
187
+ g_avCaptureRect.origin.x + g_avCaptureRect.size.width <= actualImageSize.width &&
188
+ g_avCaptureRect.origin.y + g_avCaptureRect.size.height <= actualImageSize.height) {
189
+ NSLog(@"โœ… Coordinates validated within physical display bounds %.0fx%.0f", actualImageSize.width, actualImageSize.height);
170
190
  } else {
171
- NSLog(@"โš ๏ธ Coordinates may be outside logical display bounds - clipping may occur");
191
+ NSLog(@"โš ๏ธ Coordinates may be outside physical display bounds - clipping may occur");
172
192
  }
173
193
  } else {
174
194
  g_avCaptureRect = CGRectZero; // Full screen
175
- NSLog(@"๐Ÿ–ฅ๏ธ Full screen capture using logical dimensions %.0fx%.0f", logicalSize.width, logicalSize.height);
195
+ NSLog(@"๐Ÿ–ฅ๏ธ Full screen capture using physical dimensions %.0fx%.0f", actualImageSize.width, actualImageSize.height);
176
196
  }
177
197
 
178
198
  g_avFrameNumber = 0;
@@ -20,6 +20,7 @@ static bool g_isFirstWrite = true;
20
20
  // Forward declaration
21
21
  void cursorTimerCallback();
22
22
  void writeToFile(NSDictionary *cursorData);
23
+ NSDictionary* getDisplayScalingInfo(CGPoint globalPoint);
23
24
 
24
25
  // Timer helper class
25
26
  @interface CursorTimerTarget : NSObject
@@ -198,7 +199,28 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
198
199
  return event;
199
200
  }
200
201
 
201
- CGPoint location = CGEventGetLocation(event);
202
+ CGPoint rawLocation = CGEventGetLocation(event);
203
+
204
+ // Apply DPR scaling correction for Retina displays
205
+ NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
206
+ CGPoint location = rawLocation;
207
+
208
+ if (scalingInfo) {
209
+ CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
210
+ NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
211
+
212
+ if (scaleFactor > 1.1) {
213
+ // Convert to logical coordinates
214
+ CGFloat displayRelativeX = rawLocation.x - displayBounds.origin.x;
215
+ CGFloat displayRelativeY = rawLocation.y - displayBounds.origin.y;
216
+
217
+ CGFloat logicalRelativeX = displayRelativeX / scaleFactor;
218
+ CGFloat logicalRelativeY = displayRelativeY / scaleFactor;
219
+
220
+ location.x = displayBounds.origin.x + logicalRelativeX;
221
+ location.y = displayBounds.origin.y + logicalRelativeY;
222
+ }
223
+ }
202
224
  NSDate *currentDate = [NSDate date];
203
225
  NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
204
226
  NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
@@ -254,13 +276,34 @@ void cursorTimerCallback() {
254
276
  return;
255
277
  }
256
278
 
257
- // CGEventGetLocation direkt global koordinat verir - รงoklu ekran desteฤŸi iรงin daha doฤŸru
279
+ // Get cursor position with DPR scaling correction
258
280
  CGEventRef event = CGEventCreate(NULL);
259
- CGPoint location = CGEventGetLocation(event);
281
+ CGPoint rawLocation = CGEventGetLocation(event);
260
282
  if (event) {
261
283
  CFRelease(event);
262
284
  }
263
285
 
286
+ // Apply DPR scaling correction for Retina displays
287
+ NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
288
+ CGPoint location = rawLocation;
289
+
290
+ if (scalingInfo) {
291
+ CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
292
+ NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
293
+
294
+ if (scaleFactor > 1.1) {
295
+ // Convert to logical coordinates
296
+ CGFloat displayRelativeX = rawLocation.x - displayBounds.origin.x;
297
+ CGFloat displayRelativeY = rawLocation.y - displayBounds.origin.y;
298
+
299
+ CGFloat logicalRelativeX = displayRelativeX / scaleFactor;
300
+ CGFloat logicalRelativeY = displayRelativeY / scaleFactor;
301
+
302
+ location.x = displayBounds.origin.x + logicalRelativeX;
303
+ location.y = displayBounds.origin.y + logicalRelativeY;
304
+ }
305
+ }
306
+
264
307
  NSDate *currentDate = [NSDate date];
265
308
  NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
266
309
  NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
@@ -435,19 +478,94 @@ Napi::Value StopCursorTracking(const Napi::CallbackInfo& info) {
435
478
  }
436
479
  }
437
480
 
481
+ // Helper function to get display scaling info for cursor coordinates
482
+ NSDictionary* getDisplayScalingInfo(CGPoint globalPoint) {
483
+ @try {
484
+ // Get all displays
485
+ uint32_t displayCount;
486
+ CGDirectDisplayID displayIDs[32];
487
+ CGGetActiveDisplayList(32, displayIDs, &displayCount);
488
+
489
+ // Find which display contains this point
490
+ for (uint32_t i = 0; i < displayCount; i++) {
491
+ CGDirectDisplayID displayID = displayIDs[i];
492
+ CGRect displayBounds = CGDisplayBounds(displayID);
493
+
494
+ // Check if point is within this display
495
+ if (CGRectContainsPoint(displayBounds, globalPoint)) {
496
+ // Get display scaling info
497
+ CGSize logicalSize = displayBounds.size;
498
+ CGSize physicalSize = CGSizeMake(CGDisplayPixelsWide(displayID), CGDisplayPixelsHigh(displayID));
499
+
500
+ CGFloat scaleX = physicalSize.width / logicalSize.width;
501
+ CGFloat scaleY = physicalSize.height / logicalSize.height;
502
+ CGFloat scaleFactor = MAX(scaleX, scaleY);
503
+
504
+ return @{
505
+ @"displayID": @(displayID),
506
+ @"logicalSize": [NSValue valueWithSize:NSMakeSize(logicalSize.width, logicalSize.height)],
507
+ @"physicalSize": [NSValue valueWithSize:NSMakeSize(physicalSize.width, physicalSize.height)],
508
+ @"scaleFactor": @(scaleFactor),
509
+ @"displayBounds": [NSValue valueWithRect:NSMakeRect(displayBounds.origin.x, displayBounds.origin.y, displayBounds.size.width, displayBounds.size.height)]
510
+ };
511
+ }
512
+ }
513
+
514
+ // Fallback to main display
515
+ CGDirectDisplayID mainDisplay = CGMainDisplayID();
516
+ CGRect displayBounds = CGDisplayBounds(mainDisplay);
517
+ CGSize logicalSize = displayBounds.size;
518
+ CGSize physicalSize = CGSizeMake(CGDisplayPixelsWide(mainDisplay), CGDisplayPixelsHigh(mainDisplay));
519
+
520
+ return @{
521
+ @"displayID": @(mainDisplay),
522
+ @"logicalSize": [NSValue valueWithSize:NSMakeSize(logicalSize.width, logicalSize.height)],
523
+ @"physicalSize": [NSValue valueWithSize:NSMakeSize(physicalSize.width, physicalSize.height)],
524
+ @"scaleFactor": @(MAX(physicalSize.width / logicalSize.width, physicalSize.height / logicalSize.height)),
525
+ @"displayBounds": [NSValue valueWithRect:NSMakeRect(displayBounds.origin.x, displayBounds.origin.y, displayBounds.size.width, displayBounds.size.height)]
526
+ };
527
+ } @catch (NSException *exception) {
528
+ return nil;
529
+ }
530
+ }
531
+
438
532
  // NAPI Function: Get Current Cursor Position
439
533
  Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
440
534
  Napi::Env env = info.Env();
441
535
 
442
536
  @try {
443
- // NSEvent mouseLocation zaten global koordinatlarda (all displays combined)
444
- // CGEventGetLocation kullanarak direkt global koordinat al - daha doฤŸru
537
+ // Get raw cursor position (may be scaled on Retina displays)
445
538
  CGEventRef event = CGEventCreate(NULL);
446
- CGPoint location = CGEventGetLocation(event);
539
+ CGPoint rawLocation = CGEventGetLocation(event);
447
540
  if (event) {
448
541
  CFRelease(event);
449
542
  }
450
543
 
544
+ // Get display scaling information
545
+ NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
546
+ CGPoint logicalLocation = rawLocation;
547
+
548
+ if (scalingInfo) {
549
+ CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
550
+ NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
551
+
552
+ // CRITICAL FIX: Convert physical coordinates to logical coordinates
553
+ // CGEventGetLocation returns physical coordinates on Retina displays
554
+ if (scaleFactor > 1.1) {
555
+ // Convert to display-relative, then scale down to logical, then back to global logical
556
+ CGFloat displayRelativeX = rawLocation.x - displayBounds.origin.x;
557
+ CGFloat displayRelativeY = rawLocation.y - displayBounds.origin.y;
558
+
559
+ // Scale down to logical coordinates
560
+ CGFloat logicalRelativeX = displayRelativeX / scaleFactor;
561
+ CGFloat logicalRelativeY = displayRelativeY / scaleFactor;
562
+
563
+ // Convert back to global logical coordinates
564
+ logicalLocation.x = displayBounds.origin.x + logicalRelativeX;
565
+ logicalLocation.y = displayBounds.origin.y + logicalRelativeY;
566
+ }
567
+ }
568
+
451
569
  NSString *cursorType = getCursorType();
452
570
 
453
571
  // Mouse button state'ini kontrol et
@@ -479,11 +597,19 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
479
597
  g_rightMouseDown = currentRightMouseDown;
480
598
 
481
599
  Napi::Object result = Napi::Object::New(env);
482
- result.Set("x", Napi::Number::New(env, (int)location.x));
483
- result.Set("y", Napi::Number::New(env, (int)location.y));
600
+ result.Set("x", Napi::Number::New(env, (int)logicalLocation.x));
601
+ result.Set("y", Napi::Number::New(env, (int)logicalLocation.y));
484
602
  result.Set("cursorType", Napi::String::New(env, [cursorType UTF8String]));
485
603
  result.Set("eventType", Napi::String::New(env, [eventType UTF8String]));
486
604
 
605
+ // Add scaling debug info
606
+ if (scalingInfo) {
607
+ CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
608
+ result.Set("scaleFactor", Napi::Number::New(env, scaleFactor));
609
+ result.Set("rawX", Napi::Number::New(env, (int)rawLocation.x));
610
+ result.Set("rawY", Napi::Number::New(env, (int)rawLocation.y));
611
+ }
612
+
487
613
  return result;
488
614
 
489
615
  } @catch (NSException *exception) {