node-mac-recorder 2.23.0 → 2.23.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/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.23.0",
3
+ "version": "2.23.2",
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,
@@ -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
- NSDictionary *videoSettings = nil;
143
- if (useProRes) {
144
- MRLog(@"🎬 AVFoundation encoder (high): %dx%d, codec=ProRes 422 HQ",
145
- (int)recordingSize.width, (int)recordingSize.height);
146
- videoSettings = @{
147
- AVVideoCodecKey: AVVideoCodecTypeAppleProRes422HQ,
148
- AVVideoWidthKey: @((int)recordingSize.width),
149
- AVVideoHeightKey: @((int)recordingSize.height)
150
- };
151
- } else {
152
- NSString *codecKey = AVVideoCodecTypeH264;
153
- NSInteger multiplier = [normalizedQuality isEqualToString:@"medium"] ? 18 : 10;
154
- NSInteger minBitrate = [normalizedQuality isEqualToString:@"medium"] ? (18 * 1000 * 1000) : (10 * 1000 * 1000);
155
- NSInteger maxBitrate = [normalizedQuality isEqualToString:@"medium"] ? (80 * 1000 * 1000) : (45 * 1000 * 1000);
156
- NSInteger bitrate = (NSInteger)(recordingSize.width * recordingSize.height * multiplier);
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
- NSNumber *qualityHint = [normalizedQuality isEqualToString:@"medium"] ? @0.9 : @0.85;
160
+ qualityHint = @0.9;
161
+ }
160
162
 
161
- MRLog(@"🎬 AVFoundation encoder (%@): %dx%d, codec=H.264, bitrate=%.2fMbps",
162
- normalizedQuality,
163
- (int)recordingSize.width,
164
- (int)recordingSize.height,
165
- bitrate / (1000.0 * 1000.0));
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
- videoSettings = @{
168
- AVVideoCodecKey: codecKey,
169
- AVVideoWidthKey: @((int)recordingSize.width),
170
- AVVideoHeightKey: @((int)recordingSize.height),
171
- AVVideoCompressionPropertiesKey: @{
172
- AVVideoAverageBitRateKey: @(bitrate),
173
- AVVideoMaxKeyFrameIntervalKey: @((int)fps),
174
- AVVideoAllowFrameReorderingKey: @YES,
175
- AVVideoExpectedSourceFrameRateKey: @((int)fps),
176
- AVVideoQualityKey: qualityHint,
177
- AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
178
- AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
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 - 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;
@@ -777,58 +778,50 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
777
778
  }
778
779
 
779
780
  NSString *normalizedQuality = SCKNormalizeQualityPreset(g_qualityPreset);
780
- BOOL useProRes = [normalizedQuality isEqualToString:@"high"];
781
-
782
- NSDictionary *videoSettings = nil;
783
-
784
- if (useProRes) {
785
- // Screen Studio-style: ProRes 422 HQ intra-frame, visually lossless,
786
- // hardware-accelerated on Apple Silicon. Best for editor scrubbing & high-zoom clarity.
787
- MRLog(@"🎬 Screen encoder (high): %ldx%ld, codec=ProRes 422 HQ",
788
- (long)width, (long)height);
789
-
790
- videoSettings = @{
791
- AVVideoCodecKey: AVVideoCodecTypeAppleProRes422HQ,
792
- AVVideoWidthKey: @(width),
793
- AVVideoHeightKey: @(height)
794
- };
795
- } else {
796
- NSInteger bitrate = 0;
797
- NSInteger bitrateMultiplier = 0;
798
- NSInteger minBitrate = 0;
799
- NSInteger maxBitrate = 0;
800
- SCKQualityBitrateForDimensions(normalizedQuality, width, height, &bitrate, &bitrateMultiplier, &minBitrate, &maxBitrate);
801
-
802
- NSNumber *qualityHint = [normalizedQuality isEqualToString:@"medium"] ? @0.9 : @0.85;
803
-
804
- MRLog(@"🎬 Screen encoder (%@): %ldx%ld, codec=H.264, multiplier=%ld, bitrate=%.2fMbps (min=%ldMbps max=%ldMbps)",
805
- normalizedQuality,
806
- (long)width,
807
- (long)height,
808
- (long)bitrateMultiplier,
809
- bitrate / (1000.0 * 1000.0),
810
- (long)(minBitrate / (1000 * 1000)),
811
- (long)(maxBitrate / (1000 * 1000)));
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
+ // Renk etiketleri: kayıt sRGB uzayında yakalanıyor (streamConfig.colorSpaceName),
809
+ // BT.709 olarak etiketle ki Chromium/QuickTime decode'da doğru yorumlasın.
810
+ // Etiket olmadan player'lar tahmin yürütüyor ve renkler soluk/kaymış görünüyordu.
811
+ // Bitrate/boyut maliyeti sıfırdır.
812
+ NSDictionary *colorProps = @{
813
+ AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
814
+ AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
815
+ AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
816
+ };
817
+
818
+ NSDictionary *videoSettings = @{
819
+ AVVideoCodecKey: AVVideoCodecTypeH264,
820
+ AVVideoWidthKey: @(width),
821
+ AVVideoHeightKey: @(height),
822
+ AVVideoColorPropertiesKey: colorProps,
823
+ AVVideoCompressionPropertiesKey: compressionProps
824
+ };
832
825
 
833
826
  g_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
834
827
  g_videoInput.expectsMediaDataInRealTime = YES;
@@ -1552,6 +1545,12 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1552
1545
  streamConfig.shouldBeOpaque = YES;
1553
1546
  MRLog(@"🎯 Using SCCaptureResolutionBest + shouldBeOpaque for maximum quality (macOS 14+)");
1554
1547
  }
1548
+ if (@available(macOS 13.0, *)) {
1549
+ // Frame'leri bilinen bir renk uzayında (sRGB) iste; encoder tarafında
1550
+ // BT.709 etiketiyle birlikte uçtan uca tutarlı renk sağlar.
1551
+ // Aksi halde P3 ekranlarda display-native, etiketsiz piksel gelir.
1552
+ streamConfig.colorSpaceName = kCGColorSpaceSRGB;
1553
+ }
1555
1554
 
1556
1555
  BOOL shouldCaptureMic = includeMicrophone ? [includeMicrophone boolValue] : NO;
1557
1556
  BOOL shouldCaptureSystemAudio = includeSystemAudio ? [includeSystemAudio boolValue] : NO;