跳转到内容

能力 · API

WebCodecs API

发布于

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 中都可用,编码循环也应放在那里,以免占用主线程。

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() 并喂一帧关键帧。

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

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

Section titled “配置前检查编解码器支持,回退到 MediaRecorder”

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

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 回调

Section titled “解码解封装后的块并上报 error 回调”

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

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 帧并定期强制关键帧

Section titled “在 worker 中编码 canvas 帧并定期强制关键帧”

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

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;把它和流头一起保存。

规范

规范状态
WebCodecs: VideoDecoder.configure() methodW3C
WebCodecs: VideoEncoder.encode() methodW3C
WebCodecs: codec processing modelW3C