node-mac-recorder 2.22.34 → 2.23.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.
@@ -50,7 +50,13 @@
50
50
  "Bash(ffmpeg:*)",
51
51
  "Bash(timeout 30 node:*)",
52
52
  "Bash(MAC_RECORDER_DEBUG=1 node test-camera-audio-sync.js:*)",
53
- "WebSearch"
53
+ "WebSearch",
54
+ "mcp__plugin_context-mode_context-mode__ctx_execute_file",
55
+ "mcp__plugin_context-mode_context-mode__ctx_execute",
56
+ "mcp__plugin_context-mode_context-mode__ctx_batch_execute",
57
+ "Bash(nm -D /System/Library/Frameworks/AppKit.framework/AppKit)",
58
+ "Bash(otool -L /System/Library/Frameworks/AppKit.framework/AppKit)",
59
+ "Bash(mdfind -name CoreCursor)"
54
60
  ],
55
61
  "deny": [],
56
62
  "ask": []
package/AGENTS.md ADDED
@@ -0,0 +1,192 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ **ELECTRON-FIRST PRIORITY**: node-mac-recorder is a Node.js native addon specifically designed and optimized for Electron.js applications. It provides macOS screen recording capabilities using ScreenCaptureKit (macOS 15+) and AVFoundation (macOS 13+). The package allows recording of full screens, specific windows, or custom areas, with support for multi-display setups, audio capture, and cursor tracking.
8
+
9
+ ### **🚀 CRITICAL: Electron.js Support**
10
+ This module is **PRIMARILY BUILT FOR ELECTRON.JS**. All features work seamlessly in Electron applications without any restrictions. Both ScreenCaptureKit and AVFoundation are fully supported in Electron environments.
11
+
12
+ ## Build System & Commands
13
+
14
+ ### Building the Native Module
15
+
16
+ ```bash
17
+ npm run build # Build the native module using node-gyp
18
+ npm run rebuild # Clean rebuild of the native module
19
+ npm run clean # Clean build artifacts
20
+ npm install # Runs install.js which builds the module automatically
21
+ ```
22
+
23
+ ### Testing
24
+
25
+ ```bash
26
+ npm test # Run the main test suite (test.js)
27
+ node cursor-test.js # Test cursor tracking functionality only
28
+ node test.js # Run comprehensive API tests
29
+ ```
30
+
31
+ ### Development
32
+
33
+ The package uses node-gyp for building the native C++/Objective-C module. Requires:
34
+
35
+ - macOS 10.15+ (Catalina or later)
36
+ - Xcode Command Line Tools
37
+ - Node.js 14+
38
+
39
+ ## Architecture
40
+
41
+ ### Core Components
42
+
43
+ **Main Entry Point**
44
+
45
+ - `index.js` - Main MacRecorder class (EventEmitter-based)
46
+ - Handles all high-level recording operations and coordinate transformations
47
+
48
+ **Native Module** (`src/`)
49
+
50
+ - `mac_recorder.mm` - Main native module entry point and N-API bindings
51
+ - `screen_capture.mm` - AVFoundation-based screen/window recording
52
+ - `audio_capture.mm` - Audio device enumeration and capture
53
+ - `cursor_tracker.mm` - Real-time cursor position and event tracking
54
+
55
+ **Build Configuration**
56
+
57
+ - `binding.gyp` - Native module build configuration
58
+ - Links against AVFoundation, ScreenCaptureKit, AppKit, and other macOS frameworks
59
+
60
+ ### Key Features
61
+
62
+ 1. **Multi-Display Support**: Automatic display detection and coordinate conversion
63
+ 2. **Window Recording**: Smart window detection with thumbnail generation
64
+ 3. **Audio Control**: Separate microphone and system audio controls with device selection
65
+ 4. **Cursor Tracking**: Real-time cursor position, type, and click event capture
66
+ 5. **Permission Management**: Built-in macOS permission checking and requesting
67
+
68
+ ### Coordinate System Handling
69
+
70
+ The package handles complex multi-display coordinate transformations:
71
+
72
+ - Global macOS coordinates (can be negative for secondary displays)
73
+ - Display-relative coordinates (always positive, 0-based)
74
+ - Automatic window-to-display mapping for recording
75
+
76
+ ## API Structure
77
+
78
+ ### Main Class Methods
79
+
80
+ - `startRecording(outputPath, options)` - Begin screen/window recording
81
+ - `stopRecording()` - Stop recording and finalize video file
82
+ - `getWindows()` - List all recordable application windows
83
+ - `getDisplays()` - Get all available displays with metadata
84
+ - `getAudioDevices()` - Enumerate available audio input devices
85
+ - `checkPermissions()` - Verify macOS recording permissions
86
+
87
+ ### Cursor Tracking
88
+
89
+ - `startCursorCapture(filepath, options)` - Begin real-time cursor tracking to JSON
90
+ - `options.windowInfo` - Window information for window-relative coordinates
91
+ - `options.windowRelative` - Set to true for window-relative coordinates
92
+ - `stopCursorCapture()` - Stop tracking and close output file
93
+ - `getCursorPosition()` - Get current cursor position and state
94
+
95
+ ### Events
96
+
97
+ The MacRecorder class emits the following events:
98
+
99
+ - `recordingStarted` - Emitted immediately when recording starts with recording details
100
+ - `started` - Emitted when recording is confirmed started (legacy event)
101
+ - `stopped` - Emitted when recording stops
102
+ - `completed` - Emitted when recording file is finalized
103
+ - `timeUpdate` - Emitted every second with elapsed time
104
+ - `cursorCaptureStarted` - Emitted when cursor capture begins
105
+ - `cursorCaptureStopped` - Emitted when cursor capture ends
106
+
107
+ ### Thumbnails
108
+
109
+ - `getWindowThumbnail(windowId, options)` - Capture window preview image
110
+ - `getDisplayThumbnail(displayId, options)` - Capture display preview image
111
+
112
+ ## Development Notes
113
+
114
+ ### Testing Strategy
115
+
116
+ - Use `npm test` for full API validation
117
+ - `cursor-test.js` for testing cursor tracking specifically
118
+ - Test files create output in `test-output/` directory
119
+
120
+ ### Common Development Patterns
121
+
122
+ - All recording operations are Promise-based
123
+ - Event emission for recording state changes (`recordingStarted`, `started`, `stopped`, `completed`)
124
+ - `recordingStarted` event provides immediate notification with recording details
125
+ - Automatic permission checking before operations
126
+ - Error handling with descriptive messages for permission issues
127
+ - Cursor tracking supports multiple coordinate systems:
128
+ - Global coordinates (default)
129
+ - Display-relative coordinates (when recording)
130
+ - Window-relative coordinates (with windowInfo parameter)
131
+
132
+ ### Platform Requirements & Framework Selection
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)
138
+ - Native module compilation required on install
139
+ - Requires screen recording and accessibility permissions
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
148
+
149
+ ### File Outputs
150
+
151
+ - Video recordings: `.mov` format (H.264/AAC)
152
+ - Cursor data: JSON format with timestamped events
153
+ - `x`, `y`: Cursor coordinates (coordinate system dependent)
154
+ - `timestamp`: Time from capture start (ms)
155
+ - `unixTimeMs`: Unix timestamp
156
+ - `cursorType`: macOS cursor type
157
+ - `type`: Event type (move, click, etc.)
158
+ - `coordinateSystem`: "global", "display-relative", or "window-relative"
159
+ - `windowInfo`: Window metadata (when using window-relative coordinates)
160
+ - Thumbnails: Base64-encoded PNG data URIs
161
+
162
+ ## Troubleshooting
163
+
164
+ ### Build Issues
165
+
166
+ 1. Ensure Xcode Command Line Tools: `xcode-select --install`
167
+ 2. Clean rebuild: `npm run clean && npm run build`
168
+ 3. Check Node.js version compatibility (14+)
169
+
170
+ ### Runtime Issues
171
+
172
+ 1. Permission failures: Check System Preferences > Security & Privacy
173
+ 2. Recording failures: Verify target windows/displays are accessible
174
+ 3. Audio issues: Check audio device availability and permissions
175
+
176
+ ### Native Module Loading
177
+
178
+ The module tries loading from `build/Release/` first, then falls back to `build/Debug/` with helpful error messages if neither exists.
179
+
180
+ ### **⚡ CRITICAL IMPLEMENTATION NOTES**
181
+
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.22.34",
3
+ "version": "2.23.1",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -25,6 +25,7 @@ static int64_t g_avFrameNumber = 0;
25
25
  static CMTime g_avStartTime;
