# WebCodecs API

> VideoDecoder、VideoEncoder、AudioDecoder 与 AudioEncoder 提供逐帧的编解码器访问：configure、decode、encode、flush、每个异常，以及可运行示例。

WebCodecs 把浏览器的媒体编解码器逐帧暴露出来：`VideoDecoder` 把 `EncodedVideoChunk` 解成 `VideoFrame`，`VideoEncoder` 反向编码，`AudioDecoder` 与 `AudioEncoder` 对 `EncodedAudioChunk` 和 `AudioData` 做同样的事。每个编解码器是一条带 `output` 与 `error` 回调的队列，而不是返回 Promise 的方法，视频编辑器、会议客户端或云游戏串流正是靠这一点在保留硬件加速的同时逐帧控制时序。容器不在范围内：API 不读写 MP4 或 WebM，封装与解封装库要放在两侧。

Chrome 94 与 Edge 94 发布了全部四个编解码器；Firefox 130 在桌面端跟进（Android 版 Firefox 没有）；Safari 16.4 加入 `VideoDecoder` 与 `VideoEncoder`，Safari 26 加入 `AudioDecoder` 与 `AudioEncoder`（BCD `api.VideoDecoder`、`api.AudioDecoder`）。所有接口在专用 worker 中都可用，编码循环也应放在那里，以免占用主线程。

## 语法

```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` 与 `AudioEncoder` 形态相同，方法为 `decode(chunk)` 与 `encode(data)`。`configure()`、`decode()`、`encode()`、`reset()` 与 `close()` 返回 `undefined`，作用于编解码器的控制消息队列；`flush()` 返回一个 Promise，在排队的每条消息都产出结果后兑现；`isConfigSupported()` 返回 `{ supported, config }` 的 Promise，其中 `config` 已剔除不认识的成员。属性有 `state`（`"unconfigured"`、`"configured"`、`"closed"`）、`decodeQueueSize` 或 `encodeQueueSize`，以及队列缩短时触发的 `dequeue` 事件（Chrome 106）。接口都是 `[SecureContext]`。

## 参数

构造函数的 init 有两个必填回调。`configure()` 接受配置字典，其 `codec` 必须是完整限定的字符串：`"vp09.00.10.08"`、`"avc1.42001E"`、`"av01.0.04M.08"`、`"opus"` 或 `"mp4a.40.2"`；`"vp9"`、`"h264"` 这类简写会被判为无效。

| 字典 | 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `VideoDecoderInit` / `VideoEncoderInit` | `output` | 回调 | 是 | 接收每个 `VideoFrame`（解码器）或 `EncodedVideoChunk` 加 `EncodedVideoChunkMetadata`（编码器）。 |
| 同上 | `error` | `WebCodecsErrorCallback` | 是 | 接收导致编解码器关闭的 `DOMException`。 |
| `VideoDecoderConfig` | `codec` | `DOMString` | 是 | 完整限定的编解码器字符串。 |
| `VideoDecoderConfig` | `description` | `AllowSharedBufferSource` | 否 | 编解码器专用的 extradata，例如 `avcC` box；AVC 格式的 H.264 必需，Annex B 则不用。 |
| `VideoDecoderConfig` | `codedWidth`、`codedHeight` | `unsigned long` | 否 | 编码帧的尺寸；要么都给，要么都不给。 |
| `VideoDecoderConfig` | `displayAspectWidth`、`displayAspectHeight` | `unsigned long` | 否 | 覆盖显示宽高比。 |
| `VideoDecoderConfig` | `colorSpace` | `VideoColorSpaceInit` | 否 | `primaries`、`transfer`、`matrix`、`fullRange`。 |
| `VideoDecoderConfig` | `hardwareAcceleration` | `HardwareAcceleration` | 否，默认 `"no-preference"` | `"prefer-hardware"` 或 `"prefer-software"`。 |
| `VideoDecoderConfig` | `optimizeForLatency` | `boolean` | 否 | 请求最小的解码队列深度。 |
| `VideoDecoderConfig` | `rotation`、`flip` | `double`、`boolean` | 否 | 施加到输出帧的方向（Chrome 138）。 |
| `VideoEncoderConfig` | `codec`、`width`、`height` | | 是 | 编解码器字符串，以及传给 `encode()` 的每一帧的尺寸。 |
| `VideoEncoderConfig` | `displayWidth`、`displayHeight` | `unsigned long` | 否 | 写入码流的显示尺寸。 |
| `VideoEncoderConfig` | `bitrate` | `unsigned long long` | 否 | 目标码率（bit/s）。 |
| `VideoEncoderConfig` | `framerate` | `double` | 否 | 预期帧率，用于码率控制。 |
| `VideoEncoderConfig` | `bitrateMode` | `VideoEncoderBitrateMode` | 否，默认 `"variable"` | `"constant"`、`"variable"` 或 `"quantizer"`。 |
| `VideoEncoderConfig` | `latencyMode` | `LatencyMode` | 否，默认 `"quality"` | `"realtime"` 用压缩率换低延迟。 |
| `VideoEncoderConfig` | `scalabilityMode` | `DOMString` | 否 | SVC 模式，例如 `"L1T2"`。 |
| `VideoEncoderConfig` | `alpha` | `AlphaOption` | 否，默认 `"discard"` | `"keep"` 在编解码器允许时编码 alpha 通道。 |
| `VideoEncoderConfig` | `hardwareAcceleration`、`contentHint` | | 否 | 同解码器；`contentHint` 取 `"detail"`、`"text"` 或 `"motion"`。 |
| `VideoEncoderEncodeOptions` | `keyFrame` | `boolean` | 否，默认 `false` | 强制把这一帧编为关键帧。 |

