操纵视频流组件。
现代 Web 技术提供了多种处理视频的方式。 Media Stream API、Media Recording API、Media Source API 和 WebRTC API 共同构成了一套丰富的工具,可用于录制、传输和播放视频流。虽然这些 API 可用于解决某些高级别任务,但无法让 Web 程序员处理视频流的各个组件,例如帧和未混合的编码视频或音频块。为了获得对这些基本组件的低级别访问权限,开发者一直使用 WebAssembly 将视频和音频编解码器引入浏览器。但鉴于现代浏览器已经随附各种编解码器(通常由硬件加速),将它们重新打包为 WebAssembly 似乎会浪费人力和计算机资源。
WebCodecs API 通过为程序员提供一种使用浏览器中已有的媒体组件的方式,消除了这种低效性。具体而言:
- 视频和音频解码器
- 视频和音频编码器
- 原始视频帧
- 图像解码器
WebCodecs API 适用于需要完全控制媒体内容处理方式的 Web 应用,例如视频编辑器、视频会议、视频流式传输等。
视频处理工作流程
帧是视频处理的核心。因此,在 WebCodecs 中,大多数类要么使用帧,要么生成帧。视频编码器将帧转换为编码块。视频解码器则相反。
此外,VideoFrame
还是一个 CanvasImageSource
,并且具有接受 CanvasImageSource
的 constructor,因此可以与其他 Web API 很好地搭配使用。
因此,它可用于 drawImage()
和 texImage2D()
等函数。它还可以通过画布、位图、视频元素和其他视频帧来构建。
WebCodecs API 可与 Insertable Streams API 中的类完美搭配使用,从而将 WebCodecs 连接到媒体流轨道。
MediaStreamTrackProcessor
将媒体轨道分解为单个帧。MediaStreamTrackGenerator
可从帧流创建媒体轨道。
WebCodecs 和网络工作器
根据设计,WebCodecs API 会异步执行所有繁重任务,并且不会在主线程上执行。但由于帧和块回调通常每秒可被多次调用,因此它们可能会使主线程变得杂乱,从而降低网站的响应速度。 因此,最好将单个帧和编码块的处理移到 Web Worker 中。
为了解决这个问题,ReadableStream 提供了一种便捷的方式,可自动将来自媒体轨道的所有帧传输到工作器。例如,MediaStreamTrackProcessor
可用于获取来自网络摄像头的媒体流轨道的 ReadableStream
。之后,视频流会转移到 Web 工作器,在该工作器中,帧会逐个读取并排队到 VideoEncoder
中。
借助 HTMLCanvasElement.transferControlToOffscreen
,即使是渲染也可以在主线程之外完成。但如果所有高级别工具都不方便,VideoFrame
本身是可转移的,可以在工作器之间移动。
WebCodecs 的实际运用
编码

Canvas
或 ImageBitmap
到网络或存储空间的路径一切都从 VideoFrame
开始。
有三种方法可以构建视频帧。
从画布、图片位图或视频元素等图片来源。
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
使用
MediaStreamTrackProcessor
从MediaStreamTrack
中拉取帧const stream = await navigator.mediaDevices.getUserMedia({…}); const track = stream.getTracks()[0]; const trackProcessor = new MediaStreamTrackProcessor(track); const reader = trackProcessor.readable.getReader(); while (true) { const result = await reader.read(); if (result.done) break; const frameFromCamera = result.value; }
根据
BufferSource
中的二进制像素表示形式创建帧const pixelSize = 4; const init = { timestamp: 0, codedWidth: 320, codedHeight: 200, format: "RGBA", }; const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize); for (let x = 0; x < init.codedWidth; x++) { for (let y = 0; y < init.codedHeight; y++) { const offset = (y * init.codedWidth + x) * pixelSize; data[offset] = 0x7f; // Red data[offset + 1] = 0xff; // Green data[offset + 2] = 0xd4; // Blue data[offset + 3] = 0x0ff; // Alpha } } const frame = new VideoFrame(data, init);
无论帧来自何处,都可以使用 VideoEncoder
将其编码为 EncodedVideoChunk
对象。
在编码之前,需要为 VideoEncoder
提供两个 JavaScript 对象:
- 使用两个函数初始化字典,用于处理编码块和错误。这些函数由开发者定义,在传递给
VideoEncoder
构造函数后便无法更改。 - 编码器配置对象,包含输出视频流的参数。您稍后可以通过调用
configure()
更改这些参数。
如果浏览器不支持相应配置,configure()
方法将抛出 NotSupportedError
。建议您使用配置调用静态方法 VideoEncoder.isConfigSupported()
,以便提前检查配置是否受支持并等待其 promise。
const init = {
output: handleChunk,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
width: 640,
height: 480,
bitrate: 2_000_000, // 2 Mbps
framerate: 30,
};
const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
const encoder = new VideoEncoder(init);
encoder.configure(config);
} else {
// Try another config.
}
设置好编码器后,它就可以通过 encode()
方法接收帧了。configure()
和 encode()
都会立即返回,而无需等待实际工作完成。它允许同时将多个帧排队等待编码,而 encodeQueueSize
则显示有多少请求在队列中等待之前的编码完成。
如果实参或方法调用顺序违反了 API 约定,系统会立即抛出异常来报告错误;如果编解码器实现中遇到问题,系统会调用 error()
回调来报告错误。
如果编码成功完成,系统会调用 output()
回调,并将新的编码块作为实参传递给该回调。
这里另一个重要的细节是,需要通过调用 close()
来告知帧何时不再需要。
let frameCounter = 0;
const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);
const reader = trackProcessor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done) break;
const frame = result.value;
if (encoder.encodeQueueSize > 2) {
// Too many frames in flight, encoder is overwhelmed
// let's drop this frame.
frame.close();
} else {
frameCounter++;
const keyFrame = frameCounter % 150 == 0;
encoder.encode(frame, { keyFrame });
frame.close();
}
}
最后,我们需要编写一个函数来处理从编码器输出的编码视频块,从而完成编码代码的编写。 通常,此函数会通过网络发送数据块,或将数据块多路复用到媒体容器中以进行存储。
function handleChunk(chunk, metadata) {
if (metadata.decoderConfig) {
// Decoder needs to be configured (or reconfigured) with new parameters
// when metadata has a new decoderConfig.
// Usually it happens in the beginning or when the encoder has a new
// codec specific binary configuration. (VideoDecoderConfig.description).
fetch("/upload_extra_data", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: metadata.decoderConfig.description,
});
}
// actual bytes of encoded data
const chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: chunkData,
});
}
如果您需要在某个时间点确保所有待处理的编码请求都已完成,可以调用 flush()
并等待其 promise。
await encoder.flush();
解码

