node-mac-recorder 2.21.40 → 2.21.42
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 +29 -1
- package/CREAVIT_CODE_SNIPPETS.md +832 -0
- package/CREAVIT_INTEGRATION.md +590 -0
- package/CURSOR_MAPPING.md +112 -0
- package/DUAL_RECORDING_PLAN.md +243 -0
- package/MULTI_RECORDING.md +270 -0
- package/MultiWindowRecorder.js +546 -0
- package/README.md +51 -0
- package/binding.gyp +1 -0
- package/index-multiprocess.js +238 -0
- package/index.js +174 -19
- package/package.json +1 -1
- package/recorder-worker.js +399 -0
- package/src/audio_mixer.mm +269 -0
- package/src/audio_recorder.mm +9 -0
- package/src/camera_recorder.mm +457 -702
- package/src/cursor_tracker.mm +75 -60
- package/src/mac_recorder.mm +305 -68
- package/src/screen_capture_kit.h +18 -5
- package/src/screen_capture_kit.mm +1113 -433
- package/cursor-data-1751364226346.json +0 -1
- package/cursor-data-1751364314136.json +0 -1
- package/cursor-data.json +0 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Process MacRecorder
|
|
3
|
+
* Spawns each recorder in its own child process for true parallel recording
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { EventEmitter } = require('events');
|
|
7
|
+
const { fork } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class MacRecorderMultiProcess extends EventEmitter {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
super();
|
|
13
|
+
|
|
14
|
+
this.worker = null;
|
|
15
|
+
this.isRecording = false;
|
|
16
|
+
this.outputPath = null;
|
|
17
|
+
this.ready = false;
|
|
18
|
+
this.pendingRequests = new Map();
|
|
19
|
+
this.requestId = 0;
|
|
20
|
+
|
|
21
|
+
// Auto-spawn worker
|
|
22
|
+
this._spawnWorker();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_spawnWorker() {
|
|
26
|
+
const workerPath = path.join(__dirname, 'recorder-worker.js');
|
|
27
|
+
|
|
28
|
+
console.log(`🚀 Spawning recorder worker: ${workerPath}`);
|
|
29
|
+
|
|
30
|
+
this.worker = fork(workerPath, [], {
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
32
|
+
env: { ...process.env }
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.worker.on('message', (msg) => this._handleWorkerMessage(msg));
|
|
36
|
+
|
|
37
|
+
this.worker.on('error', (error) => {
|
|
38
|
+
console.error('❌ Worker error:', error);
|
|
39
|
+
this.emit('error', error);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.worker.on('exit', (code, signal) => {
|
|
43
|
+
console.log(`🛑 Worker exited: code=${code}, signal=${signal}`);
|
|
44
|
+
this.ready = false;
|
|
45
|
+
this.isRecording = false;
|
|
46
|
+
|
|
47
|
+
// Reject all pending requests
|
|
48
|
+
for (const [id, { reject }] of this.pendingRequests) {
|
|
49
|
+
reject(new Error('Worker process exited'));
|
|
50
|
+
}
|
|
51
|
+
this.pendingRequests.clear();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.worker.stdout.on('data', (data) => {
|
|
55
|
+
console.log(`[Worker] ${data.toString().trim()}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.worker.stderr.on('data', (data) => {
|
|
59
|
+
console.error(`[Worker Error] ${data.toString().trim()}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_handleWorkerMessage(msg) {
|
|
64
|
+
// Handle ready message
|
|
65
|
+
if (msg.type === 'ready') {
|
|
66
|
+
this.ready = true;
|
|
67
|
+
console.log('✅ Worker ready');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle events
|
|
72
|
+
if (msg.type === 'event') {
|
|
73
|
+
this.emit(msg.event, msg.data);
|
|
74
|
+
|
|
75
|
+
// Update local state based on events
|
|
76
|
+
if (msg.event === 'recordingStarted') {
|
|
77
|
+
this.isRecording = true;
|
|
78
|
+
} else if (msg.event === 'stopped') {
|
|
79
|
+
this.isRecording = false;
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle errors
|
|
85
|
+
if (msg.type === 'error') {
|
|
86
|
+
console.error('❌ Worker error:', msg.message);
|
|
87
|
+
this.emit('error', new Error(msg.message));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Handle responses to specific requests
|
|
92
|
+
if (msg.type.endsWith(':response')) {
|
|
93
|
+
const requestType = msg.type.replace(':response', '');
|
|
94
|
+
|
|
95
|
+
// Find matching pending request
|
|
96
|
+
for (const [id, { type, resolve, reject }] of this.pendingRequests) {
|
|
97
|
+
if (type === requestType) {
|
|
98
|
+
this.pendingRequests.delete(id);
|
|
99
|
+
|
|
100
|
+
if (msg.success === false) {
|
|
101
|
+
reject(new Error(msg.error || 'Request failed'));
|
|
102
|
+
} else {
|
|
103
|
+
resolve(msg.data);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_sendRequest(type, data = null, timeout = 30000) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
if (!this.worker) {
|
|
114
|
+
return reject(new Error('Worker not initialized'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!this.ready) {
|
|
118
|
+
return reject(new Error('Worker not ready'));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const id = ++this.requestId;
|
|
122
|
+
|
|
123
|
+
// Store pending request
|
|
124
|
+
this.pendingRequests.set(id, { type, resolve, reject });
|
|
125
|
+
|
|
126
|
+
// Set timeout
|
|
127
|
+
const timeoutId = setTimeout(() => {
|
|
128
|
+
if (this.pendingRequests.has(id)) {
|
|
129
|
+
this.pendingRequests.delete(id);
|
|
130
|
+
reject(new Error(`Request timeout: ${type}`));
|
|
131
|
+
}
|
|
132
|
+
}, timeout);
|
|
133
|
+
|
|
134
|
+
// Send message to worker
|
|
135
|
+
this.worker.send({ type, data, id });
|
|
136
|
+
|
|
137
|
+
// Clear timeout on completion
|
|
138
|
+
const originalResolve = resolve;
|
|
139
|
+
const originalReject = reject;
|
|
140
|
+
|
|
141
|
+
this.pendingRequests.set(id, {
|
|
142
|
+
type,
|
|
143
|
+
resolve: (value) => {
|
|
144
|
+
clearTimeout(timeoutId);
|
|
145
|
+
originalResolve(value);
|
|
146
|
+
},
|
|
147
|
+
reject: (error) => {
|
|
148
|
+
clearTimeout(timeoutId);
|
|
149
|
+
originalReject(error);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getWindows() {
|
|
156
|
+
return this._sendRequest('getWindows');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async getDisplays() {
|
|
160
|
+
const displays = await this._sendRequest('getDisplays');
|
|
161
|
+
return displays.map((display, index) => ({
|
|
162
|
+
id: display.id,
|
|
163
|
+
name: display.name,
|
|
164
|
+
width: display.width,
|
|
165
|
+
height: display.height,
|
|
166
|
+
x: display.x,
|
|
167
|
+
y: display.y,
|
|
168
|
+
isPrimary: display.isPrimary,
|
|
169
|
+
resolution: `${display.width}x${display.height}`
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async startRecording(outputPath, options = {}) {
|
|
174
|
+
if (this.isRecording) {
|
|
175
|
+
throw new Error('Recording already in progress');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!outputPath) {
|
|
179
|
+
throw new Error('Output path is required');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.outputPath = outputPath;
|
|
183
|
+
|
|
184
|
+
const result = await this._sendRequest('startRecording', {
|
|
185
|
+
outputPath,
|
|
186
|
+
options
|
|
187
|
+
}, 60000); // Longer timeout for recording start
|
|
188
|
+
|
|
189
|
+
return result.outputPath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async stopRecording() {
|
|
193
|
+
if (!this.isRecording) {
|
|
194
|
+
throw new Error('No recording in progress');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const result = await this._sendRequest('stopRecording', null, 10000);
|
|
198
|
+
this.isRecording = false;
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async startCursorCapture(filepath, options = {}) {
|
|
204
|
+
if (!this.ready) {
|
|
205
|
+
throw new Error('Worker not ready');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const result = await this._sendRequest('startCursorCapture', {
|
|
209
|
+
filepath,
|
|
210
|
+
options
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async stopCursorCapture() {
|
|
217
|
+
const result = await this._sendRequest('stopCursorCapture');
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getStatus() {
|
|
222
|
+
return this._sendRequest('getStatus');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Cleanup
|
|
226
|
+
destroy() {
|
|
227
|
+
if (this.worker) {
|
|
228
|
+
this.worker.kill();
|
|
229
|
+
this.worker = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.ready = false;
|
|
233
|
+
this.isRecording = false;
|
|
234
|
+
this.pendingRequests.clear();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = MacRecorderMultiProcess;
|
package/index.js
CHANGED
|
@@ -2,20 +2,42 @@ const { EventEmitter } = require("events");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
|
|
5
|
+
// Auto-switch to Electron-safe implementation when running under Electron and binary exists
|
|
6
|
+
let USE_ELECTRON_SAFE = false;
|
|
7
|
+
let ElectronSafeMacRecorder = null;
|
|
8
|
+
try {
|
|
9
|
+
const isElectron = !!(process && process.versions && process.versions.electron);
|
|
10
|
+
const preferElectronSafe = process.env.PREFER_ELECTRON_SAFE === "1" || process.env.USE_ELECTRON_SAFE === "1";
|
|
11
|
+
if (isElectron || preferElectronSafe) {
|
|
12
|
+
const rel = path.join(__dirname, "build", "Release", "mac_recorder_electron.node");
|
|
13
|
+
const dbg = path.join(__dirname, "build", "Debug", "mac_recorder_electron.node");
|
|
14
|
+
if (fs.existsSync(rel) || fs.existsSync(dbg) || preferElectronSafe) {
|
|
15
|
+
// Defer requiring native .node; use JS wrapper which loads it
|
|
16
|
+
ElectronSafeMacRecorder = require("./electron-safe-index");
|
|
17
|
+
USE_ELECTRON_SAFE = true;
|
|
18
|
+
console.log("✅ Auto-enabled Electron-safe MacRecorder");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch (_) {
|
|
22
|
+
// Ignore auto-switch errors; fall back to standard binding
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
// Native modülü yükle
|
|
6
26
|
let nativeBinding;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
if (!USE_ELECTRON_SAFE) {
|
|
28
|
+
try {
|
|
29
|
+
nativeBinding = require("./build/Release/mac_recorder.node");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
try {
|
|
32
|
+
nativeBinding = require("./build/Debug/mac_recorder.node");
|
|
33
|
+
} catch (debugError) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'Native module not found. Please run "npm run build" to compile the native module.\n' +
|
|
36
|
+
"Original error: " +
|
|
37
|
+
error.message
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
19
41
|
}
|
|
20
42
|
|
|
21
43
|
class MacRecorder extends EventEmitter {
|
|
@@ -26,6 +48,9 @@ class MacRecorder extends EventEmitter {
|
|
|
26
48
|
this.recordingTimer = null;
|
|
27
49
|
this.recordingStartTime = null;
|
|
28
50
|
|
|
51
|
+
// MULTI-SESSION: Unique session ID for this recorder instance
|
|
52
|
+
this.nativeSessionId = null; // Will be generated when recording starts
|
|
53
|
+
|
|
29
54
|
// Cursor capture variables
|
|
30
55
|
this.cursorCaptureInterval = null;
|
|
31
56
|
this.cursorCaptureFile = null;
|
|
@@ -155,6 +180,9 @@ class MacRecorder extends EventEmitter {
|
|
|
155
180
|
*/
|
|
156
181
|
setOptions(options = {}) {
|
|
157
182
|
// Merge options instead of replacing to preserve previously set values
|
|
183
|
+
if (options.sessionTimestamp !== undefined) {
|
|
184
|
+
this.options.sessionTimestamp = options.sessionTimestamp;
|
|
185
|
+
}
|
|
158
186
|
if (options.includeMicrophone !== undefined) {
|
|
159
187
|
this.options.includeMicrophone = options.includeMicrophone === true;
|
|
160
188
|
}
|
|
@@ -189,6 +217,10 @@ class MacRecorder extends EventEmitter {
|
|
|
189
217
|
this.options.frameRate = Math.min(Math.max(fps, 1), 120);
|
|
190
218
|
}
|
|
191
219
|
}
|
|
220
|
+
// Prefer ScreenCaptureKit (macOS 15+) toggle
|
|
221
|
+
if (options.preferScreenCaptureKit !== undefined) {
|
|
222
|
+
this.options.preferScreenCaptureKit = options.preferScreenCaptureKit === true;
|
|
223
|
+
}
|
|
192
224
|
if (options.cameraDeviceId !== undefined) {
|
|
193
225
|
this.options.cameraDeviceId =
|
|
194
226
|
typeof options.cameraDeviceId === "string" && options.cameraDeviceId.length > 0
|
|
@@ -448,9 +480,18 @@ class MacRecorder extends EventEmitter {
|
|
|
448
480
|
|
|
449
481
|
return new Promise(async (resolve, reject) => {
|
|
450
482
|
try {
|
|
451
|
-
//
|
|
452
|
-
|
|
483
|
+
// MULTI-SESSION: Generate unique session ID for this recording
|
|
484
|
+
// Use provided sessionTimestamp from options, or generate new one
|
|
485
|
+
const sessionTimestamp = this.options.sessionTimestamp || Date.now();
|
|
453
486
|
this.sessionTimestamp = sessionTimestamp;
|
|
487
|
+
this.nativeSessionId = `rec_${sessionTimestamp}_${Math.random().toString(36).substr(2, 9)}`;
|
|
488
|
+
|
|
489
|
+
console.log(`🎬 Starting recording with session ID: ${this.nativeSessionId}`);
|
|
490
|
+
if (this.options.sessionTimestamp) {
|
|
491
|
+
console.log(` ⏰ Using provided sessionTimestamp: ${this.options.sessionTimestamp}`);
|
|
492
|
+
} else {
|
|
493
|
+
console.log(` ⏰ Generated new sessionTimestamp: ${sessionTimestamp}`);
|
|
494
|
+
}
|
|
454
495
|
|
|
455
496
|
// CRITICAL FIX: Ensure main video file also uses sessionTimestamp
|
|
456
497
|
// This guarantees ALL files have the exact same timestamp
|
|
@@ -505,8 +546,12 @@ class MacRecorder extends EventEmitter {
|
|
|
505
546
|
captureCamera: this.options.captureCamera === true,
|
|
506
547
|
cameraDeviceId: this.options.cameraDeviceId || null,
|
|
507
548
|
sessionTimestamp,
|
|
549
|
+
// MULTI-SESSION: Pass unique session ID to native code
|
|
550
|
+
nativeSessionId: this.nativeSessionId,
|
|
508
551
|
frameRate: this.options.frameRate || 60,
|
|
509
552
|
quality: this.options.quality || "high",
|
|
553
|
+
// Hint native side to use ScreenCaptureKit on macOS 15+
|
|
554
|
+
preferScreenCaptureKit: this.options.preferScreenCaptureKit === true,
|
|
510
555
|
};
|
|
511
556
|
|
|
512
557
|
if (cameraFilePath) {
|
|
@@ -554,10 +599,34 @@ class MacRecorder extends EventEmitter {
|
|
|
554
599
|
|
|
555
600
|
// Only start cursor if native recording started successfully
|
|
556
601
|
if (success) {
|
|
602
|
+
// For ScreenCaptureKit (async startup), wait briefly until native fully initialized
|
|
603
|
+
// ScreenCaptureKit needs ~150-300ms to start + ~150ms for first 10 frames
|
|
604
|
+
const waitStart = Date.now();
|
|
605
|
+
try {
|
|
606
|
+
while (Date.now() - waitStart < 600) {
|
|
607
|
+
try {
|
|
608
|
+
const nativeStatus = nativeBinding && nativeBinding.getRecordingStatus ? nativeBinding.getRecordingStatus() : true;
|
|
609
|
+
if (nativeStatus) {
|
|
610
|
+
console.log(`✅ SYNC: Native recording fully ready after ${Date.now() - waitStart}ms`);
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
} catch (_) {}
|
|
614
|
+
await new Promise(r => setTimeout(r, 30));
|
|
615
|
+
}
|
|
616
|
+
} catch (_) {}
|
|
557
617
|
this.sessionTimestamp = sessionTimestamp;
|
|
618
|
+
|
|
619
|
+
// CURSOR SYNC FIX: Wait additional 300ms for first frames to start
|
|
620
|
+
// This ensures cursor tracking aligns with actual video timeline
|
|
621
|
+
// ScreenCaptureKit needs ~200-350ms to actually start capturing frames
|
|
622
|
+
// We wait 300ms to ensure cursor starts AFTER first video frame
|
|
623
|
+
console.log('⏳ CURSOR SYNC: Waiting 300ms for first video frames...');
|
|
624
|
+
await new Promise(r => setTimeout(r, 300));
|
|
625
|
+
|
|
558
626
|
const syncTimestamp = Date.now();
|
|
559
627
|
this.syncTimestamp = syncTimestamp;
|
|
560
628
|
this.recordingStartTime = syncTimestamp;
|
|
629
|
+
console.log(`🎯 CURSOR SYNC: Cursor tracking will use timestamp: ${syncTimestamp}`);
|
|
561
630
|
|
|
562
631
|
const standardCursorOptions = {
|
|
563
632
|
videoRelative: true,
|
|
@@ -1100,6 +1169,27 @@ class MacRecorder extends EventEmitter {
|
|
|
1100
1169
|
// SYNC FIX: Use pre-defined timestamp if provided for synchronization
|
|
1101
1170
|
const syncStartTime = options.startTimestamp || Date.now();
|
|
1102
1171
|
|
|
1172
|
+
// Fetch window bounds for multi-window recording
|
|
1173
|
+
if (options.multiWindowBounds && options.multiWindowBounds.length > 0) {
|
|
1174
|
+
try {
|
|
1175
|
+
const allWindows = await this.getWindows();
|
|
1176
|
+
// Match window IDs and populate bounds
|
|
1177
|
+
for (const windowInfo of options.multiWindowBounds) {
|
|
1178
|
+
const windowData = allWindows.find(w => w.id === windowInfo.windowId);
|
|
1179
|
+
if (windowData) {
|
|
1180
|
+
windowInfo.bounds = {
|
|
1181
|
+
x: windowData.x || 0,
|
|
1182
|
+
y: windowData.y || 0,
|
|
1183
|
+
width: windowData.width || 0,
|
|
1184
|
+
height: windowData.height || 0
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
console.warn('Failed to fetch window bounds for multi-window cursor tracking:', error.message);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1103
1193
|
// Use video-relative coordinate system for all recording types
|
|
1104
1194
|
if (options.videoRelative && options.displayInfo) {
|
|
1105
1195
|
// Calculate video offset based on recording type
|
|
@@ -1139,7 +1229,9 @@ class MacRecorder extends EventEmitter {
|
|
|
1139
1229
|
recordingType: options.recordingType || 'display',
|
|
1140
1230
|
// Store additional context for debugging
|
|
1141
1231
|
captureArea: options.captureArea,
|
|
1142
|
-
windowId: options.windowId
|
|
1232
|
+
windowId: options.windowId,
|
|
1233
|
+
// Multi-window bounds for location detection
|
|
1234
|
+
multiWindowBounds: options.multiWindowBounds || null
|
|
1143
1235
|
};
|
|
1144
1236
|
} else if (this.recordingDisplayInfo) {
|
|
1145
1237
|
// Fallback: Use recording display info if available
|
|
@@ -1154,7 +1246,8 @@ class MacRecorder extends EventEmitter {
|
|
|
1154
1246
|
videoWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
|
|
1155
1247
|
videoHeight: this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
|
|
1156
1248
|
videoRelative: true,
|
|
1157
|
-
recordingType: options.recordingType || 'display'
|
|
1249
|
+
recordingType: options.recordingType || 'display',
|
|
1250
|
+
multiWindowBounds: options.multiWindowBounds || null
|
|
1158
1251
|
};
|
|
1159
1252
|
} else {
|
|
1160
1253
|
// Final fallback: Main display global coordinates
|
|
@@ -1168,6 +1261,7 @@ class MacRecorder extends EventEmitter {
|
|
|
1168
1261
|
y: mainDisplay.y,
|
|
1169
1262
|
width: parseInt(mainDisplay.resolution.split("x")[0]),
|
|
1170
1263
|
height: parseInt(mainDisplay.resolution.split("x")[1]),
|
|
1264
|
+
multiWindowBounds: options.multiWindowBounds || null
|
|
1171
1265
|
};
|
|
1172
1266
|
}
|
|
1173
1267
|
} catch (error) {
|
|
@@ -1187,6 +1281,8 @@ class MacRecorder extends EventEmitter {
|
|
|
1187
1281
|
this.cursorCaptureStartTime = syncStartTime;
|
|
1188
1282
|
this.cursorCaptureFirstWrite = true;
|
|
1189
1283
|
this.lastCapturedData = null;
|
|
1284
|
+
// Store session timestamp for sync metadata
|
|
1285
|
+
this.cursorCaptureSessionTimestamp = this.sessionTimestamp;
|
|
1190
1286
|
|
|
1191
1287
|
// JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak için)
|
|
1192
1288
|
this.cursorCaptureInterval = setInterval(() => {
|
|
@@ -1236,14 +1332,73 @@ class MacRecorder extends EventEmitter {
|
|
|
1236
1332
|
height: this.cursorDisplayInfo.videoHeight,
|
|
1237
1333
|
offsetX: this.cursorDisplayInfo.videoOffsetX,
|
|
1238
1334
|
offsetY: this.cursorDisplayInfo.videoOffsetY
|
|
1239
|
-
} :
|
|
1335
|
+
} : {},
|
|
1240
1336
|
displayInfo: this.cursorDisplayInfo ? {
|
|
1241
1337
|
displayId: this.cursorDisplayInfo.displayId,
|
|
1242
1338
|
width: this.cursorDisplayInfo.displayWidth,
|
|
1243
1339
|
height: this.cursorDisplayInfo.displayHeight
|
|
1244
|
-
} :
|
|
1340
|
+
} : {}
|
|
1245
1341
|
};
|
|
1246
1342
|
|
|
1343
|
+
// Multi-window location detection with window-relative coordinates
|
|
1344
|
+
if (this.cursorDisplayInfo?.multiWindowBounds && this.cursorDisplayInfo.multiWindowBounds.length > 0) {
|
|
1345
|
+
const location = { hover: null, click: null };
|
|
1346
|
+
let windowRelativeCoords = null;
|
|
1347
|
+
|
|
1348
|
+
// Detect which window the cursor is over
|
|
1349
|
+
for (const windowInfo of this.cursorDisplayInfo.multiWindowBounds) {
|
|
1350
|
+
if (windowInfo.bounds) {
|
|
1351
|
+
const { x: wx, y: wy, width: ww, height: wh } = windowInfo.bounds;
|
|
1352
|
+
// Check if cursor is inside window bounds (using global coordinates)
|
|
1353
|
+
if (position.x >= wx && position.x <= wx + ww &&
|
|
1354
|
+
position.y >= wy && position.y <= wy + wh) {
|
|
1355
|
+
location.hover = windowInfo.windowId;
|
|
1356
|
+
|
|
1357
|
+
// Calculate window-relative coordinates
|
|
1358
|
+
// These coords are relative to the window's top-left corner (0,0)
|
|
1359
|
+
// This allows the desktop app to position cursor correctly
|
|
1360
|
+
// regardless of where the window is placed on canvas
|
|
1361
|
+
windowRelativeCoords = {
|
|
1362
|
+
windowId: windowInfo.windowId,
|
|
1363
|
+
x: position.x - wx,
|
|
1364
|
+
y: position.y - wy,
|
|
1365
|
+
// Also include window dimensions for reference
|
|
1366
|
+
windowWidth: ww,
|
|
1367
|
+
windowHeight: wh
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
// If this is a click event, mark click location
|
|
1371
|
+
// Native eventType values: 'mousedown', 'mouseup', 'rightmousedown', 'rightmouseup'
|
|
1372
|
+
const eventType = position.eventType || '';
|
|
1373
|
+
if (eventType === 'mousedown' ||
|
|
1374
|
+
eventType === 'mouseup' ||
|
|
1375
|
+
eventType === 'rightmousedown' ||
|
|
1376
|
+
eventType === 'rightmouseup') {
|
|
1377
|
+
location.click = windowInfo.windowId;
|
|
1378
|
+
}
|
|
1379
|
+
break; // Found the window, stop searching
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Add location info to cursor data
|
|
1385
|
+
cursorData.location = location;
|
|
1386
|
+
|
|
1387
|
+
// Add window-relative coordinates if cursor is over a window
|
|
1388
|
+
if (windowRelativeCoords) {
|
|
1389
|
+
cursorData.windowRelative = windowRelativeCoords;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Add sync metadata to first event only
|
|
1394
|
+
if (this.cursorCaptureFirstWrite && this.cursorCaptureSessionTimestamp) {
|
|
1395
|
+
cursorData._syncMetadata = {
|
|
1396
|
+
videoStartTime: this.cursorCaptureSessionTimestamp,
|
|
1397
|
+
cursorStartTime: this.cursorCaptureStartTime,
|
|
1398
|
+
offsetMs: this.cursorCaptureStartTime - this.cursorCaptureSessionTimestamp
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1247
1402
|
// Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
|
|
1248
1403
|
if (this.shouldCaptureEvent(cursorData)) {
|
|
1249
1404
|
// Dosyaya ekle
|
|
@@ -1519,4 +1674,4 @@ class MacRecorder extends EventEmitter {
|
|
|
1519
1674
|
// WindowSelector modülünü de export edelim
|
|
1520
1675
|
MacRecorder.WindowSelector = require('./window-selector');
|
|
1521
1676
|
|
|
1522
|
-
module.exports = MacRecorder;
|
|
1677
|
+
module.exports = USE_ELECTRON_SAFE ? ElectronSafeMacRecorder : MacRecorder;
|