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 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} filepath - Cursor data JSON dosya yolu
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
- // Recording başlatılmışsa o display'i kullan, yoksa main display kullan
532
- if (this.recordingDisplayInfo) {
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ı display-relative'e çevir
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
- // Display offset'lerini çıkar
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
- // Display bounds kontrolü - cursor display dışındaysa kaydetme
581
- if (
582
- x < 0 ||
583
- y < 0 ||
584
- x >= this.cursorDisplayInfo.width ||
585
- y >= this.cursorDisplayInfo.height
586
- ) {
587
- return; // Bu frame'i skip et
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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 select button in center
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
- // Position button in center of screen
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
- [g_overlayView addSubview:g_selectButton];
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