node-mac-recorder
Version:
Native macOS screen recording package for Node.js applications
400 lines (352 loc) • 10.1 kB
JavaScript
const { EventEmitter } = require("events");
const path = require("path");
const fs = require("fs");
// Electron-safe native module loading
let electronSafeNativeBinding;
function loadElectronSafeModule() {
try {
// Try to load electron-safe build first
electronSafeNativeBinding = require("./build/Release/mac_recorder_electron.node");
console.log("✅ Loaded Electron-safe native module (Release)");
return true;
} catch (error) {
try {
electronSafeNativeBinding = require("./build/Debug/mac_recorder_electron.node");
console.log("✅ Loaded Electron-safe native module (Debug)");
return true;
} catch (debugError) {
console.error(
"❌ Electron-safe native module not found. Run: npm run build:electron-safe"
);
console.error("Original error:", error.message);
console.error("Debug error:", debugError.message);
return false;
}
}
}
class ElectronSafeMacRecorder extends EventEmitter {
constructor() {
super();
// Load the module safely
if (!loadElectronSafeModule()) {
throw new Error("Failed to load Electron-safe native module");
}
this.isRecording = false;
this.outputPath = null;
this.recordingTimer = null;
this.recordingStartTime = null;
this.options = {
includeMicrophone: false,
includeSystemAudio: false,
quality: "medium",
frameRate: 30,
captureArea: null,
captureCursor: false,
showClicks: false,
displayId: null,
windowId: null,
};
console.log("🔌 ElectronSafeMacRecorder initialized");
}
/**
* Set recording options safely
*/
setOptions(options = {}) {
this.options = {
...this.options,
...options,
};
// Ensure boolean values
this.options.includeMicrophone = options.includeMicrophone === true;
this.options.includeSystemAudio = options.includeSystemAudio === true;
this.options.captureCursor = options.captureCursor === true;
console.log("⚙️ Options updated:", this.options);
}
/**
* Start recording with Electron-safe implementation
*/
async startRecording(outputPath, options = {}) {
if (this.isRecording) {
throw new Error("Recording is already in progress");
}
if (!outputPath) {
throw new Error("Output path is required");
}
// Update options
this.setOptions(options);
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
this.outputPath = outputPath;
return new Promise((resolve, reject) => {
try {
console.log("🎬 Starting Electron-safe recording...");
console.log("📁 Output path:", outputPath);
console.log("⚙️ Options:", this.options);
// Call native function with timeout protection
const startTimeout = setTimeout(() => {
this.isRecording = false;
reject(new Error("Recording start timeout - Electron protection"));
}, 10000); // 10 second timeout
const success = electronSafeNativeBinding.startRecording(
outputPath,
this.options
);
clearTimeout(startTimeout);
if (success) {
this.isRecording = true;
this.recordingStartTime = Date.now();
// Start progress timer
this.recordingTimer = setInterval(() => {
const elapsed = Math.floor(
(Date.now() - this.recordingStartTime) / 1000
);
this.emit("timeUpdate", elapsed);
}, 1000);
// Emit started event
setTimeout(() => {
this.emit("recordingStarted", {
outputPath: this.outputPath,
timestamp: this.recordingStartTime,
options: this.options,
electronSafe: true,
});
}, 100);
this.emit("started", this.outputPath);
console.log("✅ Electron-safe recording started successfully");
resolve(this.outputPath);
} else {
console.error("❌ Failed to start Electron-safe recording");
reject(new Error("Failed to start recording - check permissions"));
}
} catch (error) {
console.error("❌ Exception during recording start:", error);
this.isRecording = false;
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
reject(error);
}
});
}
/**
* Stop recording with Electron-safe implementation
*/
async stopRecording() {
if (!this.isRecording) {
throw new Error("No recording in progress");
}
return new Promise((resolve, reject) => {
try {
console.log("🛑 Stopping Electron-safe recording...");
// Call native function with timeout protection
const stopTimeout = setTimeout(() => {
this.isRecording = false;
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
reject(new Error("Recording stop timeout - forced cleanup"));
}, 10000); // 10 second timeout
const success = electronSafeNativeBinding.stopRecording();
clearTimeout(stopTimeout);
// Always cleanup
this.isRecording = false;
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
const result = {
code: success ? 0 : 1,
outputPath: this.outputPath,
electronSafe: true,
};
this.emit("stopped", result);
if (success) {
// Check if file exists
setTimeout(() => {
if (fs.existsSync(this.outputPath)) {
this.emit("completed", this.outputPath);
console.log("✅ Recording completed successfully");
} else {
console.warn("⚠️ Recording completed but file not found");
}
}, 1000);
}
resolve(result);
} catch (error) {
console.error("❌ Exception during recording stop:", error);
// Force cleanup
this.isRecording = false;
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
reject(error);
}
});
}
/**
* Get recording status with Electron-safe implementation
*/
getStatus() {
try {
const nativeStatus = electronSafeNativeBinding.getRecordingStatus();
return {
isRecording: this.isRecording && nativeStatus.isRecording,
outputPath: this.outputPath,
options: this.options,
recordingTime: this.recordingStartTime
? Math.floor((Date.now() - this.recordingStartTime) / 1000)
: 0,
electronSafe: true,
nativeStatus: nativeStatus,
};
} catch (error) {
console.error("❌ Exception getting status:", error);
return {
isRecording: this.isRecording,
outputPath: this.outputPath,
options: this.options,
recordingTime: this.recordingStartTime
? Math.floor((Date.now() - this.recordingStartTime) / 1000)
: 0,
electronSafe: true,
error: error.message,
};
}
}
/**
* Get available displays with Electron-safe implementation
*/
async getDisplays() {
try {
const displays = electronSafeNativeBinding.getDisplays();
console.log(`📺 Found ${displays.length} displays`);
return displays;
} catch (error) {
console.error("❌ Exception getting displays:", error);
return [];
}
}
/**
* Get available windows with Electron-safe implementation
*/
async getWindows() {
try {
const windows = electronSafeNativeBinding.getWindows();
console.log(`🪟 Found ${windows.length} windows`);
return windows;
} catch (error) {
console.error("❌ Exception getting windows:", error);
return [];
}
}
/**
* Check permissions with Electron-safe implementation
*/
async checkPermissions() {
try {
const hasPermission = electronSafeNativeBinding.checkPermissions();
return {
screenRecording: hasPermission,
accessibility: hasPermission,
microphone: hasPermission,
electronSafe: true,
};
} catch (error) {
console.error("❌ Exception checking permissions:", error);
return {
screenRecording: false,
accessibility: false,
microphone: false,
electronSafe: true,
error: error.message,
};
}
}
/**
* Get cursor position with Electron-safe implementation
*/
getCursorPosition() {
try {
return electronSafeNativeBinding.getCursorPosition();
} catch (error) {
console.error("❌ Exception getting cursor position:", error);
throw new Error("Failed to get cursor position: " + error.message);
}
}
/**
* Get window thumbnail with Electron-safe implementation
*/
async getWindowThumbnail(windowId, options = {}) {
try {
const { maxWidth = 300, maxHeight = 200 } = options;
const base64Image = electronSafeNativeBinding.getWindowThumbnail(
windowId,
maxWidth,
maxHeight
);
if (base64Image) {
return `data:image/png;base64,${base64Image}`;
} else {
throw new Error("Failed to capture window thumbnail");
}
} catch (error) {
console.error("❌ Exception getting window thumbnail:", error);
throw error;
}
}
/**
* Get display thumbnail with Electron-safe implementation
*/
async getDisplayThumbnail(displayId, options = {}) {
try {
const { maxWidth = 300, maxHeight = 200 } = options;
const base64Image = electronSafeNativeBinding.getDisplayThumbnail(
displayId,
maxWidth,
maxHeight
);
if (base64Image) {
return `data:image/png;base64,${base64Image}`;
} else {
throw new Error("Failed to capture display thumbnail");
}
} catch (error) {
console.error("❌ Exception getting display thumbnail:", error);
throw error;
}
}
/**
* Get audio devices with Electron-safe implementation
*/
async getAudioDevices() {
try {
const devices = electronSafeNativeBinding.getAudioDevices();
console.log(`🔊 Found ${devices.length} audio devices`);
return devices;
} catch (error) {
console.error("❌ Exception getting audio devices:", error);
return [];
}
}
/**
* Get module information
*/
getModuleInfo() {
return {
version: require("./package.json").version,
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
nativeModule: "mac_recorder_electron.node",
electronSafe: true,
buildTime: new Date().toISOString(),
};
}
}
module.exports = ElectronSafeMacRecorder;