node-mac-recorder 2.21.34 → 2.21.36

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.
@@ -3,7 +3,8 @@
3
3
  "allow": [
4
4
  "Bash(cat:*)",
5
5
  "Bash(pkill:*)",
6
- "Bash(for f in test-output/*1761946670140.mov)"
6
+ "Bash(for f in test-output/*1761946670140.mov)",
7
+ "Bash(node test.js:*)"
7
8
  ],
8
9
  "deny": [],
9
10
  "ask": []
package/make-canvas.js ADDED
@@ -0,0 +1,233 @@
1
+ const MacRecorder = require('./index.js');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const http = require('http');
5
+
6
+ async function startHttpServer(port = 8080) {
7
+ return new Promise((resolve, reject) => {
8
+ const server = http.createServer((req, res) => {
9
+ const rootDir = __dirname;
10
+ let filePath = path.join(rootDir, req.url === '/' ? 'canvas-player.html' : req.url);
11
+
12
+ // Security: prevent directory traversal
13
+ if (!filePath.startsWith(rootDir)) {
14
+ res.writeHead(403);
15
+ res.end('Forbidden');
16
+ return;
17
+ }
18
+
19
+ // Check if file exists
20
+ if (!fs.existsSync(filePath)) {
21
+ res.writeHead(404);
22
+ res.end('Not found');
23
+ return;
24
+ }
25
+
26
+ // Determine content type
27
+ const ext = path.extname(filePath);
28
+ const contentTypes = {
29
+ '.html': 'text/html',
30
+ '.js': 'text/javascript',
31
+ '.json': 'application/json',
32
+ '.mov': 'video/quicktime',
33
+ '.mp4': 'video/mp4',
34
+ '.webm': 'video/webm',
35
+ '.css': 'text/css'
36
+ };
37
+
38
+ const contentType = contentTypes[ext] || 'application/octet-stream';
39
+
40
+ // Read and serve file
41
+ fs.readFile(filePath, (err, data) => {
42
+ if (err) {
43
+ res.writeHead(500);
44
+ res.end('Error loading file');
45
+ return;
46
+ }
47
+
48
+ res.writeHead(200, {
49
+ 'Content-Type': contentType,
50
+ 'Access-Control-Allow-Origin': '*'
51
+ });
52
+ res.end(data);
53
+ });
54
+ });
55
+
56
+ server.listen(port, () => {
57
+ console.log(`\n🌐 HTTP Server started at http://localhost:${port}`);
58
+ resolve(server);
59
+ });
60
+
61
+ server.on('error', (err) => {
62
+ if (err.code === 'EADDRINUSE') {
63
+ console.log(` Port ${port} is busy, trying ${port + 1}...`);
64
+ startHttpServer(port + 1).then(resolve).catch(reject);
65
+ } else {
66
+ reject(err);
67
+ }
68
+ });
69
+ });
70
+ }
71
+
72
+ async function runCanvasTest() {
73
+ console.log('šŸŽ¬ Canvas Test: Starting 10-second recording with all features...\n');
74
+
75
+ const recorder = new MacRecorder();
76
+ const outputDir = path.join(__dirname, 'test-output');
77
+
78
+ // Ensure output directory exists
79
+ if (!fs.existsSync(outputDir)) {
80
+ fs.mkdirSync(outputDir, { recursive: true });
81
+ }
82
+
83
+ try {
84
+ // Check permissions first
85
+ const permissions = await recorder.checkPermissions();
86
+ console.log('šŸ“‹ Permissions:', permissions);
87
+
88
+ if (!permissions.screenRecording) {
89
+ console.error('āŒ Screen recording permission not granted!');
90
+ console.error(' Please enable screen recording in System Preferences > Security & Privacy');
91
+ process.exit(1);
92
+ }
93
+
94
+ // Get available devices
95
+ console.log('\nšŸ” Detecting devices...');
96
+ const cameras = await recorder.getCameraDevices();
97
+ const audioDevices = await recorder.getAudioDevices();
98
+ const displays = await recorder.getDisplays();
99
+
100
+ console.log(` šŸ“¹ Cameras found: ${cameras.length}`);
101
+ if (cameras.length > 0) {
102
+ cameras.forEach((cam, i) => {
103
+ console.log(` ${i + 1}. ${cam.name} (${cam.position})`);
104
+ });
105
+ }
106
+
107
+ console.log(` šŸŽ™ļø Audio devices found: ${audioDevices.length}`);
108
+ if (audioDevices.length > 0) {
109
+ audioDevices.forEach((dev, i) => {
110
+ console.log(` ${i + 1}. ${dev.name}${dev.isDefault ? ' (default)' : ''}`);
111
+ });
112
+ }
113
+
114
+ console.log(` šŸ–„ļø Displays found: ${displays.length}`);
115
+ displays.forEach((display, i) => {
116
+ console.log(` ${i + 1}. ${display.name} ${display.resolution}${display.isPrimary ? ' (primary)' : ''}`);
117
+ });
118
+
119
+ // Setup recording options
120
+ const outputPath = path.join(outputDir, 'screen.mov');
121
+ const recordingOptions = {
122
+ includeMicrophone: true,
123
+ includeSystemAudio: false, // Typically off to avoid feedback
124
+ captureCursor: true,
125
+ captureCamera: cameras.length > 0,
126
+ cameraDeviceId: cameras.length > 0 ? cameras[0].id : null,
127
+ quality: 'high',
128
+ frameRate: 60
129
+ };
130
+
131
+ console.log('\nāš™ļø Recording options:', recordingOptions);
132
+ console.log('\nšŸŽ„ Starting recording...');
133
+
134
+ // Event listeners for tracking
135
+ recorder.on('recordingStarted', (info) => {
136
+ console.log('\nāœ… Recording started!');
137
+ console.log(' Screen output:', info.outputPath);
138
+ if (info.cameraOutputPath) {
139
+ console.log(' Camera output:', info.cameraOutputPath);
140
+ }
141
+ if (info.audioOutputPath) {
142
+ console.log(' Audio output:', info.audioOutputPath);
143
+ }
144
+ if (info.cursorOutputPath) {
145
+ console.log(' Cursor data:', info.cursorOutputPath);
146
+ }
147
+ console.log(' Session timestamp:', info.sessionTimestamp);
148
+ });
149
+
150
+ recorder.on('timeUpdate', (seconds) => {
151
+ process.stdout.write(`\rā±ļø Recording: ${seconds}/10 seconds`);
152
+ });
153
+
154
+ // Start recording
155
+ await recorder.startRecording(outputPath, recordingOptions);
156
+
157
+ // Record for 10 seconds
158
+ await new Promise(resolve => setTimeout(resolve, 10000));
159
+
160
+ console.log('\n\nšŸ›‘ Stopping recording...');
161
+ const result = await recorder.stopRecording();
162
+
163
+ console.log('\nāœ… Recording completed!');
164
+ console.log(' Screen:', result.outputPath);
165
+ if (result.cameraOutputPath) {
166
+ console.log(' Camera:', result.cameraOutputPath);
167
+ }
168
+ if (result.audioOutputPath) {
169
+ console.log(' Audio:', result.audioOutputPath);
170
+ }
171
+
172
+ // Find cursor data file
173
+ const files = fs.readdirSync(outputDir);
174
+ const cursorFile = files.find(f => f.startsWith('temp_cursor_') && f.endsWith('.json'));
175
+ const cursorPath = cursorFile ? path.join(outputDir, cursorFile) : null;
176
+
177
+ if (cursorPath && fs.existsSync(cursorPath)) {
178
+ console.log(' Cursor:', cursorPath);
179
+
180
+ // Validate cursor data
181
+ const cursorData = JSON.parse(fs.readFileSync(cursorPath, 'utf8'));
182
+ console.log(` Cursor events captured: ${cursorData.length}`);
183
+ }
184
+
185
+ // Create metadata file for the player
186
+ const metadata = {
187
+ recordingTimestamp: result.sessionTimestamp,
188
+ syncTimestamp: result.syncTimestamp,
189
+ duration: 10,
190
+ files: {
191
+ screen: path.basename(result.outputPath),
192
+ camera: result.cameraOutputPath ? path.basename(result.cameraOutputPath) : null,
193
+ audio: result.audioOutputPath ? path.basename(result.audioOutputPath) : null,
194
+ cursor: cursorFile
195
+ },
196
+ options: recordingOptions
197
+ };
198
+
199
+ const metadataPath = path.join(outputDir, 'recording-metadata.json');
200
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
201
+ console.log(' Metadata:', metadataPath);
202
+
203
+ // Start HTTP server to avoid CORS issues
204
+ console.log('\nšŸŽØ Starting Canvas Player...');
205
+ const server = await startHttpServer(8080);
206
+ const serverPort = server.address().port;
207
+ const url = `http://localhost:${serverPort}/canvas-player.html`;
208
+
209
+ console.log(` URL: ${url}`);
210
+ console.log('\n✨ Opening player in browser...');
211
+ console.log(' Press Ctrl+C to stop the server when done.\n');
212
+
213
+ // Open in browser (macOS)
214
+ const { exec } = require('child_process');
215
+ exec(`open "${url}"`);
216
+
217
+ // Keep server running
218
+ process.on('SIGINT', () => {
219
+ console.log('\n\nšŸ‘‹ Shutting down server...');
220
+ server.close(() => {
221
+ console.log('āœ… Server closed. Goodbye!');
222
+ process.exit(0);
223
+ });
224
+ });
225
+
226
+ } catch (error) {
227
+ console.error('\nāŒ Error:', error.message);
228
+ console.error(error.stack);
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ runCanvasTest();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.34",
3
+ "version": "2.21.36",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "build:electron-safe": "node build-electron-safe.js",
44
44
  "test:electron-safe": "node test-electron-safe.js",
45
45
  "clean:electron-safe": "node-gyp clean && rm -rf build",
46
- "canvas": "node canvas-test.js"
46
+ "canvas": "node make-canvas.js"
47
47
  },
48
48
  "dependencies": {
49
49
  "node-addon-api": "^7.0.0",
@@ -747,17 +747,19 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
747
747
  }
748
748
 
749
749
  // Delay stop slightly so camera ends close to audio length.
750
- // Tunable via env var CAMERA_TAIL_SECONDS (default 0.11s)
751
- NSTimeInterval cameraTailSeconds = 1.7;
750
+ // SYNC FIX: Optimized tail seconds for audio/camera sync
751
+ // This compensates for camera cold-start delay and trailing frame capture
752
+ // Tunable via env var CAMERA_TAIL_SECONDS (default 0.55s for optimal sync)
753
+ NSTimeInterval cameraTailSeconds = 0.55;
752
754
  const char *tailEnv = getenv("CAMERA_TAIL_SECONDS");
753
755
  if (tailEnv) {
754
756
  double parsed = atof(tailEnv);
755
- if (parsed >= 0.0 && parsed <= 1.0) {
757
+ if (parsed >= 0.0 && parsed <= 2.0) {
756
758
  cameraTailSeconds = parsed;
757
759
  }
758
760
  }
759
- MRLog(@"ā³ CameraRecorder: Delaying stop by %.3fs for tail capture", cameraTailSeconds);
760
761
  if (cameraTailSeconds > 0) {
762
+ MRLog(@"ā³ CameraRecorder: Delaying stop by %.3fs for tail capture", cameraTailSeconds);
761
763
  [NSThread sleepForTimeInterval:cameraTailSeconds];
762
764
  }
763
765
 
@@ -379,21 +379,21 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
379
379
 
380
380
  if (isElectron) {
381
381
  MRLog(@"⚔ Electron environment detected");
382
- MRLog(@"šŸ”§ CRITICAL FIX: Forcing AVFoundation for Electron stability");
383
- MRLog(@" Reason: ScreenCaptureKit has thread safety issues in Electron (SIGTRAP crashes)");
382
+ MRLog(@"āœ… ELECTRON-FIRST: Both ScreenCaptureKit and AVFoundation fully supported");
384
383
  }
385
384
 
386
- // CRITICAL FIX: Always use AVFoundation for stability
387
- // ScreenCaptureKit has file writing issues in Node.js environment
388
- // AVFoundation works reliably in both Node.js and Electron
389
- BOOL forceAVFoundation = YES;
385
+ // CRITICAL FIX: Use ScreenCaptureKit on macOS 15+ for best compatibility
386
+ // Both ScreenCaptureKit and AVFoundation work in Electron (Electron-first design)
387
+ // Only fallback to AVFoundation if ScreenCaptureKit is unavailable
388
+ BOOL forceAVFoundation = NO; // Let system choose based on availability
390
389
 
391
- MRLog(@"šŸ”§ FRAMEWORK SELECTION: Using AVFoundation for stability");
390
+ MRLog(@"šŸ”§ FRAMEWORK SELECTION: Smart selection based on macOS version");
392
391
  MRLog(@" Environment: %@", isElectron ? @"Electron" : @"Node.js");
393
392
  MRLog(@" macOS: %ld.%ld.%ld", (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
394
393
 
395
- // Electron-first priority: ALWAYS use AVFoundation in Electron for stability
396
- // ScreenCaptureKit has severe thread safety issues in Electron causing SIGTRAP crashes
394
+ // Electron-first priority: Both frameworks fully supported in Electron
395
+ // macOS 15+: Try ScreenCaptureKit first (better performance)
396
+ // macOS 14/13: Use AVFoundation (ScreenCaptureKit not available)
397
397
  if (isM15Plus && !forceAVFoundation) {
398
398
  if (isElectron) {
399
399
  MRLog(@"⚔ ELECTRON PRIORITY: macOS 15+ Electron → ScreenCaptureKit with full support");