node-mac-recorder 1.14.0 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +26 -2
- package/index.js +63 -14
- package/package.json +1 -1
- package/src/window_selector.mm +126 -5
package/CLAUDE.md
CHANGED
|
@@ -73,10 +73,22 @@ The package handles complex multi-display coordinate transformations:
|
|
|
73
73
|
- `checkPermissions()` - Verify macOS recording permissions
|
|
74
74
|
|
|
75
75
|
### Cursor Tracking
|
|
76
|
-
- `startCursorCapture(filepath)` - Begin real-time cursor tracking to JSON
|
|
76
|
+
- `startCursorCapture(filepath, options)` - Begin real-time cursor tracking to JSON
|
|
77
|
+
- `options.windowInfo` - Window information for window-relative coordinates
|
|
78
|
+
- `options.windowRelative` - Set to true for window-relative coordinates
|
|
77
79
|
- `stopCursorCapture()` - Stop tracking and close output file
|
|
78
80
|
- `getCursorPosition()` - Get current cursor position and state
|
|
79
81
|
|
|
82
|
+
### Events
|
|
83
|
+
The MacRecorder class emits the following events:
|
|
84
|
+
- `recordingStarted` - Emitted immediately when recording starts with recording details
|
|
85
|
+
- `started` - Emitted when recording is confirmed started (legacy event)
|
|
86
|
+
- `stopped` - Emitted when recording stops
|
|
87
|
+
- `completed` - Emitted when recording file is finalized
|
|
88
|
+
- `timeUpdate` - Emitted every second with elapsed time
|
|
89
|
+
- `cursorCaptureStarted` - Emitted when cursor capture begins
|
|
90
|
+
- `cursorCaptureStopped` - Emitted when cursor capture ends
|
|
91
|
+
|
|
80
92
|
### Thumbnails
|
|
81
93
|
- `getWindowThumbnail(windowId, options)` - Capture window preview image
|
|
82
94
|
- `getDisplayThumbnail(displayId, options)` - Capture display preview image
|
|
@@ -90,9 +102,14 @@ The package handles complex multi-display coordinate transformations:
|
|
|
90
102
|
|
|
91
103
|
### Common Development Patterns
|
|
92
104
|
- All recording operations are Promise-based
|
|
93
|
-
- Event emission for recording state changes (`started`, `stopped`, `completed`)
|
|
105
|
+
- Event emission for recording state changes (`recordingStarted`, `started`, `stopped`, `completed`)
|
|
106
|
+
- `recordingStarted` event provides immediate notification with recording details
|
|
94
107
|
- Automatic permission checking before operations
|
|
95
108
|
- Error handling with descriptive messages for permission issues
|
|
109
|
+
- Cursor tracking supports multiple coordinate systems:
|
|
110
|
+
- Global coordinates (default)
|
|
111
|
+
- Display-relative coordinates (when recording)
|
|
112
|
+
- Window-relative coordinates (with windowInfo parameter)
|
|
96
113
|
|
|
97
114
|
### Platform Requirements
|
|
98
115
|
- macOS only (enforced in install.js)
|
|
@@ -102,6 +119,13 @@ The package handles complex multi-display coordinate transformations:
|
|
|
102
119
|
### File Outputs
|
|
103
120
|
- Video recordings: `.mov` format (H.264/AAC)
|
|
104
121
|
- Cursor data: JSON format with timestamped events
|
|
122
|
+
- `x`, `y`: Cursor coordinates (coordinate system dependent)
|
|
123
|
+
- `timestamp`: Time from capture start (ms)
|
|
124
|
+
- `unixTimeMs`: Unix timestamp
|
|
125
|
+
- `cursorType`: macOS cursor type
|
|
126
|
+
- `type`: Event type (move, click, etc.)
|
|
127
|
+
- `coordinateSystem`: "global", "display-relative", or "window-relative"
|
|
128
|
+
- `windowInfo`: Window metadata (when using window-relative coordinates)
|
|
105
129
|
- Thumbnails: Base64-encoded PNG data URIs
|
|
106
130
|
|
|
107
131
|
## Troubleshooting
|
package/index.js
CHANGED
|
@@ -302,6 +302,13 @@ class MacRecorder extends EventEmitter {
|
|
|
302
302
|
this.emit("timeUpdate", elapsed);
|
|
303
303
|
}, 1000);
|
|
304
304
|
|
|
305
|
+
// Kayıt tam başladığı anda event emit et
|
|
306
|
+
this.emit("recordingStarted", {
|
|
307
|
+
outputPath: this.outputPath,
|
|
308
|
+
timestamp: this.recordingStartTime,
|
|
309
|
+
options: this.options
|
|
310
|
+
});
|
|
311
|
+
|
|
305
312
|
this.emit("started", this.outputPath);
|
|
306
313
|
resolve(this.outputPath);
|
|
307
314
|
} else {
|
|
@@ -506,9 +513,12 @@ class MacRecorder extends EventEmitter {
|
|
|
506
513
|
/**
|
|
507
514
|
* Cursor capture başlatır - otomatik olarak dosyaya yazmaya başlar
|
|
508
515
|
* Recording başlatılmışsa otomatik olarak display-relative koordinatlar kullanır
|
|
509
|
-
* @param {string}
|
|
516
|
+
* @param {string|number} intervalOrFilepath - Cursor data JSON dosya yolu veya interval
|
|
517
|
+
* @param {Object} options - Cursor capture seçenekleri
|
|
518
|
+
* @param {Object} options.windowInfo - Pencere bilgileri (window-relative koordinatlar için)
|
|
519
|
+
* @param {boolean} options.windowRelative - Koordinatları pencereye göre relative yap
|
|
510
520
|
*/
|
|
511
|
-
async startCursorCapture(intervalOrFilepath = 100) {
|
|
521
|
+
async startCursorCapture(intervalOrFilepath = 100, options = {}) {
|
|
512
522
|
let filepath;
|
|
513
523
|
let interval = 20; // Default 50 FPS
|
|
514
524
|
|
|
@@ -528,8 +538,20 @@ class MacRecorder extends EventEmitter {
|
|
|
528
538
|
throw new Error("Cursor capture is already running");
|
|
529
539
|
}
|
|
530
540
|
|
|
531
|
-
//
|
|
532
|
-
if (
|
|
541
|
+
// Koordinat sistemi belirle: window-relative, display-relative veya global
|
|
542
|
+
if (options.windowRelative && options.windowInfo) {
|
|
543
|
+
// Window-relative koordinatlar için pencere bilgilerini kullan
|
|
544
|
+
this.cursorDisplayInfo = {
|
|
545
|
+
displayId: options.windowInfo.displayId || null,
|
|
546
|
+
x: options.windowInfo.x || 0,
|
|
547
|
+
y: options.windowInfo.y || 0,
|
|
548
|
+
width: options.windowInfo.width,
|
|
549
|
+
height: options.windowInfo.height,
|
|
550
|
+
windowRelative: true,
|
|
551
|
+
windowInfo: options.windowInfo
|
|
552
|
+
};
|
|
553
|
+
} else if (this.recordingDisplayInfo) {
|
|
554
|
+
// Recording başlatılmışsa o display'i kullan
|
|
533
555
|
this.cursorDisplayInfo = this.recordingDisplayInfo;
|
|
534
556
|
} else {
|
|
535
557
|
// Main display bilgisini al (her zaman relative koordinatlar için)
|
|
@@ -568,23 +590,42 @@ class MacRecorder extends EventEmitter {
|
|
|
568
590
|
const position = nativeBinding.getCursorPosition();
|
|
569
591
|
const timestamp = Date.now() - this.cursorCaptureStartTime;
|
|
570
592
|
|
|
571
|
-
// Global koordinatları
|
|
593
|
+
// Global koordinatları relative koordinatlara çevir
|
|
572
594
|
let x = position.x;
|
|
573
595
|
let y = position.y;
|
|
596
|
+
let coordinateSystem = "global";
|
|
574
597
|
|
|
575
598
|
if (this.cursorDisplayInfo) {
|
|
576
|
-
//
|
|
599
|
+
// Offset'leri çıkar (display veya window)
|
|
577
600
|
x = position.x - this.cursorDisplayInfo.x;
|
|
578
601
|
y = position.y - this.cursorDisplayInfo.y;
|
|
579
602
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
603
|
+
if (this.cursorDisplayInfo.windowRelative) {
|
|
604
|
+
// Window-relative koordinatlar
|
|
605
|
+
coordinateSystem = "window-relative";
|
|
606
|
+
|
|
607
|
+
// Window bounds kontrolü - cursor window dışındaysa kaydetme
|
|
608
|
+
if (
|
|
609
|
+
x < 0 ||
|
|
610
|
+
y < 0 ||
|
|
611
|
+
x >= this.cursorDisplayInfo.width ||
|
|
612
|
+
y >= this.cursorDisplayInfo.height
|
|
613
|
+
) {
|
|
614
|
+
return; // Bu frame'i skip et - cursor pencere dışında
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
// Display-relative koordinatlar
|
|
618
|
+
coordinateSystem = "display-relative";
|
|
619
|
+
|
|
620
|
+
// Display bounds kontrolü
|
|
621
|
+
if (
|
|
622
|
+
x < 0 ||
|
|
623
|
+
y < 0 ||
|
|
624
|
+
x >= this.cursorDisplayInfo.width ||
|
|
625
|
+
y >= this.cursorDisplayInfo.height
|
|
626
|
+
) {
|
|
627
|
+
return; // Bu frame'i skip et - cursor display dışında
|
|
628
|
+
}
|
|
588
629
|
}
|
|
589
630
|
}
|
|
590
631
|
|
|
@@ -595,6 +636,14 @@ class MacRecorder extends EventEmitter {
|
|
|
595
636
|
unixTimeMs: Date.now(),
|
|
596
637
|
cursorType: position.cursorType,
|
|
597
638
|
type: position.eventType || "move",
|
|
639
|
+
coordinateSystem: coordinateSystem,
|
|
640
|
+
...(this.cursorDisplayInfo?.windowRelative && {
|
|
641
|
+
windowInfo: {
|
|
642
|
+
width: this.cursorDisplayInfo.width,
|
|
643
|
+
height: this.cursorDisplayInfo.height,
|
|
644
|
+
originalWindow: this.cursorDisplayInfo.windowInfo
|
|
645
|
+
}
|
|
646
|
+
})
|
|
598
647
|
};
|
|
599
648
|
|
|
600
649
|
// Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
|
package/package.json
CHANGED
package/src/window_selector.mm
CHANGED
|
@@ -16,6 +16,7 @@ static NSDictionary *g_selectedWindowInfo = nil;
|
|
|
16
16
|
static NSMutableArray *g_allWindows = nil;
|
|
17
17
|
static NSDictionary *g_currentWindowUnderCursor = nil;
|
|
18
18
|
static bool g_bringToFrontEnabled = true; // Default enabled
|
|
19
|
+
static id g_windowKeyEventMonitor = nil;
|
|
19
20
|
|
|
20
21
|
// Recording preview overlay state
|
|
21
22
|
static NSWindow *g_recordingPreviewWindow = nil;
|
|
@@ -213,6 +214,7 @@ bool hideScreenRecordingPreview();
|
|
|
213
214
|
@interface WindowSelectorDelegate : NSObject
|
|
214
215
|
- (void)selectButtonClicked:(id)sender;
|
|
215
216
|
- (void)screenSelectButtonClicked:(id)sender;
|
|
217
|
+
- (void)cancelButtonClicked:(id)sender;
|
|
216
218
|
- (void)timerUpdate:(NSTimer *)timer;
|
|
217
219
|
@end
|
|
218
220
|
|
|
@@ -241,6 +243,16 @@ bool hideScreenRecordingPreview();
|
|
|
241
243
|
}
|
|
242
244
|
}
|
|
243
245
|
|
|
246
|
+
- (void)cancelButtonClicked:(id)sender {
|
|
247
|
+
NSLog(@"🚫 CANCEL BUTTON CLICKED: Selection cancelled");
|
|
248
|
+
// Clean up without selecting anything
|
|
249
|
+
if (g_isScreenSelecting) {
|
|
250
|
+
cleanupScreenSelector();
|
|
251
|
+
} else {
|
|
252
|
+
cleanupWindowSelector();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
244
256
|
- (void)timerUpdate:(NSTimer *)timer {
|
|
245
257
|
updateOverlay();
|
|
246
258
|
}
|
|
@@ -495,14 +507,33 @@ void updateOverlay() {
|
|
|
495
507
|
[(WindowSelectorOverlayView *)g_overlayView setWindowInfo:windowUnderCursor];
|
|
496
508
|
[g_overlayView setNeedsDisplay:YES];
|
|
497
509
|
|
|
498
|
-
// Position
|
|
510
|
+
// Position buttons - Start Record in center, Cancel below it
|
|
499
511
|
if (g_selectButton) {
|
|
500
512
|
NSSize buttonSize = [g_selectButton frame].size;
|
|
501
513
|
NSPoint buttonCenter = NSMakePoint(
|
|
502
514
|
(width - buttonSize.width) / 2,
|
|
503
|
-
(height - buttonSize.height) / 2
|
|
515
|
+
(height - buttonSize.height) / 2 + 30 // Slightly above center
|
|
504
516
|
);
|
|
505
517
|
[g_selectButton setFrameOrigin:buttonCenter];
|
|
518
|
+
|
|
519
|
+
// Position cancel button below the main button
|
|
520
|
+
NSButton *cancelButton = nil;
|
|
521
|
+
for (NSView *subview in [g_overlayWindow.contentView subviews]) {
|
|
522
|
+
if ([subview isKindOfClass:[NSButton class]] &&
|
|
523
|
+
[[(NSButton*)subview title] isEqualToString:@"Cancel"]) {
|
|
524
|
+
cancelButton = (NSButton*)subview;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (cancelButton) {
|
|
530
|
+
NSSize cancelButtonSize = [cancelButton frame].size;
|
|
531
|
+
NSPoint cancelButtonCenter = NSMakePoint(
|
|
532
|
+
(width - cancelButtonSize.width) / 2,
|
|
533
|
+
buttonCenter.y - buttonSize.height - 20 // 20px below main button
|
|
534
|
+
);
|
|
535
|
+
[cancelButton setFrameOrigin:cancelButtonCenter];
|
|
536
|
+
}
|
|
506
537
|
}
|
|
507
538
|
|
|
508
539
|
[g_overlayWindow orderFront:nil];
|
|
@@ -535,6 +566,12 @@ void cleanupWindowSelector() {
|
|
|
535
566
|
g_trackingTimer = nil;
|
|
536
567
|
}
|
|
537
568
|
|
|
569
|
+
// Remove key event monitor
|
|
570
|
+
if (g_windowKeyEventMonitor) {
|
|
571
|
+
[NSEvent removeMonitor:g_windowKeyEventMonitor];
|
|
572
|
+
g_windowKeyEventMonitor = nil;
|
|
573
|
+
}
|
|
574
|
+
|
|
538
575
|
// Close overlay window
|
|
539
576
|
if (g_overlayWindow) {
|
|
540
577
|
[g_overlayWindow close];
|
|
@@ -752,14 +789,52 @@ bool startScreenSelection() {
|
|
|
752
789
|
[selectButton setTarget:g_delegate];
|
|
753
790
|
[selectButton setAction:@selector(screenSelectButtonClicked:)];
|
|
754
791
|
|
|
755
|
-
//
|
|
792
|
+
// Create cancel button for screen selection
|
|
793
|
+
NSButton *screenCancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 120, 40)];
|
|
794
|
+
[screenCancelButton setTitle:@"Cancel"];
|
|
795
|
+
[screenCancelButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
796
|
+
[screenCancelButton setBezelStyle:NSBezelStyleRounded];
|
|
797
|
+
[screenCancelButton setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightMedium]];
|
|
798
|
+
|
|
799
|
+
// Gray cancel button styling
|
|
800
|
+
[screenCancelButton setWantsLayer:YES];
|
|
801
|
+
[screenCancelButton.layer setBackgroundColor:[[NSColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.8] CGColor]];
|
|
802
|
+
[screenCancelButton.layer setCornerRadius:6.0];
|
|
803
|
+
[screenCancelButton.layer setBorderColor:[[NSColor colorWithRed:0.4 green:0.4 blue:0.4 alpha:1.0] CGColor]];
|
|
804
|
+
[screenCancelButton.layer setBorderWidth:1.0];
|
|
805
|
+
|
|
806
|
+
// White text for cancel button
|
|
807
|
+
NSMutableAttributedString *screenCancelTitleString = [[NSMutableAttributedString alloc]
|
|
808
|
+
initWithString:[screenCancelButton title]];
|
|
809
|
+
[screenCancelTitleString addAttribute:NSForegroundColorAttributeName
|
|
810
|
+
value:[NSColor whiteColor]
|
|
811
|
+
range:NSMakeRange(0, [screenCancelTitleString length])];
|
|
812
|
+
[screenCancelButton setAttributedTitle:screenCancelTitleString];
|
|
813
|
+
|
|
814
|
+
// Add shadow for cancel button
|
|
815
|
+
[screenCancelButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
|
|
816
|
+
[screenCancelButton.layer setShadowOffset:NSMakeSize(0, -1)];
|
|
817
|
+
[screenCancelButton.layer setShadowRadius:2.0];
|
|
818
|
+
[screenCancelButton.layer setShadowOpacity:0.2];
|
|
819
|
+
|
|
820
|
+
[screenCancelButton setTarget:g_delegate];
|
|
821
|
+
[screenCancelButton setAction:@selector(cancelButtonClicked:)];
|
|
822
|
+
|
|
823
|
+
// Position buttons - Start Record in center, Cancel below it
|
|
756
824
|
NSPoint buttonCenter = NSMakePoint(
|
|
757
825
|
(screenFrame.size.width - [selectButton frame].size.width) / 2,
|
|
758
|
-
(screenFrame.size.height - [selectButton frame].size.height) / 2
|
|
826
|
+
(screenFrame.size.height - [selectButton frame].size.height) / 2 + 30 // Slightly above center
|
|
759
827
|
);
|
|
760
828
|
[selectButton setFrameOrigin:buttonCenter];
|
|
761
829
|
|
|
830
|
+
NSPoint cancelButtonCenter = NSMakePoint(
|
|
831
|
+
(screenFrame.size.width - [screenCancelButton frame].size.width) / 2,
|
|
832
|
+
buttonCenter.y - [selectButton frame].size.height - 20 // 20px below main button
|
|
833
|
+
);
|
|
834
|
+
[screenCancelButton setFrameOrigin:cancelButtonCenter];
|
|
835
|
+
|
|
762
836
|
[overlayView addSubview:selectButton];
|
|
837
|
+
[overlayView addSubview:screenCancelButton];
|
|
763
838
|
[overlayWindow orderFront:nil];
|
|
764
839
|
[overlayWindow makeKeyAndOrderFront:nil];
|
|
765
840
|
|
|
@@ -946,13 +1021,59 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
|
946
1021
|
[g_selectButton setTarget:g_delegate];
|
|
947
1022
|
[g_selectButton setAction:@selector(selectButtonClicked:)];
|
|
948
1023
|
|
|
949
|
-
|
|
1024
|
+
// Add select button directly to window (not view) for proper layering
|
|
1025
|
+
[g_overlayWindow.contentView addSubview:g_selectButton];
|
|
1026
|
+
|
|
1027
|
+
// Create cancel button
|
|
1028
|
+
NSButton *cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 120, 40)];
|
|
1029
|
+
[cancelButton setTitle:@"Cancel"];
|
|
1030
|
+
[cancelButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
1031
|
+
[cancelButton setBezelStyle:NSBezelStyleRounded];
|
|
1032
|
+
[cancelButton setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightMedium]];
|
|
1033
|
+
|
|
1034
|
+
// Gray cancel button styling
|
|
1035
|
+
[cancelButton setWantsLayer:YES];
|
|
1036
|
+
[cancelButton.layer setBackgroundColor:[[NSColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.8] CGColor]];
|
|
1037
|
+
[cancelButton.layer setCornerRadius:6.0];
|
|
1038
|
+
[cancelButton.layer setBorderColor:[[NSColor colorWithRed:0.4 green:0.4 blue:0.4 alpha:1.0] CGColor]];
|
|
1039
|
+
[cancelButton.layer setBorderWidth:1.0];
|
|
1040
|
+
|
|
1041
|
+
// White text for cancel button
|
|
1042
|
+
NSMutableAttributedString *cancelTitleString = [[NSMutableAttributedString alloc]
|
|
1043
|
+
initWithString:[cancelButton title]];
|
|
1044
|
+
[cancelTitleString addAttribute:NSForegroundColorAttributeName
|
|
1045
|
+
value:[NSColor whiteColor]
|
|
1046
|
+
range:NSMakeRange(0, [cancelTitleString length])];
|
|
1047
|
+
[cancelButton setAttributedTitle:cancelTitleString];
|
|
1048
|
+
|
|
1049
|
+
// Add shadow for cancel button
|
|
1050
|
+
[cancelButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
|
|
1051
|
+
[cancelButton.layer setShadowOffset:NSMakeSize(0, -1)];
|
|
1052
|
+
[cancelButton.layer setShadowRadius:2.0];
|
|
1053
|
+
[cancelButton.layer setShadowOpacity:0.2];
|
|
1054
|
+
|
|
1055
|
+
[cancelButton setTarget:g_delegate];
|
|
1056
|
+
[cancelButton setAction:@selector(cancelButtonClicked:)];
|
|
1057
|
+
|
|
1058
|
+
// Add cancel button to window
|
|
1059
|
+
[g_overlayWindow.contentView addSubview:cancelButton];
|
|
1060
|
+
|
|
1061
|
+
// Cancel button reference will be found dynamically in positioning code
|
|
950
1062
|
|
|
951
1063
|
// Timer approach doesn't work well with Node.js
|
|
952
1064
|
// Instead, we'll use JavaScript polling via getWindowSelectionStatus
|
|
953
1065
|
// The JS side will call this function repeatedly to trigger overlay updates
|
|
954
1066
|
g_trackingTimer = nil; // No timer for now
|
|
955
1067
|
|
|
1068
|
+
// Add ESC key event monitor to cancel selection
|
|
1069
|
+
g_windowKeyEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
|
|
1070
|
+
handler:^(NSEvent *event) {
|
|
1071
|
+
if ([event keyCode] == 53) { // ESC key
|
|
1072
|
+
NSLog(@"🪟 WINDOW SELECTION: ESC pressed - cancelling selection");
|
|
1073
|
+
cleanupWindowSelector();
|
|
1074
|
+
}
|
|
1075
|
+
}];
|
|
1076
|
+
|
|
956
1077
|
g_isWindowSelecting = true;
|
|
957
1078
|
g_selectedWindowInfo = nil;
|
|
958
1079
|
|