Skip to content

Latest commit

 

History

History
124 lines (92 loc) · 5.31 KB

File metadata and controls

124 lines (92 loc) · 5.31 KB

Known Issues

--headless=new is not supported

Sadly, it is so. For Puppeteer v23+, the plugin enforces use of the chrome-headless-shell binary.

Bad Chrome versions

  • 117.0.5938.88 (default for puppeteer version(s) 21.3.0 ): reacts with targetCrashed
  • 117.0.5938.92 (default for puppeteer version(s) 21.3.2...21.3.6): reacts with targetCrashed
  • 117.0.5938.149 (default for puppeteer version(s) 21.3.7...21.3.8): reacts with targetCrashed
  • 118.0.5993.70 (default for puppeteer version(s) 21.4.0...21.4.1): reacts with targetCrashed
  • 119.0.6045.105 (default for puppeteer version(s) 21.5.0...21.7.0): reacts with targetCrashed
  • 120.0.6099.109 (default for puppeteer version(s) 21.8.0): reacts with targetCrashed

macOS is not supported

Unfortunately, it is so.

No capturing == Nothing happens

This relates to timers, animations, clicks, etc. To process interaction with the page, frame requests have to be submitted and thus capturing have to be active.

Setting defaultViewport causes rendering to freeze

The exact origin of the issue is not yet known, yet it's likely to be related to the deterministic mode.

Calling page.setViewport() before starting the capture behaves the same, yet calling it after starting the capture works yet not always. Thus it's safe to assume that there's some sort of race condition, since adding recorder.waitForTimeout(100) just before setting the viewport workarounds the issue.

Also it should be taken into account that since frame size is going to change over the time of the recording, frame size autodetection will fail. To workaround this issue, frame size have to be specified:

const recorder = await capture(page, {
  size: `${viewportWidth}x${viewportHeight}`,
})
await recorder.start('capture.mp4', { waitForFirstFrame: false })
await recorder.waitForTimeout(100)
await page.setViewport({
  width: viewportWidth,
  height: viewportHeight,
  deviceScaleFactor: 1.0,
})

A friendlier workaround is enabled by default: recorder.start() automatically waits for the first frame to be captured. This approach seems to allow bypassing the alleged race condition:

const recorder = await capture(page, {
  size: `${viewportWidth}x${viewportHeight}`,
})
await recorder.start('capture.mp4')
await page.setViewport({
  width: viewportWidth,
  height: viewportHeight,
  deviceScaleFactor: 1.0,
})

Time-related functions are affected

The following functions have to be overridden with injected versions:

  • setTimeout & clearTimeout
  • setInterval & clearInterval
  • requestAnimationFrame & cancelAnimationFrame
  • Date() & Date.now()
  • performance.now()

The injection should happen before page content loads:

const recorder = await capture(page) // Injection happens here during attach()
await page.goto('https://google.com') // Possible capture would happen here, thus injected versions would be captured

HTML5 <video> and <audio> elements are not supported

Chrome's media engine operates below the JavaScript and compositor layers that puppeteer-capture controls. The virtual time injection affects JS timing APIs, and HeadlessExperimental.beginFrame advances the compositor clock, but neither mechanism controls the internal media decoder/renderer. As a result, <video> and <audio> elements appear frozen during capture even though JavaScript-observable properties like currentTime may advance.

This also applies to animated GIFs, which use a separate image animation pipeline.

Why this can't be fixed at the CDP level

Research into Chrome's architecture (#191) confirmed this is a fundamental gap:

  • The CDP Media domain is observation-only — it monitors player state but has no playback control methods
  • The CDP Animation domain covers CSS/Web Animations only — <video>/<audio> are architecturally separate
  • The media pipeline clock runs on wall-clock time in headless mode (normally driven by the sound card, which doesn't exist in chrome-headless-shell). There is no CDP command or Chrome flag to make it respect virtual time
  • video.currentTime seeking is asynchronous and frame-approximate — rapid sequential seeks trigger known Chromium bugs and are far too slow for frame-by-frame capture

Workarounds

For exact results, decode video server-side (e.g., with ffmpeg), extract frames as images, and display them as <img> elements on a timed sequence — these render deterministically under puppeteer-capture.

For approximate results, a seek + canvas pattern can be used in page scripts:

// On each frame (inside requestAnimationFrame callback):
video.currentTime = desiredTimeInSeconds
video.addEventListener('seeked', () => {
  ctx.drawImage(video, 0, 0)
}, { once: true })

Note that this approach is not frame-perfect — seek accuracy depends on codec keyframe placement, audio is not captured, and native video controls are not visible on the canvas.

See #9 for the original report.