`EncodedVideoChunk` 用 `{ type: "key" | "delta", timestamp, duration, data }` 构造；`VideoFrame` 可由 canvas、图像或 `ArrayBuffer` 加 `{ timestamp }` 构造，用完必须调用 `frame.close()`，否则 GPU 缓冲池会耗尽。

## 异常

| 方法 | 异常 | 条件 |
|---|---|---|
| `configure()` | `TypeError` | 配置无效：`codec` 为空、`codedWidth` 与 `codedHeight` 只给了一个、编码器 `width` 或 `height` 为 0。 |
| `configure()` | `InvalidStateError` | `state` 为 `"closed"`。 |
| `configure()`（异步） | `NotSupportedError` | 浏览器无法支持该配置；送到 `error` 回调，编解码器随即关闭。 |
| `decode()` | `InvalidStateError` | `state` 不是 `"configured"`。 |
| `decode()` | `DataError` | 需要关键帧（`configure()` 或 `flush()` 后的第一块）而 `chunk.type` 是 `"delta"`，或标为 `"key"` 的块被发现并非关键帧。 |
| `decode()` / `encode()`（异步） | `EncodingError` | 解码或编码失败；送到 `error`，编解码器关闭。 |
| `encode()` | `InvalidStateError` | `state` 不是 `"configured"`。 |
| `encode()` | `TypeError` | `VideoFrame` 或 `AudioData` 已分离（已关闭）。 |
| `encode()` | `DataError` | 帧的 rotation 或 flip 与 `configure()` 后编码的第一帧不同。 |
| `flush()` | `InvalidStateError` | `state` 不是 `"configured"` 时返回被拒绝的 Promise。 |
| `flush()`（未完成） | `AbortError` | flush 完成前调用了 `reset()` 或 `close()`。 |
| `reset()`、`close()` | `InvalidStateError` | `state` 已经是 `"closed"`。 |
| `isConfigSupported()` | `TypeError` | 配置无效时返回被拒绝的 Promise；有效但不受支持的配置以 `supported: false` 兑现。 |

`close()` 是终态：它内部使用的 `AbortError` 不会触发 `error` 回调，之后需要新建实例。`reset()` 把编解码器退回 `"unconfigured"`，需要重新 `configure()` 并喂一帧关键帧。

:::observed
Chrome 的提示字符串来自 [`codec_state_helper.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webcodecs/codec_state_helper.cc) 与 [`video_decoder.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webcodecs/video_decoder.cc)（chromium.googlesource.com）：`configure()` 之前调用 `decode()` 抛出 `InvalidStateError: Cannot call 'decode' on an unconfigured codec.`；`close()` 之后调用抛出 `InvalidStateError: Cannot call 'decode' on a closed codec.`；第一块就喂 delta 块抛出 `DataError: A key frame is required after configure() or flush().`；`close()` 时仍未完成的 `flush()` Promise 以 `AbortError: Aborted due to close()` 拒绝。对已关闭的帧调用 `VideoEncoder.encode()` 抛出 `TypeError: Cannot encode closed input.`。
:::

## 示例

每个示例都先检测编解码器接口，并给出没有它们时的分支。三段代码都应在专用 worker 中运行；主线程版本只是帧的来源不同。

### 配置前检查编解码器支持，回退到 MediaRecorder

`isConfigSupported()` 不分配编解码器就能作答，所以先探测首选 profile，在 WebCodecs 缺失或 profile 不受支持时退到 `MediaRecorder`（它写出容器文件，但看不到单帧）。

```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' };
}
```

`isConfigSupported()` 兑现的 `config` 是去掉未知成员后的字典；把它而不是原对象传给 `configure()`，可以避免之后的 `NotSupportedError`。

### 解码解封装后的块并上报 error 回调

解封装器产出 `{ type, timestamp, duration, data }` 记录；把每条包成 `EncodedVideoChunk`，并保证第一块是关键帧。`error` 回调是 `EncodingError` 或 `NotSupportedError` 唯一到达的地方，所以把它接到界面上，而不是指望异常。

```js
function createDecoder(config, onFrame, onFatal) {
  if (typeof VideoDecoder === 'undefined') {
    onFatal(new Error('WebCodecs 不可用，请改用 <video> 元素播放。'));
    return null;
  }
  const decoder = new VideoDecoder({
    output: (frame) => {
      onFrame(frame); // 调用方必须调用 frame.close()
    },
    error: (err) => onFatal(err), // 回调触发后解码器已关闭
  });
  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();
}
```

`decodeQueueSize` 检查让内存有界；没有它，解封装器一快，整个文件会在第一帧解出之前全部排进队列。

### 在 worker 中编码 canvas 帧并定期强制关键帧

从 `OffscreenCanvas` 取帧，每 60 帧请求一个关键帧以便观众中途加入，并在 `encode()` 接手后关闭每个 `VideoFrame`。

```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` 出现在第一个输出块上，带着 AVC 解码器所需的 `description`；把它和流头一起保存。

## 另请参阅

- [WebGPU](/zh/reference/capabilities/webgpu/)，解码后 `VideoFrame` 的常见消费方
- [WebRTC](/zh/reference/capabilities/webrtc/)，其 insertable streams 在 WebCodecs 之间传递 `VideoFrame`
- [WebTransport](/zh/reference/capabilities/webtransport/)，不带容器直接传输编码块的通道
- [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）