Native macOS screen recording package for Node.js applications
npm install node-mac-recorderThis 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
- š„ļø Full Screen Recording - Capture entire displays with ScreenCaptureKit
- šŖ Window-Specific Recording - Record individual application windows
- šÆ Area Selection - Record custom screen regions
- š±ļø Multi-Display Support - Automatic display detection and selection
- šØ Cursor Control - Toggle cursor visibility in recordings
- š±ļø Cursor Tracking - Track mouse position, cursor types, and click events
- š· Camera Recording - Capture silent camera video alongside screen recordings
- š Audio Capture - Record microphone/system audio into synchronized companion files
- š« Automatic Overlay Exclusion - Overlay windows automatically excluded from recordings
- ā” Electron Compatible - Enhanced crash protection for Electron applications
- š¬ Multi-Window Recording - ⨠NEW! Record multiple windows/displays simultaneously
šµ Granular Audio Controls
- š¤ Microphone Audio - Separate microphone control (default: off)
- š System Audio - System audio capture (default: on)
- š» Audio Device Listing - Enumerate available audio devices
- šļø Device Selection - Choose specific audio input devices
š§ Smart Window Management
- š Window Discovery - List all visible application windows
- šÆ Automatic Coordinate Conversion - Handle multi-display coordinate systems
- š Display ID Detection - Automatically select correct display for window recording
- š¼ļø Window Filtering - Smart filtering of recordable windows
- šļø Preview Thumbnails - Generate window and display preview images
āļø Customization Options
- š¬ Quality Control - Adjustable recording quality presets
- šļø Frame Rate Control - Custom frame rate settings
- š Flexible Output - Custom output paths and formats
- š Permission Management - Built-in permission checking
> Note: The screen recording container remains silent. Audio is saved separately as temp_audio_ so 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:
- šÆ Native Overlay Exclusion: Overlay windows are automatically filtered out during recording
- š Enhanced Performance: Direct system-level recording with optimized resource usage
- š”ļø Crash Protection: Advanced safety layers for Electron applications
- š± Future-Proof: Built on Apple's latest screen capture technology
- šØ Better Quality: Improved frame handling and video encoding
> Note: For applications requiring overlay exclusion (like screen recording tools with floating UI), ScreenCaptureKit automatically handles window filtering without manual intervention.
``bash`
npm install node-mac-recorder
- macOS 12.3+ (Monterey or later) - Required for ScreenCaptureKit
- Node.js 14+
- Xcode Command Line Tools
- Screen Recording Permission (automatically requested)
- CPU Architecture: Intel (x64) and Apple Silicon (ARM64) supported
`bashInstall Xcode Command Line Tools
xcode-select --install
Apple Silicon Support: The package automatically builds for the correct architecture (ARM64 on Apple Silicon, x64 on Intel) during installation. No additional configuration required.
Quick Start
`javascript
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();
`Multi-Window Recording ⨠NEW!
Record multiple windows or displays simultaneously using child processes:
`javascript
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:
- ā
No native code changes required
- ā
Each recorder runs in its own process
- ā
True parallel recording
- ā
Separate output files
- ā
Independent control
See:
MULTI_RECORDING.md for detailed documentation and examples.API Reference
$3
`javascript
const recorder = new MacRecorder();
`$3
####
startRecording(outputPath, options?)Starts screen recording with the specified options.
`javascript
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.
`javascript
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.
`javascript
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.
`javascript
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.
`javascript
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.
`javascript
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 }
// },
// ...
// ]
`$3
-
setCameraEnabled(enabled) ā toggles simultaneous camera recording (video-only)
- setCameraDevice(deviceId) ā selects the camera by unique macOS identifier
- isCameraEnabled() ā returns current camera toggle state
- getCameraCaptureStatus() ā returns { isCapturing, outputFile, deviceId, sessionTimestamp }A typical workflow looks like this:
`javascript
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_.webm
await recorder.stopRecording();
`> The camera clip is saved alongside the screen recording as
temp_camera_. 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.$3
-
setAudioDevice(deviceId) ā select the microphone input
- setSystemAudioEnabled(enabled) / isSystemAudioEnabled()
- setSystemAudioDevice(deviceId) ā prefer loopback devices when you need system audio only
- getAudioCaptureStatus() ā 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.
`javascript
const permissions = await recorder.checkPermissions();
console.log(permissions);
// {
// screenRecording: true,
// microphone: true,
// accessibility: true
// }
`####
getStatus()Returns current recording status and options.
`javascript
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.
`javascript
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
tags or saved as file
`####
getDisplayThumbnail(displayId, options?)Captures a thumbnail preview of a specific display.
`javascript
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
`$3
####
startCursorCapture(outputPath)Starts automatic cursor tracking and saves data to JSON file in real-time.
`javascript
await recorder.startCursorCapture("./cursor-data.json");
// Cursor tracking started - automatically writing to file
`####
stopCursorCapture()Stops cursor tracking and closes the output file.
`javascript
await recorder.stopCursorCapture();
// Tracking stopped, file closed
`JSON Output Format:
`json
[
{
"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, rightmouseupUsage Examples
$3
`javascript
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();
`$3
`javascript
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();
`$3
`javascript
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();
`$3
`javascript
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:
1. BlackHole (Free): https://github.com/ExistentialAudio/BlackHole
2. Soundflower (Free): https://github.com/mattingalls/Soundflower
3. Loopback (Paid): https://rogueamoeba.com/loopback/
These create aggregate audio devices that the package can detect and use for system audio capture.
$3
`javascript
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");
`$3
`javascript
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:
// 
} catch (error) {
console.log(
No preview available: ${error.message});
}
}
`$3
`javascript
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;
}
`$3
`javascript
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();
`$3
`javascript
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();
`Integration Examples
$3
`javascript
// 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();
});
`$3
`javascript
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);
});
`Advanced Features
$3
When recording windows, the package automatically:
1. Detects Window Location - Determines which display contains the window
2. Converts Coordinates - Translates global coordinates to display-relative coordinates
3. Sets Display ID - Automatically selects the correct display for recording
4. Handles Multi-Monitor - Works seamlessly across multiple displays
`javascript
// 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
});
`$3
The
getWindows() method automatically filters out:- System windows (Dock, Menu Bar)
- Hidden windows
- Very small windows (< 50x50 pixels)
- Windows without names
$3
- Native Implementation - Uses AVFoundation for optimal performance
- Minimal Overhead - Low CPU usage during recording
- Memory Efficient - Proper memory management in native layer
- Quality Presets - Balanced quality/performance options
Testing
Run the included demo to test cursor tracking:
`bash
node cursor-test.js
`This will:
- ā
Start cursor tracking for 5 seconds
- š± Capture mouse movements and clicks
- š Save data to
cursor-data.json
- š±ļø Report clicks detectedTroubleshooting
$3
If recording fails, check macOS permissions:
`bash
Open System Preferences > Security & Privacy > Screen Recording
Ensure your app/terminal has permission
`$3
`bash
Reinstall with verbose output
npm install node-mac-recorder --verboseClear npm cache
npm cache clean --forceEnsure Xcode tools are installed
xcode-select --install
`$3
1. Empty/Black Video: Check screen recording permissions
2. No Audio: Verify audio permissions and device availability
3. Window Not Found: Ensure target window is visible and not minimized
4. Coordinate Issues: Window may be on different display (handled automatically)
$3
`javascript
// 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);
`Performance Considerations
- Recording Quality: Higher quality increases file size and CPU usage
- Frame Rate: 30fps recommended for most use cases, 60fps for smooth motion
- Audio: System audio capture adds minimal overhead
- Window Recording: Slightly more efficient than full-screen recording
- Multi-Display: No significant performance impact
File Formats
- Output Format: MOV (QuickTime)
- Video Codec: H.264
- Audio Codec: AAC
- Container: QuickTime compatible
Contributing
1. Fork the repository
2. Create your feature branch (
git checkout -b feature/amazing-feature)
3. Commit your changes (git commit -m 'Add amazing feature')
4. Push to the branch (git push origin feature/amazing-feature)
5. Open a Pull RequestLicense
MIT License - see LICENSE file for details.
Changelog
$3
- ā
Cursor Tracking: Track mouse position, cursor types, and click events with JSON export
- ā
Window Recording: Automatic coordinate conversion for multi-display setups
- ā
Audio Controls: Separate microphone and system audio controls
- ā
Display Selection: Multi-monitor support with automatic detection
- ā
Smart Filtering: Improved window detection and filtering
- ā
Performance: Optimized native implementation
---
Made for macOS š | Built with AVFoundation š¹ | Node.js Ready š
> š”ļø Permissions: Ensure your host application's
Info.plist declares camera and microphone usage descriptions (see MACOS_PERMISSIONS.md`).