node-mac-recorder 2.18.10 → 2.19.1

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.
@@ -6,7 +6,9 @@
6
6
  "Bash(jq:*)",
7
7
  "Bash(FORCE_AVFOUNDATION=1 timeout 15 node test-cursor-position.js)",
8
8
  "Bash(FORCE_AVFOUNDATION=1 timeout 8 node test-cursor-debug.js)",
9
- "Bash(FORCE_AVFOUNDATION=1 timeout 5 node test-cursor-debug.js)"
9
+ "Bash(FORCE_AVFOUNDATION=1 timeout 5 node test-cursor-debug.js)",
10
+ "Bash(FORCE_AVFOUNDATION=1 timeout 8 node test-unified-cursor.js)",
11
+ "Bash(FORCE_AVFOUNDATION=1 timeout 5 node test-unified-cursor.js)"
10
12
  ],
11
13
  "deny": [],
12
14
  "ask": []
package/CLAUDE.md CHANGED
@@ -131,20 +131,18 @@ The MacRecorder class emits the following events:
131
131
 
132
132
  ### Platform Requirements & Framework Selection
133
133
 
134
- **ELECTRON-FIRST ARCHITECTURE:**
135
- - **Primary Target**: Electron.js applications (full support, no restrictions)
136
- - **Secondary**: Node.js standalone applications
137
- - macOS only (enforced in install.js)
134
+ **SCREENCAPTUREKIT-FIRST ARCHITECTURE:**
135
+ - **Primary Target**: ScreenCaptureKit for consistent behavior across all environments
136
+ - **Support**: Both Electron.js applications and Node.js standalone applications
137
+ - macOS 12.3+ only (ScreenCaptureKit requirement)
138
138
  - Native module compilation required on install
139
- - Requires screen recording and accessibility permissions
139
+ - Requires screen recording permissions
140
140
 
141
- **Framework Selection Logic (Electron Priority):**
142
- - **macOS 15+ + Electron**: ScreenCaptureKit with full capabilities
143
- - **macOS 15+ + Node.js**: ScreenCaptureKit with full capabilities
144
- - **macOS 14 + Electron**: AVFoundation with full capabilities
145
- - **macOS 13 + Electron**: AVFoundation with limited features
146
- - **macOS 14 + Node.js**: AVFoundation with full capabilities
147
- - **macOS 13 + Node.js**: AVFoundation with limited features
141
+ **Framework Selection Logic (ScreenCaptureKit Priority):**
142
+ - **macOS 12.3+ + Electron**: ScreenCaptureKit with full capabilities
143
+ - **macOS 12.3+ + Node.js**: ScreenCaptureKit with full capabilities
144
+ - **macOS < 12.3**: Not supported (requires macOS 12.3+ for ScreenCaptureKit)
145
+ - **AVFoundation**: Available only as fallback with FORCE_AVFOUNDATION environment variable
148
146
 
149
147
  ### File Outputs
150
148
 
