WebCodecs:低延迟音视频编解码 API
```javascript
// 获取摄像头流
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const video = document.getElementById('sourceVideo');
video.srcObject = stream;
await video.play();
// 使用 MediaStreamTrackProcessor(需要 chromium 内核浏览器)
const track = stream.getVideoTracks()[0];
const processor = new MediaStreamTrackProcessor({ track });
const reader = processor.readable.getReader();
4.2 初始化视频编码器
需要指定编码格式(如 AVC/H.264)和相关配置。
const encoder = new VideoEncoder({
output: (chunk, metadata) => {
// 每个编码好的 chunk 会在此回调中收到
// 可以在这里将 chunk 发送到网络或保存
console.log('编码输出 chunk:', chunk, 'metadata:', metadata);
},
error: (e) => console.error('编码错误:', e)
});
const config = {
codec: 'avc1.42001E', // H.264 Baseline Profile
width: 640,
height: 480,
bitrate: 1_000_000, // 1 Mbps
framerate: 30,
};
await encoder.configure(config);
4.3 编码循环
从 reader 持续读取 VideoFrame,交给 encoder 编码,然后及时关闭 frame(释放 GPU 内存)。
async function encodeFrames() {
while (true) {
const { done, value: frame } = await reader.read();
if (done) break;
// 调用 encoder 的 encode() 会消耗 frame,必须在同一事件循环内 close()
encoder.encode(frame, { keyFrame: false });
frame.close(); // 释放底层资源
}
}
encodeFrames();
// 完成后调用 encoder.flush() 等待所有编码完成,然后 encoder.close()
⚠️ 关键点:每帧调用
frame.close()避免 GPU 内存泄漏;encode第二个参数可强制关键帧。
5. 视频解码实战:从 H.264 到 Canvas 显示
将编码块解码为 VideoFrame,并绘制到 Canvas 上。
5.1 初始化解码器
const decoder = new VideoDecoder({
output: (frame) => {
// 解码出的 VideoFrame 直接可以绘制到 canvas
const canvas = document.getElementById('outputCanvas');
const ctx = canvas.getContext('2d');
ctx.drawImage(frame, 0, 0);
frame.close(); // 同样需要及时释放
},
error: (e) => console.error('解码错误:', e)
});
await decoder.configure({
codec: 'avc1.42001E',
// width, height 可选,但建议提供,有助于硬件加速
});
5.2 喂入编码块
假设我们从网络接收到 EncodedVideoChunk 对象(或自己构造)。
// 构造 EncodedVideoChunk 示例,通常 data 来源于前面编码器的输出
async function decodeChunk(encodedData, timestamp, isKeyFrame) {
const chunk = new EncodedVideoChunk({
type: isKeyFrame ? 'key' : 'delta',
timestamp: timestamp, // 微秒
duration: 33333, // 每帧持续时间 ≈ 33.3ms
data: encodedData // Uint8Array
});
decoder.decode(chunk);
}
// 待所有 chunk 发送后调用 decoder.flush()
6. 音频编解码速览
音频流程与视频完全对称。
音频编码
从麦克风获取 AudioData,送入 AudioEncoder 输出 EncodedAudioChunk。
const audioTrack = audioStream.getAudioTracks()[0];
const audioProcessor = new MediaStreamTrackProcessor({ track: audioTrack });
const audioReader = audioProcessor.readable.getReader();
const audioEncoder = new AudioEncoder({
output: (chunk) => { /* 发送或存储 */ },
error: (e) => console.error(e)
});
await audioEncoder.configure({
codec: 'opus', // 或 'mp3', 'aac' 等
numberOfChannels: 1,
sampleRate: 48000,
bitrate: 64000
});
// 读取循环
while (true) {
const { done, value: audioData } = await audioReader.read();
if (done) break;
audioEncoder.encode(audioData);
audioData.close();
}
音频解码
相反过程:EncodedAudioChunk → AudioDecoder → AudioData(可送入 AudioWorklet 播放或保存)。
const audioDecoder = new AudioDecoder({
output: (audioData) => {
// 使用 Web Audio API 播放
// 或存储 AudioData 的 buffer 再处理
audioData.close();
},
error: (e) => console.error(e)
});
await audioDecoder.configure({ codec: 'opus', sampleRate: 48000, numberOfChannels: 1 });
// 解码 chunk 类似视频
7. 配置编解码器细节
7.1 编解码器字符串
通常格式为 "video/mp4; codecs=avc1.42001E" 或简洁形式 "avc1.42001E"。常用编码标识符:
| 类型 | 编码标识符示例 | 说明 |
|---|---|---|
| 视频 | avc1.42001E |
H.264 Baseline Profile, Level 1 |
| 视频 | vp8 |
VP8 |
| 视频 | vp09.00.10.08 |
VP9 Profile 0, Level 1.0, 8-bit |
| 视频 | hev1.1.6.L93.B0 |
HEVC (H.265) 示例 |
| 音频 | opus |
Opus |
| 音频 | mp4a.40.2 |
AAC-LC |
| 音频 | mp3 |
MP3 |
可通过 VideoEncoder.isConfigSupported() 和 AudioEncoder.isConfigSupported() 测试支持情况。
7.2 硬件加速
默认编码器会尝试使用硬件加速。可以通过 hardwareAcceleration 选项控制:
await encoder.configure({
...config,
hardwareAcceleration: 'prefer-hardware', // 'no-preference', 'prefer-hardware', 'prefer-software'
});
8. 开发者必知的生命周期管理
关闭与刷新
encoder.close()/decoder.close():立即丢弃所有待处理任务,释放资源。调用了close()后不能再使用。encoder.flush()/decoder.flush():等待所有已提交的帧/块被处理完毕,并产生最终输出。返回 Promise,完成后通常紧接着调用close()。
错误处理
所有编解码器都有 error 回调(或 onerror)、state 属性("unconfigured", "configured", "closed")。务必处理错误,避免静默失效。
9. 完整低延迟示例:摄像头编码 + 解码显示
假设我们有两个 Canvas:一个隐藏用于提取帧(或直接用 processor),一个用于显示解码结果。下面简化使用处理器实现本地环回演示。
// ----- 编码部分 -----
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const track = stream.getVideoTracks()[0];
const processor = new MediaStreamTrackProcessor({ track });
const reader = processor.readable.getReader();
const encoder = new VideoEncoder({
output: (chunk) => { queueEncodedChunk(chunk); },
error: (e) => console.error('encode err:', e)
});
await encoder.configure({
codec: 'vp8', width: 640, height: 480, bitrate: 1_500_000, framerate: 30
});
// 将编码块存入队列,供解码器使用
const chunkQueue = [];
function queueEncodedChunk(chunk) {
// 实际场景可立即推送服务端或送入解码器
chunkQueue.push(chunk);
}
// 编码帧循环
(async function encodeLoop() {
for (;;) {
const { done, value: frame } = await reader.read();
if (done) break;
const keyFrame = true; // 每隔若干帧设一次
encoder.encode(frame, { keyFrame });
frame.close();
}
await encoder.flush();
encoder.close();
})();
// ----- 解码部分 -----
const decoder = new VideoDecoder({
output: (frame) => {
const canvas = document.getElementById('displayCanvas');
canvas.width = frame.displayWidth;
canvas.height = frame.displayHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(frame, 0, 0);
frame.close();
},
error: (e) => console.error('decode err:', e)
});
await decoder.configure({ codec: 'vp8' });
// 解码队列中的块
async function consumeQueue() {
while (chunkQueue.length > 0) {
const chunk = chunkQueue.shift();
decoder.decode(chunk);
}
requestAnimationFrame(consumeQueue); // 简单轮询,实际可用微任务
}
consumeQueue();
💡 性能提示:生产环境应避免
requestAnimationFrame轮询,推荐使用 readable stream 或自定义事件机制。
10. 浏览器支持与降级方案
目前 WebCodecs 在 Chrome 94+ 和 Edge 94+ 中可用,Firefox 支持尚在开发中(截至 2025 年初尚未默认开启),Safari 未实施。
检测支持:
if ('VideoEncoder' in window) {
// 可用
} else {
// 降级到 WebAssembly 方案(如 FFmpeg.wasm)或上传服务器处理
}