Skip to content

tux-tn/node-screen-capture

Repository files navigation

@screen-capture/node

CI npm version npm downloads Node.js version License

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.

Features

  • 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

Table of contents

Requirements

  • 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-portal ScreenCast backend

Important

macOS requires Screen Recording permission in System Settings. Check isSupported() and captureApiSupport() before starting capture.

Platform support

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.

Installation

npm install @screen-capture/node
# or
pnpm add @screen-capture/node

Quick start

Capture 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.

Examples

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 300

Note

The cross-platform capture examples open the desktop portal on Wayland. Windows-only examples fail immediately on other platforms.

Screen capture

ScreenCapture

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 to undefined after stop() 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
}

Capture targets and options

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 })

Capability checks

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.

Frames

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
}
  • buffer contains packed pixel bytes with GPU row padding removed.
  • rowPitch is the packed byte count per row.
  • depthPitch is the packed byte count for the entire frame.
  • timestamp uses 100-nanosecond ticks. Windows uses the platform capture clock; macOS and Wayland timestamps are relative to the current capture session.
  • dirtyRegions contains 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() and saveAsImage() support rgba8 and bgra8. rgba16F frames must be converted before image encoding.
  • saveAsImage() defaults to PNG when format is 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()

Monitor and window discovery

Monitor functions

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.

Window functions

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

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[]
}
  • monitorIndex defaults to monitor 1.
  • supportedFormats is optional. If omitted, the native crate negotiates a supported format.
  • Rgb10A2 and Rgb10XrA2 sessions are converted to packed rgba8 frames before crossing into JavaScript.
  • acquireNextFrame() returns null when 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.
  • refreshRate is [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)

Image encoding

ImageEncoder

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.

Video encoding (Windows only)

VideoEncoder

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 path for file output. finish() returns null for file output.
  • Omit path for in-memory output. finish() returns the encoded Buffer.
  • In-memory output is limited to 512 MiB; use file output for longer recordings.
  • Audio is disabled when audio is omitted.
  • sendFrame() and sendFrameWithAudio() 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()

Constants and supported values

The values below are exported as const enums. Use the enum members in application code, for example ImageFormat.Png.

ColorFormat

enum ColorFormat {
  Rgba16F = 'rgba16F',
  Rgba8 = 'rgba8',
  Bgra8 = 'bgra8',
}

ImageFormat

enum ImageFormat {
  Jpeg = 'jpeg',
  Png = 'png',
  Gif = 'gif',
  Tiff = 'tiff',
  Bmp = 'bmp',
  JpegXr = 'jpegXr',
}

JpegXr is Windows-only.

ImageEncoderPixelFormat

enum ImageEncoderPixelFormat {
  Rgb16F = 'rgb16F',
  Bgra8 = 'bgra8',
  Rgba8 = 'rgba8',
}

Rgb16F is Windows-only.

DxgiDuplicationFormat

enum DxgiDuplicationFormat {
  Rgba16F = 'rgba16F',
  Rgb10A2 = 'rgb10A2',
  Rgb10XrA2 = 'rgb10XrA2',
  Rgba8 = 'rgba8',
  Rgba8Srgb = 'rgba8Srgb',
  Bgra8 = 'bgra8',
  Bgra8Srgb = 'bgra8Srgb',
}

Windows-only.

Video and audio codecs

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.

ContainerFormat

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.

Rust compatibility

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.

Development

pnpm install
pnpm build:addon
pnpm build
pnpm lint
pnpm format
pnpm test
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings

Rust 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.

Contributing

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.

Pull requests

  • 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.

License

This software is released under the MIT License.

About

Native screen capture for Node.js, with high-performance desktop capture and image/video encoding on Windows and Linux.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors