node-mac-recorder 1.2.8 → 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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node:*)",
5
+ "Bash(timeout:*)",
6
+ "Bash(rm:*)",
7
+ "Bash(python3:*)",
8
+ "Bash(grep:*)",
9
+ "Bash(cat:*)"
10
+ ],
11
+ "deny": []
12
+ }
13
+ }
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.
@@ -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"}]
@@ -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
@@ -585,6 +585,7 @@ class MacRecorder extends EventEmitter {
585
585
  x: x,
586
586
  y: y,
587
587
  timestamp: timestamp,
588
+ unixTimeMs: Date.now(),
588
589
  cursorType: position.cursorType,
589
590
  type: position.eventType || "move",
590
591
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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"}]
@@ -199,7 +199,9 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
199
199
  }
200
200
 
201
201
  CGPoint location = CGEventGetLocation(event);
202
- NSTimeInterval timestamp = [[NSDate date] timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
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": @((int)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
- NSTimeInterval timestamp = [[NSDate date] timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
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": @((int)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
+ }