node-mac-recorder 2.10.15 → 2.10.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.10.15",
3
+ "version": "2.10.17",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -56,6 +56,8 @@ void updateScreenOverlays();
56
56
  @property (nonatomic, strong) NSDictionary *windowInfo;
57
57
  @property (nonatomic) BOOL isActiveWindow;
58
58
  @property (nonatomic) BOOL isToggled;
59
+ @property (nonatomic) NSRect highlightFrame;
60
+ - (void)setHighlightFrame:(NSRect)frame;
59
61
  @end
60
62
 
61
63
  @implementation WindowSelectorOverlayView
@@ -63,41 +65,62 @@ void updateScreenOverlays();
63
65
  - (instancetype)initWithFrame:(NSRect)frameRect {
64
66
  self = [super initWithFrame:frameRect];
65
67
  if (self) {
66
- // Use layer for background instead of custom drawing
68
+ // Full-screen transparent overlay for window highlighting
67
69
  self.wantsLayer = YES;
68
- self.isActiveWindow = YES; // Default to active for current window under mouse
70
+ self.isActiveWindow = YES;
71
+ self.highlightFrame = NSZeroRect;
69
72
 
70
- // Set purple background with border using layer
71
- [self updateAppearance];
73
+ // Transparent background for full-screen overlay
74
+ self.layer.backgroundColor = [[NSColor clearColor] CGColor];
72
75
 
73
76
  // Window selector overlay view created
74
77
  }
75
78
  return self;
76
79
  }
77
80
 
78
- - (void)updateAppearance {
81
+ - (void)setHighlightFrame:(NSRect)frame {
82
+ _highlightFrame = frame;
83
+ [self setNeedsDisplay:YES]; // Trigger redraw
84
+ }
85
+
86
+ - (void)drawRect:(NSRect)dirtyRect {
87
+ [super drawRect:dirtyRect];
88
+
89
+ if (NSEqualRects(self.highlightFrame, NSZeroRect)) {
90
+ return; // No window to highlight
91
+ }
92
+
93
+ // Draw highlight rectangle for the selected window
94
+ NSBezierPath *highlightPath = [NSBezierPath bezierPathWithRoundedRect:self.highlightFrame
95
+ xRadius:8.0
96
+ yRadius:8.0];
97
+
98
+ // Fill color based on toggle state
99
+ NSColor *fillColor;
100
+ NSColor *strokeColor;
101
+ CGFloat lineWidth;
102
+
79
103
  if (self.isToggled) {
80
- // Toggled window: same background, thick border with darker version of fill color
81
- self.layer.backgroundColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4] CGColor];
82
- self.layer.borderColor = [[NSColor colorWithRed:0.45 green:0.25 blue:0.75 alpha:0.9] CGColor]; // Darker purple
83
- self.layer.borderWidth = 3.0;
84
- } else if (self.isActiveWindow) {
85
- // Active window: brighter background, thin white border
86
- self.layer.backgroundColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4] CGColor];
87
- self.layer.borderColor = [[NSColor whiteColor] CGColor];
88
- self.layer.borderWidth = 1.0;
104
+ fillColor = [NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4];
105
+ strokeColor = [NSColor colorWithRed:0.45 green:0.25 blue:0.75 alpha:0.9];
106
+ lineWidth = 3.0;
89
107
  } else {
90
- // Inactive window: dimmer background, thin white border
91
- self.layer.backgroundColor = [[NSColor colorWithRed:0.4 green:0.2 blue:0.6 alpha:0.25] CGColor];
92
- self.layer.borderColor = [[NSColor whiteColor] CGColor];
93
- self.layer.borderWidth = 1.0;
108
+ fillColor = [NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4];
109
+ strokeColor = [NSColor whiteColor];
110
+ lineWidth = 1.0;
94
111
  }
95
112
 
96
- self.layer.cornerRadius = 8.0;
97
- self.layer.masksToBounds = YES;
98
- self.layer.shadowOpacity = 0.0;
99
- self.layer.shadowRadius = 0.0;
100
- self.layer.shadowOffset = NSMakeSize(0, 0);
113
+ [fillColor setFill];
114
+ [highlightPath fill];
115
+
116
+ [strokeColor setStroke];
117
+ [highlightPath setLineWidth:lineWidth];
118
+ [highlightPath stroke];
119
+ }
120
+
121
+ - (void)updateAppearance {
122
+ // Appearance is now handled in drawRect
123
+ [self setNeedsDisplay:YES];
101
124
  }
102
125
 
