npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@screen-capture/node

v1.1.0

Published

Cross-platform native screen capture for Node.js on Windows, macOS, and Linux/Wayland

Readme

@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

Argb32  Bgra8  D16  H263  H264  H264Es  Hevc  HevcEs  Iyuv
L8  L16  Mjpg  Nv12  Mpeg1  Mpeg2  Rgb24  Rgb32  Wmv3  Wvc1
Vp9  Yuy2  Yv12
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.