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

puppeteer-capture

v1.49.0

Published

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

Readme

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

  • Deterministic frame timing — frames are captured on demand via CDP, not in real time
  • Frame-perfect reproducible output — the same page produces the same video, every time
  • CDP-powered — uses HeadlessExperimental.beginFrame for precise frame control
  • Virtual time controlwaitForTimeout() advances the page's own timeline
  • Works with any Puppeteer workflow — drop-in alongside existing Puppeteer scripts

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()
  • options — Optional PuppeteerLaunchOptions from puppeteer-core. The headless option is overridden to 'shell' and the required Chrome arguments are appended to args automatically.
  • Returns Promise<PuppeteerBrowser>

capture(page, options?)

Creates a PuppeteerCapture instance for the given page.

import { capture } from 'puppeteer-capture'

const recorder = await capture(page)
  • page — A PuppeteerPage to capture.
  • options — Optional PuppeteerCaptureOptions extended with:
    • attach (boolean, default: true) — When false, the recorder is created without attaching to the page. Call recorder.attach(page) later.
  • Returns Promise<PuppeteerCapture>

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

  • preset — x264 encoding preset (default: 'ultrafast'). One of: 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
  • videoCodec — Video codec (default: 'libx264').
  • Returns a format configurator function.
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():

  • --deterministic-mode
  • --enable-begin-frame-control
  • --disable-new-content-rendering-timeout
  • --run-all-compositor-stages-before-draw
  • --disable-threaded-animation
  • --disable-threaded-scrolling
  • --disable-checker-imaging
  • --disable-image-animation-resync
  • --enable-surface-synchronization

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

  • beginFrame is not deprecated and remains actively implemented in the Chromium source.
  • enable/disable are marked deprecated in the protocol definition (they are no-ops and have no functional impact).
  • The domain is labeled Experimental, meaning it can change without notice.
  • beginFrame is exclusive to chrome-headless-shell (the old headless architecture). It is not available in --headless=new.

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:

  • Frame timing depends on wall-clock time and system load (non-deterministic).
  • CSS animations, transitions, and compositor-driven effects cannot be synchronized to a virtual timeline.
  • Two runs of the same page may produce different frame counts and visual output.

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

  • Remotion and other ecosystem projects also depend on chrome-headless-shell for deterministic rendering, creating broader pressure to maintain the binary.
  • Active Chromium issues (e.g., #40550372 — making BeginFrameControl work with Viz) indicate ongoing investment, not abandonment.
  • Chromium's deprecation process typically involves long warning periods and migration paths.

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