103
126
  - (void)setIsActiveWindow:(BOOL)isActiveWindow {
@@ -500,7 +523,7 @@ void updateOverlay() {
500
523
  CGFloat screenHeight = [mainScreen frame].size.height;
501
524
  CGPoint globalPoint = CGPointMake(mouseLocation.x, screenHeight - mouseLocation.y);
502
525
 
503
- // Find window under cursor
526
+ // Find window under cursor (no need to refresh g_allWindows frequently since windows can't move)
504
527
  NSDictionary *windowUnderCursor = getWindowUnderCursor(globalPoint);
505
528
 
506
529
  // Check if we need to update overlay (new window or position change of current window)
@@ -653,11 +676,10 @@ void updateOverlay() {
653
676
  }
654
677
  }
655
678
 
656
- // Ensure overlay is on the correct screen
657
- [g_overlayWindow setFrame:overlayFrame display:YES];
658
-
659
- // Update overlay view window info
679
+ // No need to resize window since it's full-screen, just update the highlight area
680
+ // Update overlay view window info for highlighting
660
681
  [(WindowSelectorOverlayView *)g_overlayView setWindowInfo:targetWindow];
682
+ [(WindowSelectorOverlayView *)g_overlayView setHighlightFrame:NSMakeRect(x, [g_overlayView frame].size.height - y - height, width, height)];
661
683
 
662
684
  // Only reset toggle state when switching to different window (not for position updates)
663
685
  if (!g_hasToggledWindow) {
@@ -755,18 +777,20 @@ void updateOverlay() {
755
777
  NSString *labelAppName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown App";
756
778
  [infoLabel setStringValue:[NSString stringWithFormat:@"%@\n%@", labelAppName, labelWindowTitle]];
757
779
 
758
- // Position buttons - Start Record in center, Cancel below it
780
+ // Position buttons - Start Record in center of highlighted window
759
781
  if (g_selectButton) {
760
782
  NSSize buttonSize = [g_selectButton frame].size;
783
+ // Convert window coordinates to overlay view coordinates
784
+ NSRect highlightFrame = NSMakeRect(x, [g_overlayView frame].size.height - y - height, width, height);
761
785
  NSPoint buttonCenter = NSMakePoint(
762
- (width - buttonSize.width) / 2,
763
- (height - buttonSize.height) / 2 + 15 // Slightly above center
786
+ highlightFrame.origin.x + (highlightFrame.size.width - buttonSize.width) / 2,
787
+ highlightFrame.origin.y + (highlightFrame.size.height - buttonSize.height) / 2 + 15
764
788
  );
765
789
  [g_selectButton setFrameOrigin:buttonCenter];
766
790
 
767
- // Position app icon above text label
791
+ // Position app icon above text label within highlighted area
768
792
  NSPoint iconCenter = NSMakePoint(
769
- (width - 96) / 2, // Center horizontally (icon is 96px wide)
793
+ highlightFrame.origin.x + (highlightFrame.size.width - 96) / 2, // Center horizontally within highlight
770
794
  buttonCenter.y + buttonSize.height + 60 + 10 // Above label + text height + margin
771
795
  );
772
796
  [appIconView setFrameOrigin:iconCenter];
@@ -787,9 +811,9 @@ void updateOverlay() {
787
811
  floatAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
788
812
  [appIconView.layer addAnimation:floatAnimationX forKey:@"floatAnimationX"];
789
813
 
790
- // Position info label at overlay center, above button
814
+ // Position info label within highlighted area, above button
791
815
  NSPoint labelCenter = NSMakePoint(
792
- (width - [infoLabel frame].size.width) / 2, // Center horizontally
816
+ highlightFrame.origin.x + (highlightFrame.size.width - [infoLabel frame].size.width) / 2, // Center horizontally within highlight
793
817
  buttonCenter.y + buttonSize.height + 10 // 10px above button, below icon
794
818
  );
795
819
  [infoLabel setFrameOrigin:labelCenter];
@@ -807,7 +831,7 @@ void updateOverlay() {
807
831
  if (cancelButton) {
808
832
  NSSize cancelButtonSize = [cancelButton frame].size;
809
833
  NSPoint cancelButtonCenter = NSMakePoint(
810
- (width - cancelButtonSize.width) / 2,
834
+ highlightFrame.origin.x + (highlightFrame.size.width - cancelButtonSize.width) / 2,
811
835
  buttonCenter.y - buttonSize.height - 20 // 20px below main button
812
836
  );
813
837
  [cancelButton setFrameOrigin:cancelButtonCenter];
@@ -1570,9 +1594,10 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
1570
1594
  return env.Null();
1571
1595
  }
1572
1596
 
1573
- // Create overlay window (initially hidden)
1574
- NSRect initialFrame = NSMakeRect(0, 0, 100, 100);
1575
- g_overlayWindow = [[NSWindow alloc] initWithContentRect:initialFrame
1597
+ // Create full-screen overlay window to prevent window dragging
1598
+ NSScreen *mainScreen = [NSScreen mainScreen];
1599
+ NSRect fullScreenFrame = [mainScreen frame];
1600
+ g_overlayWindow = [[NSWindow alloc] initWithContentRect:fullScreenFrame
1576
1601
  styleMask:NSWindowStyleMaskBorderless
1577
1602
  backing:NSBackingStoreBuffered
1578
1603
  defer:NO];
@@ -1583,11 +1608,11 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
1583
1608
  [g_overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)]; // Absolute highest level
1584
1609
  [g_overlayWindow setOpaque:NO];
1585
1610
  [g_overlayWindow setBackgroundColor:[NSColor clearColor]];
1586
- [g_overlayWindow setIgnoresMouseEvents:NO];
1611
+ [g_overlayWindow setIgnoresMouseEvents:NO]; // Capture mouse events to prevent window dragging
1587
1612
  [g_overlayWindow setAcceptsMouseMovedEvents:YES];
1588
1613
  [g_overlayWindow setHasShadow:NO];
1589
1614
  [g_overlayWindow setAlphaValue:1.0];
1590
- [g_overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
1615
+ [g_overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorFullScreenAuxiliary];
1591
1616
 
1592
1617
  // Remove any default window decorations and borders
1593
1618
  [g_overlayWindow setTitlebarAppearsTransparent:YES];
@@ -1600,8 +1625,8 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
1600
1625
  [g_overlayWindow setOpaque:NO];
1601
1626
  [g_overlayWindow setBackgroundColor:[NSColor clearColor]];
1602
1627
 
1603
- // Create overlay view
1604
- g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:initialFrame];
1628
+ // Create overlay view covering full screen
1629
+ g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:fullScreenFrame];
1605
1630
  [g_overlayWindow setContentView:g_overlayView];
1606
1631
 
1607
1632
  // Note: NSWindow doesn't have setWantsLayer method, only NSView does
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test script to analyze overlay tracking issues during window movement
5
+ */
6
+
7
+ const WindowSelector = require('./window-selector');
8
+
9
+ async function testOverlayTracking() {
10
+ console.log('šŸ”¬ OVERLAY TRACKING ANALYSIS');
11
+ console.log('=============================\n');
12
+
13
+ const selector = new WindowSelector();
14
+
15
+ try {
16
+ // Track mouse vs overlay position data
17
+ let mousePositions = [];
18
+ let overlayUpdates = [];
19
+ let windowMovements = [];
20
+
21
+ selector.on('windowEntered', (window) => {
22
+ const timestamp = Date.now();
23
+ overlayUpdates.push({
24
+ timestamp,
25
+ type: 'entered',
26
+ window: {
27
+ id: window.id,
28
+ title: window.title,
29
+ appName: window.appName,
30
+ x: window.x,
31
+ y: window.y,
32
+ width: window.width,
33
+ height: window.height
34
+ }
35
+ });
36
+
37
+ console.log(`šŸ  [${new Date(timestamp).toISOString().substr(11,12)}] ENTERED: "${window.title}" at (${window.x}, ${window.y}) ${window.width}Ɨ${window.height}`);
38
+ });
39
+
40
+ selector.on('windowLeft', (window) => {
41
+ const timestamp = Date.now();
42
+ overlayUpdates.push({
43
+ timestamp,
44
+ type: 'left',
45
+ window: {
46
+ id: window.id,
47
+ title: window.title,
48
+ x: window.x,
49
+ y: window.y
50
+ }
51
+ });
52
+
53
+ console.log(`🚪 [${new Date(timestamp).toISOString().substr(11,12)}] LEFT: "${window.title}" from (${window.x}, ${window.y})`);
54
+ });
55
+
56
+ console.log('šŸŽÆ Starting window selection...');
57
+ console.log('šŸ“ Move your cursor over windows and drag them around');
58
+ console.log('ā° Test will run for 30 seconds\n');
59
+
60
+ await selector.startSelection();
61
+
62
+ // Periodically log mouse position and compare with overlay state
63
+ const trackingInterval = setInterval(async () => {
64
+ try {
65
+ const status = selector.getStatus();
66
+ const timestamp = Date.now();
67
+
68
+ if (status.nativeStatus?.currentWindow) {
69
+ const window = status.nativeStatus.currentWindow;
70
+ mousePositions.push({
71
+ timestamp,
72
+ windowId: window.id,
73
+ windowPos: { x: window.x, y: window.y },
74
+ windowSize: { width: window.width, height: window.height }
75
+ });
76
+
77
+ // Check for window movement by comparing with previous position
78
+ const prevPos = mousePositions.find(p =>
79
+ p.windowId === window.id &&
80
+ p.timestamp < timestamp - 100 && // at least 100ms ago
81
+ (p.windowPos.x !== window.x || p.windowPos.y !== window.y)
82
+ );
83
+
84
+ if (prevPos) {
85
+ windowMovements.push({
86
+ timestamp,
87
+ windowId: window.id,
88
+ title: window.title,
89
+ from: prevPos.windowPos,
90
+ to: { x: window.x, y: window.y },
91
+ deltaX: window.x - prevPos.windowPos.x,
92
+ deltaY: window.y - prevPos.windowPos.y
93
+ });
94
+
95
+ console.log(`šŸ“ [${new Date(timestamp).toISOString().substr(11,12)}] MOVED: "${window.title}" (${prevPos.windowPos.x}, ${prevPos.windowPos.y}) → (${window.x}, ${window.y}) Ī”(${window.x - prevPos.windowPos.x}, ${window.y - prevPos.windowPos.y})`);
96
+ }
97
+ }
98
+ } catch (err) {
99
+ // Ignore errors during status check
100
+ }
101
+ }, 50); // Check every 50ms for high resolution tracking
102
+
103
+ // Run test for 30 seconds
104
+ await new Promise(resolve => setTimeout(resolve, 30000));
105
+
106
+ clearInterval(trackingInterval);
107
+ console.log('\nā¹ļø Test completed. Analyzing data...\n');
108
+
109
+ // Analysis
110
+ console.log('šŸ“Š ANALYSIS RESULTS:');
111
+ console.log('====================\n');
112
+
113
+ console.log(`šŸ“ Total overlay updates: ${overlayUpdates.length}`);
114
+ console.log(`šŸ“ Total mouse position samples: ${mousePositions.length}`);
115
+ console.log(`šŸ“ Detected window movements: ${windowMovements.length}\n`);
116
+
117
+ // Analyze window movement patterns
118
+ if (windowMovements.length > 0) {
119
+ console.log('šŸ” WINDOW MOVEMENT PATTERNS:');
120
+
121
+ const movementsByWindow = {};
122
+ windowMovements.forEach(move => {
123
+ if (!movementsByWindow[move.windowId]) {
124
+ movementsByWindow[move.windowId] = [];
125
+ }
126
+ movementsByWindow[move.windowId].push(move);
127
+ });
128
+
129
+ for (const [windowId, moves] of Object.entries(movementsByWindow)) {
130
+ const firstMove = moves[0];
131
+ const lastMove = moves[moves.length - 1];
132
+ const totalDeltaX = Math.abs(lastMove.to.x - firstMove.from.x);
133
+ const totalDeltaY = Math.abs(lastMove.to.y - firstMove.from.y);
134
+ const duration = lastMove.timestamp - firstMove.timestamp;
135
+
136
+ console.log(` Window "${firstMove.title}" (ID: ${windowId}):`);
137
+ console.log(` Movements: ${moves.length}`);
138
+ console.log(` Total displacement: (${totalDeltaX}, ${totalDeltaY}) pixels`);
139
+ console.log(` Duration: ${duration}ms`);
140
+ console.log(` Average speed: ${(Math.sqrt(totalDeltaX*totalDeltaX + totalDeltaY*totalDeltaY) / (duration/1000)).toFixed(1)} px/sec\n`);
141
+ }
142
+ }
143
+
144
+ // Analyze update frequency
145
+ if (overlayUpdates.length > 1) {
146
+ console.log('ā±ļø OVERLAY UPDATE TIMING:');
147
+ const intervals = [];
148
+ for (let i = 1; i < overlayUpdates.length; i++) {
149
+ intervals.push(overlayUpdates[i].timestamp - overlayUpdates[i-1].timestamp);
150
+ }
151
+
152
+ if (intervals.length > 0) {
153
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
154
+ const minInterval = Math.min(...intervals);
155
+ const maxInterval = Math.max(...intervals);
156
+
157
+ console.log(` Average update interval: ${avgInterval.toFixed(1)}ms (${(1000/avgInterval).toFixed(1)} FPS)`);
158
+ console.log(` Min interval: ${minInterval}ms`);
159
+ console.log(` Max interval: ${maxInterval}ms\n`);
160
+ }
161
+ }
162
+
163
+ } catch (error) {
164
+ console.error('āŒ Test failed:', error.message);
165
+ } finally {
166
+ console.log('šŸ›‘ Cleaning up...');
167
+ await selector.cleanup();
168
+ }
169
+ }
170
+
171
+ if (require.main === module) {
172
+ testOverlayTracking().catch(console.error);
173
+ }
174
+
175
+ module.exports = testOverlayTracking;