# WebCodecs API

> VideoDecoder, VideoEncoder, AudioDecoder, and AudioEncoder give per-frame codec access: configure, decode, encode, flush, every exception, runnable examples.

WebCodecs exposes the browser's media codecs one frame at a time: `VideoDecoder` turns `EncodedVideoChunk` objects into `VideoFrame`s, `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

```js
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

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

| 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.

:::observed
Chrome's messages, from [`codec_state_helper.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webcodecs/codec_state_helper.cc) and [`video_decoder.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webcodecs/video_decoder.cc) (chromium.googlesource.com): `decode()` before `configure()` throws `InvalidStateError: Cannot call 'decode' on an unconfigured codec.`; after `close()` it throws `InvalidStateError: Cannot call 'decode' on a closed codec.`; feeding a delta chunk first throws `DataError: A key frame is required after configure() or flush().`; and a `flush()` promise outstanding at `close()` rejects with `AbortError: Aborted due to close()`. A `VideoEncoder.encode()` on a frame already closed throws `TypeError: Cannot encode closed input.`.
:::

## 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

`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.

```js
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

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.

```js
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

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.

```js
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

- [WebGPU](/reference/capabilities/webgpu/), the usual consumer of decoded `VideoFrame`s
- [WebRTC](/reference/capabilities/webrtc/), whose insertable streams hand `VideoFrame`s to and from WebCodecs
- [WebTransport](/reference/capabilities/webtransport/), a transport for encoded chunks without a container
- [WebCodecs: VideoDecoder.configure() method](https://www.w3.org/TR/webcodecs/#dom-videodecoder-configure) (w3.org)
- [WebCodecs: codec processing model](https://www.w3.org/TR/webcodecs/#codec-processing-model) (w3.org)
- [Video processing with WebCodecs](https://developer.chrome.com/docs/web-platform/best-practices/webcodecs) (developer.chrome.com)
- [WebKit features in Safari 16.4](https://webkit.org/blog/13966/webkit-features-in-safari-16-4/) (webkit.org)
- [Firefox 130 for developers](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/130) (developer.mozilla.org)