WebRTC 全景实战 (9):音视频编解码与 Simulcast 入门
Ch4 SDP 协商的核心是编解码器选择——a=rtpmap 行的背后,是数十年来视频压缩技术的积累。Ron Frederick 在 1992 年为实现 nv 工具而手写软件视频压缩(Curious — RTP 历史),因为 MPEG-1 当时无法实时编码——今天 WebRTC 的 Codec 选择同样是在压缩率、延迟、专利、硬件加速之间权衡。
音频方面,Opus 的故事同样精彩:Skype 团队在收购后于 2010 年 IETF 会议上推动 Opus 标准化,Maastricht 午餐会时已完成大部分工作(Curious 历史)。Opus 继承了 Skype 的 SILK 和 Xiph 的 CELT 两条技术路线,成为 WebRTC 唯一的「指定音频编解码器」。
本章覆盖 RFC 7587 Opus、VP8/VP9/H.264/AV1 对比、Simulcast 三档发布与 RTCRtpEncodingParameters 实战配置。
零、Skype 与 Opus 的诞生
Curious 历史 记录了 Opus 标准化过程中一段有趣的插曲。2010 年 IETF 81 会议在 Maastricht 举行,Skype 工程师 Jean-Marc Valin 和 Koen Vos 带着几乎完成的 Opus 草案参会——午餐桌上就完成了大部分互操作测试。
| 技术 lineage | 来源 | 擅长场景 |
|---|---|---|
| SILK | Skype 收购前自研 | 8–40 kbps 语音,强抗丢包 |
| CELT | Xiph.Org(Ogg/Vorbis 团队) | 48–510 kbps 音乐,低延迟 |
| Opus | 2012 合并标准化 | 全码率覆盖,WebRTC 指定音频 Codec |
Microsoft 2011 年收购 Skype 后,Skype 团队将 SILK 技术贡献给 IETF,与开源社区 CELT 合并为 Opus。RFC 7587 2015 年发布,RFC 8871 将 Opus 列为 WebRTC Mandatory to Implement (MTI) 音频编解码器——所有 WebRTC 实现必须支持 Opus。
视频侧,Ron Frederick 1992 年为 nv 工具手写软件视频压缩(因为 MPEG-1 无法实时编码),这与今天 VP8/AV1 追求「实时 + 高压缩 + 免专利」的路线一脉相承。
本篇术语表
| 术语 | 英文 | 解释 |
|---|---|---|
| Codec | Coder-Decoder | 编解码器,压缩/解压媒体数据 |
| Opus | — | WebRTC 标准音频编解码器,RFC 7587 |
| VP8 | — | Google 开源视频编解码器,RFC 7741 |
| VP9 | — | VP8 继任者,更高压缩率,无 royalty |
| H.264/AVC | — | ITU/MPEG 标准,硬件加速最广泛 |
| AV1 | — | AOMedia 开源编解码器,最高压缩率 |
| Simulcast | — | 同时编码发送同一视频的多个分辨率层 |
| rid | RTP Stream ID | Simulcast 层的标识符(h/m/l) |
| SSRC | Synchronization Source | 每层 Simulcast 分配独立 SSRC |
| RTCRtpEncodingParameters | — | 控制每路编码的码率、分辨率、rid |
| scaleResolutionDownBy | — | 相对原始分辨率的下采样倍数 |
| maxBitrate | — | 该层编码的最大码率上限 |
| active | — | 该层是否激活发送 |
| Keyframe | I 帧 / IDR | 可独立解码的完整帧 |
| DTX | Discontinuous Transmission | 静音时停止发送,节省带宽 |
| FEC | Forward Error Correction | 带内前向纠错 |
| profile-level-id | — | H.264 的 profile 与 level 标识 |
| contentHint | — | 提示编码器内容类型:motion / detail / text |
| Unified Plan | — | WebRTC 现代 SDP 语义,Simulcast 必须使用 |
| MTI | Mandatory to Implement | WebRTC 强制实现的编解码器 |
一、编解码在媒体管线中的位置
编解码器是 WebRTC 媒体管线中CPU/GPU 消耗最大的环节。选择 Codec 不仅影响画质,还决定了:
- 端到端延迟(编码复杂度)
- 带宽消耗(压缩效率)
- 设备兼容性(硬件加速支持)
- 专利成本(H.264 vs royalty-free)
二、音频:Opus(RFC 7587)
WebRTC 音频几乎总是 Opus——Skype 团队在 IETF 标准化,2012 年发布 RFC 6381,2015 年更新为 RFC 7587。
2.1 Opus 的双模架构
Opus 是两种编解码器的结合:
| 特性 | 说明 |
|---|---|
| Bitrate | 6–510 kbps 自适应 |
| 帧长 | 2.5ms / 5ms / 10ms / 20ms / 40ms / 60ms |
| FEC | 内置前向纠错,抗丢包 |
| DTX | 静音检测,停止发送节省带宽 |
| 采样率 | 8kHz–48kHz(WebRTC 固定 48kHz) |
| 声道 | 1(mono)或 2(stereo) |
2.2 SDP 中的 Opus 参数
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;stereo=1;maxaveragebitrate=32000
| fmtp 参数 | 含义 |
|---|---|
minptime=10 | 最小打包时长 10ms |
useinbandfec=1 | 启用带内 FEC |
stereo=1 | 声明支持立体声 |
maxaveragebitrate=32000 | 限制平均码率 32kbps |
cbr=1 | 恒定码率模式 |
usedtx=1 | 启用 DTX 静音检测 |
2.3 Opus 码率与质量
| 场景 | 推荐码率 | 帧长 |
|---|---|---|
| 窄带语音(电话) | 12–20 kbps | 20ms |
| 宽带语音(会议) | 24–32 kbps | 20ms |
| 高质量语音 | 48–64 kbps | 20ms |
| 音乐 / 屏幕共享音频 | 64–128 kbps | 20ms |
// 限制 Opus 码率
const sender = pc.getSenders().find((s) => s.track?.kind === "audio");
const params = sender.getParameters();
params.encodings[0].maxBitrate = 32_000;
await sender.setParameters(params);
2.4 RED(冗余编码)
SDP 中常见的 RED payload type 是 Opus 的冗余封装:
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111/111/111/111
RED 将多个 Opus 帧打包在一个 RTP 包中,提供包级别的冗余(与 in-band FEC 互补)。WebRTC 中 RED 的使用因浏览器而异。
2.5 Opus 编码模式选择
Opus 编码器根据码率和内容自动切换 SILK/CELT/Hybrid 模式,但应用层可通过 SDP 和参数施加约束:
| 模式 | 触发条件 | 典型码率 |
|---|---|---|
| SILK | 语音主导,低码率 | 6–40 kbps |
| Hybrid | 语音 + 宽带 | 32–64 kbps |
| CELT | 音乐/高保真 | 64–510 kbps |
// 屏幕共享时音频轨 often 是系统音——提高码率上限
const audioSender = pc.getSenders().find((s) => s.track?.kind === "audio");
const audioParams = audioSender.getParameters();
audioParams.encodings[0].maxBitrate = 128_000;
await audioSender.setParameters(audioParams);
三、视频编解码对比
3.1 详细对比表
| Codec | 压缩率 | 编码延迟 | 硬件加速 | 专利 | WebRTC 支持 | 推荐场景 |
|---|---|---|---|---|---|---|
| VP8 | 中 | 低 | 广泛 | royalty-free | 全平台 | 默认首选、低延迟 |
| VP9 | 高(比 VP8 约 30-50%) | 中 | 较广泛 | royalty-free | 桌面为主 | 带宽受限、高分辨率 |
| H.264 | 高 | 低(硬编) | 最广泛 | 有专利池 | 全平台 | 移动端、硬编优先 |
| AV1 | 最高(比 H.264 约 30-50%) | 高(软编) | 新兴 | royalty-free | Chrome 117+ | 未来方向、带宽极受限 |
3.2 VP8(RFC 7741)
Google 2010 年收购 On2 后开源 VP8,成为 WebRTC 最早的视频编解码器。
| 特性 | 说明 |
|---|---|
| 最大分辨率 | 16384×16384(实际受设备限制) |
| 关键帧间隔 | 可配置,WebRTC 默认约 3 秒或 PLI 触发 |
| 错误恢复 | 黄金帧(Golden Frame)机制 |
| RTP 打包 | 单帧可拆多个 RTP 包,M bit 标记最后一包 |
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 transport-cc
3.3 VP9
VP9 是 VP8 的继任者,主要优势在高分辨率和带宽节省:
| 对比 VP8 | VP9 |
|---|---|
| 同画质码率 | 降低 30-50% |
| 编码 CPU | 高约 2-3x |
| SVC 支持 | 原生 SVC(Ch11) |
| 硬件加速 | Android 较广泛,iOS 不支持 |
a=rtpmap:98 VP9/90000
a=fmtp:98 profile-id=0
3.4 H.264/AVC
H.264 是 ITU-T 和 MPEG 联合标准,硬件加速支持最广泛——几乎所有手机芯片都有 H.264 硬编硬解。
a=rtpmap:102 H264/90000
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
| fmtp 参数 | 含义 |
|---|---|
profile-level-id=42e01f | Baseline Profile, Level 3.1 |
packetization-mode=1 | 非交错模式(WebRTC 要求) |
level-asymmetry-allowed=1 | 允许双方 level 不对称 |
profile-level-id 解码:
42e01f →
42 = Baseline Profile (0x42)
e0 = constraint_set flags
1f = Level 3.1 (0x1f = 31)
双方 profile-level-id 必须兼容。Offer 中列出多个 H.264 PT(不同 profile),Answer 选择双方都支持的那个。profile 不匹配是 H.264 协商失败的首要原因。
3.5 AV1
AV1 由 AOMedia(Google/Mozilla/Netflix 等)开发,压缩率最高但编码复杂度也最高。
| 特性 | 说明 |
|---|---|
| 压缩率 | 比 H.264 高 30-50%,比 VP9 高 20-30% |
| 编码速度 | 软编极慢(实时场景需硬编) |
| 硬件加速 | Intel/AMD/NVIDIA 新一代 GPU 开始支持 |
| WebRTC | Chrome 117+ 支持,Safari/Firefox 逐步跟进 |
a=rtpmap:41 AV1/90000
a=fmtp:41 profile=0;level-idx=5;tier=0
3.6 Codec 选择策略
// 设置 Codec 偏好
const transceiver = pc.addTransceiver("video", { direction: "sendrecv" });
const caps = RTCRtpSender.getCapabilities("video");
// 优先 H.264,其次 VP8
const preferred = caps.codecs.sort((a, b) => {
const order = { "video/H264": 0, "video/VP8": 1, "video/VP9": 2, "video/AV1": 3 };
return (order[a.mimeType] ?? 99) - (order[b.mimeType] ?? 99);
});
transceiver.setCodecPreferences(preferred);
3.7 屏幕共享 vs 摄像头:contentHint
屏幕共享(getDisplayMedia)与摄像头的内容特性截然不同——静态文字需要 sharp edges,摄像头需要运动平滑:
const screenTrack = screenStream.getVideoTracks()[0];
screenTrack.contentHint = "detail"; // 或 "text" / "motion"
const cameraTrack = cameraStream.getVideoTracks()[0];
cameraTrack.contentHint = "motion";
| contentHint | 编码器行为 | 适用 |
|---|---|---|
motion | 优先帧率,允许模糊 | 摄像头、游戏 |
detail | 优先清晰度,保文字边缘 | 屏幕共享 |
text | 最高清晰度,低帧率可接受 | 文档/代码演示 |
3.8 H.264 与 Simulcast 的限制
H.264 硬件编码器通常不支持 Simulcast 多路独立编码——同一时刻只能输出一路。Chrome 在 H.264 Simulcast 场景可能回退到软编或只发送单层。生产会议系统常见策略:
| 策略 | 说明 |
|---|---|
| VP8/VP9 Simulcast | 桌面端默认,多层无专利顾虑 |
| H.264 单流 + SFU 转码 | 移动端发送 H.264,SFU 转码给其他订阅者 |
| SVC(VP9/AV1) | 单编码多分层,见 Ch11 |
四、Simulcast 三档发布
Simulcast = 同时编码并发送同一视频的多个分辨率/码率层,SFU 按订阅者带宽选择转发哪一层(Ch11 详解 SFU 侧路由)。
4.1 为什么需要 Simulcast?
| 场景 | 无 Simulcast | 有 Simulcast |
|---|---|---|
| 100 人会议,带宽各异 | 所有人收到相同质量 | 每人按带宽收不同层 |
| 大屏 + 小窗 | 小窗浪费高分辨率带宽 | 小窗收 l 层 |
| 带宽波动 | 重协商 Codec/分辨率 | SFU 无缝切换层 |
Simulcast 的代价是发送端 CPU/带宽:三档意味着编码器运行 3 次。但发送端只需上行 1.5Mbps(最高层),而非 100 × 1.5Mbps——这就是 SFU 架构的优势。
Simulcast 要求 Unified Plan SDP 语义(现代浏览器默认)。Plan B 已废弃——若 pc.getConfiguration().sdpSemantics !== 'unified-plan',Simulcast 不会生效。每个 rid 对应 RTCRtpSender 的一个 encoding,而非独立 m-line。
4.2 RTCRtpEncodingParameters 实战
const transceiver = pc.addTransceiver("video", {
direction: "sendonly",
sendEncodings: [
{
rid: "h",
maxBitrate: 1_500_000,
maxFramerate: 30,
scaleResolutionDownBy: 1.0,
active: true,
},
{
rid: "m",
maxBitrate: 500_000,
maxFramerate: 15,
scaleResolutionDownBy: 2.0,
active: true,
},
{
rid: "l",
maxBitrate: 150_000,
maxFramerate: 7.5,
scaleResolutionDownBy: 4.0,
active: true,
},
],
});
4.3 EncodingParameters 字段详解
| 字段 | 类型 | 含义 | 示例 |
|---|---|---|---|
rid | string | RTP Stream ID,标识 Simulcast 层 | "h" / "m" / "l" |
active | boolean | 是否激活该层发送 | true |
maxBitrate | number | 最大码率(bps) | 1500000 |
minBitrate | number | 最小码率(bps) | 30000 |
maxFramerate | number | 最大帧率 | 30 |
scaleResolutionDownBy | number | 下采样倍数(相对原始) | 2.0 = 宽高各减半 |
priority | string | 层优先级 | "high" / "low" |
networkPriority | string | 网络优先级 | "high" / "low" |
ssrc | number | 手动指定 SSRC(通常自动生成) | — |
4.4 SDP 中的 Simulcast 协商
Offer 中 Simulcast 相关属性:
a=simulcast:send h;m;l
a=rid:1 send h
a=rid:2 send m
a=rid:3 send l
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
| 属性 | 含义 |
|---|---|
a=simulcast:send h;m;l | 发送三档,rid 分别为 h/m/l |
a=rid:1 send h | rid=h 映射到 mid 内的层 1 |
rtp-stream-id extmap | RTP 头扩展携带 rid |
repaired-rtp-stream-id | RTX 重传时关联原始 rid |
4.5 动态层管理
// 运行时关闭低质量层(节省 CPU)
const sender = pc.getSenders().find((s) => s.track?.kind === "video");
const params = sender.getParameters();
params.encodings.forEach((enc) => {
if (enc.rid === "l") {
enc.active = false; // 关闭 l 层
}
});
await sender.setParameters(params);
// 运行时调整码率
params.encodings.forEach((enc) => {
if (enc.rid === "h") {
enc.maxBitrate = 2_000_000; // 提升 h 层到 2Mbps
}
});
await sender.setParameters(params);
4.6 Simulcast vs SVC
| 特性 | Simulcast | SVC(Ch11) |
|---|---|---|
| 编码次数 | N 次(每层独立) | 1 次(分层编码) |
| CPU 消耗 | 高 | 低 |
| SFU 复杂度 | 按 SSRC 选层 | 按 temporal/spatial layer 选层 |
| 兼容性 | 全平台 | 需要 Codec 支持 SVC(VP9/AV1) |
| 灵活性 | 每层独立参数 | 层间有依赖关系 |
五、码率控制
WebRTC 的码率控制是多层协作的:
5.1 应用层码率限制
const sender = pc.getSenders().find((s) => s.track?.kind === "video");
const params = sender.getParameters();
// 限制所有层
params.encodings.forEach((enc) => {
enc.maxBitrate = 500_000;
});
await sender.setParameters(params);
5.2 GCC 自适应
GCC(Ch10)会在 maxBitrate 之下进一步自适应:
实际发送码率 = min(maxBitrate, GCC估计带宽, CPU允许码率)
getStats() 中 qualityLimitationReason 会告诉你瓶颈在哪:
| 值 | 含义 |
|---|---|
bandwidth | GCC 降低了码率 |
cpu | 编码器跟不上,主动降质 |
none | 无限制 |
5.3 初始码率与快速启动
// 部分浏览器支持 degradationPreference
const params = sender.getParameters();
params.degradationPreference = "maintain-framerate";
// 其他选项: "maintain-resolution" | "balanced"
await sender.setParameters(params);
| degradationPreference | 行为 |
|---|---|
maintain-framerate | 带宽不足时降分辨率,保帧率 |
maintain-resolution | 带宽不足时降帧率,保分辨率 |
balanced | 帧率和分辨率等比降低 |
六、编解码器协商过程
6.1 查看浏览器能力
const audioCodecs = RTCRtpSender.getCapabilities("audio").codecs;
const videoCodecs = RTCRtpSender.getCapabilities("video").codecs;
console.log("Audio:", audioCodecs.map((c) => c.mimeType));
console.log("Video:", videoCodecs.map((c) =>
`${c.mimeType} pt=${c.preferredPayloadType} hw=${c.hardwareAccelerated}`
));
6.2 强制特定 Codec
const transceiver = pc.addTransceiver("video", { direction: "sendrecv" });
const caps = RTCRtpSender.getCapabilities("video");
// 只保留 VP8
const vp8Only = caps.codecs.filter((c) => c.mimeType === "video/VP8");
transceiver.setCodecPreferences(vp8Only);
七、常见问题与排查
| 问题 | 原因 | 解决 |
|---|---|---|
| 无 Simulcast 层 | 未用 Unified Plan + sendEncodings | 用 addTransceiver + rid |
SDP 无 a=simulcast | 浏览器不支持或只有 1 层 | 确认 Chrome 72+ / Firefox 66+ |
| H.264 协商失败 | profile-level-id 不匹配 | 检查 fmtp 参数兼容性 |
| 画质模糊 | 码率过低或 scaleResolutionDownBy 过大 | 提高 maxBitrate |
| 编码 CPU 过高 | Simulcast 3 层 + 软编 | 减少层数或启用硬编 |
| AV1 不生效 | 浏览器/设备不支持 | 回退 VP9/VP8 |
| 音频断断续续 | Opus 码率过低 | 提高 maxBitrate 到 32kbps+ |
| setParameters 失败 | 参数不合法或顺序错误 | 先 getParameters 再修改 |
7.1 Simulcast 不生效排查
// 1. 确认 SDP 中有 simulcast 属性
console.log(pc.localDescription.sdp.includes("simulcast"));
// 2. 确认有 3 个 outbound-rtp
const stats = await pc.getStats();
let outboundCount = 0;
stats.forEach((r) => {
if (r.type === "outbound-rtp" && r.kind === "video") outboundCount++;
});
console.log("Outbound video streams:", outboundCount); // 应为 3
// 3. 确认 rid 存在
stats.forEach((r) => {
if (r.type === "outbound-rtp" && r.kind === "video") {
console.log("rid:", r.rid, "ssrc:", r.ssrc, "bitrate:", r.targetBitrate);
}
});
八、实战 Lab
Lab 1:Codec 能力探测
- 在控制台运行
RTCRtpSender.getCapabilities("video") - 列出所有支持的 mimeType 和 hardwareAccelerated 标志
- 对比 Chrome 桌面 vs Chrome Android 的差异
Lab 2:Simulcast 三档验证
- 用
addTransceiver配置 h/m/l 三档 createOffer()后搜索 SDP 中的a=simulcast和a=ridgetStats()确认 3 个outbound-rtp报告,各有不同 rid 和 SSRC
Lab 3:带宽降层观察
- 建立 Simulcast 通话
- Chrome DevTools 限速 300 kbps
- 观察 SFU 或 P2P 对端收到的层从 h → m → l 切换
- 记录切换时
inbound-rtp的 SSRC 变化
Lab 4:VP8 vs H.264 画质对比
// 测试 1:强制 VP8
transceiver.setCodecPreferences(
caps.codecs.filter((c) => c.mimeType === "video/VP8")
);
// 测试 2:强制 H.264
transceiver.setCodecPreferences(
caps.codecs.filter((c) => c.mimeType === "video/H264")
);
// 在相同 maxBitrate 下对比主观画质和 CPU 占用
Lab 5:动态层管理
- 开启 3 层 Simulcast
- 30 秒后关闭 l 层:
enc.active = false getStats()确认 outbound-rtp 从 3 个变为 2 个- 重新开启 l 层,确认恢复
Lab 6:Opus 码率与质量
- 分别设置 maxBitrate 为 16k / 32k / 64k
- 播放音乐片段,记录主观质量差异
- 启用
useinbandfec=1,10% 丢包下对比有无 FEC 的concealedSamples
九、本章小结
| 要点 | 内容 |
|---|---|
| 音频 | Opus 是唯一标准,注意 FEC/DTX/码率配置 |
| 视频 | VP8 默认,H.264 移动端,VP9/AV1 高压缩 |
| Simulcast | rid + sendEncodings,每层独立 SSRC |
| 码率 | 应用层 maxBitrate + GCC 自适应 |
| 选型 | 兼容性 > 压缩率 > 专利自由度 |
从 Ron Frederick 1992 年手写软件视频压缩,到 Skype 团队标准化 Opus,再到今天 AV1 的硬件加速普及——编解码技术的演进直接塑造了 WebRTC 的能力边界。理解 Codec 特性,才能在延迟、画质、兼容性之间做出正确权衡。
下一篇(Ch10):带宽估计与 GCC——TWCC 反馈如何驱动发送端码率自适应。
系列导航
章节 主题 状态 0 架构全景与协议栈地图 ✅ 已发布 1 浏览器媒体 API 与设备管理 ✅ 已发布 2 第一个 P2P 视频通话 ✅ 已发布 3 信令服务器设计与会话状态机 ✅ 已发布 4 SDP 解剖与媒体协商 ✅ 已发布 5 ICE、STUN、TURN 与 NAT 穿透 ✅ 已发布 6 Data Channel 与 SCTP over DTLS ✅ 已发布 7 DTLS 握手与 SRTP 加密体系 ✅ 已发布 8 RTP/RTCP 媒体传输与 QoS ✅ 已发布 9 音视频编解码与 Simulcast 入门 ✅ 已发布 10 带宽估计与拥塞控制 GCC ✅ 已发布 11 Simulcast、SVC 与选择性订阅 ✅ 已发布 12 SFU/MCU/Mesh 架构与 Pion 实战 ✅ 已发布 13 调试工具链与可观测性 ✅ 已发布 14 TURN 集群部署与多区域扩展 ✅ 已发布 15 Capstone 生产级视频会议系统 ✅ 已发布
References
- RFC 7587 — RTP Payload Format for Opus
- RFC 7741 — RTP Payload Format for VP8
- RFC 6184 — RTP Payload Format for H.264
- RFC 8871 — WebRTC Video Processing and Codec Requirements
- RFC 6381 — Opus 首版
- WebRTC for the Curious — RTP 历史 / Opus / Skype
- webrtcH4cKS — An Intro to Simulcast
- MDN — RTCRtpSender.setParameters()
- MDN — RTCRtpTransceiver