API in Capabilities
WebCodecs API
Published
WebCodecs exposes the browser’s media codecs one frame at a time: VideoDecoder turns EncodedVideoChunk objects into VideoFrames, VideoEncoder does the reverse, and AudioDecoder and AudioEncoder do the same for EncodedAudioChunk and AudioData. Each codec is a queue with output and error callbacks rather than promise-returning methods, which is what lets a video editor, a conferencing client, or a cloud-gaming stream keep hardware acceleration while controlling timing per frame. Containers are out of scope: nothing in the API reads or writes MP4 or WebM, so a muxer or demuxer library sits on either side.
Chrome 94 and Edge 94 shipped all four codecs; Firefox 130 followed on desktop (Firefox for Android has none); Safari 16.4 added VideoDecoder and VideoEncoder and Safari 26 added AudioDecoder and AudioEncoder (BCD api.VideoDecoder, api.AudioDecoder). Every interface is available in dedicated workers, and that is where encoding loops belong so the main thread stays free.
Syntax
Section titled “Syntax”const decoder = new VideoDecoder({ output, error })decoder.configure(config)decoder.decode(chunk)decoder.flush()decoder.reset()decoder.close()VideoDecoder.isConfigSupported(config)
const encoder = new VideoEncoder({ output, error })encoder.configure(config)encoder.encode(frame)encoder.encode(frame, options)encoder.flush()VideoEncoder.isConfigSupported(config)AudioDecoder and AudioEncoder have the same shape with decode(chunk) and encode(data). configure(), decode(), encode(), reset(), and close() return undefined and act on the codec’s control-message queue; flush() returns a promise that resolves once every queued message has produced its output; isConfigSupported() returns a promise for { supported, config } with the unrecognised members stripped. Attributes: state ("unconfigured", "configured", "closed"), decodeQueueSize or encodeQueueSize, and a dequeue event (Chrome 106) that fires when the queue shrinks. The interfaces are [SecureContext].
Parameters
Section titled “Parameters”The constructor init has two required callbacks. configure() takes a config dictionary whose codec must be a fully specified string: "vp09.00.10.08", "avc1.42001E", "av01.0.04M.08", "opus", or "mp4a.40.2"; the short forms "vp9" and "h264" are rejected as invalid.
| Dictionary | Member | Type | Required | Description |
|---|---|---|---|---|
VideoDecoderInit / VideoEncoderInit |
output |
callback | Yes | Receives each VideoFrame (decoder) or EncodedVideoChunk plus EncodedVideoChunkMetadata (encoder). |
| same | error |
WebCodecsErrorCallback |
Yes | Receives the DOMException that closed the codec. |
VideoDecoderConfig |
codec |
DOMString |
Yes | Fully specified codec string. |
VideoDecoderConfig |
description |
AllowSharedBufferSource |
No | Codec-specific extradata such as the avcC box; required for H.264 in AVC format, absent for Annex B. |
VideoDecoderConfig |
codedWidth, codedHeight |
unsigned long |
No | Dimensions of the coded frames; both or neither. |
VideoDecoderConfig |
displayAspectWidth, displayAspectHeight |
unsigned long |
No | Display aspect ratio override. |
VideoDecoderConfig |
colorSpace |
VideoColorSpaceInit |
No | primaries, transfer, matrix, fullRange. |
VideoDecoderConfig |
hardwareAcceleration |
HardwareAcceleration |
No, default "no-preference" |
"prefer-hardware" or "prefer-software". |
VideoDecoderConfig |
optimizeForLatency |
boolean |
No | Ask for minimal decode queue depth. |
VideoDecoderConfig |
rotation, flip |
double, boolean |
No | Orientation applied to output frames (Chrome 138). |
VideoEncoderConfig |
codec, width, height |
Yes | Codec string and the dimensions of every frame passed to encode(). |
|
VideoEncoderConfig |
displayWidth, displayHeight |
unsigned long |
No | Display size signalled in the stream. |
VideoEncoderConfig |
bitrate |
unsigned long long |
No | Target bits per second. |
VideoEncoderConfig |
framerate |
double |
No | Expected frames per second, used for rate control. |
VideoEncoderConfig |
bitrateMode |
VideoEncoderBitrateMode |
No, default "variable" |
"constant", "variable", or "quantizer". |
VideoEncoderConfig |
latencyMode |
LatencyMode |
No, default "quality" |
"realtime" trades compression for low delay. |
VideoEncoderConfig |
scalabilityMode |
DOMString |
No | SVC mode such as "L1T2". |
VideoEncoderConfig |
alpha |
AlphaOption |
No, default "discard" |
"keep" encodes an alpha channel where the codec allows it. |
VideoEncoderConfig |
hardwareAcceleration, contentHint |
No | As for the decoder; contentHint is "detail", "text", or "motion". |
|
VideoEncoderEncodeOptions |
keyFrame |
boolean |
No, default false |
Force a key frame for this input. |
EncodedVideoChunk is constructed with { type: "key" | "delta", timestamp, duration, data }; VideoFrame from a canvas, image, or ArrayBuffer with { timestamp } and must be closed with frame.close() once consumed, or the pool of GPU-backed buffers runs dry.
Exceptions
Section titled “Exceptions”| Method | Exception | Condition |
|---|---|---|
configure() |
TypeError |
The config is not valid: empty codec, only one of codedWidth and codedHeight, zero encoder width or height. |
configure() |
InvalidStateError |
state is "closed". |
configure() (async) |
NotSupportedError |
The browser cannot support the config; delivered to the error callback and the codec is closed. |
decode() |
InvalidStateError |
state is not "configured". |
decode() |
DataError |
A key chunk is required (first chunk after configure() or flush()) and chunk.type is "delta", or a chunk marked "key" is found not to be one. |
decode() / encode() (async) |
EncodingError |
Decoding or encoding failed; delivered to error and the codec is closed. |
encode() |
InvalidStateError |
state is not "configured". |
encode() |
TypeError |
The VideoFrame or AudioData is detached (already closed). |
encode() |
DataError |
The frame’s rotation or flip differs from the first frame encoded since configure(). |
flush() |
InvalidStateError |
Rejected promise when state is not "configured". |
flush() (pending) |
AbortError |
reset() or close() was called before the flush completed. |
reset(), close() |
InvalidStateError |
state is already "closed". |
isConfigSupported() |
TypeError |
Rejected promise for an invalid config; an unsupported but valid config resolves with supported: false. |
close() is final: the error callback is not invoked for the AbortError it uses internally, and a new instance is needed afterwards. reset() returns the codec to "unconfigured" and requires a new configure() and a key frame.
Examples
Section titled “Examples”Each example feature-detects the codec interfaces and shows what runs without them. All three are meant to run inside a dedicated worker; the main-thread version differs only in where the frames come from.
Checking codec support before configuring, with a MediaRecorder fallback
Section titled “Checking codec support before configuring, with a MediaRecorder fallback”isConfigSupported() answers without allocating a codec, so probe the preferred profile first and drop to MediaRecorder (which writes a container but hides individual frames) where WebCodecs is absent or the profile is unsupported.
async function pickEncoder(width, height) { if (typeof VideoEncoder === 'undefined') return { kind: 'mediarecorder' };
const candidates = [ { codec: 'av01.0.04M.08', width, height, bitrate: 2_000_000, framerate: 30 }, { codec: 'vp09.00.10.08', width, height, bitrate: 2_000_000, framerate: 30 }, { codec: 'avc1.42001E', width, height, bitrate: 2_000_000, framerate: 30, avc: { format: 'annexb' } }, ]; for (const config of candidates) { const { supported } = await VideoEncoder.isConfigSupported(config); if (supported) return { kind: 'webcodecs', config }; } return { kind: 'mediarecorder' };}The resolved config from isConfigSupported() is the dictionary with unknown members removed; pass that object to configure() rather than the original to avoid a later NotSupportedError.
Decoding demuxed chunks and surfacing the error callback
Section titled “Decoding demuxed chunks and surfacing the error callback”A demuxer yields { type, timestamp, duration, data } records; wrap each in an EncodedVideoChunk and keep the first one a key frame. The error callback is the only place an EncodingError or NotSupportedError arrives, so route it to the UI instead of relying on exceptions.
function createDecoder(config, onFrame, onFatal) { if (typeof VideoDecoder === 'undefined') { onFatal(new Error('WebCodecs is not available; play through a <video> element instead.')); return null; } const decoder = new VideoDecoder({ output: (frame) => { onFrame(frame); // caller must call frame.close() }, error: (err) => onFatal(err), // the decoder is closed once this fires }); decoder.configure(config); return decoder;}
async function feed(decoder, records) { for (const r of records) { if (decoder.decodeQueueSize > 8) { await new Promise((resolve) => decoder.addEventListener('dequeue', resolve, { once: true })); } decoder.decode(new EncodedVideoChunk(r)); } await decoder.flush();}The decodeQueueSize check keeps memory bounded; without it a fast demuxer queues the whole file before the first frame is decoded.
Encoding canvas frames in a worker and forcing periodic key frames
Section titled “Encoding canvas frames in a worker and forcing periodic key frames”Capture frames from an OffscreenCanvas, request a key frame every 60 frames so a viewer can join mid-stream, and close each VideoFrame after encode() has taken it.
function startEncoder(canvas, config, onChunk) { if (typeof VideoEncoder === 'undefined') return null; const encoder = new VideoEncoder({ output: (chunk, meta) => onChunk(chunk, meta?.decoderConfig ?? null), error: (err) => console.error(`${err.name}: ${err.message}`), }); encoder.configure(config);
let index = 0; return { push(timestampMicros) { const frame = new VideoFrame(canvas, { timestamp: timestampMicros }); encoder.encode(frame, { keyFrame: index % 60 === 0 }); frame.close(); index += 1; }, async stop() { await encoder.flush(); encoder.close(); }, };}meta.decoderConfig is present on the first output chunk and carries the description a decoder needs for AVC; store it with the stream header.
See also
Section titled “See also”- WebGPU, the usual consumer of decoded
VideoFrames - WebRTC, whose insertable streams hand
VideoFrames to and from WebCodecs - WebTransport, a transport for encoded chunks without a container
- WebCodecs: VideoDecoder.configure() method (w3.org)
- WebCodecs: codec processing model (w3.org)
- Video processing with WebCodecs (developer.chrome.com)
- WebKit features in Safari 16.4 (webkit.org)
- Firefox 130 for developers (developer.mozilla.org)
Specifications
| Specification | Status |
|---|---|
| WebCodecs API | W3C |
| WebCodecs: VideoDecoder.configure() method | W3C |
| WebCodecs: VideoEncoder.encode() method | W3C |
| WebCodecs: codec processing model | W3C |
- Legend
- Yes
- Partial
- Flag
- No
- Unknown
| Browser / Platform | Support | Versions | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Desktop) | Yes | 94 | high | source | 1 |
| Chrome (Android) | Yes | 94 | high | source | 23 |
| Edge (Desktop) | Yes | 94 | high | source | 45 |
| Firefox (Desktop) | Yes | 130 | high | source | 6 |
| Firefox (Android) | No | — | high | source | 78 |
| Safari (macOS) | Yes | 16.4 | high | source | 9 |
| Safari (iOS) | Yes | 16.4 | high | source | 1011 |
| Samsung Internet | Yes | 17.0 | high | source | 1213 |
| WebView (Android) | Yes | 94 | high | source | 1415 |
| Chrome (Desktop) | Yes | 94 | high | source | 16 |
| Chrome (Android) | Yes | 94 | high | source | 1718 |
| Edge (Desktop) | Yes | 94 | high | source | 1920 |
| Firefox (Desktop) | Yes | 130 | high | source | 21 |
| Firefox (Android) | No | — | high | source | 2223 |
| Safari (macOS) | Yes | 26 | high | source | 24 |
| Safari (iOS) | Yes | 26 | high | source | 2526 |
| Samsung Internet | Yes | 17.0 | high | source | 2728 |
| WebView (Android) | Yes | 94 | high | source | 2930 |
- Covers the `VideoDecoder` entry.
- Covers the `VideoDecoder` entry.
- Derived by browser-compat-data mirroring from Chrome.
- Covers the `VideoDecoder` entry.
- Derived by browser-compat-data mirroring from Chrome.
- Covers the `VideoDecoder` entry.
- Covers the `VideoDecoder` entry.
- No Firefox for Android support is recorded in browser-compat-data.
- Covers the `VideoDecoder` entry.
- Covers the `VideoDecoder` entry.
- Derived by browser-compat-data mirroring from Safari.
- Covers the `VideoDecoder` entry.
- Derived by browser-compat-data mirroring from Chrome Android.
- Covers the `VideoDecoder` entry.
- Derived by browser-compat-data mirroring from Chrome Android.
- Covers the `AudioDecoder` entry.
- Covers the `AudioDecoder` entry.
- Derived by browser-compat-data mirroring from Chrome.
- Covers the `AudioDecoder` entry.
- Derived by browser-compat-data mirroring from Chrome.
- Covers the `AudioDecoder` entry.
- Covers the `AudioDecoder` entry.
- No Firefox for Android support is recorded in browser-compat-data.
- Covers the `AudioDecoder` entry.
- Covers the `AudioDecoder` entry.
- Derived by browser-compat-data mirroring from Safari.
- Covers the `AudioDecoder` entry.
- Derived by browser-compat-data mirroring from Chrome Android.
- Covers the `AudioDecoder` entry.
- Derived by browser-compat-data mirroring from Chrome Android.