node-mac-recorder 2.22.14 → 2.22.16
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/binding.gyp +1 -0
- package/electron-safe-binding.gyp +1 -0
- package/electron-safe-index.js +196 -104
- package/index.js +8 -355
- package/lib/cursorCapture/displayInfo.js +110 -0
- package/lib/cursorCapture/polling.js +404 -0
- package/package.json +1 -1
- package/src/cursor_tracker.mm +42 -109
- package/src/electron_safe/cursor_tracker_electron.mm +34 -0
- package/src/text_input_ax_snapshot.h +3 -0
- package/src/text_input_ax_snapshot.mm +161 -0
package/binding.gyp
CHANGED
package/electron-safe-index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { EventEmitter } = require("events");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
|
+
const cursorCapturePolling = require("./lib/cursorCapture/polling");
|
|
4
5
|
|
|
5
6
|
// Electron-safe native module loading
|
|
6
7
|
let electronSafeNativeBinding;
|
|
@@ -41,6 +42,17 @@ class ElectronSafeMacRecorder extends EventEmitter {
|
|
|
41
42
|
this.recordingTimer = null;
|
|
42
43
|
this.recordingStartTime = null;
|
|
43
44
|
|
|
45
|
+
this.cursorCaptureInterval = null;
|
|
46
|
+
this.cursorCaptureFile = null;
|
|
47
|
+
this.cursorCaptureStartTime = null;
|
|
48
|
+
this.cursorCaptureFirstWrite = true;
|
|
49
|
+
this.lastCapturedData = null;
|
|
50
|
+
this.cursorDisplayInfo = null;
|
|
51
|
+
this.recordingDisplayInfo = null;
|
|
52
|
+
this.cursorCaptureSessionTimestamp = null;
|
|
53
|
+
this.sessionTimestamp = null;
|
|
54
|
+
this.syncTimestamp = null;
|
|
55
|
+
|
|
44
56
|
this.options = {
|
|
45
57
|
includeMicrophone: false,
|
|
46
58
|
includeSystemAudio: false,
|
|
@@ -85,10 +97,8 @@ class ElectronSafeMacRecorder extends EventEmitter {
|
|
|
85
97
|
throw new Error("Output path is required");
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
// Update options
|
|
89
100
|
this.setOptions(options);
|
|
90
101
|
|
|
91
|
-
// Ensure output directory exists
|
|
92
102
|
const outputDir = path.dirname(outputPath);
|
|
93
103
|
if (!fs.existsSync(outputDir)) {
|
|
94
104
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
@@ -96,63 +106,123 @@ class ElectronSafeMacRecorder extends EventEmitter {
|
|
|
96
106
|
|
|
97
107
|
this.outputPath = outputPath;
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
109
|
+
console.log("🎬 Starting Electron-safe recording...");
|
|
110
|
+
console.log("📁 Output path:", outputPath);
|
|
111
|
+
console.log("⚙️ Options:", this.options);
|
|
112
|
+
|
|
113
|
+
const startTimeout = setTimeout(() => {
|
|
114
|
+
this.isRecording = false;
|
|
115
|
+
}, 10000);
|
|
116
|
+
|
|
117
|
+
let success = false;
|
|
118
|
+
try {
|
|
119
|
+
success = electronSafeNativeBinding.startRecording(
|
|
120
|
+
outputPath,
|
|
121
|
+
this.options,
|
|
122
|
+
);
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(startTimeout);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!success) {
|
|
128
|
+
console.error("❌ Failed to start Electron-safe recording");
|
|
129
|
+
throw new Error("Failed to start recording - check permissions");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.isRecording = true;
|
|
133
|
+
this.recordingStartTime = Date.now();
|
|
134
|
+
|
|
135
|
+
this.recordingTimer = setInterval(() => {
|
|
136
|
+
const elapsed = Math.floor(
|
|
137
|
+
(Date.now() - this.recordingStartTime) / 1000,
|
|
138
|
+
);
|
|
139
|
+
this.emit("timeUpdate", elapsed);
|
|
140
|
+
}, 1000);
|
|
141
|
+
|
|
142
|
+
const sessionTs =
|
|
143
|
+
options.sessionTimestamp ||
|
|
144
|
+
this.sessionTimestamp ||
|
|
145
|
+
this.recordingStartTime;
|
|
146
|
+
this.sessionTimestamp = sessionTs;
|
|
147
|
+
const cursorFilePath = path.join(
|
|
148
|
+
outputDir,
|
|
149
|
+
`temp_cursor_${sessionTs}.json`,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
let recordingDisplayInfo = null;
|
|
153
|
+
try {
|
|
154
|
+
const displays = await this.getDisplays();
|
|
155
|
+
const did = this.options.displayId;
|
|
156
|
+
let target = null;
|
|
157
|
+
if (did != null && did !== undefined) {
|
|
158
|
+
target = displays.find((d) => d.id === did);
|
|
159
|
+
}
|
|
160
|
+
if (!target) {
|
|
161
|
+
target = displays.find((d) => d.isPrimary) || displays[0];
|
|
162
|
+
}
|
|
163
|
+
if (target) {
|
|
164
|
+
recordingDisplayInfo = {
|
|
165
|
+
displayId: target.id,
|
|
166
|
+
x: target.x || 0,
|
|
167
|
+
y: target.y || 0,
|
|
168
|
+
width: target.width,
|
|
169
|
+
height: target.height,
|
|
170
|
+
logicalWidth: target.width,
|
|
171
|
+
logicalHeight: target.height,
|
|
172
|
+
};
|
|
154
173
|
}
|
|
155
|
-
}
|
|
174
|
+
} catch {
|
|
175
|
+
recordingDisplayInfo = null;
|
|
176
|
+
}
|
|
177
|
+
this.recordingDisplayInfo = recordingDisplayInfo;
|
|
178
|
+
|
|
179
|
+
const syncTimestamp = Date.now();
|
|
180
|
+
this.syncTimestamp = syncTimestamp;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await cursorCapturePolling.startCursorCapture(
|
|
184
|
+
this,
|
|
185
|
+
electronSafeNativeBinding,
|
|
186
|
+
cursorFilePath,
|
|
187
|
+
{
|
|
188
|
+
videoRelative: !!recordingDisplayInfo,
|
|
189
|
+
displayInfo: recordingDisplayInfo,
|
|
190
|
+
recordingType: this.options.windowId
|
|
191
|
+
? "window"
|
|
192
|
+
: this.options.captureArea
|
|
193
|
+
? "area"
|
|
194
|
+
: "display",
|
|
195
|
+
captureArea: this.options.captureArea || null,
|
|
196
|
+
windowId: this.options.windowId || null,
|
|
197
|
+
startTimestamp: syncTimestamp,
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
} catch (cursorError) {
|
|
201
|
+
console.warn(
|
|
202
|
+
"⚠️ Cursor tracking failed to start:",
|
|
203
|
+
cursorError.message,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const startPayloadTs = this.syncTimestamp || this.recordingStartTime;
|
|
208
|
+
const fileTimestampPayload = this.sessionTimestamp;
|
|
209
|
+
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
this.emit("recordingStarted", {
|
|
212
|
+
outputPath: this.outputPath,
|
|
213
|
+
timestamp: startPayloadTs,
|
|
214
|
+
options: this.options,
|
|
215
|
+
electronSafe: true,
|
|
216
|
+
cursorOutputPath: cursorFilePath,
|
|
217
|
+
sessionTimestamp: fileTimestampPayload,
|
|
218
|
+
syncTimestamp: startPayloadTs,
|
|
219
|
+
fileTimestamp: fileTimestampPayload,
|
|
220
|
+
});
|
|
221
|
+
}, 100);
|
|
222
|
+
|
|
223
|
+
this.emit("started", this.outputPath);
|
|
224
|
+
console.log("✅ Electron-safe recording started successfully");
|
|
225
|
+
return this.outputPath;
|
|
156
226
|
}
|
|
157
227
|
|
|
158
228
|
/**
|
|
@@ -163,64 +233,66 @@ class ElectronSafeMacRecorder extends EventEmitter {
|
|
|
163
233
|
throw new Error("No recording in progress");
|
|
164
234
|
}
|
|
165
235
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const success = electronSafeNativeBinding.stopRecording();
|
|
181
|
-
clearTimeout(stopTimeout);
|
|
236
|
+
try {
|
|
237
|
+
console.log("🛑 Stopping Electron-safe recording...");
|
|
238
|
+
|
|
239
|
+
if (this.cursorCaptureInterval) {
|
|
240
|
+
try {
|
|
241
|
+
await cursorCapturePolling.stopCursorCapture(this);
|
|
242
|
+
} catch (cursorErr) {
|
|
243
|
+
console.warn(
|
|
244
|
+
"⚠️ Cursor capture stop:",
|
|
245
|
+
cursorErr.message,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
182
249
|
|
|
183
|
-
|
|
250
|
+
const stopTimeout = setTimeout(() => {
|
|
184
251
|
this.isRecording = false;
|
|
185
252
|
if (this.recordingTimer) {
|
|
186
253
|
clearInterval(this.recordingTimer);
|
|
187
254
|
this.recordingTimer = null;
|
|
188
255
|
}
|
|
256
|
+
}, 10000);
|
|
189
257
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
outputPath: this.outputPath,
|
|
193
|
-
electronSafe: true,
|
|
194
|
-
};
|
|
258
|
+
const success = electronSafeNativeBinding.stopRecording();
|
|
259
|
+
clearTimeout(stopTimeout);
|
|
195
260
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (fs.existsSync(this.outputPath)) {
|
|
202
|
-
this.emit("completed", this.outputPath);
|
|
203
|
-
console.log("✅ Recording completed successfully");
|
|
204
|
-
} else {
|
|
205
|
-
console.warn("⚠️ Recording completed but file not found");
|
|
206
|
-
}
|
|
207
|
-
}, 1000);
|
|
208
|
-
}
|
|
261
|
+
this.isRecording = false;
|
|
262
|
+
if (this.recordingTimer) {
|
|
263
|
+
clearInterval(this.recordingTimer);
|
|
264
|
+
this.recordingTimer = null;
|
|
265
|
+
}
|
|
209
266
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
267
|
+
const result = {
|
|
268
|
+
code: success ? 0 : 1,
|
|
269
|
+
outputPath: this.outputPath,
|
|
270
|
+
electronSafe: true,
|
|
271
|
+
};
|
|
213
272
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.
|
|
219
|
-
|
|
273
|
+
this.emit("stopped", result);
|
|
274
|
+
|
|
275
|
+
if (success) {
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
if (fs.existsSync(this.outputPath)) {
|
|
278
|
+
this.emit("completed", this.outputPath);
|
|
279
|
+
console.log("✅ Recording completed successfully");
|
|
280
|
+
} else {
|
|
281
|
+
console.warn("⚠️ Recording completed but file not found");
|
|
282
|
+
}
|
|
283
|
+
}, 1000);
|
|
284
|
+
}
|
|
220
285
|
|
|
221
|
-
|
|
286
|
+
return result;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error("❌ Exception during recording stop:", error);
|
|
289
|
+
this.isRecording = false;
|
|
290
|
+
if (this.recordingTimer) {
|
|
291
|
+
clearInterval(this.recordingTimer);
|
|
292
|
+
this.recordingTimer = null;
|
|
222
293
|
}
|
|
223
|
-
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
224
296
|
}
|
|
225
297
|
|
|
226
298
|
/**
|
|
@@ -380,6 +452,26 @@ class ElectronSafeMacRecorder extends EventEmitter {
|
|
|
380
452
|
}
|
|
381
453
|
}
|
|
382
454
|
|
|
455
|
+
async startCursorCapture(intervalOrFilepath, options = {}) {
|
|
456
|
+
if (!loadElectronSafeModule()) {
|
|
457
|
+
throw new Error("Failed to load Electron-safe native module");
|
|
458
|
+
}
|
|
459
|
+
return cursorCapturePolling.startCursorCapture(
|
|
460
|
+
this,
|
|
461
|
+
electronSafeNativeBinding,
|
|
462
|
+
intervalOrFilepath,
|
|
463
|
+
options,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async stopCursorCapture() {
|
|
468
|
+
loadElectronSafeModule();
|
|
469
|
+
if (!electronSafeNativeBinding) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
return cursorCapturePolling.stopCursorCapture(this);
|
|
473
|
+
}
|
|
474
|
+
|
|
383
475
|
/**
|
|
384
476
|
* Get module information
|
|
385
477
|
*/
|