node-mac-recorder 2.23.0 → 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.
- package/AGENTS.md +192 -0
- package/package.json +1 -1
- package/src/avfoundation_recorder.mm +39 -38
- package/src/screen_capture_kit.mm +38 -56
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
|
@@ -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,
|
|
@@ -133,52 +134,52 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
133
134
|
normalizedQuality = @"high";
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
BOOL useProRes = [normalizedQuality isEqualToString:@"high"];
|
|
137
|
-
|
|
138
137
|
double fps = requestedFrameRate > 0 ? requestedFrameRate : 60.0;
|
|
139
138
|
if (fps < 1.0) fps = 1.0;
|
|
140
139
|
if (fps > 120.0) fps = 120.0;
|
|
141
140
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
NSInteger
|
|
156
|
-
NSInteger
|
|
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);
|
|
157
158
|
bitrate = MAX(bitrate, minBitrate);
|
|
158
159
|
bitrate = MIN(bitrate, maxBitrate);
|
|
159
|
-
|
|
160
|
+
qualityHint = @0.9;
|
|
161
|
+
}
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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));
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
169
|
+
NSDictionary *videoSettings = @{
|
|
170
|
+
AVVideoCodecKey: codecKey,
|
|
171
|
+
AVVideoWidthKey: @((int)recordingSize.width),
|
|
172
|
+
AVVideoHeightKey: @((int)recordingSize.height),
|
|
173
|
+
AVVideoCompressionPropertiesKey: @{
|
|
174
|
+
AVVideoAverageBitRateKey: @(bitrate),
|
|
175
|
+
AVVideoMaxKeyFrameIntervalKey: @((int)fps),
|
|
176
|
+
AVVideoAllowFrameReorderingKey: @YES,
|
|
177
|
+
AVVideoExpectedSourceFrameRateKey: @((int)fps),
|
|
178
|
+
AVVideoQualityKey: qualityHint,
|
|
179
|
+
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
|
180
|
+
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
|
|
181
|
+
}
|
|
182
|
+
};
|
|
182
183
|
|
|
183
184
|
// Create video input
|
|
184
185
|
g_avVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
|
|
@@ -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 -
|
|
217
|
-
multiplier =
|
|
218
|
-
minBitrate =
|
|
219
|
-
maxBitrate =
|
|
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;
|
|
@@ -777,58 +778,39 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
777
778
|
}
|
|
778
779
|
|
|
779
780
|
NSString *normalizedQuality = SCKNormalizeQualityPreset(g_qualityPreset);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
NSDictionary *compressionProps = @{
|
|
814
|
-
AVVideoAverageBitRateKey: @(bitrate),
|
|
815
|
-
AVVideoMaxKeyFrameIntervalKey: @(MAX(1, g_targetFPS)),
|
|
816
|
-
AVVideoAllowFrameReorderingKey: @YES,
|
|
817
|
-
AVVideoExpectedSourceFrameRateKey: @(MAX(1, g_targetFPS)),
|
|
818
|
-
AVVideoQualityKey: qualityHint,
|
|
819
|
-
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
|
820
|
-
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
|
|
821
|
-
AVVideoAverageNonDroppableFrameRateKey: @(MAX(1, g_targetFPS)),
|
|
822
|
-
AVVideoMaxKeyFrameIntervalDurationKey: @(1.0)
|
|
823
|
-
};
|
|
824
|
-
|
|
825
|
-
videoSettings = @{
|
|
826
|
-
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
827
|
-
AVVideoWidthKey: @(width),
|
|
828
|
-
AVVideoHeightKey: @(height),
|
|
829
|
-
AVVideoCompressionPropertiesKey: compressionProps
|
|
830
|
-
};
|
|
831
|
-
}
|
|
781
|
+
NSInteger bitrate = 0;
|
|
782
|
+
NSInteger minBitrate = 0;
|
|
783
|
+
NSInteger maxBitrate = 0;
|
|
784
|
+
SCKQualityBitrateForDimensions(normalizedQuality, width, height, &bitrate, NULL, &minBitrate, &maxBitrate);
|
|
785
|
+
|
|
786
|
+
NSNumber *qualityHint = [normalizedQuality isEqualToString:@"high"] ? @1.0 : ([normalizedQuality isEqualToString:@"medium"] ? @0.9 : @0.85);
|
|
787
|
+
|
|
788
|
+
MRLog(@"🎬 Screen encoder (%@): %ldx%ld, codec=H.264 High Profile, bitrate=%.2fMbps (min=%ldMbps max=%ldMbps)",
|
|
789
|
+
normalizedQuality,
|
|
790
|
+
(long)width,
|
|
791
|
+
(long)height,
|
|
792
|
+
bitrate / (1000.0 * 1000.0),
|
|
793
|
+
(long)(minBitrate / (1000 * 1000)),
|
|
794
|
+
(long)(maxBitrate / (1000 * 1000)));
|
|
795
|
+
|
|
796
|
+
NSDictionary *compressionProps = @{
|
|
797
|
+
AVVideoAverageBitRateKey: @(bitrate),
|
|
798
|
+
AVVideoMaxKeyFrameIntervalKey: @(MAX(1, g_targetFPS)),
|
|
799
|
+
AVVideoAllowFrameReorderingKey: @YES,
|
|
800
|
+
AVVideoExpectedSourceFrameRateKey: @(MAX(1, g_targetFPS)),
|
|
801
|
+
AVVideoQualityKey: qualityHint,
|
|
802
|
+
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
|
803
|
+
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
|
|
804
|
+
AVVideoAverageNonDroppableFrameRateKey: @(MAX(1, g_targetFPS)),
|
|
805
|
+
AVVideoMaxKeyFrameIntervalDurationKey: @(1.0)
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
NSDictionary *videoSettings = @{
|
|
809
|
+
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
810
|
+
AVVideoWidthKey: @(width),
|
|
811
|
+
AVVideoHeightKey: @(height),
|
|
812
|
+
AVVideoCompressionPropertiesKey: compressionProps
|
|
813
|
+
};
|
|
832
814
|
|
|
833
815
|
g_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
|
|
834
816
|
g_videoInput.expectsMediaDataInRealTime = YES;
|