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.
@@ -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
+ });