node-mac-recorder 1.2.7 → 1.2.9
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/settings.local.json +13 -0
- package/CLAUDE.md +120 -0
- package/cursor-data.json +1 -0
- package/debug-cursor-output.json +1 -0
- package/debug-cursor.js +50 -0
- package/index.js +19 -0
- package/package.json +1 -1
- package/quick-cursor-test.js +20 -0
- package/quick-cursor-test.json +1 -0
- package/src/cursor_tracker.mm +10 -4
- package/test-both-cursor.json +1 -0
- package/test-both.js +46 -0
- package/test-cursor-visible.js +41 -0
- package/test-native-cursor.js +31 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
node-mac-recorder is a Node.js native addon that provides macOS screen recording capabilities using AVFoundation. 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
|
+
## Build System & Commands
|
|
10
|
+
|
|
11
|
+
### Building the Native Module
|
|
12
|
+
```bash
|
|
13
|
+
npm run build # Build the native module using node-gyp
|
|
14
|
+
npm run rebuild # Clean rebuild of the native module
|
|
15
|
+
npm run clean # Clean build artifacts
|
|
16
|
+
npm install # Runs install.js which builds the module automatically
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Testing
|
|
20
|
+
```bash
|
|
21
|
+
npm test # Run the main test suite (test.js)
|
|
22
|
+
node cursor-test.js # Test cursor tracking functionality only
|
|
23
|
+
node test.js # Run comprehensive API tests
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Development
|
|
27
|
+
The package uses node-gyp for building the native C++/Objective-C module. Requires:
|
|
28
|
+
- macOS 10.15+ (Catalina or later)
|
|
29
|
+
- Xcode Command Line Tools
|
|
30
|
+
- Node.js 14+
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
### Core Components
|
|
35
|
+
|
|
36
|
+
**Main Entry Point**
|
|
37
|
+
- `index.js` - Main MacRecorder class (EventEmitter-based)
|
|
38
|
+
- Handles all high-level recording operations and coordinate transformations
|
|
39
|
+
|
|
40
|
+
**Native Module** (`src/`)
|
|
41
|
+
- `mac_recorder.mm` - Main native module entry point and N-API bindings
|
|
42
|
+
- `screen_capture.mm` - AVFoundation-based screen/window recording
|
|
43
|
+
- `audio_capture.mm` - Audio device enumeration and capture
|
|
44
|
+
- `cursor_tracker.mm` - Real-time cursor position and event tracking
|
|
45
|
+
|
|
46
|
+
**Build Configuration**
|
|
47
|
+
- `binding.gyp` - Native module build configuration
|
|
48
|
+
- Links against AVFoundation, ScreenCaptureKit, AppKit, and other macOS frameworks
|
|
49
|
+
|
|
50
|
+
### Key Features
|
|
51
|
+
|
|
52
|
+
1. **Multi-Display Support**: Automatic display detection and coordinate conversion
|
|
53
|
+
2. **Window Recording**: Smart window detection with thumbnail generation
|
|
54
|
+
3. **Audio Control**: Separate microphone and system audio controls with device selection
|
|
55
|
+
4. **Cursor Tracking**: Real-time cursor position, type, and click event capture
|
|
56
|
+
5. **Permission Management**: Built-in macOS permission checking and requesting
|
|
57
|
+
|
|
58
|
+
### Coordinate System Handling
|
|
59
|
+
|
|
60
|
+
The package handles complex multi-display coordinate transformations:
|
|
61
|
+
- Global macOS coordinates (can be negative for secondary displays)
|
|
62
|
+
- Display-relative coordinates (always positive, 0-based)
|
|
63
|
+
- Automatic window-to-display mapping for recording
|
|
64
|
+
|
|
65
|
+
## API Structure
|
|
66
|
+
|
|
67
|
+
### Main Class Methods
|
|
68
|
+
- `startRecording(outputPath, options)` - Begin screen/window recording
|
|
69
|
+
- `stopRecording()` - Stop recording and finalize video file
|
|
70
|
+
- `getWindows()` - List all recordable application windows
|
|
71
|
+
- `getDisplays()` - Get all available displays with metadata
|
|
72
|
+
- `getAudioDevices()` - Enumerate available audio input devices
|
|
73
|
+
- `checkPermissions()` - Verify macOS recording permissions
|
|
74
|
+
|
|
75
|
+
### Cursor Tracking
|
|
76
|
+
- `startCursorCapture(filepath)` - Begin real-time cursor tracking to JSON
|
|
77
|
+
- `stopCursorCapture()` - Stop tracking and close output file
|
|
78
|
+
- `getCursorPosition()` - Get current cursor position and state
|
|
79
|
+
|
|
80
|
+
### Thumbnails
|
|
81
|
+
- `getWindowThumbnail(windowId, options)` - Capture window preview image
|
|
82
|
+
- `getDisplayThumbnail(displayId, options)` - Capture display preview image
|
|
83
|
+
|
|
84
|
+
## Development Notes
|
|
85
|
+
|
|
86
|
+
### Testing Strategy
|
|
87
|
+
- Use `npm test` for full API validation
|
|
88
|
+
- `cursor-test.js` for testing cursor tracking specifically
|
|
89
|
+
- Test files create output in `test-output/` directory
|
|
90
|
+
|
|
91
|
+
### Common Development Patterns
|
|
92
|
+
- All recording operations are Promise-based
|
|
93
|
+
- Event emission for recording state changes (`started`, `stopped`, `completed`)
|
|
94
|
+
- Automatic permission checking before operations
|
|
95
|
+
- Error handling with descriptive messages for permission issues
|
|
96
|
+
|
|
97
|
+
### Platform Requirements
|
|
98
|
+
- macOS only (enforced in install.js)
|
|
99
|
+
- Native module compilation required on install
|
|
100
|
+
- Requires screen recording and accessibility permissions
|
|
101
|
+
|
|
102
|
+
### File Outputs
|
|
103
|
+
- Video recordings: `.mov` format (H.264/AAC)
|
|
104
|
+
- Cursor data: JSON format with timestamped events
|
|
105
|
+
- Thumbnails: Base64-encoded PNG data URIs
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
### Build Issues
|
|
110
|
+
1. Ensure Xcode Command Line Tools: `xcode-select --install`
|
|
111
|
+
2. Clean rebuild: `npm run clean && npm run build`
|
|
112
|
+
3. Check Node.js version compatibility (14+)
|
|
113
|
+
|
|
114
|
+
### Runtime Issues
|
|
115
|
+
1. Permission failures: Check System Preferences > Security & Privacy
|
|
116
|
+
2. Recording failures: Verify target windows/displays are accessible
|
|
117
|
+
3. Audio issues: Check audio device availability and permissions
|
|
118
|
+
|
|
119
|
+
### Native Module Loading
|
|
120
|
+
The module tries loading from `build/Release/` first, then falls back to `build/Debug/` with helpful error messages if neither exists.
|
package/cursor-data.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"x":1151,"y":726,"timestamp":20,"cursorType":"text","type":"move"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"x":48,"y":72,"timestamp":22,"unixTimeMs":1752410259890,"cursorType":"pointer","type":"move"},{"x":47,"y":71,"timestamp":87,"unixTimeMs":1752410259955,"cursorType":"pointer","type":"mousedown"},{"x":47,"y":71,"timestamp":107,"unixTimeMs":1752410259975,"cursorType":"pointer","type":"move"},{"x":47,"y":71,"timestamp":169,"unixTimeMs":1752410260037,"cursorType":"pointer","type":"mouseup"},{"x":47,"y":71,"timestamp":781,"unixTimeMs":1752410260649,"cursorType":"default","type":"move"}]
|
package/debug-cursor.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const MacRecorder = require('./index.js');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
// Create a minimal direct test
|
|
5
|
+
const recorder = new MacRecorder();
|
|
6
|
+
|
|
7
|
+
// Get current cursor position to test the new field
|
|
8
|
+
console.log('Testing cursor position API...');
|
|
9
|
+
const pos = recorder.getCursorPosition();
|
|
10
|
+
console.log('Current cursor position:', JSON.stringify(pos, null, 2));
|
|
11
|
+
|
|
12
|
+
// Test if we can start/stop cursor tracking
|
|
13
|
+
console.log('\nTesting cursor tracking start/stop...');
|
|
14
|
+
const testFile = 'debug-cursor-output.json';
|
|
15
|
+
|
|
16
|
+
// Remove existing file
|
|
17
|
+
if (fs.existsSync(testFile)) {
|
|
18
|
+
fs.unlinkSync(testFile);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const started = recorder.startCursorCapture(testFile);
|
|
22
|
+
console.log('Start result:', started);
|
|
23
|
+
|
|
24
|
+
if (started) {
|
|
25
|
+
// Wait 1 second and check what gets written
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
recorder.stopCursorCapture();
|
|
28
|
+
console.log('Stopped tracking');
|
|
29
|
+
|
|
30
|
+
// Check file content
|
|
31
|
+
if (fs.existsSync(testFile)) {
|
|
32
|
+
const content = fs.readFileSync(testFile, 'utf8');
|
|
33
|
+
console.log('\nFile content:');
|
|
34
|
+
console.log(content);
|
|
35
|
+
|
|
36
|
+
// Parse and pretty print
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(content);
|
|
39
|
+
console.log('\nParsed data:');
|
|
40
|
+
console.log(JSON.stringify(data, null, 2));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log('Error parsing JSON:', e.message);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
console.log('No output file created');
|
|
46
|
+
}
|
|
47
|
+
}, 1000);
|
|
48
|
+
} else {
|
|
49
|
+
console.log('Failed to start cursor tracking');
|
|
50
|
+
}
|
package/index.js
CHANGED
|
@@ -50,6 +50,9 @@ class MacRecorder extends EventEmitter {
|
|
|
50
50
|
// Display cache için async initialization
|
|
51
51
|
this.cachedDisplays = null;
|
|
52
52
|
this.refreshDisplayCache();
|
|
53
|
+
|
|
54
|
+
// Native cursor warm-up (cold start delay'ini önlemek için)
|
|
55
|
+
this.warmUpCursor();
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/**
|
|
@@ -582,6 +585,7 @@ class MacRecorder extends EventEmitter {
|
|
|
582
585
|
x: x,
|
|
583
586
|
y: y,
|
|
584
587
|
timestamp: timestamp,
|
|
588
|
+
unixTimeMs: Date.now(),
|
|
585
589
|
cursorType: position.cursorType,
|
|
586
590
|
type: position.eventType || "move",
|
|
587
591
|
};
|
|
@@ -735,6 +739,21 @@ class MacRecorder extends EventEmitter {
|
|
|
735
739
|
}
|
|
736
740
|
}
|
|
737
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Native cursor modülünü warm-up yapar (cold start delay'ini önler)
|
|
744
|
+
*/
|
|
745
|
+
warmUpCursor() {
|
|
746
|
+
// Async warm-up to prevent blocking constructor
|
|
747
|
+
setTimeout(() => {
|
|
748
|
+
try {
|
|
749
|
+
// Silent warm-up call
|
|
750
|
+
nativeBinding.getCursorPosition();
|
|
751
|
+
} catch (error) {
|
|
752
|
+
// Ignore warm-up errors
|
|
753
|
+
}
|
|
754
|
+
}, 10); // 10ms delay to not block initialization
|
|
755
|
+
}
|
|
756
|
+
|
|
738
757
|
/**
|
|
739
758
|
* getCurrentCursorPosition alias for getCursorPosition (backward compatibility)
|
|
740
759
|
*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const MacRecorder = require('./index.js');
|
|
2
|
+
const recorder = new MacRecorder();
|
|
3
|
+
|
|
4
|
+
console.log('Starting 3-second cursor test...');
|
|
5
|
+
const testPath = 'quick-cursor-test.json';
|
|
6
|
+
|
|
7
|
+
// Start cursor tracking
|
|
8
|
+
const started = recorder.startCursorCapture(testPath);
|
|
9
|
+
if (started) {
|
|
10
|
+
console.log('Cursor tracking started, collecting data for 3 seconds...');
|
|
11
|
+
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
console.log('Stopping cursor tracking...');
|
|
14
|
+
recorder.stopCursorCapture();
|
|
15
|
+
console.log('Done! Check quick-cursor-test.json');
|
|
16
|
+
}, 3000);
|
|
17
|
+
} else {
|
|
18
|
+
console.log('Failed to start cursor tracking');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"x":798,"y":526,"timestamp":20,"unixTimeMs":1752410795804,"cursorType":"text","type":"move"}]
|
package/src/cursor_tracker.mm
CHANGED
|
@@ -199,7 +199,9 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
CGPoint location = CGEventGetLocation(event);
|
|
202
|
-
|
|
202
|
+
NSDate *currentDate = [NSDate date];
|
|
203
|
+
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
204
|
+
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
203
205
|
NSString *cursorType = getCursorType();
|
|
204
206
|
NSString *eventType = @"move";
|
|
205
207
|
|
|
@@ -230,7 +232,8 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
230
232
|
NSDictionary *cursorInfo = @{
|
|
231
233
|
@"x": @((int)location.x),
|
|
232
234
|
@"y": @((int)location.y),
|
|
233
|
-
@"timestamp": @(
|
|
235
|
+
@"timestamp": @(timestamp),
|
|
236
|
+
@"unixTimeMs": @(unixTimeMs),
|
|
234
237
|
@"cursorType": cursorType,
|
|
235
238
|
@"type": eventType
|
|
236
239
|
};
|
|
@@ -258,14 +261,17 @@ void cursorTimerCallback() {
|
|
|
258
261
|
CFRelease(event);
|
|
259
262
|
}
|
|
260
263
|
|
|
261
|
-
|
|
264
|
+
NSDate *currentDate = [NSDate date];
|
|
265
|
+
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
266
|
+
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
262
267
|
NSString *cursorType = getCursorType();
|
|
263
268
|
|
|
264
269
|
// Cursor data oluştur
|
|
265
270
|
NSDictionary *cursorInfo = @{
|
|
266
271
|
@"x": @((int)location.x),
|
|
267
272
|
@"y": @((int)location.y),
|
|
268
|
-
@"timestamp": @(
|
|
273
|
+
@"timestamp": @(timestamp),
|
|
274
|
+
@"unixTimeMs": @(unixTimeMs),
|
|
269
275
|
@"cursorType": cursorType,
|
|
270
276
|
@"type": @"move"
|
|
271
277
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"x":798,"y":526,"timestamp":21,"unixTimeMs":1752410827986,"cursorType":"text","type":"move"}]
|
package/test-both.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const MacRecorder = require('./index.js');
|
|
2
|
+
const recorder = new MacRecorder();
|
|
3
|
+
|
|
4
|
+
console.log('Testing screen recording + cursor tracking together...');
|
|
5
|
+
|
|
6
|
+
async function testBoth() {
|
|
7
|
+
try {
|
|
8
|
+
// Start both simultaneously
|
|
9
|
+
console.log('Starting screen recording...');
|
|
10
|
+
await recorder.startRecording('./test-recordings/test-both.mov');
|
|
11
|
+
|
|
12
|
+
console.log('Starting cursor tracking...');
|
|
13
|
+
await recorder.startCursorCapture('./test-both-cursor.json');
|
|
14
|
+
|
|
15
|
+
console.log('Both running for 3 seconds...');
|
|
16
|
+
|
|
17
|
+
// Let them run together for 3 seconds
|
|
18
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
19
|
+
|
|
20
|
+
// Stop both
|
|
21
|
+
console.log('Stopping both...');
|
|
22
|
+
await recorder.stopRecording();
|
|
23
|
+
recorder.stopCursorCapture();
|
|
24
|
+
|
|
25
|
+
console.log('✅ Both completed successfully!');
|
|
26
|
+
|
|
27
|
+
// Check results
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
if (fs.existsSync('./test-recordings/test-both.mov')) {
|
|
30
|
+
const stats = fs.statSync('./test-recordings/test-both.mov');
|
|
31
|
+
console.log(`Video file: ${stats.size} bytes`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync('./test-both-cursor.json')) {
|
|
35
|
+
const content = fs.readFileSync('./test-both-cursor.json', 'utf8');
|
|
36
|
+
const data = JSON.parse(content);
|
|
37
|
+
console.log(`Cursor data: ${data.length} entries`);
|
|
38
|
+
console.log('Sample entry:', JSON.stringify(data[0], null, 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error:', error.message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
testBoth();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const MacRecorder = require('./index.js');
|
|
2
|
+
const recorder = new MacRecorder();
|
|
3
|
+
|
|
4
|
+
async function testCursorVisibility() {
|
|
5
|
+
console.log('Testing cursor visibility in screen recording...');
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
// Test 1: Cursor hidden (default)
|
|
9
|
+
console.log('🎬 Recording with cursor HIDDEN...');
|
|
10
|
+
await recorder.startRecording('./test-recordings/cursor-hidden.mov', {
|
|
11
|
+
captureCursor: false
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
15
|
+
await recorder.stopRecording();
|
|
16
|
+
console.log('✅ Hidden cursor recording done');
|
|
17
|
+
|
|
18
|
+
// Test 2: Cursor visible
|
|
19
|
+
console.log('🎬 Recording with cursor VISIBLE...');
|
|
20
|
+
await recorder.startRecording('./test-recordings/cursor-visible.mov', {
|
|
21
|
+
captureCursor: true
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
25
|
+
await recorder.stopRecording();
|
|
26
|
+
console.log('✅ Visible cursor recording done');
|
|
27
|
+
|
|
28
|
+
// Check file sizes
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const hiddenStats = fs.statSync('./test-recordings/cursor-hidden.mov');
|
|
31
|
+
const visibleStats = fs.statSync('./test-recordings/cursor-visible.mov');
|
|
32
|
+
|
|
33
|
+
console.log(`Hidden cursor video: ${hiddenStats.size} bytes`);
|
|
34
|
+
console.log(`Visible cursor video: ${visibleStats.size} bytes`);
|
|
35
|
+
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error:', error.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
testCursorVisibility();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const nativeBinding = require('./build/Release/mac_recorder.node');
|
|
2
|
+
|
|
3
|
+
console.log('Testing native cursor tracking...');
|
|
4
|
+
|
|
5
|
+
// Test native startCursorTracking function directly
|
|
6
|
+
const testFile = 'native-cursor-test.json';
|
|
7
|
+
const started = nativeBinding.startCursorTracking(testFile);
|
|
8
|
+
|
|
9
|
+
console.log('Native tracking started:', started);
|
|
10
|
+
|
|
11
|
+
if (started) {
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
const stopped = nativeBinding.stopCursorTracking();
|
|
14
|
+
console.log('Native tracking stopped:', stopped);
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
if (fs.existsSync(testFile)) {
|
|
18
|
+
const content = fs.readFileSync(testFile, 'utf8');
|
|
19
|
+
console.log('\nNative output:');
|
|
20
|
+
try {
|
|
21
|
+
const data = JSON.parse(content);
|
|
22
|
+
console.log(JSON.stringify(data.slice(0, 3), null, 2)); // Show first 3 entries
|
|
23
|
+
console.log('Total entries:', data.length);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.log('Raw content:', content.substring(0, 500));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}, 2000);
|
|
29
|
+
} else {
|
|
30
|
+
console.log('Failed to start native tracking');
|
|
31
|
+
}
|