Canvas
或 ImageBitmap
的路径。设置 VideoDecoder
与为 VideoEncoder
所做的设置类似:创建解码器时会传递两个函数,并且会将编解码器参数提供给 configure()
。
编解码器参数集因编解码器而异。例如,H.264 编解码器可能需要 AVCC 的二进制 blob,除非它以所谓的 Annex B 格式 (encoderConfig.avc = { format: "annexb" }
) 进行编码。
const init = {
output: handleFrame,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
codedWidth: 640,
codedHeight: 480,
};
const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
const decoder = new VideoDecoder(init);
decoder.configure(config);
} else {
// Try another config.
}
解码器初始化后,您就可以开始向其馈送 EncodedVideoChunk
对象了。如需创建分块,您需要:
- 已编码视频数据的
BufferSource
- 块的开始时间戳(以微秒为单位)(块中第一个编码帧的媒体时间)
- 块的类型,可以是以下类型之一:
- 如果相应块可以独立于之前的块进行解码,则为
key
- 如果只有在解码一个或多个之前的块之后才能解码相应块,则为
delta
- 如果相应块可以独立于之前的块进行解码,则为
此外,编码器发出的任何块都可直接供解码器使用。上述有关错误报告和编码器方法异步性质的所有内容同样适用于解码器。
const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
const chunk = new EncodedVideoChunk({
timestamp: responses[i].timestamp,
type: responses[i].key ? "key" : "delta",
data: new Uint8Array(responses[i].body),
});
decoder.decode(chunk);
}
await decoder.flush();
现在,我们来展示如何将新解码的帧显示在网页上。最好确保解码器输出回调 (handleFrame()
) 快速返回。在下面的示例中,它仅将帧添加到准备好进行渲染的帧队列中。
渲染是单独进行的,包含两个步骤:
- 等待合适的时间显示帧。
- 在画布上绘制帧。
当不再需要某个帧时,请在垃圾收集器处理该帧之前调用 close()
来释放底层内存,这样可以减少 Web 应用使用的平均内存量。
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;
function handleFrame(frame) {
pendingFrames.push(frame);
if (underflow) setTimeout(renderFrame, 0);
}
function calculateTimeUntilNextFrame(timestamp) {
if (baseTime == 0) baseTime = performance.now();
let mediaTime = performance.now() - baseTime;
return Math.max(0, timestamp / 1000 - mediaTime);
}
async function renderFrame() {
underflow = pendingFrames.length == 0;
if (underflow) return;
const frame = pendingFrames.shift();
// Based on the frame's timestamp calculate how much of real time waiting
// is needed before showing the next frame.
const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
await new Promise((r) => {
setTimeout(r, timeUntilNextFrame);
});
ctx.drawImage(frame, 0, 0);
frame.close();
// Immediately schedule rendering of the next frame
setTimeout(renderFrame, 0);
}
开发者提示
使用 Chrome 开发者工具中的媒体面板查看媒体日志并调试 WebCodecs。

演示
此演示展示了如何处理画布中的动画帧:
- 以 25 fps 的帧速率捕获到
ReadableStream
中,由MediaStreamTrackProcessor
完成 - 转移到 Web Worker
- 编码为 H.264 视频格式
- 再次解码为一系列视频帧
- 并使用
transferControlToOffscreen()
在第二个画布上呈现
其他演示
您还可以查看我们的其他演示:
使用 WebCodecs API
功能检测
如需检查 WebCodecs 支持情况,请执行以下操作:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
请注意,WebCodecs API 仅在安全上下文中可用,因此如果 self.isSecureContext
为 false,检测将失败。
反馈
Chrome 团队希望了解您在使用 WebCodecs API 方面的体验。
介绍 API 设计
API 是否存在某些方面未按预期运行?或者,是否有缺少的方法或属性需要您实现自己的想法?对安全模型有疑问或意见?在相应的 GitHub 代码库中提交规范问题,或在现有问题中添加您的想法。
报告实现方面的问题
您是否发现 Chrome 的实现存在 bug?还是实现与规范不同?请前往 new.crbug.com 提交 bug 报告。请务必尽可能详细地提供信息,并提供简单的重现说明,然后在组件框中输入 Blink>Media>WebCodecs
。
显示对 API 的支持
您是否打算使用 WebCodecs API?您的公开支持有助于 Chrome 团队确定功能优先级,并向其他浏览器供应商展示支持这些功能的重要性。
请发送电子邮件至 media-dev@chromium.org 或发送推文至 @ChromiumDev,并在其中使用 #WebCodecs
主题标签,告诉我们您在何处以及如何使用该功能。
主打图片,由 Denise Jans 在 Unsplash 上发布。