WebCodecs:低延迟音视频编解码 API

FreeGuideOnline 最新 2026-07-03


```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)或上传服务器处理
}