Your <video> plays fine when you open the page yourself. In Playwright, it just doesn’t. No error thrown, no failed request. readyState sits at 0, duration is NaN, screenshots come back black, and video.error.code is 4, MEDIA_ERR_SRC_NOT_SUPPORTED.
This is almost never your app. It’s the browser.
Why the bundled Chromium can’t play your video
Playwright ships open-source Chromium builds. Open-source Chromium only includes royalty-free codecs: VP8, VP9, AV1, Opus, Vorbis, basically the WebM/WebRTC family. H.264 and AAC are patent-encumbered, so they’re compiled out.
Which is a problem, because H.264 in an MP4 container is still what most of the real internet serves. Same story for DRM: there’s no Widevine CDM in Chromium, so anything going through Encrypted Media Extensions fails before it starts.
Google Chrome is the same engine plus the proprietary bits. So the fix isn’t a flag, it’s a different binary.
The fix
Install the real thing:
npx playwright install chromeThen point a project at it:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ projects: [ { name: 'chrome', use: { ...devices['Desktop Chrome'], channel: 'chrome', // branded Chrome, not bundled Chromium }, }, ],});channel: 'msedge' works too, if that’s your target.
Confirm it actually took
Cheap sanity check: run it once in CI and you’ll never debug this twice.
test('browser can decode H.264', async ({ page }) => { const support = await page.evaluate(() => document .createElement('video') .canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') ); expect(support).toBe('probably'); // '' on bundled Chromium});While you’re here
Codec support gets you a decodable file. Autoplay policy still has opinions.
use: { channel: 'chrome', launchOptions: { args: [ '--autoplay-policy=no-user-gesture-required', // for getUserMedia work: '--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream', // deterministic camera input: '--use-file-for-fake-video-capture=/abs/path/sample.y4m', ], },}And assert on state rather than pixels: readyState >= 3, currentTime advancing, paused === false. A video that decodes one frame and stalls will pass a screenshot test all day.
The trade-offs, honestly
Version drift. Playwright pins its Chromium; Chrome auto-updates underneath you. Great for catching real-world regressions, less great for reproducibility. Pin it in CI if that matters.
CI images. Chrome has to be installed on the runner. npx playwright install --with-deps chrome in the workflow, or a base image that already has it.
Not a full swap. Keep Chromium for the fast bulk of your suite, use Chrome for the media-specific projects. Two projects, one config.
Shameless plug
All of the above is a fix for a problem you shouldn’t have had. At Klarent we run tests on real Chrome, full stop. So the codec question never comes up. No channel config, no custom CI images, no “works locally, black frames in the pipeline.” If your app plays a video in a browser, it plays a video in our tests.
If you’ve been avoiding video coverage because the browser fought you, that reason is gone.






