Cross-platform native screen capture for Node.js. Windows uses windows-capture 2.0.0, macOS uses screencapturekit 8, and Linux/Wayland uses the XDG ScreenCast portal and the pipewire crate.
- Native capture through Windows Graphics Capture, ScreenCaptureKit, or the XDG ScreenCast portal and PipeWire
- Promise-based and async-iterator APIs with bounded frame buffering
- Packed RGBA/BGRA frames with cropping, dirty-region metadata, timestamps, and image encoding
- Native source pickers on Windows and macOS, plus portal-based source selection on Wayland
- Windows-only DXGI Desktop Duplication and Media Foundation video encoding
- Features
- Requirements
- Platform support
- Installation
- Quick start
- Examples
- Screen capture
- Frames
- Monitor and window discovery
- DXGI Desktop Duplication
- Image encoding
- Video encoding (Windows only)
- Constants and supported values
- Rust compatibility
- Contributing
- License
- Node.js 20.17+, 22.13+, or 23.5+
- Windows 10 version 1903 or newer with the MSVC runtime
- macOS 12.3 or newer with Screen Recording permission; the native picker requires macOS 14 or newer
- Linux with Wayland, PipeWire 0.3, and a working
xdg-desktop-portalScreenCast backend
Important
macOS requires Screen Recording permission in System Settings. Check isSupported() and captureApiSupport() before starting capture.
| Capability | Windows | macOS | Linux/Wayland |
|---|---|---|---|
| Screen or window capture | Yes | Yes, through ScreenCaptureKit | Yes, through the XDG ScreenCast portal |
| Promise and async-iterator frame API | Yes | Yes | Yes |
| Cursor capture | Yes | Yes | Yes, when the portal supports it |
| Native content picker | Yes | Yes, on macOS 14+ | Yes, through the portal |
| Monitor and window discovery | Yes | Yes | No; Wayland intentionally does not expose global source enumeration |
| Target selection by index, title, or native handle | Yes | Yes | No; the portal owns source selection |
| Frame crop and packed RGBA/BGRA buffers | Yes | Yes | Yes |
| JPEG, PNG, GIF, TIFF, and BMP encoding | Yes | Yes | Yes |
| JPEG XR and RGB16F image encoding | Yes | No | No |
| DXGI Desktop Duplication | Yes | No | No |
| Video encoding | Yes | No | No |
Warning
DXGI Desktop Duplication and VideoEncoder are Windows-only. Calling an unsupported platform-specific API throws an explicit error instead of silently changing behavior.
Note
On Wayland, every new capture session opens the desktop portal. The user chooses the actual monitor or window; applications cannot bypass that consent dialog or enumerate sources ahead of time.
npm install @screen-capture/node
# or
pnpm add @screen-capture/nodeCapture the primary monitor, save one PNG, and stop the native session:
import {
ImageFormat,
ScreenCapture,
} from '@screen-capture/node'
const capture = new ScreenCapture(
process.platform === 'win32' || process.platform === 'darwin'
? { monitorIndex: 1, colorFormat: 'bgra8' }
: { usePicker: true, colorFormat: 'bgra8' },
)
await capture.start()
const frame = await capture.nextFrame()
if (frame) {
console.log({
width: frame.width,
height: frame.height,
timestamp: frame.timestamp,
})
frame.saveAsImage('screenshot.png', ImageFormat.Png)
}
await capture.stop()monitorIndex is one-based on Windows and macOS. On Wayland, omit direct target selectors; the portal asks the user which monitor or window to share.
Runnable examples are available in examples/:
| Example | Platforms | Purpose |
|---|---|---|
capture-screenshot.js |
Windows, macOS, Wayland | Capture one frame and save it as PNG |
stream-frames.js |
Windows, macOS, Wayland | Consume frames through the async iterator |
discover-sources.js |
Windows, macOS | Enumerate monitors and top-level windows |
windows-dxgi-screenshot.js |
Windows | Capture a synchronous DXGI desktop frame |
windows-record-video.js |
Windows | Record H.264 video with Media Foundation |
Build the package before running examples from a repository checkout:
pnpm build
node examples/capture-screenshot.js screenshot.png
node examples/stream-frames.js 120
node examples/discover-sources.js
node examples/windows-dxgi-screenshot.js dxgi-screenshot.png
node examples/windows-record-video.js capture.mp4 300Note
The cross-platform capture examples open the desktop portal on Wayland. Windows-only examples fail immediately on other platforms.
class ScreenCapture implements AsyncIterable<Frame> {
constructor(options: CaptureOptions)
start(): Promise<void>
nextFrame(): Promise<Frame | undefined>
stop(): Promise<void>
}start()starts the native capture session. Calling it twice without stopping rejects.nextFrame()waits for the latest available frame. It resolves toundefinedafterstop()or when the capture item closes.- Only one
nextFrame()call may be pending at a time. - The JavaScript queue keeps at most one pending frame; older frames are dropped instead of accumulating buffers.
stop()joins the native capture thread and closes pending frame reads.- The async iterator starts and stops the session automatically:
import { ScreenCapture } from '@screen-capture/node'
const capture = new ScreenCapture({ monitorIndex: 1 })
for await (const frame of capture) {
processFrame(frame)
if (shouldStop()) break
}Exactly one target may be selected on Windows and macOS. If all target fields are omitted, Windows and macOS capture monitor 1; Wayland opens the portal and lets the user choose a source.
interface CaptureOptions {
monitorIndex?: number
windowName?: string
windowHandle?: number
usePicker?: boolean
cursorCapture?: boolean
drawBorder?: boolean
includeSecondaryWindows?: boolean
minimumUpdateIntervalMs?: number
dirtyRegions?: boolean
colorFormat?: ColorFormat
}| Option | Description |
|---|---|
monitorIndex |
Windows and macOS: one-based monitor index. Wayland: unsupported as a direct selector; omit it and let the portal choose. |
windowName |
Windows and macOS: captures the first top-level window whose title contains this string. Wayland: unsupported; use the portal picker. |
windowHandle |
Windows: native HWND. macOS: ScreenCaptureKit window ID. Wayland: unsupported; use the portal picker. |
usePicker |
Opens the native Windows or macOS picker, or allows monitor/window selection in the Wayland portal. The macOS picker requires macOS 14+. |
cursorCapture |
Includes or excludes the cursor when supported. |
drawBorder |
Windows-only capture-border control. |
includeSecondaryWindows |
Windows-only secondary-window control. |
minimumUpdateIntervalMs |
Windows-only minimum interval between updates. |
dirtyRegions |
Windows-only dirty-region reporting and rendering control. |
colorFormat |
Requested frame format. Defaults to ColorFormat.Bgra8; macOS and Wayland support rgba8 and bgra8. |
Examples of each target:
import { ScreenCapture } from '@screen-capture/node'
new ScreenCapture({ monitorIndex: 2 })
new ScreenCapture({ windowName: 'Visual Studio Code' })
new ScreenCapture({ windowHandle: hwnd })
new ScreenCapture({ usePicker: true })interface CaptureApiSupport {
graphicsCapture: boolean
cursorSettings: boolean
borderSettings: boolean
secondaryWindows: boolean
minimumUpdateInterval: boolean
dirtyRegions: boolean
}import {
captureApiSupport,
isSupported,
} from '@screen-capture/node'
if (!isSupported()) {
throw new Error('Native screen capture is unavailable')
}
console.log(captureApiSupport())isSupported() checks the native capture backend for the current platform. captureApiSupport() reports the core backend and each optional setting independently.
class Frame {
readonly buffer: Buffer
readonly width: number
readonly height: number
readonly rowPitch: number
readonly depthPitch: number
readonly timestamp: number
readonly colorFormat: ColorFormat
readonly dirtyRegions: DirtyRegion[]
crop(startX: number, startY: number, endX: number, endY: number): Frame
encode(format: ImageFormat): Buffer
saveAsImage(path: string, format?: ImageFormat): void
}
interface DirtyRegion {
x: number
y: number
width: number
height: number
}buffercontains packed pixel bytes with GPU row padding removed.rowPitchis the packed byte count per row.depthPitchis the packed byte count for the entire frame.timestampuses 100-nanosecond ticks. Windows uses the platform capture clock; macOS and Wayland timestamps are relative to the current capture session.dirtyRegionscontains changed rectangles on Windows when reporting is enabled. macOS and Wayland currently return an empty array.crop()uses an exclusive end coordinate. Invalid rectangles throw.encode()andsaveAsImage()supportrgba8andbgra8.rgba16Fframes must be converted before image encoding.saveAsImage()defaults to PNG whenformatis omitted.
import {
ImageFormat,
ScreenCapture,
} from '@screen-capture/node'
const capture = new ScreenCapture({ monitorIndex: 1 })
await capture.start()
const frame = await capture.nextFrame()
if (frame) {
const cropped = frame.crop(100, 100, 900, 700)
const png = cropped.encode(ImageFormat.Png)
cropped.saveAsImage('crop.png', ImageFormat.Png)
console.log(png.byteLength)
}
await capture.stop()function enumerateMonitors(): MonitorInfo[]
function primaryMonitor(): MonitorInfo
function monitorFromIndex(index: number): MonitorInfo
interface MonitorInfo {
index: number
name: string
deviceName: string
deviceString: string
width: number
height: number
refreshRate: number
handle: number
}These functions are available on Windows and macOS. index is one-based. Windows monitor handles are native HMONITOR values; macOS handles are ScreenCaptureKit display IDs. Wayland callers receive an explicit unsupported error because source enumeration is not available through the ScreenCast portal.
function enumerateWindows(): WindowInfo[]
function foregroundWindow(): WindowInfo
function windowFromName(title: string): WindowInfo
function windowFromContainsName(title: string): WindowInfo
function windowFromHandle(handle: number): WindowInfo
interface WindowInfo {
title: string
processId: number
processName: string
rect: Rect
titleBarHeight: number
width: number
height: number
isValid: boolean
handle: number
monitorIndex?: number
}
interface Rect {
left: number
top: number
right: number
bottom: number
}import {
enumerateMonitors,
enumerateWindows,
foregroundWindow,
monitorFromIndex,
primaryMonitor,
windowFromContainsName,
windowFromHandle,
windowFromName,
} from '@screen-capture/node'
console.table(enumerateMonitors())
console.table(enumerateWindows())
console.log(primaryMonitor())
console.log(monitorFromIndex(1))
console.log(foregroundWindow())
console.log(windowFromName('Notepad'))
console.log(windowFromContainsName('Visual Studio'))
console.log(windowFromHandle(hwnd))DXGI Desktop Duplication is a Windows-only synchronous API. On macOS or Wayland, use the asynchronous ScreenCapture API.
class DxgiDuplicationSession {
constructor(options?: DxgiSessionOptions)
readonly width: number
readonly height: number
readonly format: DxgiDuplicationFormat
readonly refreshRate: number[]
acquireNextFrame(timeoutMs?: number): Frame | null
recreate(): void
switchMonitor(monitorIndex: number): void
}
interface DxgiSessionOptions {
monitorIndex?: number
supportedFormats?: DxgiDuplicationFormat[]
}monitorIndexdefaults to monitor 1.supportedFormatsis optional. If omitted, the native crate negotiates a supported format.Rgb10A2andRgb10XrA2sessions are converted to packedrgba8frames before crossing into JavaScript.acquireNextFrame()returnsnullwhen no desktop update arrives before the timeout. The default timeout is 16 ms.recreate()rebuilds the session after display-mode changes or DXGI access loss.switchMonitor()recreates the session on another one-based monitor index.refreshRateis[numerator, denominator].
import {
DxgiDuplicationFormat,
DxgiDuplicationSession,
ImageFormat,
} from '@screen-capture/node'
const session = new DxgiDuplicationSession({
monitorIndex: 1,
supportedFormats: [
DxgiDuplicationFormat.Bgra8,
DxgiDuplicationFormat.Rgba8,
],
})
const frame = session.acquireNextFrame(33)
if (frame) {
frame.saveAsImage('desktop.png', ImageFormat.Png)
}
// Call after a display mode or desktop switch if the session reports access loss.
session.recreate()
session.switchMonitor(2)Use ImageEncoder for raw pixels that do not come from a Frame.
class ImageEncoder {
constructor(format: ImageFormat, pixelFormat: ImageEncoderPixelFormat)
encode(buffer: Buffer, width: number, height: number): Buffer
}import {
ImageEncoder,
ImageEncoderPixelFormat,
ImageFormat,
} from '@screen-capture/node'
const encoder = new ImageEncoder(
ImageFormat.Png,
ImageEncoderPixelFormat.Bgra8,
)
const png = encoder.encode(rawBgraBuffer, width, height)JPEG XR and RGB16F image encoding are Windows-only. The macOS and Wayland backends support JPEG, PNG, GIF, TIFF, and BMP from packed rgba8 or bgra8 pixels.
VideoEncoder uses Windows Media Foundation. Constructing it on macOS or Wayland throws an explicit unsupported error.
All VideoEncoder methods are synchronous and may perform CPU-intensive conversion or blocking Media Foundation I/O. For latency-sensitive applications, run encoding in a Node.js worker thread rather than on the main event loop.
class VideoEncoder {
constructor(options: VideoEncoderOptions)
sendFrame(frame: Frame): void
sendFrameWithAudio(frame: Frame, audioBuffer: Buffer): void
sendFrameBuffer(buffer: Buffer, timestamp: number): void
sendAudioBuffer(buffer: Buffer, timestamp?: number): void
finish(): Buffer | null
}
interface VideoEncoderOptions {
path?: string
video: VideoSettings
audio?: AudioSettings
container?: ContainerFormat
}
interface VideoSettings {
width: number
height: number
codec?: VideoCodec
bitrate?: number
frameRate?: number
pixelAspectRatioNumerator?: number
pixelAspectRatioDenominator?: number
disabled?: boolean
}
interface AudioSettings {
codec?: AudioCodec
bitrate?: number
channelCount?: number
sampleRate?: number
bitsPerSample?: number
disabled?: boolean
}- Set
pathfor file output.finish()returnsnullfor file output. - Omit
pathfor in-memory output.finish()returns the encodedBuffer. - In-memory output is limited to 512 MiB; use file output for longer recordings.
- Audio is disabled when
audiois omitted. sendFrame()andsendFrameWithAudio()use the frame's timestamp.sendAudioBuffer()uses the encoder's audio clock; its optional timestamp is accepted for API compatibility.- Always call
finish()to flush and finalize the container.
import {
ContainerFormat,
VideoCodec,
VideoEncoder,
ScreenCapture,
} from '@screen-capture/node'
const capture = new ScreenCapture({ monitorIndex: 1 })
await capture.start()
const firstFrame = await capture.nextFrame()
if (firstFrame) {
const encoder = new VideoEncoder({
path: 'capture.mp4',
video: {
width: firstFrame.width,
height: firstFrame.height,
codec: VideoCodec.H264,
bitrate: 15_000_000,
frameRate: 60,
},
container: ContainerFormat.Mpeg4,
})
try {
encoder.sendFrame(firstFrame)
const nextFrame = await capture.nextFrame()
if (nextFrame) encoder.sendFrame(nextFrame)
} finally {
await capture.stop()
encoder.finish()
}
}sendFrame(frame) validates the configured dimensions, converts rgba8 or bgra8 input to BGRA, and reverses the rows for the Windows Media Foundation encoder. Raw sendFrameBuffer() input must already be packed BGRA in bottom-to-top row order and must match the configured dimensions.
In-memory output:
import {
ContainerFormat,
VideoEncoder,
} from '@screen-capture/node'
const encoder = new VideoEncoder({
video: { width: 1280, height: 720 },
container: ContainerFormat.Mpeg4,
})
encoder.sendFrameBuffer(bgraBuffer, timestamp)
const encodedVideo = encoder.finish()Audio input is interleaved PCM:
import {
AudioCodec,
ContainerFormat,
VideoCodec,
VideoEncoder,
} from '@screen-capture/node'
const encoder = new VideoEncoder({
path: 'capture-with-audio.mp4',
video: {
width: 1920,
height: 1080,
codec: VideoCodec.H264,
frameRate: 60,
},
audio: {
codec: AudioCodec.Aac,
bitrate: 192_000,
channelCount: 2,
sampleRate: 48_000,
bitsPerSample: 16,
},
container: ContainerFormat.Mpeg4,
})
encoder.sendFrameWithAudio(frame, pcmBuffer)
encoder.finish()The values below are exported as const enums. Use the enum members in application code, for example ImageFormat.Png.
enum ColorFormat {
Rgba16F = 'rgba16F',
Rgba8 = 'rgba8',
Bgra8 = 'bgra8',
}enum ImageFormat {
Jpeg = 'jpeg',
Png = 'png',
Gif = 'gif',
Tiff = 'tiff',
Bmp = 'bmp',
JpegXr = 'jpegXr',
}JpegXr is Windows-only.
enum ImageEncoderPixelFormat {
Rgb16F = 'rgb16F',
Bgra8 = 'bgra8',
Rgba8 = 'rgba8',
}Rgb16F is Windows-only.
enum DxgiDuplicationFormat {
Rgba16F = 'rgba16F',
Rgb10A2 = 'rgb10A2',
Rgb10XrA2 = 'rgb10XrA2',
Rgba8 = 'rgba8',
Rgba8Srgb = 'rgba8Srgb',
Bgra8 = 'bgra8',
Bgra8Srgb = 'bgra8Srgb',
}Windows-only.
VideoCodec values
Argb32 Bgra8 D16 H263 H264 H264Es Hevc HevcEs Iyuv
L8 L16 Mjpg Nv12 Mpeg1 Mpeg2 Rgb24 Rgb32 Wmv3 Wvc1
Vp9 Yuy2 Yv12
AudioCodec values
Aac Ac3 AacAdts AacHdcp Ac3Spdif Ac3Hdcp Adts Alac
AmrNb AwrWb Dts Eac3 Flac Float Mp3 Mpeg Opus Pcm
Wma8 Wma9 Vorbis
Both codec enums are Windows-only and are used by VideoEncoder.
enum ContainerFormat {
Asf = 'asf',
Mp3 = 'mp3',
Mpeg4 = 'mpeg4',
Avi = 'avi',
Mpeg2 = 'mpeg2',
Wave = 'wave',
AacAdts = 'aacAdts',
Adts = 'adts',
ThreeGp = 'threeGp',
Amr = 'amr',
Flac = 'flac',
}Windows-only.
On Windows, the binding follows the application-facing portions of windows-capture 2.0.0:
| Rust capability | Node.js API |
|---|---|
Graphics Capture API and Settings |
ScreenCapture and CaptureOptions |
GraphicsCaptureApiHandler frame lifecycle |
Frame, nextFrame(), and async iteration |
| Capture thread lifecycle | Promise-based ScreenCapture.start(), nextFrame(), and stop() |
Frame and FrameBuffer |
Frame and packed Node.js Buffer data |
Monitor and Window |
Monitor and window discovery functions |
GraphicsCapturePicker |
usePicker: true |
DxgiDuplicationApi |
DxgiDuplicationSession |
ImageEncoder |
ImageEncoder, Frame.encode(), and Frame.saveAsImage() |
VideoEncoder |
VideoEncoder, audio settings, codecs, and containers |
| Graphics Capture feature probes | isSupported() and captureApiSupport() |
On macOS, screencapturekit 8 supplies display/window discovery, ScreenCaptureKit streams, the macOS 14+ content picker, BGRA pixel buffers, and Screen Recording permission checks. Frames are copied into packed Node.js buffers before crossing the native callback boundary.
On Linux, ScreenCapture implements the same promise and async-iterator lifecycle with the XDG ScreenCast portal and the pipewire crate. The portal's security model prevents parity for source discovery and direct source selection; DXGI and Windows Media Foundation APIs remain Windows-only.
Raw COM interfaces, D3D11 devices, textures, and surfaces are intentionally not exposed to JavaScript. Frames are represented as owned Node.js buffers so their lifetime is safe across the promise and async-iterator boundaries.
pnpm install
pnpm build:addon
pnpm build
pnpm lint
pnpm format
pnpm test
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warningsRust formatting uses cargo fmt; TypeScript and JavaScript formatting use Oxfmt; linting uses Oxlint. Native builds require the Windows MSVC toolchain, Xcode with a macOS 14+ SDK, or Linux PipeWire development files.
Run pnpm test:e2e separately on an interactive desktop. It opens the native source picker on macOS and Wayland and may show a capture-permission prompt.
Contributions are welcome. Before opening a pull request, run the development checks above and test native capture on every affected platform with the required native toolchain and libraries.
- Keep changes focused and update the README when public APIs change.
- Add or update Vitest coverage for observable JavaScript behavior.
- Keep generated NAPI artifacts synchronized with native API changes.
- Run the relevant Windows, macOS, or Wayland capture checks for platform-specific changes.
- Include a concise description of the behavior changed and the verification performed.
This software is released under the MIT License.