node-mac-recorder 2.15.0 → 2.15.2
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 +3 -1
- package/ELECTRON_SAFE_README.md +372 -0
- package/build-electron-safe.js +135 -0
- package/electron-safe-binding.gyp +67 -0
- package/electron-safe-index.js +399 -0
- package/examples/electron-integration-example.js +230 -0
- package/examples/electron-preload.js +46 -0
- package/examples/electron-renderer.html +634 -0
- package/package.json +5 -2
- package/src/electron_safe/audio_capture_electron.mm +137 -0
- package/src/electron_safe/cursor_tracker_electron.mm +90 -0
- package/src/electron_safe/mac_recorder_electron.mm +337 -0
- package/src/electron_safe/screen_capture_electron.h +30 -0
- package/src/electron_safe/screen_capture_electron.mm +558 -0
- package/src/electron_safe/window_selector_electron.mm +279 -0
- package/src/screen_capture_kit.mm +77 -23
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
const { EventEmitter } = require("events");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
|
|
5
|
+
// Electron-safe native module loading
|
|
6
|
+
let electronSafeNativeBinding;
|
|
7
|
+
|
|
8
|
+
function loadElectronSafeModule() {
|
|
9
|
+
try {
|
|
10
|
+
// Try to load electron-safe build first
|
|
11
|
+
electronSafeNativeBinding = require("./build/Release/mac_recorder_electron.node");
|
|
12
|
+
console.log("✅ Loaded Electron-safe native module (Release)");
|
|
13
|
+
return true;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
try {
|
|
16
|
+
electronSafeNativeBinding = require("./build/Debug/mac_recorder_electron.node");
|
|
17
|
+
console.log("✅ Loaded Electron-safe native module (Debug)");
|
|
18
|
+
return true;
|
|
19
|
+
} catch (debugError) {
|
|
20
|
+
console.error(
|
|
21
|
+
"❌ Electron-safe native module not found. Run: npm run build:electron-safe"
|
|
22
|
+
);
|
|
23
|
+
console.error("Original error:", error.message);
|
|
24
|
+
console.error("Debug error:", debugError.message);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class ElectronSafeMacRecorder extends EventEmitter {
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
|
|
34
|
+
// Load the module safely
|
|
35
|
+
if (!loadElectronSafeModule()) {
|
|
36
|
+
throw new Error("Failed to load Electron-safe native module");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.isRecording = false;
|
|
40
|
+
this.outputPath = null;
|
|
41
|
+
this.recordingTimer = null;
|
|
42
|
+
this.recordingStartTime = null;
|
|
43
|
+
|
|
44
|
+
this.options = {
|
|
45
|
+
includeMicrophone: false,
|
|
46
|
+
includeSystemAudio: false,
|
|
47
|
+
quality: "medium",
|
|
48
|
+
frameRate: 30,
|
|
49
|
+
captureArea: null,
|
|
50
|
+
captureCursor: false,
|
|
51
|
+
showClicks: false,
|
|
52
|
+
displayId: null,
|
|
53
|
+
windowId: null,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
console.log("🔌 ElectronSafeMacRecorder initialized");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set recording options safely
|
|
61
|
+
*/
|
|
62
|
+
setOptions(options = {}) {
|
|
63
|
+
this.options = {
|
|
64
|
+
...this.options,
|
|
65
|
+
...options,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Ensure boolean values
|
|
69
|
+
this.options.includeMicrophone = options.includeMicrophone === true;
|
|
70
|
+
this.options.includeSystemAudio = options.includeSystemAudio === true;
|
|
71
|
+
this.options.captureCursor = options.captureCursor === true;
|
|
72
|
+
|
|
73
|
+
console.log("⚙️ Options updated:", this.options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Start recording with Electron-safe implementation
|
|
78
|
+
*/
|
|
79
|
+
async startRecording(outputPath, options = {}) {
|
|
80
|
+
if (this.isRecording) {
|
|
81
|
+
throw new Error("Recording is already in progress");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!outputPath) {
|
|
85
|
+
throw new Error("Output path is required");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Update options
|
|
89
|
+
this.setOptions(options);
|
|
90
|
+
|
|
91
|
+
// Ensure output directory exists
|
|
92
|
+
const outputDir = path.dirname(outputPath);
|
|
93
|
+
if (!fs.existsSync(outputDir)) {
|
|
94
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.outputPath = outputPath;
|
|
98
|
+
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
try {
|
|
101
|
+
console.log("🎬 Starting Electron-safe recording...");
|
|
102
|
+
console.log("📁 Output path:", outputPath);
|
|
103
|
+
console.log("⚙️ Options:", this.options);
|
|
104
|
+
|
|
105
|
+
// Call native function with timeout protection
|
|
106
|
+
const startTimeout = setTimeout(() => {
|
|
107
|
+
this.isRecording = false;
|
|
108
|
+
reject(new Error("Recording start timeout - Electron protection"));
|
|
109
|
+
}, 10000); // 10 second timeout
|
|
110
|
+
|
|
111
|
+
const success = electronSafeNativeBinding.startRecording(
|
|
112
|
+
outputPath,
|
|
113
|
+
this.options
|
|
114
|
+
);
|
|
115
|
+
clearTimeout(startTimeout);
|
|
116
|
+
|
|
117
|
+
if (success) {
|
|
118
|
+
this.isRecording = true;
|
|
119
|
+
this.recordingStartTime = Date.now();
|
|
120
|
+
|
|
121
|
+
// Start progress timer
|
|
122
|
+
this.recordingTimer = setInterval(() => {
|
|
123
|
+
const elapsed = Math.floor(
|
|
124
|
+
(Date.now() - this.recordingStartTime) / 1000
|
|
125
|
+
);
|
|
126
|
+
this.emit("timeUpdate", elapsed);
|
|
127
|
+
}, 1000);
|
|
128
|
+
|
|
129
|
+
// Emit started event
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
this.emit("recordingStarted", {
|
|
132
|
+
outputPath: this.outputPath,
|
|
133
|
+
timestamp: this.recordingStartTime,
|
|
134
|
+
options: this.options,
|
|
135
|
+
electronSafe: true,
|
|
136
|
+
});
|
|
137
|
+
}, 100);
|
|
138
|
+
|
|
139
|
+
this.emit("started", this.outputPath);
|
|
140
|
+
console.log("✅ Electron-safe recording started successfully");
|
|
141
|
+
resolve(this.outputPath);
|
|
142
|
+
} else {
|
|
143
|
+
console.error("❌ Failed to start Electron-safe recording");
|
|
144
|
+
reject(new Error("Failed to start recording - check permissions"));
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("❌ Exception during recording start:", error);
|
|
148
|
+
this.isRecording = false;
|
|
149
|
+
if (this.recordingTimer) {
|
|
150
|
+
clearInterval(this.recordingTimer);
|
|
151
|
+
this.recordingTimer = null;
|
|
152
|
+
}
|
|
153
|
+
reject(error);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Stop recording with Electron-safe implementation
|
|
160
|
+
*/
|
|
161
|
+
async stopRecording() {
|
|
162
|
+
if (!this.isRecording) {
|
|
163
|
+
throw new Error("No recording in progress");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
try {
|
|
168
|
+
console.log("🛑 Stopping Electron-safe recording...");
|
|
169
|
+
|
|
170
|
+
// Call native function with timeout protection
|
|
171
|
+
const stopTimeout = setTimeout(() => {
|
|
172
|
+
this.isRecording = false;
|
|
173
|
+
if (this.recordingTimer) {
|
|
174
|
+
clearInterval(this.recordingTimer);
|
|
175
|
+
this.recordingTimer = null;
|
|
176
|
+
}
|
|
177
|
+
reject(new Error("Recording stop timeout - forced cleanup"));
|
|
178
|
+
}, 10000); // 10 second timeout
|
|
179
|
+
|
|
180
|
+
const success = electronSafeNativeBinding.stopRecording();
|
|
181
|
+
clearTimeout(stopTimeout);
|
|
182
|
+
|
|
183
|
+
// Always cleanup
|
|
184
|
+
this.isRecording = false;
|
|
185
|
+
if (this.recordingTimer) {
|
|
186
|
+
clearInterval(this.recordingTimer);
|
|
187
|
+
this.recordingTimer = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = {
|
|
191
|
+
code: success ? 0 : 1,
|
|
192
|
+
outputPath: this.outputPath,
|
|
193
|
+
electronSafe: true,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
this.emit("stopped", result);
|
|
197
|
+
|
|
198
|
+
if (success) {
|
|
199
|
+
// Check if file exists
|
|
200
|
+
setTimeout(() => {
|
|
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
|
+
}
|
|
209
|
+
|
|
210
|
+
resolve(result);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error("❌ Exception during recording stop:", error);
|
|
213
|
+
|
|
214
|
+
// Force cleanup
|
|
215
|
+
this.isRecording = false;
|
|
216
|
+
if (this.recordingTimer) {
|
|
217
|
+
clearInterval(this.recordingTimer);
|
|
218
|
+
this.recordingTimer = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
reject(error);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get recording status with Electron-safe implementation
|
|
228
|
+
*/
|
|
229
|
+
getStatus() {
|
|
230
|
+
try {
|
|
231
|
+
const nativeStatus = electronSafeNativeBinding.getRecordingStatus();
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
isRecording: this.isRecording && nativeStatus.isRecording,
|
|
235
|
+
outputPath: this.outputPath,
|
|
236
|
+
options: this.options,
|
|
237
|
+
recordingTime: this.recordingStartTime
|
|
238
|
+
? Math.floor((Date.now() - this.recordingStartTime) / 1000)
|
|
239
|
+
: 0,
|
|
240
|
+
electronSafe: true,
|
|
241
|
+
nativeStatus: nativeStatus,
|
|
242
|
+
};
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error("❌ Exception getting status:", error);
|
|
245
|
+
return {
|
|
246
|
+
isRecording: this.isRecording,
|
|
247
|
+
outputPath: this.outputPath,
|
|
248
|
+
options: this.options,
|
|
249
|
+
recordingTime: this.recordingStartTime
|
|
250
|
+
? Math.floor((Date.now() - this.recordingStartTime) / 1000)
|
|
251
|
+
: 0,
|
|
252
|
+
electronSafe: true,
|
|
253
|
+
error: error.message,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get available displays with Electron-safe implementation
|
|
260
|
+
*/
|
|
261
|
+
async getDisplays() {
|
|
262
|
+
try {
|
|
263
|
+
const displays = electronSafeNativeBinding.getDisplays();
|
|
264
|
+
console.log(`📺 Found ${displays.length} displays`);
|
|
265
|
+
return displays;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error("❌ Exception getting displays:", error);
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get available windows with Electron-safe implementation
|
|
274
|
+
*/
|
|
275
|
+
async getWindows() {
|
|
276
|
+
try {
|
|
277
|
+
const windows = electronSafeNativeBinding.getWindows();
|
|
278
|
+
console.log(`🪟 Found ${windows.length} windows`);
|
|
279
|
+
return windows;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error("❌ Exception getting windows:", error);
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Check permissions with Electron-safe implementation
|
|
288
|
+
*/
|
|
289
|
+
async checkPermissions() {
|
|
290
|
+
try {
|
|
291
|
+
const hasPermission = electronSafeNativeBinding.checkPermissions();
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
screenRecording: hasPermission,
|
|
295
|
+
accessibility: hasPermission,
|
|
296
|
+
microphone: hasPermission,
|
|
297
|
+
electronSafe: true,
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error("❌ Exception checking permissions:", error);
|
|
301
|
+
return {
|
|
302
|
+
screenRecording: false,
|
|
303
|
+
accessibility: false,
|
|
304
|
+
microphone: false,
|
|
305
|
+
electronSafe: true,
|
|
306
|
+
error: error.message,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get cursor position with Electron-safe implementation
|
|
313
|
+
*/
|
|
314
|
+
getCursorPosition() {
|
|
315
|
+
try {
|
|
316
|
+
return electronSafeNativeBinding.getCursorPosition();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error("❌ Exception getting cursor position:", error);
|
|
319
|
+
throw new Error("Failed to get cursor position: " + error.message);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get window thumbnail with Electron-safe implementation
|
|
325
|
+
*/
|
|
326
|
+
async getWindowThumbnail(windowId, options = {}) {
|
|
327
|
+
try {
|
|
328
|
+
const { maxWidth = 300, maxHeight = 200 } = options;
|
|
329
|
+
const base64Image = electronSafeNativeBinding.getWindowThumbnail(
|
|
330
|
+
windowId,
|
|
331
|
+
maxWidth,
|
|
332
|
+
maxHeight
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (base64Image) {
|
|
336
|
+
return `data:image/png;base64,${base64Image}`;
|
|
337
|
+
} else {
|
|
338
|
+
throw new Error("Failed to capture window thumbnail");
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error("❌ Exception getting window thumbnail:", error);
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get display thumbnail with Electron-safe implementation
|
|
348
|
+
*/
|
|
349
|
+
async getDisplayThumbnail(displayId, options = {}) {
|
|
350
|
+
try {
|
|
351
|
+
const { maxWidth = 300, maxHeight = 200 } = options;
|
|
352
|
+
const base64Image = electronSafeNativeBinding.getDisplayThumbnail(
|
|
353
|
+
displayId,
|
|
354
|
+
maxWidth,
|
|
355
|
+
maxHeight
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (base64Image) {
|
|
359
|
+
return `data:image/png;base64,${base64Image}`;
|
|
360
|
+
} else {
|
|
361
|
+
throw new Error("Failed to capture display thumbnail");
|
|
362
|
+
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error("❌ Exception getting display thumbnail:", error);
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get audio devices with Electron-safe implementation
|
|
371
|
+
*/
|
|
372
|
+
async getAudioDevices() {
|
|
373
|
+
try {
|
|
374
|
+
const devices = electronSafeNativeBinding.getAudioDevices();
|
|
375
|
+
console.log(`🔊 Found ${devices.length} audio devices`);
|
|
376
|
+
return devices;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error("❌ Exception getting audio devices:", error);
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get module information
|
|
385
|
+
*/
|
|
386
|
+
getModuleInfo() {
|
|
387
|
+
return {
|
|
388
|
+
version: require("./package.json").version,
|
|
389
|
+
platform: process.platform,
|
|
390
|
+
arch: process.arch,
|
|
391
|
+
nodeVersion: process.version,
|
|
392
|
+
nativeModule: "mac_recorder_electron.node",
|
|
393
|
+
electronSafe: true,
|
|
394
|
+
buildTime: new Date().toISOString(),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = ElectronSafeMacRecorder;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Electron Integration Example for node-mac-recorder
|
|
2
|
+
// This example shows how to use the Electron-safe version in an Electron app
|
|
3
|
+
|
|
4
|
+
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
// Import the Electron-safe version
|
|
8
|
+
const ElectronSafeMacRecorder = require("../electron-safe-index");
|
|
9
|
+
|
|
10
|
+
let mainWindow;
|
|
11
|
+
let recorder;
|
|
12
|
+
|
|
13
|
+
function createWindow() {
|
|
14
|
+
// Create the browser window
|
|
15
|
+
mainWindow = new BrowserWindow({
|
|
16
|
+
width: 1200,
|
|
17
|
+
height: 800,
|
|
18
|
+
webPreferences: {
|
|
19
|
+
nodeIntegration: false,
|
|
20
|
+
contextIsolation: true,
|
|
21
|
+
preload: path.join(__dirname, "electron-preload.js"),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Load the app
|
|
26
|
+
mainWindow.loadFile("electron-renderer.html");
|
|
27
|
+
|
|
28
|
+
// Initialize the Electron-safe recorder
|
|
29
|
+
try {
|
|
30
|
+
recorder = new ElectronSafeMacRecorder();
|
|
31
|
+
console.log("✅ ElectronSafeMacRecorder initialized");
|
|
32
|
+
|
|
33
|
+
// Setup event listeners
|
|
34
|
+
recorder.on("recordingStarted", (data) => {
|
|
35
|
+
console.log("🎬 Recording started:", data);
|
|
36
|
+
mainWindow.webContents.send("recording-started", data);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
recorder.on("stopped", (result) => {
|
|
40
|
+
console.log("🛑 Recording stopped:", result);
|
|
41
|
+
mainWindow.webContents.send("recording-stopped", result);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
recorder.on("completed", (outputPath) => {
|
|
45
|
+
console.log("✅ Recording completed:", outputPath);
|
|
46
|
+
mainWindow.webContents.send("recording-completed", outputPath);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
recorder.on("timeUpdate", (elapsed) => {
|
|
50
|
+
mainWindow.webContents.send("recording-time-update", elapsed);
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error("❌ Failed to initialize recorder:", error);
|
|
54
|
+
dialog.showErrorBox(
|
|
55
|
+
"Recorder Error",
|
|
56
|
+
"Failed to initialize screen recorder. Please ensure the Electron-safe module is built."
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// IPC handlers for safe communication with renderer
|
|
62
|
+
ipcMain.handle("recorder:getModuleInfo", async () => {
|
|
63
|
+
try {
|
|
64
|
+
return recorder ? recorder.getModuleInfo() : null;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Error getting module info:", error);
|
|
67
|
+
return { error: error.message };
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
ipcMain.handle("recorder:checkPermissions", async () => {
|
|
72
|
+
try {
|
|
73
|
+
return await recorder.checkPermissions();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error("Error checking permissions:", error);
|
|
76
|
+
return { error: error.message };
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ipcMain.handle("recorder:getDisplays", async () => {
|
|
81
|
+
try {
|
|
82
|
+
return await recorder.getDisplays();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error("Error getting displays:", error);
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
ipcMain.handle("recorder:getWindows", async () => {
|
|
90
|
+
try {
|
|
91
|
+
return await recorder.getWindows();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error("Error getting windows:", error);
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
ipcMain.handle(
|
|
99
|
+
"recorder:startRecording",
|
|
100
|
+
async (event, outputPath, options) => {
|
|
101
|
+
try {
|
|
102
|
+
if (!recorder) {
|
|
103
|
+
throw new Error("Recorder not initialized");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log("🎬 Starting recording with options:", options);
|
|
107
|
+
const result = await recorder.startRecording(outputPath, options);
|
|
108
|
+
return { success: true, result };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("Error starting recording:", error);
|
|
111
|
+
return { success: false, error: error.message };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
ipcMain.handle("recorder:stopRecording", async () => {
|
|
117
|
+
try {
|
|
118
|
+
if (!recorder) {
|
|
119
|
+
throw new Error("Recorder not initialized");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log("🛑 Stopping recording");
|
|
123
|
+
const result = await recorder.stopRecording();
|
|
124
|
+
return { success: true, result };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("Error stopping recording:", error);
|
|
127
|
+
return { success: false, error: error.message };
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
ipcMain.handle("recorder:getStatus", async () => {
|
|
132
|
+
try {
|
|
133
|
+
return recorder ? recorder.getStatus() : null;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("Error getting status:", error);
|
|
136
|
+
return { error: error.message };
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
ipcMain.handle("recorder:getCursorPosition", async () => {
|
|
141
|
+
try {
|
|
142
|
+
return recorder ? recorder.getCursorPosition() : null;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error("Error getting cursor position:", error);
|
|
145
|
+
return { error: error.message };
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
ipcMain.handle(
|
|
150
|
+
"recorder:getDisplayThumbnail",
|
|
151
|
+
async (event, displayId, options) => {
|
|
152
|
+
try {
|
|
153
|
+
return await recorder.getDisplayThumbnail(displayId, options);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("Error getting display thumbnail:", error);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
ipcMain.handle(
|
|
162
|
+
"recorder:getWindowThumbnail",
|
|
163
|
+
async (event, windowId, options) => {
|
|
164
|
+
try {
|
|
165
|
+
return await recorder.getWindowThumbnail(windowId, options);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error("Error getting window thumbnail:", error);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
ipcMain.handle("dialog:showSaveDialog", async () => {
|
|
174
|
+
const result = await dialog.showSaveDialog(mainWindow, {
|
|
175
|
+
title: "Save Recording",
|
|
176
|
+
defaultPath: "recording.mov",
|
|
177
|
+
filters: [{ name: "Movies", extensions: ["mov", "mp4"] }],
|
|
178
|
+
});
|
|
179
|
+
return result;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// App event listeners
|
|
183
|
+
app.whenReady().then(createWindow);
|
|
184
|
+
|
|
185
|
+
app.on("window-all-closed", () => {
|
|
186
|
+
// Stop any ongoing recording before quitting
|
|
187
|
+
if (recorder && recorder.getStatus().isRecording) {
|
|
188
|
+
console.log("🛑 Stopping recording before quit");
|
|
189
|
+
recorder.stopRecording().finally(() => {
|
|
190
|
+
if (process.platform !== "darwin") {
|
|
191
|
+
app.quit();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
if (process.platform !== "darwin") {
|
|
196
|
+
app.quit();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
app.on("activate", () => {
|
|
202
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
203
|
+
createWindow();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Handle app termination gracefully
|
|
208
|
+
process.on("SIGINT", async () => {
|
|
209
|
+
console.log("🛑 SIGINT received, stopping recording...");
|
|
210
|
+
if (recorder && recorder.getStatus().isRecording) {
|
|
211
|
+
try {
|
|
212
|
+
await recorder.stopRecording();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error("Error stopping recording on exit:", error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
app.quit();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
process.on("SIGTERM", async () => {
|
|
221
|
+
console.log("🛑 SIGTERM received, stopping recording...");
|
|
222
|
+
if (recorder && recorder.getStatus().isRecording) {
|
|
223
|
+
try {
|
|
224
|
+
await recorder.stopRecording();
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error("Error stopping recording on exit:", error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
app.quit();
|
|
230
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Preload script for secure IPC communication
|
|
2
|
+
const { contextBridge, ipcRenderer } = require("electron");
|
|
3
|
+
|
|
4
|
+
// Expose protected methods that allow the renderer process to use
|
|
5
|
+
// the ipcRenderer without exposing the entire object
|
|
6
|
+
contextBridge.exposeInMainWorld("electronAPI", {
|
|
7
|
+
// Recorder API
|
|
8
|
+
recorder: {
|
|
9
|
+
getModuleInfo: () => ipcRenderer.invoke("recorder:getModuleInfo"),
|
|
10
|
+
checkPermissions: () => ipcRenderer.invoke("recorder:checkPermissions"),
|
|
11
|
+
getDisplays: () => ipcRenderer.invoke("recorder:getDisplays"),
|
|
12
|
+
getWindows: () => ipcRenderer.invoke("recorder:getWindows"),
|
|
13
|
+
startRecording: (outputPath, options) =>
|
|
14
|
+
ipcRenderer.invoke("recorder:startRecording", outputPath, options),
|
|
15
|
+
stopRecording: () => ipcRenderer.invoke("recorder:stopRecording"),
|
|
16
|
+
getStatus: () => ipcRenderer.invoke("recorder:getStatus"),
|
|
17
|
+
getCursorPosition: () => ipcRenderer.invoke("recorder:getCursorPosition"),
|
|
18
|
+
getDisplayThumbnail: (displayId, options) =>
|
|
19
|
+
ipcRenderer.invoke("recorder:getDisplayThumbnail", displayId, options),
|
|
20
|
+
getWindowThumbnail: (windowId, options) =>
|
|
21
|
+
ipcRenderer.invoke("recorder:getWindowThumbnail", windowId, options),
|
|
22
|
+
|
|
23
|
+
// Event listeners
|
|
24
|
+
onRecordingStarted: (callback) =>
|
|
25
|
+
ipcRenderer.on("recording-started", callback),
|
|
26
|
+
onRecordingStopped: (callback) =>
|
|
27
|
+
ipcRenderer.on("recording-stopped", callback),
|
|
28
|
+
onRecordingCompleted: (callback) =>
|
|
29
|
+
ipcRenderer.on("recording-completed", callback),
|
|
30
|
+
onTimeUpdate: (callback) =>
|
|
31
|
+
ipcRenderer.on("recording-time-update", callback),
|
|
32
|
+
|
|
33
|
+
// Remove listeners
|
|
34
|
+
removeAllListeners: () => {
|
|
35
|
+
ipcRenderer.removeAllListeners("recording-started");
|
|
36
|
+
ipcRenderer.removeAllListeners("recording-stopped");
|
|
37
|
+
ipcRenderer.removeAllListeners("recording-completed");
|
|
38
|
+
ipcRenderer.removeAllListeners("recording-time-update");
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Dialog API
|
|
43
|
+
dialog: {
|
|
44
|
+
showSaveDialog: () => ipcRenderer.invoke("dialog:showSaveDialog"),
|
|
45
|
+
},
|
|
46
|
+
});
|