26
26
  static void* g_avAudioRecorder = nil;
27
27
  static NSString* g_avAudioOutputPath = nil;
28
+ static const NSInteger kAVFoundationHighQualityVideoBitrate = 50 * 1000 * 1000;
28
29
 
29
30
  // AVFoundation screen recording implementation
30
31
  extern "C" bool startAVFoundationRecording(const std::string& outputPath,
@@ -36,7 +37,8 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
36
37
  bool includeSystemAudio,
37
38
  NSString* audioDeviceId,
38
39
  NSString* audioOutputPath,
39
- double requestedFrameRate) {
40
+ double requestedFrameRate,
41
+ NSString* qualityPreset) {
40
42
 
41
43
  if (g_avIsRecording) {
42
44
  NSLog(@"❌ AVFoundation recording already in progress");
@@ -123,27 +125,47 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
123
125
 
124
126
  MRLog(@"🎯 Recording size: %.0fx%.0f (using actual physical dimensions for Retina fix)", recordingSize.width, recordingSize.height);
125
127
 
126
- // Video settings with macOS compatibility
127
- NSString *codecKey;
128
- if (@available(macOS 10.13, *)) {
129
- codecKey = AVVideoCodecTypeH264;
130
- } else {
131
- // Fallback for older macOS versions
132
- codecKey = AVVideoCodecH264;
128
+ NSString *normalizedQuality = @"high";
129
+ if ([qualityPreset isKindOfClass:[NSString class]] && qualityPreset.length > 0) {
130
+ normalizedQuality = [qualityPreset lowercaseString];
131
+ if (![normalizedQuality isEqualToString:@"low"] &&
132
+ ![normalizedQuality isEqualToString:@"medium"] &&
133
+ ![normalizedQuality isEqualToString:@"high"]) {
134
+ normalizedQuality = @"high";
135
+ }
133
136
  }
134
-
135
- NSInteger bitrate = (NSInteger)(recordingSize.width * recordingSize.height * 100);
136
- bitrate = MAX(bitrate, 120 * 1000 * 1000);
137
- bitrate = MIN(bitrate, 500 * 1000 * 1000);
138
-
139
- NSLog(@"🎬 ULTRA QUALITY AVFoundation: %dx%d, bitrate=%.2fMbps",
140
- (int)recordingSize.width, (int)recordingSize.height, bitrate / (1000.0 * 1000.0));
141
-
142
- // Resolve target FPS
143
137
  double fps = requestedFrameRate > 0 ? requestedFrameRate : 60.0;
144
138
  if (fps < 1.0) fps = 1.0;
145
139
  if (fps > 120.0) fps = 120.0;
146
140
 
141
+ NSString *codecKey = AVVideoCodecTypeH264;
142
+ NSInteger bitrate = kAVFoundationHighQualityVideoBitrate;
143
+ NSNumber *qualityHint = @1.0;
144
+
145
+ if ([normalizedQuality isEqualToString:@"low"]) {
146
+ NSInteger multiplier = 10;
147
+ NSInteger minBitrate = 10 * 1000 * 1000;
148
+ NSInteger maxBitrate = 45 * 1000 * 1000;
149
+ bitrate = (NSInteger)(recordingSize.width * recordingSize.height * multiplier);
150
+ bitrate = MAX(bitrate, minBitrate);
151
+ bitrate = MIN(bitrate, maxBitrate);
152
+ qualityHint = @0.85;
153
+ } else if ([normalizedQuality isEqualToString:@"medium"]) {
154
+ NSInteger multiplier = 18;
155
+ NSInteger minBitrate = 18 * 1000 * 1000;
156
+ NSInteger maxBitrate = 80 * 1000 * 1000;
157
+ bitrate = (NSInteger)(recordingSize.width * recordingSize.height * multiplier);
158
+ bitrate = MAX(bitrate, minBitrate);
159
+ bitrate = MIN(bitrate, maxBitrate);
160
+ qualityHint = @0.9;
161
+ }
162
+
163
+ MRLog(@"🎬 AVFoundation encoder (%@): %dx%d, codec=H.264 High Profile, bitrate=%.2fMbps",
164
+ normalizedQuality,
165
+ (int)recordingSize.width,
166
+ (int)recordingSize.height,
167
+ bitrate / (1000.0 * 1000.0));
168
+
147
169
  NSDictionary *videoSettings = @{
148
170
  AVVideoCodecKey: codecKey,
149
171
  AVVideoWidthKey: @((int)recordingSize.width),
@@ -153,13 +175,11 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
153
175
  AVVideoMaxKeyFrameIntervalKey: @((int)fps),
154
176
  AVVideoAllowFrameReorderingKey: @YES,
155
177
  AVVideoExpectedSourceFrameRateKey: @((int)fps),
156
- AVVideoQualityKey: @(1.0),
178
+ AVVideoQualityKey: qualityHint,
157
179
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
158
180
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
159
181
  }
160
182
  };
161
-
162
- NSLog(@"🔧 Using codec: %@", codecKey);
163
183
 
164
184
  // Create video input
165
185
  g_avVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
@@ -21,7 +21,8 @@ extern "C" {
21
21
  bool includeSystemAudio,
22
22
  NSString* audioDeviceId,
23
23
  NSString* audioOutputPath,
24
- double frameRate);
24
+ double frameRate,
25
+ NSString* qualityPreset);
25
26
  bool stopAVFoundationRecording();
26
27
  bool isAVFoundationRecording();
27
28
  NSString* getAVFoundationAudioPath();
@@ -728,7 +729,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
728
729
  bool includeSystemAudio,
729
730
  NSString* audioDeviceId,
730
731
  NSString* audioOutputPath,
731
- double frameRate);
732
+ double frameRate,
733
+ NSString* qualityPreset);
732
734
 