@@ -179,14 +177,13 @@ The module tries loading from `build/Release/` first, then falls back to `build/
179
177
 
180
178
  ### **⚡ CRITICAL IMPLEMENTATION NOTES**
181
179
 
182
- **ELECTRON.JS IS THE PRIMARY TARGET:**
183
- 1. **No Electron Restrictions**: All Electron detection logic is for optimization, NOT blocking
184
- 2. **Framework Support**: Both ScreenCaptureKit and AVFoundation work perfectly in Electron
185
- 3. **Environment Detection**: Code detects Electron to provide enhanced logging and optimization
186
- 4. **macOS Compatibility**:
187
- - macOS 15+ (Sequoia+): Use ScreenCaptureKit for best performance
188
- - macOS 14 (Sonoma): Use AVFoundation with full feature set
189
- - macOS 13 (Ventura): Use AVFoundation with basic features
190
- 5. **Error Handling**: Any "Recording failed to start" errors should trigger fallback logic, never blocking
191
-
192
- **NEVER BLOCK ELECTRON ENVIRONMENTS** - This is a core design principle.
180
+ **SCREENCAPTUREKIT-FIRST DESIGN:**
181
+ 1. **Unified Framework**: ScreenCaptureKit is used for all macOS 12.3+ environments (Electron + Node.js)
182
+ 2. **Simplified Architecture**: Single framework reduces complexity and maintenance overhead
183
+ 3. **Environment Agnostic**: Both Electron and Node.js use the same ScreenCaptureKit implementation
184
+ 4. **macOS Compatibility**:
185
+ - macOS 12.3+ (Monterey+): Use ScreenCaptureKit for all features
186
+ - macOS < 12.3: Not supported (upgrade required)
187
+ 5. **Fallback**: AVFoundation available only with FORCE_AVFOUNDATION environment variable
188
+
189
+ **CONSISTENT BEHAVIOR ACROSS ENVIRONMENTS** - Single framework ensures predictable results.
package/binding.gyp CHANGED
@@ -21,7 +21,7 @@
21
21
  "xcode_settings": {
22
22
  "GCC_ENABLE_CPP_EXCEPTIONS": "YES",
23
23
  "CLANG_CXX_LIBRARY": "libc++",
24
- "MACOSX_DEPLOYMENT_TARGET": "13.1",
24
+ "MACOSX_DEPLOYMENT_TARGET": "10.15",
25
25
  "OTHER_CFLAGS": [
26
26
  "-ObjC++"
27
27
  ]
package/index.js CHANGED
@@ -252,12 +252,6 @@ 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: For now, use logical dimensions to avoid cross-display scaling issues
257
- // This prevents using wrong display's physical dimensions
258
- let displayScalingInfo = null;
259
- // TODO: Implement proper per-display scaling detection in native layer
260
-
261
255
  this.recordingDisplayInfo = {
262
256
  displayId: targetDisplayId,
263
257
  x: targetDisplay.x,
@@ -267,10 +261,6 @@ class MacRecorder extends EventEmitter {
267
261
  // Add scaling information for cursor coordinate transformation
268
262
  logicalWidth: parseInt(targetDisplay.resolution.split("x")[0]),
269
263
  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,
274
264
  };
275
265
  }
276
266
 
@@ -308,11 +298,6 @@ class MacRecorder extends EventEmitter {
308
298
  }
309
299
 
310
300
  if (targetDisplay) {
311
- // CRITICAL FIX: For now, use logical dimensions to avoid cross-display scaling issues
312
- // This prevents using wrong display's physical dimensions
313
- const displayScalingInfo = null;
314
- // TODO: Implement proper per-display scaling detection in native layer
315
-
316
301
  this.recordingDisplayInfo = {
317
302
  displayId: targetDisplay.id,
318
303
  x: targetDisplay.x || 0,
@@ -322,10 +307,6 @@ class MacRecorder extends EventEmitter {
322
307
  // Add scaling information for cursor coordinate transformation
323
308
  logicalWidth: parseInt(targetDisplay.resolution.split("x")[0]),
324
309
  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,
329
310
  };
330
311
  }
331
312
  } catch (error) {
@@ -705,8 +686,6 @@ class MacRecorder extends EventEmitter {
705
686
  // Calculate video offset based on recording type
706
687
  let videoOffsetX = 0;
707
688
  let videoOffsetY = 0;
708
- // UNIFIED CURSOR SYSTEM: Always use logical dimensions for cursor coordinates
709
- // This ensures cursor tracking works consistently across all recording engines
710
689
  let videoWidth = options.displayInfo.width || options.displayInfo.logicalWidth;
711
690
  let videoHeight = options.displayInfo.height || options.displayInfo.logicalHeight;
712
691
 
@@ -733,19 +712,10 @@ class MacRecorder extends EventEmitter {
733
712
  displayY: options.displayInfo.y || 0,
734
713
  displayWidth: options.displayInfo.width || options.displayInfo.logicalWidth,
735
714
  displayHeight: options.displayInfo.height || options.displayInfo.logicalHeight,
736
- // CRITICAL: Add physical dimensions for proper cursor scaling
737
- physicalWidth: options.displayInfo.physicalWidth || options.displayInfo.width || options.displayInfo.logicalWidth,
738
- physicalHeight: options.displayInfo.physicalHeight || options.displayInfo.height || options.displayInfo.logicalHeight,
739
- scaleFactor: options.displayInfo.scaleFactor || 1.0,
740
715
  videoOffsetX: videoOffsetX,
741
716
  videoOffsetY: videoOffsetY,
742
717
  videoWidth: videoWidth,
743
718
  videoHeight: videoHeight,
744
- // CRITICAL: Video output size for proper cursor scaling
745
- videoOutputWidth: (options.displayInfo.scaleFactor > 1.0) ?
746
- (options.displayInfo.physicalWidth || videoWidth) : videoWidth,
747
- videoOutputHeight: (options.displayInfo.scaleFactor > 1.0) ?
748
- (options.displayInfo.physicalHeight || videoHeight) : videoHeight,
749
719
  videoRelative: true,
750
720
  recordingType: options.recordingType || 'display',
751
721
  // Store additional context for debugging
@@ -760,22 +730,10 @@ class MacRecorder extends EventEmitter {
760
730
  displayY: this.recordingDisplayInfo.y || 0,
761
731
  displayWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
762
732
  displayHeight: this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
763
- // CRITICAL: Include physical dimensions from recording display info
764
- physicalWidth: this.recordingDisplayInfo.physicalWidth || this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
765
- physicalHeight: this.recordingDisplayInfo.physicalHeight || this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
766
- scaleFactor: this.recordingDisplayInfo.scaleFactor || 1.0,
767
733
  videoOffsetX: 0,
768
734
  videoOffsetY: 0,
769
- // UNIFIED CURSOR SYSTEM: Always use logical dimensions
770
735
  videoWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
771
736
  videoHeight: this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
772
- // CRITICAL: Video output size for proper cursor scaling
773
- videoOutputWidth: (this.recordingDisplayInfo.scaleFactor > 1.0) ?
774
- (this.recordingDisplayInfo.physicalWidth || this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth) :
775
- (this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth),
776
- videoOutputHeight: (this.recordingDisplayInfo.scaleFactor > 1.0) ?
777
- (this.recordingDisplayInfo.physicalHeight || this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight) :
778
- (this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight),
779
737
  videoRelative: true,
780
738
  recordingType: options.recordingType || 'display'
781
739
  };
@@ -823,23 +781,6 @@ class MacRecorder extends EventEmitter {
823
781
 
824
782
  // Apply video-relative transformation for all recording types
825
783
  if (this.cursorDisplayInfo && this.cursorDisplayInfo.videoRelative) {
826
- // FILTER: Only record cursor when it's on the target display
827
- // Use cached display info to avoid async calls
828
- const displayX = this.cursorDisplayInfo.displayX;
829
- const displayY = this.cursorDisplayInfo.displayY;
830
- const displayWidth = this.cursorDisplayInfo.displayWidth;
831
- const displayHeight = this.cursorDisplayInfo.displayHeight;
832
-
833
- const onTargetDisplay = position.x >= displayX &&
834
- position.x < (displayX + displayWidth) &&
835
- position.y >= displayY &&
836
- position.y < (displayY + displayHeight);
837
-
838
- if (!onTargetDisplay) {
839
- // Skip cursor data when cursor is on different display
840
- return;
841
- }
842
-
843
784
  // Step 1: Transform global → display-relative coordinates
844
785
  const displayRelativeX = position.x - this.cursorDisplayInfo.displayX;
845
786
  const displayRelativeY = position.y - this.cursorDisplayInfo.displayY;
@@ -847,31 +788,16 @@ class MacRecorder extends EventEmitter {
847
788
  // Step 2: Transform display-relative → video-relative coordinates
848
789
  x = displayRelativeX - this.cursorDisplayInfo.videoOffsetX;
849
790
  y = displayRelativeY - this.cursorDisplayInfo.videoOffsetY;
791
+ coordinateSystem = "video-relative";
850
792
 
851
- // Step 3: Scale to match actual video output size (for AVFoundation physical scaling)
852
- if (this.cursorDisplayInfo.videoOutputWidth && this.cursorDisplayInfo.videoOutputHeight) {
853
- const scaleX = this.cursorDisplayInfo.videoOutputWidth / this.cursorDisplayInfo.videoWidth;
854
- const scaleY = this.cursorDisplayInfo.videoOutputHeight / this.cursorDisplayInfo.videoHeight;
855
-
856
- if (scaleX !== 1.0 || scaleY !== 1.0) {
857
- x = x * scaleX;
858
- y = y * scaleY;
859
- coordinateSystem = "video-output-scaled";
860
- } else {
861
- coordinateSystem = "video-relative";
862
- }
863
- } else {
864
- coordinateSystem = "video-relative";
865
- }
866
-
867
- // Bounds check for video area using actual video output dimensions
868
- const checkWidth = this.cursorDisplayInfo.videoOutputWidth || this.cursorDisplayInfo.videoWidth;
869
- const checkHeight = this.cursorDisplayInfo.videoOutputHeight || this.cursorDisplayInfo.videoHeight;
870
- const outsideVideo = x < 0 || y < 0 || x >= checkWidth || y >= checkHeight;
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;
871
797
 
872
798
  // For debugging - add metadata if cursor is outside video area
873
799
  if (outsideVideo) {
874
- coordinateSystem = coordinateSystem + "-outside";
800
+ coordinateSystem = "video-relative-outside";
875
801
  }
876
802
  }
877
803
 
@@ -889,19 +815,12 @@ class MacRecorder extends EventEmitter {
889
815
  width: this.cursorDisplayInfo.videoWidth,
890
816
  height: this.cursorDisplayInfo.videoHeight,
891
817
  offsetX: this.cursorDisplayInfo.videoOffsetX,
892
- offsetY: this.cursorDisplayInfo.videoOffsetY,
893
- // CRITICAL: Add physical dimensions for debugging
894
- physicalWidth: this.cursorDisplayInfo.physicalWidth,
895
- physicalHeight: this.cursorDisplayInfo.physicalHeight
818
+ offsetY: this.cursorDisplayInfo.videoOffsetY
896
819
  } : null,
897
820
  displayInfo: this.cursorDisplayInfo ? {
898
821
  displayId: this.cursorDisplayInfo.displayId,
899
822
  width: this.cursorDisplayInfo.displayWidth,
900
- height: this.cursorDisplayInfo.displayHeight,
901
- // CRITICAL: Add scaling info for debugging AVFoundation cursor issues
902
- physicalWidth: this.cursorDisplayInfo.physicalWidth,
903
- physicalHeight: this.cursorDisplayInfo.physicalHeight,
904
- scaleFactor: this.cursorDisplayInfo.scaleFactor
823
+ height: this.cursorDisplayInfo.displayHeight
905
824
  } : null
906
825
  };
907
826
 
@@ -954,13 +873,6 @@ class MacRecorder extends EventEmitter {
954
873
  this.cursorCaptureFile = null;
955
874
  }
956
875
 
957
- // Native cursor tracking'i durdur
958
- try {
959
- nativeBinding.stopCursorTracking();
960
- } catch (nativeError) {
961
- console.warn('Native cursor tracking stop failed:', nativeError.message);
962
- }
963
-
964
876
  // Değişkenleri temizle
965
877
  this.lastCapturedData = null;
966
878
  this.cursorCaptureStartTime = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.18.10",
3
+ "version": "2.19.1",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -6,83 +6,6 @@
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
-
86
9
  static AVAssetWriter *g_avWriter = nil;
87
10
  static AVAssetWriterInput *g_avVideoInput = nil;
88
11
  static AVAssetWriterInputPixelBufferAdaptor *g_avPixelBufferAdaptor = nil;
@@ -130,35 +53,23 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
130
53
  return false;
131
54
  }
132
55
 
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
- }
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);
162
73
 
163
74
  // CRITICAL FIX: Use actual image dimensions to match what CGDisplayCreateImage returns
164
75
  // This prevents the "1/4 recording area" bug on Retina displays
@@ -174,18 +85,18 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
174
85
  recordingSize = actualImageSize;
175
86
  }
176
87
 
177
- NSLog(@"🎯 ADVANCED: Logical %.0fx%.0f → Actual image %.0fx%.0f",
88
+ NSLog(@"🎯 CRITICAL: Logical %.0fx%.0f → Actual image %.0fx%.0f",
178
89
  logicalSize.width, logicalSize.height, actualImageSize.width, actualImageSize.height);
179
-
90
+
180
91
  NSLog(@"🖥️ Display bounds (logical): %.0fx%.0f", logicalSize.width, logicalSize.height);
181
- NSLog(@"🖥️ Display actual physical: %.0fx%.0f", actualImageSize.width, actualImageSize.height);
182
-
92
+ NSLog(@"🖥️ Display pixels (physical): %.0fx%.0f", physicalSize.width, physicalSize.height);
93
+
183
94
  if (scaleFactor > 1.5) {
184
- NSLog(@"🔍 ADVANCED Scale factor: %.2fx → Retina display detected", scaleFactor);
95
+ NSLog(@"🔍 Scale factor: %.1fx → Retina display detected (macOS 14/13 scaling fix applied)", scaleFactor);
185
96
  } else if (scaleFactor > 1.1) {
186
- NSLog(@"🔍 ADVANCED Scale factor: %.2fx → Non-standard scaling detected", scaleFactor);
97
+ NSLog(@"🔍 Scale factor: %.1fx → Non-standard scaling detected", scaleFactor);
187
98
  } else {
188
- NSLog(@"🔍 ADVANCED Scale factor: %.2fx → Standard display", scaleFactor);
99
+ NSLog(@"🔍 Scale factor: %.1fx → Standard display", scaleFactor);
189
100
  }
190
101
 
191
102
  NSLog(@"🎯 Recording size: %.0fx%.0f (using actual physical dimensions for Retina fix)", recordingSize.width, recordingSize.height);
@@ -192,27 +192,25 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
192
192
 
193
193
  // Check macOS version for ScreenCaptureKit compatibility
194
194
  NSOperatingSystemVersion osVersion = [[NSProcessInfo processInfo] operatingSystemVersion];
195
- BOOL isM15Plus = (osVersion.majorVersion >= 15);
196
- BOOL isM14Plus = (osVersion.majorVersion >= 14);
197
- BOOL isM13Plus = (osVersion.majorVersion >= 13);
198
-
199
- NSLog(@"🖥️ macOS Version: %ld.%ld.%ld",
195
+ BOOL isM12_3Plus = (osVersion.majorVersion > 12) ||
196
+ (osVersion.majorVersion == 12 && osVersion.minorVersion >= 3);
197
+
198
+ NSLog(@"🖥️ macOS Version: %ld.%ld.%ld",
200
199
  (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
201
-
202
- // Force AVFoundation for debugging/testing
200
+
201
+ // Force AVFoundation for debugging/testing (still available but discouraged)
203
202
  BOOL forceAVFoundation = (getenv("FORCE_AVFOUNDATION") != NULL);
204
203
  if (forceAVFoundation) {
205
- NSLog(@"🔧 FORCE_AVFOUNDATION environment variable detected - skipping ScreenCaptureKit");
204
+ NSLog(@"🔧 FORCE_AVFOUNDATION environment variable detected - will fallback to AVFoundation");
206
205
  }
207
-
208
- // Electron-first priority: This application is built for Electron.js
209
- // macOS 15+ ScreenCaptureKit (including Electron)
210
- // macOS 14/13 AVFoundation (including Electron)
211
- if (isM15Plus && !forceAVFoundation) {
206
+
207
+ // ScreenCaptureKit-first approach: Use ScreenCaptureKit for macOS 12.3+
208
+ // This simplifies the codebase and provides consistent behavior
209
+ if (isM12_3Plus && !forceAVFoundation) {
212
210
  if (isElectron) {
213
- NSLog(@"⚡ ELECTRON PRIORITY: macOS 15+ Electron → ScreenCaptureKit with full support");
211
+ NSLog(@"⚡ ELECTRON: macOS 12.3+ Electron → ScreenCaptureKit with full support");
214
212
  } else {
215
- NSLog(@"✅ macOS 15+ Node.js → ScreenCaptureKit available with full compatibility");
213
+ NSLog(@"✅ macOS 12.3+ Node.js → ScreenCaptureKit available with full compatibility");
216
214
  }
217
215
 
218
216
  // Try ScreenCaptureKit with extensive safety measures
@@ -287,65 +285,53 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
287
285
  // If we reach here, ScreenCaptureKit failed, so fall through to AVFoundation
288
286
  NSLog(@"⏭️ ScreenCaptureKit failed - falling back to AVFoundation");
289
287
  } else {
290
- // macOS 14/13 or forced AVFoundation → ALWAYS use AVFoundation (Electron supported!)
291
- if (isElectron) {
292
- if (isM14Plus) {
293
- NSLog(@" ELECTRON PRIORITY: macOS 14/13 Electron → AVFoundation with full support");
294
- } else if (isM13Plus) {
295
- NSLog(@"⚡ ELECTRON PRIORITY: macOS 13 Electron → AVFoundation with limited features");
296
- }
297
- } else {
298
- if (isM15Plus) {
299
- NSLog(@"🎯 macOS 15+ Node.js with FORCE_AVFOUNDATION → using AVFoundation");
300
- } else if (isM14Plus) {
301
- NSLog(@"🎯 macOS 14 Node.js → using AVFoundation (primary method)");
302
- } else if (isM13Plus) {
303
- NSLog(@"🎯 macOS 13 Node.js → using AVFoundation (limited features)");
304
- }
305
- }
306
-
307
- if (!isM13Plus) {
308
- NSLog(@"❌ macOS version too old (< 13.0) - Not supported");
288
+ // macOS < 12.3 or forced AVFoundation → Use AVFoundation as fallback
289
+ if (!isM12_3Plus) {
290
+ NSLog(@"❌ macOS version too old (< 12.3) - ScreenCaptureKit not supported");
291
+ NSLog(@" This version requires macOS 12.3+ for ScreenCaptureKit");
309
292
  return Napi::Boolean::New(env, false);
310
293
  }
311
-
312
- // DIRECT AVFoundation for all environments (Node.js + Electron)
313
- NSLog(@"⏭️ Using AVFoundation directly - supports both Node.js and Electron");
314
- }
315
-
316
- // AVFoundation recording (either fallback from ScreenCaptureKit or direct)
317
- NSLog(@"🎥 Starting AVFoundation recording...");
318
-
319
- @try {
320
- // Import AVFoundation recording functions (if available)
321
- extern bool startAVFoundationRecording(const std::string& outputPath,
322
- CGDirectDisplayID displayID,
323
- uint32_t windowID,
324
- CGRect captureRect,
325
- bool captureCursor,
326
- bool includeMicrophone,
327
- bool includeSystemAudio,
328
- NSString* audioDeviceId);
329
-
330
- bool avResult = startAVFoundationRecording(outputPath, displayID, windowID, captureRect,
331
- captureCursor, includeMicrophone, includeSystemAudio, audioDeviceId);
332
-
333
- if (avResult) {
334
- NSLog(@"🎥 RECORDING METHOD: AVFoundation");
335
- NSLog(@"✅ AVFoundation recording started successfully");
336
- g_isRecording = true;
337
- return Napi::Boolean::New(env, true);
338
- } else {
339
- NSLog(@"❌ AVFoundation recording failed to start");
340
- NSLog(@"❌ Check permissions and output path validity");
341
- }
342
- } @catch (NSException *avException) {
343
- NSLog(@"❌ Exception during AVFoundation startup: %@", avException.reason);
344
- NSLog(@"❌ Stack trace: %@", [avException callStackSymbols]);
294
+
295
+ // Only reach here if FORCE_AVFOUNDATION is set
296
+ NSLog(@"⏭️ Using AVFoundation fallback (FORCE_AVFOUNDATION enabled)");
345
297
  }
346
298
 
347
- // Both ScreenCaptureKit and AVFoundation failed
348
- NSLog(@"❌ All recording methods failed - no recording available");
299
+ // AVFoundation recording (fallback only when FORCE_AVFOUNDATION is set)
300
+ if (forceAVFoundation) {
301
+ NSLog(@"🎥 Starting AVFoundation recording (forced fallback)...");
302
+
303
+ @try {
304
+ // Import AVFoundation recording functions (if available)
305
+ extern bool startAVFoundationRecording(const std::string& outputPath,
306
+ CGDirectDisplayID displayID,
307
+ uint32_t windowID,
308
+ CGRect captureRect,
309
+ bool captureCursor,
310
+ bool includeMicrophone,
311
+ bool includeSystemAudio,
312
+ NSString* audioDeviceId);
313
+
314
+ bool avResult = startAVFoundationRecording(outputPath, displayID, windowID, captureRect,
315
+ captureCursor, includeMicrophone, includeSystemAudio, audioDeviceId);
316
+
317
+ if (avResult) {
318
+ NSLog(@"🎥 RECORDING METHOD: AVFoundation (fallback)");
319
+ NSLog(@"✅ AVFoundation recording started successfully");
320
+ g_isRecording = true;
321
+ return Napi::Boolean::New(env, true);
322
+ } else {
323
+ NSLog(@"❌ AVFoundation recording failed to start");
324
+ NSLog(@"❌ Check permissions and output path validity");
325
+ }
326
+ } @catch (NSException *avException) {
327
+ NSLog(@"❌ Exception during AVFoundation startup: %@", avException.reason);
328
+ NSLog(@"❌ Stack trace: %@", [avException callStackSymbols]);
329
+ }
330
+ }
331
+
332
+ // ScreenCaptureKit failed and no fallback available
333
+ NSLog(@"❌ ScreenCaptureKit recording failed - no recording available");
334
+ NSLog(@"💡 Ensure macOS 12.3+ and screen recording permissions are granted");
349
335
  return Napi::Boolean::New(env, false);
350
336
 
351
337
  } @catch (NSException *exception) {
@@ -890,27 +876,32 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
890
876
 
891
877
  // Check for force AVFoundation flag
892
878
  BOOL forceAVFoundation = (getenv("FORCE_AVFOUNDATION") != NULL);
893
-
879
+
880
+ // Check macOS version for ScreenCaptureKit compatibility
881
+ BOOL isM12_3Plus = (osVersion.majorVersion > 12) ||
882
+ (osVersion.majorVersion == 12 && osVersion.minorVersion >= 3);
883
+
894
884
  // Electron detection
895
- BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
885
+ BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
896
886
  [NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
897
- (NSProcessInfo.processInfo.processName &&
887
+ (NSProcessInfo.processInfo.processName &&
898
888
  [NSProcessInfo.processInfo.processName containsString:@"Electron"]) ||
899
889
  (NSProcessInfo.processInfo.environment[@"ELECTRON_RUN_AS_NODE"] != nil);
900
-
901
- NSLog(@"🔒 Permission check for macOS %ld.%ld.%ld",
890
+
891
+ NSLog(@"🔒 Permission check for macOS %ld.%ld.%ld",
902
892
  (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
903
-
904
- // Determine which framework will be used (Electron fully supported!)
905
- BOOL willUseScreenCaptureKit = (isM15Plus && !forceAVFoundation); // Electron can use ScreenCaptureKit on macOS 15+
906
- BOOL willUseAVFoundation = (!willUseScreenCaptureKit && (isM13Plus || isM14Plus));
893
+
894
+ // Determine which framework will be used (ScreenCaptureKit-first approach)
895
+ BOOL willUseScreenCaptureKit = (isM12_3Plus && !forceAVFoundation); // ScreenCaptureKit for macOS 12.3+
896
+ BOOL willUseAVFoundation = (!willUseScreenCaptureKit && forceAVFoundation);
907
897
 
908
898
  if (willUseScreenCaptureKit) {
909
899
  NSLog(@"🎯 Will use ScreenCaptureKit - checking ScreenCaptureKit permissions");
910
900
  } else if (willUseAVFoundation) {
911
- NSLog(@"🎯 Will use AVFoundation - checking AVFoundation permissions");
901
+ NSLog(@"🎯 Will use AVFoundation fallback - checking AVFoundation permissions");
912
902
  } else {
913
- NSLog(@"❌ No compatible recording framework available");
903
+ NSLog(@"❌ macOS version < 12.3 - ScreenCaptureKit not supported");
904
+ NSLog(@"💡 This version requires macOS 12.3+ for ScreenCaptureKit");
914
905
  return Napi::Boolean::New(env, false);
915
906
  }
916
907
 
@@ -2,6 +2,7 @@
2
2
  #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
3
  // NO AVFoundation - Pure ScreenCaptureKit implementation
4
4
 
5
+ API_AVAILABLE(macos(15.0))
5
6
  @interface ScreenCaptureKitRecorder : NSObject
6
7
 
7
8
  + (BOOL)isScreenCaptureKitAvailable;
@@ -297,11 +297,9 @@ static NSString *g_outputPath = nil;
297
297
  }
298
298
  }
299
299
 
300
- if (@available(macOS 15.0, *)) {
301
- if (!g_recordingOutput) {
302
- NSLog(@"❌ Failed to create SCRecordingOutput");
303
- return;
304
- }
300
+ if (!g_recordingOutput) {
301
+ NSLog(@"❌ Failed to create SCRecordingOutput");
302
+ return;
305
303
  }
306
304
 
307
305
  NSLog(@"✅ Pure ScreenCaptureKit recording output created");
@@ -391,11 +389,9 @@ static NSString *g_outputPath = nil;
391
389
  g_isCleaningUp = YES;
392
390
  g_isRecording = NO;
393
391
 
394
- if (@available(macOS 15.0, *)) {
395
- if (g_recordingOutput) {
396
- // SCRecordingOutput finalizes automatically
397
- NSLog(@"✅ Pure recording output finalized");
398
- }
392
+ if (g_recordingOutput) {
393
+ // SCRecordingOutput finalizes automatically
394
+ NSLog(@"✅ Pure recording output finalized");
399
395
  }
400
396
 
401
397
  [ScreenCaptureKitRecorder cleanupVideoWriter];
@@ -417,11 +413,9 @@ static NSString *g_outputPath = nil;
417
413
  NSLog(@"✅ Stream reference cleared");
418
414
  }
419
415
 
420
- if (@available(macOS 15.0, *)) {
421
- if (g_recordingOutput) {
422
- g_recordingOutput = nil;
423
- NSLog(@"✅ Recording output reference cleared");
424
- }
416
+ if (g_recordingOutput) {
417
+ g_recordingOutput = nil;
418
+ NSLog(@"✅ Recording output reference cleared");
425
419
  }
426
420
 
427
421
  if (g_streamDelegate) {
@@ -0,0 +1 @@
1
+ [{"x":-2062,"y":527,"timestamp":217,"unixTimeMs":1758625424081,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2028,"y":513,"timestamp":236,"unixTimeMs":1758625424100,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2025,"y":512,"timestamp":318,"unixTimeMs":1758625424182,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2020,"y":510,"timestamp":336,"unixTimeMs":1758625424200,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2012,"y":508,"timestamp":356,"unixTimeMs":1758625424220,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2009,"y":507,"timestamp":377,"unixTimeMs":1758625424241,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2004,"y":505,"timestamp":398,"unixTimeMs":1758625424262,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2001,"y":504,"timestamp":421,"unixTimeMs":1758625424285,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-1999,"y":504,"timestamp":464,"unixTimeMs":1758625424328,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2010,"y":506,"timestamp":869,"unixTimeMs":1758625424733,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2022,"y":508,"timestamp":894,"unixTimeMs":1758625424758,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2048,"y":508,"timestamp":912,"unixTimeMs":1758625424776,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2103,"y":495,"timestamp":934,"unixTimeMs":1758625424798,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2158,"y":478,"timestamp":954,"unixTimeMs":1758625424818,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2254,"y":432,"timestamp":971,"unixTimeMs":1758625424835,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2293,"y":397,"timestamp":997,"unixTimeMs":1758625424861,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2359,"y":329,"timestamp":1015,"unixTimeMs":1758625424879,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2445,"y":239,"timestamp":1036,"unixTimeMs":1758625424900,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2468,"y":214,"timestamp":1054,"unixTimeMs":1758625424918,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2478,"y":198,"timestamp":1076,"unixTimeMs":1758625424940,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2494,"y":173,"timestamp":1097,"unixTimeMs":1758625424961,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2510,"y":155,"timestamp":1120,"unixTimeMs":1758625424984,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2529,"y":139,"timestamp":1139,"unixTimeMs":1758625425003,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2539,"y":128,"timestamp":1158,"unixTimeMs":1758625425022,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2541,"y":121,"timestamp":1177,"unixTimeMs":1758625425041,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2538,"y":110,"timestamp":1202,"unixTimeMs":1758625425066,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2535,"y":101,"timestamp":1228,"unixTimeMs":1758625425092,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2533,"y":98,"timestamp":1248,"unixTimeMs":1758625425112,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2517,"y":97,"timestamp":1308,"unixTimeMs":1758625425172,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2498,"y":97,"timestamp":1335,"unixTimeMs":1758625425199,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2491,"y":98,"timestamp":1358,"unixTimeMs":1758625425222,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2488,"y":99,"timestamp":1384,"unixTimeMs":1758625425248,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2487,"y":99,"timestamp":1530,"unixTimeMs":1758625425394,"cursorType":"default","type":"mousedown","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2487,"y":99,"timestamp":1547,"unixTimeMs":1758625425411,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2487,"y":99,"timestamp":1588,"unixTimeMs":1758625425452,"cursorType":"default","type":"mouseup","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2487,"y":99,"timestamp":1607,"unixTimeMs":1758625425471,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2487,"y":99,"timestamp":1670,"unixTimeMs":1758625425534,"cursorType":"default","type":"mousedown","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2487,"y":99,"timestamp":1695,"unixTimeMs":1758625425559,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2487,"y":99,"timestamp":1734,"unixTimeMs":1758625425598,"cursorType":"default","type":"mouseup","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2487,"y":99,"timestamp":1754,"unixTimeMs":1758625425618,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2483,"y":101,"timestamp":2675,"unixTimeMs":1758625426539,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2475,"y":105,"timestamp":2694,"unixTimeMs":1758625426558,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2470,"y":108,"timestamp":2715,"unixTimeMs":1758625426579,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2461,"y":113,"timestamp":2737,"unixTimeMs":1758625426601,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2430,"y":127,"timestamp":2765,"unixTimeMs":1758625426629,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2384,"y":148,"timestamp":2808,"unixTimeMs":1758625426672,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2233,"y":201,"timestamp":2839,"unixTimeMs":1758625426703,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-2074,"y":243,"timestamp":2858,"unixTimeMs":1758625426722,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-1985,"y":265,"timestamp":2878,"unixTimeMs":1758625426742,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-1942,"y":282,"timestamp":2899,"unixTimeMs":1758625426763,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-1920,"y":296,"timestamp":2918,"unixTimeMs":1758625426782,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-1916,"y":301,"timestamp":2937,"unixTimeMs":1758625426801,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}},{"x":-1913,"y":303,"timestamp":2958,"unixTimeMs":1758625426822,"cursorType":"default","type":"move","coordinateSystem":"video-relative-outside","recordingType":"display","videoInfo":{"width":2048,"height":1330,"offsetX":0,"offsetY":0},"displayInfo":{"displayId":1,"width":2048,"height":1330}}]