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.
npm install puppeteer-capture puppeteerimport { 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()HeadlessExperimental.beginFrame for precise frame
controlwaitForTimeout() advances the page's own timeline| 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 |
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.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.Promise<PuppeteerCapture>PuppeteerCaptureOptionsOptions 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 |
PuppeteerCaptureStartOptionsOptions 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 |
PuppeteerCaptureThe main interface returned by capture().
| 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 |
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.
target — A file path
(string) or a Writable stream.options — Optional PuppeteerCaptureStartOptions.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.
| 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 |
PuppeteerCaptureFormatFormat 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').import { capture, PuppeteerCaptureFormat } from 'puppeteer-capture'
const recorder = await capture(page, {
format: PuppeteerCaptureFormat.MP4('medium', 'libx264')
})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.
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-synchronizationHeadlessExperimental DependencyThis 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.
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).beginFrame is exclusive to chrome-headless-shell
(the old headless architecture). It is not available in
--headless=new.| 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. |
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.
chrome-headless-shell for
deterministic rendering, creating broader pressure to maintain the
binary.To track changes to this dependency:
HeadlessExperimental.pdl — protocol definition (watch
for deprecation annotations on beginFrame)headless_handler.cc — implementation (watch for removal
or functional changes)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)ffmpeg is resolved in the following order:
FFMPEG environment variable pointing to the
executablePATHffmpeg-static, if installed as a dependencySee Known Issues for platform constraints and workarounds.
See Contributing for development setup and guidelines.