
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
node-mac-recorder
Advanced tools
This package was developed for https://creavit.studio
A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and automatic overlay window exclusion. Built with ScreenCaptureKit for modern macOS with intelligent window filtering and Electron compatibility.
⨠Advanced Recording Capabilities
šµ Granular Audio Controls
š§ Smart Window Management
āļø Customization Options
Note: The screen recording container remains silent. Audio is saved separately as
temp_audio_<timestamp>.webmso you can remix microphone and system sound in post-processing.
This package leverages Apple's modern ScreenCaptureKit framework (macOS 12.3+) for superior recording capabilities:
Note: For applications requiring overlay exclusion (like screen recording tools with floating UI), ScreenCaptureKit automatically handles window filtering without manual intervention.
npm install node-mac-recorder
# Install Xcode Command Line Tools
xcode-select --install
# The package will automatically build native modules during installation
Apple Silicon Support: The package automatically builds for the correct architecture (ARM64 on Apple Silicon, x64 on Intel) during installation. No additional configuration required.
const MacRecorder = require("node-mac-recorder");
const recorder = new MacRecorder();
// Simple full-screen recording
await recorder.startRecording("./output.mov");
await new Promise((resolve) => setTimeout(resolve, 5000)); // Record for 5 seconds
await recorder.stopRecording();
Record multiple windows or displays simultaneously using child processes:
const MacRecorder = require("node-mac-recorder/index-multiprocess");
// Create separate recorders for each window
const recorder1 = new MacRecorder();
const recorder2 = new MacRecorder();
// Get available windows
const windows = await recorder1.getWindows();
// Record first window (e.g., Finder)
await recorder1.startRecording("window1.mov", {
windowId: windows[0].id,
frameRate: 30
});
// Wait for ScreenCaptureKit initialization
await new Promise(r => setTimeout(r, 1000));
// Record second window (e.g., Chrome)
await recorder2.startRecording("window2.mov", {
windowId: windows[1].id,
frameRate: 30
});
// Both recordings are now running in parallel! š
// Stop both after 10 seconds
await new Promise(r => setTimeout(r, 10000));
await recorder1.stopRecording();
await recorder2.stopRecording();
// Cleanup
recorder1.destroy();
recorder2.destroy();
Key Benefits:
See: MULTI_RECORDING.md for detailed documentation and examples.
const recorder = new MacRecorder();
startRecording(outputPath, options?)Starts screen recording with the specified options.
await recorder.startRecording("./recording.mov", {
// Audio Controls
includeMicrophone: false, // Enable microphone (default: false)
includeSystemAudio: true, // Enable system audio (default: true)
audioDeviceId: "device-id", // Specific audio input device (default: system default)
systemAudioDeviceId: "system-device-id", // Specific system audio device (auto-detected by default)
// Display & Window Selection
displayId: 0, // Display index (null = main display)
windowId: 12345, // Specific window ID
captureArea: {
// Custom area selection
x: 100,
y: 100,
width: 800,
height: 600,
},
// Recording Options
quality: "high", // 'low', 'medium', 'high'
frameRate: 30, // FPS (15, 30, 60)
captureCursor: false, // Show cursor (default: false)
// Camera Capture
captureCamera: true, // Enable simultaneous camera recording (video-only WebM)
cameraDeviceId: "built-in-camera-id", // Use specific camera (optional)
});
stopRecording()Stops the current recording.
const result = await recorder.stopRecording();
console.log("Recording saved to:", result.outputPath);
console.log("Camera clip saved to:", result.cameraOutputPath);
console.log("Audio clip saved to:", result.audioOutputPath);
console.log("Shared session timestamp:", result.sessionTimestamp);
The result object always contains cameraOutputPath and audioOutputPath. If either feature is disabled the corresponding value is null. The shared sessionTimestamp matches every automatically generated temp file name (temp_cursor_*, temp_camera_*, and temp_audio_*).
getWindows()Returns a list of all recordable windows.
const windows = await recorder.getWindows();
console.log(windows);
// [
// {
// id: 12345,
// name: "My App Window",
// appName: "MyApp",
// x: 100, y: 200,
// width: 800, height: 600
// },
// ...
// ]
getDisplays()Returns information about all available displays.
const displays = await recorder.getDisplays();
console.log(displays);
// [
// {
// id: 69733504,
// name: "Display 1",
// resolution: "2048x1330",
// x: 0, y: 0
// },
// ...
// ]
getAudioDevices()Returns a list of available audio input devices.
const devices = await recorder.getAudioDevices();
console.log(devices);
// [
// {
// id: "BuiltInMicDeviceID",
// name: "MacBook Pro Microphone",
// manufacturer: "Apple Inc.",
// isDefault: true,
// transportType: 0
// },
// ...
// ]
getCameraDevices()Lists available camera devices with resolution metadata.
const cameras = await recorder.getCameraDevices();
console.log(cameras);
// [
// {
// id: "FaceTime HD Camera",
// name: "FaceTime HD Camera",
// position: "front",
// maxResolution: { width: 1920, height: 1080, maxFrameRate: 60 }
// },
// ...
// ]
setCameraEnabled(enabled) ā toggles simultaneous camera recording (video-only)setCameraDevice(deviceId) ā selects the camera by unique macOS identifierisCameraEnabled() ā returns current camera toggle stategetCameraCaptureStatus() ā returns { isCapturing, outputFile, deviceId, sessionTimestamp }A typical workflow looks like this:
const cameras = await recorder.getCameraDevices();
const selectedCamera = cameras.find((camera) => camera.position === "front") || cameras[0];
recorder.setCameraDevice(selectedCamera?.id);
recorder.setCameraEnabled(true);
await recorder.startRecording("./output.mov");
// ...
const status = recorder.getCameraCaptureStatus();
console.log("Camera stream:", status.outputFile); // temp_camera_<timestamp>.webm
await recorder.stopRecording();
The camera clip is saved alongside the screen recording as
temp_camera_<timestamp>.webm. The file contains video frames only (no audio). Use the same device IDs in Electron to power live previews (navigator.mediaDevices.getUserMedia({ video: { deviceId } })). On macOS versions prior to 15, Apple does not expose a WebM encoder; the module falls back to a QuickTime container while keeping the same filename, so transcode or rename if an actual WebM container is required.
See CAMERA_CAPTURE.md for a deeper walkthrough and Electron integration tips.
setAudioDevice(deviceId) ā select the microphone inputsetSystemAudioEnabled(enabled) / isSystemAudioEnabled()setSystemAudioDevice(deviceId) ā prefer loopback devices when you need system audio onlygetAudioCaptureStatus() ā returns { isCapturing, outputFile, deviceIds, includeMicrophone, includeSystemAudio, sessionTimestamp }See AUDIO_CAPTURE.md for a step-by-step guide on enumerating devices, enabling microphone/system capture, and consuming the audio companion files.
checkPermissions()Checks macOS recording permissions.
const permissions = await recorder.checkPermissions();
console.log(permissions);
// {
// screenRecording: true,
// microphone: true,
// accessibility: true
// }
getStatus()Returns current recording status and options.
const status = recorder.getStatus();
console.log(status);
// {
// isRecording: true,
// outputPath: "./recording.mov",
// cameraOutputPath: "./temp_camera_1720000000000.webm",
// audioOutputPath: "./temp_audio_1720000000000.webm",
// cameraCapturing: true,
// audioCapturing: true,
// sessionTimestamp: 1720000000000,
// options: { ... },
// recordingTime: 15
// }
getWindowThumbnail(windowId, options?)Captures a thumbnail preview of a specific window.
const thumbnail = await recorder.getWindowThumbnail(12345, {
maxWidth: 400, // Maximum width (default: 300)
maxHeight: 300, // Maximum height (default: 200)
});
// Returns: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
// Can be used directly in <img> tags or saved as file
getDisplayThumbnail(displayId, options?)Captures a thumbnail preview of a specific display.
const thumbnail = await recorder.getDisplayThumbnail(0, {
maxWidth: 400, // Maximum width (default: 300)
maxHeight: 300, // Maximum height (default: 200)
});
// Returns: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
// Perfect for display selection UI
startCursorCapture(outputPath)Starts automatic cursor tracking and saves data to JSON file in real-time.
await recorder.startCursorCapture("./cursor-data.json");
// Cursor tracking started - automatically writing to file
stopCursorCapture()Stops cursor tracking and closes the output file.
await recorder.stopCursorCapture();
// Tracking stopped, file closed
JSON Output Format:
[
{
"x": 851,
"y": 432,
"timestamp": 201,
"cursorType": "default",
"type": "move"
},
{
"x": 851,
"y": 432,
"timestamp": 220,
"cursorType": "pointer",
"type": "mousedown"
}
]
Cursor Types: default, pointer, text, grab, grabbing, ew-resize, ns-resize, crosshair
Event Types: move, mousedown, mouseup, rightmousedown, rightmouseup
const recorder = new MacRecorder();
// List available windows
const windows = await recorder.getWindows();
console.log("Available windows:");
windows.forEach((win, i) => {
console.log(`${i + 1}. ${win.appName} - ${win.name}`);
});
// Record a specific window
const targetWindow = windows.find((w) => w.appName === "Safari");
await recorder.startRecording("./safari-recording.mov", {
windowId: targetWindow.id,
includeSystemAudio: false,
includeMicrophone: true,
captureCursor: true,
});
await new Promise((resolve) => setTimeout(resolve, 10000)); // 10 seconds
await recorder.stopRecording();
const recorder = new MacRecorder();
// List available displays
const displays = await recorder.getDisplays();
console.log("Available displays:");
displays.forEach((display, i) => {
console.log(`${i}: ${display.resolution} at (${display.x}, ${display.y})`);
});
// Record from second display
await recorder.startRecording("./second-display.mov", {
displayId: 1, // Second display
quality: "high",
frameRate: 60,
});
await new Promise((resolve) => setTimeout(resolve, 5000));
await recorder.stopRecording();
const recorder = new MacRecorder();
// Record specific screen area
await recorder.startRecording("./area-recording.mov", {
captureArea: {
x: 200,
y: 100,
width: 1200,
height: 800,
},
quality: "medium",
captureCursor: false,
});
await new Promise((resolve) => setTimeout(resolve, 8000));
await recorder.stopRecording();
const recorder = new MacRecorder();
// List available audio devices to find system audio devices
const audioDevices = await recorder.getAudioDevices();
console.log("Available audio devices:");
audioDevices.forEach((device, i) => {
console.log(`${i + 1}. ${device.name} (ID: ${device.id})`);
});
// Find system audio device (like BlackHole, Soundflower, etc.)
const systemAudioDevice = audioDevices.find(device =>
device.name.toLowerCase().includes('blackhole') ||
device.name.toLowerCase().includes('soundflower') ||
device.name.toLowerCase().includes('loopback') ||
device.name.toLowerCase().includes('aggregate')
);
if (systemAudioDevice) {
console.log(`Using system audio device: ${systemAudioDevice.name}`);
// Record with specific system audio device
await recorder.startRecording("./system-audio-specific.mov", {
includeMicrophone: false,
includeSystemAudio: true,
systemAudioDeviceId: systemAudioDevice.id, // Specify exact device
captureArea: { x: 0, y: 0, width: 1, height: 1 }, // Minimal video
});
} else {
console.log("No system audio device found. Installing BlackHole or Soundflower recommended.");
// Record with default system audio capture (may not work without virtual audio device)
await recorder.startRecording("./system-audio-default.mov", {
includeMicrophone: false,
includeSystemAudio: true, // Auto-detect system audio device
captureArea: { x: 0, y: 0, width: 1, height: 1 },
});
}
// Record for 10 seconds
await new Promise(resolve => setTimeout(resolve, 10000));
await recorder.stopRecording();
System Audio Setup:
For reliable system audio capture, install a virtual audio device:
These create aggregate audio devices that the package can detect and use for system audio capture.
const recorder = new MacRecorder();
// Listen to recording events
recorder.on("started", (outputPath) => {
console.log("Recording started:", outputPath);
});
recorder.on("stopped", (result) => {
console.log("Recording stopped:", result);
});
recorder.on("timeUpdate", (seconds) => {
console.log(`Recording time: ${seconds}s`);
});
recorder.on("completed", (outputPath) => {
console.log("Recording completed:", outputPath);
});
await recorder.startRecording("./event-recording.mov");
const recorder = new MacRecorder();
// Get windows with thumbnail previews
const windows = await recorder.getWindows();
console.log("Available windows with previews:");
for (const window of windows) {
console.log(`${window.appName} - ${window.name}`);
try {
// Generate thumbnail for each window
const thumbnail = await recorder.getWindowThumbnail(window.id, {
maxWidth: 200,
maxHeight: 150,
});
console.log(`Thumbnail: ${thumbnail.substring(0, 50)}...`);
// Use thumbnail in your UI:
// <img src="${thumbnail}" alt="Window Preview" />
} catch (error) {
console.log(`No preview available: ${error.message}`);
}
}
const recorder = new MacRecorder();
async function createDisplaySelector() {
const displays = await recorder.getDisplays();
const displayOptions = await Promise.all(
displays.map(async (display, index) => {
try {
const thumbnail = await recorder.getDisplayThumbnail(display.id);
return {
id: display.id,
name: `Display ${index + 1}`,
resolution: display.resolution,
thumbnail: thumbnail,
isPrimary: display.isPrimary,
};
} catch (error) {
return {
id: display.id,
name: `Display ${index + 1}`,
resolution: display.resolution,
thumbnail: null,
isPrimary: display.isPrimary,
};
}
})
);
return displayOptions;
}
const MacRecorder = require("node-mac-recorder");
async function trackUserInteraction() {
const recorder = new MacRecorder();
try {
// Start cursor tracking - automatically writes to file
await recorder.startCursorCapture("./user-interactions.json");
console.log("ā
Cursor tracking started...");
// Track for 5 seconds
console.log("š± Move mouse and click for 5 seconds...");
await new Promise((resolve) => setTimeout(resolve, 5000));
// Stop tracking
await recorder.stopCursorCapture();
console.log("ā
Cursor tracking completed!");
// Analyze the data
const fs = require("fs");
const data = JSON.parse(
fs.readFileSync("./user-interactions.json", "utf8")
);
console.log(`š ${data.length} events recorded`);
// Count clicks
const clicks = data.filter((d) => d.type === "mousedown").length;
if (clicks > 0) {
console.log(`š±ļø ${clicks} clicks detected`);
}
// Most used cursor type
const cursorTypes = {};
data.forEach((item) => {
cursorTypes[item.cursorType] = (cursorTypes[item.cursorType] || 0) + 1;
});
const mostUsed = Object.keys(cursorTypes).reduce((a, b) =>
cursorTypes[a] > cursorTypes[b] ? a : b
);
console.log(`šÆ Most used cursor: ${mostUsed}`);
} catch (error) {
console.error("ā Error:", error.message);
}
}
trackUserInteraction();
const MacRecorder = require("node-mac-recorder");
async function recordWithCursorTracking() {
const recorder = new MacRecorder();
try {
// Start both screen recording and cursor tracking
await Promise.all([
recorder.startRecording("./screen-recording.mov", {
captureCursor: false, // Don't show cursor in video
includeSystemAudio: true,
quality: "high",
}),
recorder.startCursorCapture("./cursor-data.json"),
]);
console.log("ā
Recording screen and tracking cursor...");
// Record for 10 seconds
await new Promise((resolve) => setTimeout(resolve, 10000));
// Stop both
await Promise.all([recorder.stopRecording(), recorder.stopCursorCapture()]);
console.log("ā
Recording completed!");
console.log("š Files created:");
console.log(" - screen-recording.mov");
console.log(" - cursor-data.json");
} catch (error) {
console.error("ā Error:", error.message);
}
}
recordWithCursorTracking();
// In main process
const { ipcMain } = require("electron");
const MacRecorder = require("node-mac-recorder");
const recorder = new MacRecorder();
ipcMain.handle("start-recording", async (event, options) => {
try {
await recorder.startRecording("./recording.mov", options);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle("stop-recording", async () => {
const result = await recorder.stopRecording();
return result;
});
ipcMain.handle("get-windows", async () => {
return await recorder.getWindows();
});
const express = require("express");
const MacRecorder = require("node-mac-recorder");
const app = express();
const recorder = new MacRecorder();
app.post("/start-recording", async (req, res) => {
try {
const { windowId, duration } = req.body;
await recorder.startRecording("./api-recording.mov", { windowId });
setTimeout(async () => {
await recorder.stopRecording();
}, duration * 1000);
res.json({ status: "started" });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get("/windows", async (req, res) => {
const windows = await recorder.getWindows();
res.json(windows);
});
When recording windows, the package automatically:
// Window at (-2000, 100) on second display
// Automatically converts to (440, 100) on display 1
await recorder.startRecording("./auto-display.mov", {
windowId: 12345, // Package handles display detection automatically
});
The getWindows() method automatically filters out:
Run the included demo to test cursor tracking:
node cursor-test.js
This will:
cursor-data.jsonIf recording fails, check macOS permissions:
# Open System Preferences > Security & Privacy > Screen Recording
# Ensure your app/terminal has permission
# Reinstall with verbose output
npm install node-mac-recorder --verbose
# Clear npm cache
npm cache clean --force
# Ensure Xcode tools are installed
xcode-select --install
// Get module information
const info = recorder.getModuleInfo();
console.log("Module info:", info);
// Check recording status
const status = recorder.getStatus();
console.log("Recording status:", status);
// Verify permissions
const permissions = await recorder.checkPermissions();
console.log("Permissions:", permissions);
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)MIT License - see LICENSE file for details.
Made for macOS š | Built with AVFoundation š¹ | Node.js Ready š
š”ļø Permissions: Ensure your host application's
Info.plistdeclares camera and microphone usage descriptions (seeMACOS_PERMISSIONS.md).
FAQs
Native macOS screen recording package for Node.js applications
The npm package node-mac-recorder receives a total of 934 weekly downloads. As such, node-mac-recorder popularity was classified as not popular.
We found that node-mac-recorder demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Ā It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.