Mock camera input
Use case: your app uses the camera — a QR/barcode scanner, an ID-document capture flow, a video chat preview. You want tests to “show” the camera a prepared video.
Why this is hard
Section titled “Why this is hard”There is no Playwright API for camera input, and unlike audio there’s no cross-browser library that solves it cleanly (yet). What exists is a patchwork of browser-specific switches.
Recipe
Section titled “Recipe”Chromium: fake device + video file
Section titled “Chromium: fake device + video file”Chromium can replace the camera with a looping video at launch:
export default defineConfig({ projects: [ { name: 'chromium', use: { launchOptions: { args: [ '--use-fake-ui-for-media-stream', // auto-accept the permission prompt '--use-fake-device-for-media-stream', // provide a fake camera '--use-file-for-fake-video-capture=tests/fixtures/qr-code.y4m', ], }, }, }, ],});The file must be Y4M (raw, uncompressed) or MJPEG. Convert anything with ffmpeg:
ffmpeg -i qr-code.mp4 -pix_fmt yuv420p qr-code.y4mY4M files are huge (raw frames) — keep fixtures short and small, and don’t commit them to git without LFS.
Firefox: fake test streams
Section titled “Firefox: fake test streams”use: { launchOptions: { firefoxUserPrefs: { 'media.navigator.streams.fake': true, 'media.navigator.permission.disabled': true, }, },},Firefox generates a synthetic test pattern — good enough for “the preview renders”, useless for QR scanning (you can’t choose the content).
WebKit and content-controlled streams: canvas injection
Section titled “WebKit and content-controlled streams: canvas injection”When you need specific frames on any browser, replace getUserMedia yourself with a canvas-backed stream:
await page.addInitScript(() => { const draw = (canvas: HTMLCanvasElement) => { const ctx = canvas.getContext('2d')!; const img = new Image(); img.src = '/fixtures/qr-code.png'; // serve fixtures from your test server img.onload = () => { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); requestAnimationFrame(() => draw(canvas)); }; }; const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = async (constraints) => { if (!constraints?.video) return original(constraints); const canvas = document.createElement('canvas'); canvas.width = 1280; canvas.height = 720; draw(canvas); const stream = canvas.captureStream(30); if (constraints.audio) { const audio = await original({ audio: constraints.audio }); audio.getAudioTracks().forEach((t) => stream.addTrack(t)); } return stream; };});This is the same technique playwright-audio-mocking uses for audio — the app receives a genuine MediaStream and can’t tell the difference. Swap the drawn image mid-test (e.g. via page.evaluate) to simulate “pointing the camera at something else”.
Caveats
Section titled “Caveats”--use-file-for-fake-video-captureloops the video forever and cannot be switched at runtime.- The canvas approach doesn’t emulate device labels/IDs in
enumerateDevices()unless you also patch it; apps with camera-selection UIs may need that. - WebKit on Linux CI has no camera at all — canvas injection is the only route.
Related
Section titled “Related”- Mock microphone input — the audio side, solved properly by a library.
- Test WebRTC calls — combine both fakes for a full call.