puppeteer-capture

GitHub Repo stars GitHub license node-current NPM Version GitHub Workflow Status Codecov

A Puppeteer plugin for capturing page as a video with ultimate quality.

Standard screencast approaches capture frames in real time, producing inconsistent frame timing and non-reproducible output. puppeteer-capture uses Chrome's HeadlessExperimental CDP domain to capture each frame deterministically. If you need frame-perfect, reproducible video output from Puppeteer, this is the only option.

Quick Start

npm install puppeteer-capture puppeteer
import { capture, launch } from 'puppeteer-capture'

const browser = await launch()
const page = await browser.newPage()
const recorder = await capture(page)

await page.goto('https://example.com', { waitUntil: 'networkidle0' })
await recorder.start('capture.mp4')
await recorder.waitForTimeout(1000)
await recorder.stop()
await recorder.detach()
await browser.close()

Key Features

Comparison

puppeteer-capture puppeteer-screen-recorder Playwright built-in
Approach CDP HeadlessExperimental Screencast Screencast
Frame timing Deterministic Real-time Real-time
Reproducibility Frame-perfect Varies Varies
Time control Full (virtual clock) None None
Platform Linux, Windows All All

API Reference

launch(options?)

Launches a browser configured for deterministic capture.

import { launch } from 'puppeteer-capture'

const browser = await launch()

capture(page, options?)

Creates a PuppeteerCapture instance for the given page.

import { capture } from 'puppeteer-capture'

const recorder = await capture(page)

PuppeteerCaptureOptions

Options passed to capture().

Option Type Default Description
fps number 60 Frames per second
size string Output size in ffmpeg notation (e.g. '1280x720')
format (ffmpeg: FfmpegCommand) => Promise<void> PuppeteerCaptureFormat.MP4() Output format configurator
ffmpeg string Path to the ffmpeg binary (overrides auto-detection)
customFfmpegConfig (ffmpeg: FfmpegCommand) => Promise<void> Additional ffmpeg configuration callback applied after format

PuppeteerCaptureStartOptions

Options passed to recorder.start().

Option Type Default Description
waitForFirstFrame boolean true Whether start() waits for the first frame before resolving
dropCapturedFrames boolean false When true, frames are emitted via events but not written to the output

PuppeteerCapture

The main interface returned by capture().

Properties

Property Type Description
page Page | null The attached page, or null if detached
isCapturing boolean Whether capture is in progress
captureTimestamp number Current virtual timestamp (ms) since capture start
capturedFrames number Total frames captured since last start()
dropCapturedFrames boolean Whether captured frames are dropped (read/write)
recordedFrames number Total frames written to the output

Methods

attach(page) — Attaches the recorder to a page. Required only when capture() was called with { attach: false }. Throws if already attached.

detach() — Detaches from the current page. Throws if not attached.

start(target, options?) — Starts capturing frames.

When target is a file path, parent directories are created automatically.

stop() — Stops capture and waits for ffmpeg to finalize the output.

waitForTimeout(milliseconds) — Advances the page's virtual timeline by the given number of milliseconds. Can only be called while capturing. See Time Flow.

on(event, listener) — Subscribes to a capture event.

Events

Event Listener Signature Description
captureStarted () => void Capture was started
frameCaptured (index: number, timestamp: number, data: Buffer) => void A frame was captured
frameCaptureFailed (reason?: any) => void Frame capture failed
frameRecorded (index: number, timestamp: number, data: Buffer) => void A frame was written to the output
captureStopped () => void Capture was stopped

PuppeteerCaptureFormat

Format configurators for PuppeteerCaptureOptions.format.

PuppeteerCaptureFormat.MP4(preset?, videoCodec?)

import { capture, PuppeteerCaptureFormat } from 'puppeteer-capture'

const recorder = await capture(page, {
  format: PuppeteerCaptureFormat.MP4('medium', 'libx264')
})

Error Classes

NotChromeHeadlessShell — Thrown when the browser is not chrome-headless-shell. Use launch() to ensure the correct binary.

MissingRequiredArgs — Thrown when the browser is missing one or more required Chrome arguments. Use launch() to add them automatically.

Required Chrome Arguments

The following arguments are required and are added automatically by launch():

Platform Risk: HeadlessExperimental Dependency

This library depends entirely on Chrome's HeadlessExperimental CDP domain — specifically the beginFrame method — for deterministic frame capture. This is the only mechanism in Chrome that provides compositor-level frame scheduling, which is what enables frame-perfect, reproducible video output.

Current status

Risk assessment

Timeframe Risk Rationale
Near-term (0–12 months) Low beginFrame is not deprecated; implementation receives maintenance commits; chrome-headless-shell ships with every Chrome release.
Medium-term (1–3 years) Moderate The "Experimental" label has persisted since inception without graduating to stable. No public commitment to long-term chrome-headless-shell maintenance exists.
Long-term (3+ years) Moderate–High Chrome's strategic direction favors --headless=new. If chrome-headless-shell is eventually discontinued, beginFrame goes with it.

Why there is no drop-in alternative

HeadlessExperimental.beginFrame is unique because it controls when the compositor renders each frame. The alternatives — Page.startScreencast, Page.captureScreenshot, tab capture — all capture frames produced by Chrome's own compositor timing, which means:

A partial fallback using Page.captureScreenshot with JavaScript time virtualization can achieve determinism for JS-driven content but not for CSS animations or compositor-driven effects.

Mitigating factors

Monitoring

To track changes to this dependency:

Time Flow

The browser runs in deterministic mode, so the time flow is not real time. To wait for a certain amount of time within the page's timeline, use PuppeteerCapture.waitForTimeout():

await recorder.waitForTimeout(1000)

Dependencies

ffmpeg

ffmpeg is resolved in the following order:

  1. FFMPEG environment variable pointing to the executable
  2. The executable available via PATH
  3. Via ffmpeg-static, if installed as a dependency

Known Issues

See Known Issues for platform constraints and workarounds.

Contributing

See Contributing for development setup and guidelines.

License

MIT