733
735
  // A/V SYNC: Start camera non-blocking BEFORE AVFoundation
734
736
  if (captureCamera) {
@@ -742,7 +744,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
742
744
  MRLog(@"🎯 SYNC: Starting screen recording");
743
745
  bool avResult = startAVFoundationRecording(outputPath, displayID, windowID, captureRect,
744
746
  captureCursor, includeMicrophone, includeSystemAudio,
745
- audioDeviceId, audioOutputPath, frameRate);
747
+ audioDeviceId, audioOutputPath, frameRate,
748
+ qualityPreset ?: @"high");
746
749
 
747
750
  if (avResult) {
748
751
  MRLog(@"🎥 RECORDING METHOD: AVFoundation");
@@ -165,6 +165,7 @@ static NSInteger g_targetFPS = 60;
165
165
  static NSString *g_qualityPreset = @"high";
166
166
  static NSInteger g_frameCount = 0;
167
167
  static CFAbsoluteTime g_firstFrameTime = 0;
168
+ static const NSInteger kSCKHighQualityVideoBitrate = 50 * 1000 * 1000;
168
169
 
169
170
  // Quality helpers
170
171
  static NSString *SCKNormalizeQualityPreset(id preset) {
@@ -213,10 +214,10 @@ static void SCKQualityBitrateForDimensions(NSString *preset,
213
214
  multiplier = 18;
214
215
  minBitrate = 18 * 1000 * 1000;
215
216
  maxBitrate = 80 * 1000 * 1000;
216
- } else { // high/default - ULTRA quality
217
- multiplier = 120;
218
- minBitrate = 120 * 1000 * 1000;
219
- maxBitrate = 600 * 1000 * 1000;
217
+ } else { // high/default - fixed high-quality H.264 target
218
+ multiplier = 0;
219
+ minBitrate = kSCKHighQualityVideoBitrate;
220
+ maxBitrate = kSCKHighQualityVideoBitrate;
220
221
  }
221
222
 
222
223
  double base = ((double)MAX(1, width)) * ((double)MAX(1, height)) * (double)multiplier;
@@ -776,25 +777,18 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
776
777
  return NO;
777
778
  }
778
779
 
780
+ NSString *normalizedQuality = SCKNormalizeQualityPreset(g_qualityPreset);
779
781
  NSInteger bitrate = 0;
780
- NSInteger bitrateMultiplier = 0;
781
782
  NSInteger minBitrate = 0;
782
783
  NSInteger maxBitrate = 0;
783
- NSString *normalizedQuality = SCKNormalizeQualityPreset(g_qualityPreset);
784
- SCKQualityBitrateForDimensions(normalizedQuality, width, height, &bitrate, &bitrateMultiplier, &minBitrate, &maxBitrate);
784
+ SCKQualityBitrateForDimensions(normalizedQuality, width, height, &bitrate, NULL, &minBitrate, &maxBitrate);
785
785
 
786
- NSNumber *qualityHint = @1.0;
787
- if ([normalizedQuality isEqualToString:@"medium"]) {
788
- qualityHint = @0.9;
789
- } else if ([normalizedQuality isEqualToString:@"low"]) {
790
- qualityHint = @0.85;
791
- }
786
+ NSNumber *qualityHint = [normalizedQuality isEqualToString:@"high"] ? @1.0 : ([normalizedQuality isEqualToString:@"medium"] ? @0.9 : @0.85);
792
787
 
793
- MRLog(@"🎬 Screen encoder (%@): %ldx%ld, multiplier=%ld, bitrate=%.2fMbps (min=%ldMbps max=%ldMbps)",
788
+ MRLog(@"🎬 Screen encoder (%@): %ldx%ld, codec=H.264 High Profile, bitrate=%.2fMbps (min=%ldMbps max=%ldMbps)",
794
789
  normalizedQuality,
795
790
  (long)width,
796
791
  (long)height,
797
- (long)bitrateMultiplier,
798
792
  bitrate / (1000.0 * 1000.0),
799
793
  (long)(minBitrate / (1000 * 1000)),
800
794
  (long)(maxBitrate / (1000 * 1000)));
@@ -804,12 +798,11 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
804
798
  AVVideoMaxKeyFrameIntervalKey: @(MAX(1, g_targetFPS)),
805
799
  AVVideoAllowFrameReorderingKey: @YES,
806
800
  AVVideoExpectedSourceFrameRateKey: @(MAX(1, g_targetFPS)),
807
- AVVideoQualityKey: qualityHint, // 0.0-1.0, higher is better
801
+ AVVideoQualityKey: qualityHint,
808
802
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
809
803
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
810
- // Add data rate limits for consistent quality during motion
811
804
  AVVideoAverageNonDroppableFrameRateKey: @(MAX(1, g_targetFPS)),
812
- AVVideoMaxKeyFrameIntervalDurationKey: @(1.0) // Keyframe at least every second
805
+ AVVideoMaxKeyFrameIntervalDurationKey: @(1.0)
813
806
  };
814
807
 
815
808
  NSDictionary *videoSettings = @{