13、Simulcast与SVC:多流与可伸缩编码实战
聊到视频通话的流畅度,很多同学第一反应就是「降低分辨率」。但你想过没有,一个1080p的流,强行降到360p,接收端看到的画面糊成一团,体验极差。更麻烦的是,不同接收端的网络条件千差万别——有人用5G,有人还在3G边缘挣扎。
这时候,Simulcast和SVC就派上用场了。说白了,它们就是让发送端「多准备几手」,接收端「按需取用」。我在做跨国会议系统时,就靠这俩技术解决了「一人卡顿,全员降质」的尴尬局面。
13.1 Simulcast(多流)原理
Simulcast,直译就是「同时广播」。发送端同时编码出多个不同质量的视频流,比如一个720p、一个360p、一个180p。接收端根据自己的带宽和屏幕尺寸,选一个最合适的流来解码。
嗯,这里要注意:Simulcast不是简单的「复制三份」。每个流都是独立编码的,有独立的帧率、分辨率和码率。代价就是——编码器的压力翻倍,带宽占用也翻倍。
核心要点:Simulcast是「空间冗余」——多份完整流,接收端选一份。
我在项目中遇到过一个问题:三个流同时编码,CPU直接飙到90%。后来我们做了动态策略——当检测到CPU过载时,自动降为双流甚至单流。这个坑,我建议你提前留好开关。
13.2 SVC(可伸缩视频编码)原理
SVC的思路完全不同。它只编码一份码流,但这份码流是「分层」的。比如:
- 基础层:最低分辨率、最低质量,所有接收端都能解码
- 增强层:在基础层之上叠加,提升分辨率或质量
接收端可以只解码基础层,也可以「基础层+增强层1+增强层2」逐步叠加。说白了,SVC是「时间/空间/质量」三个维度的可伸缩。
个人经验:SVC在理论上很优雅,但实际编码效率比Simulcast低10%-20%。我做过对比测试,同样画质下,SVC的码率比Simulcast高出一截。所以选哪个,得看你的场景。
为什么会这样?因为SVC的层间预测虽然节省了带宽,但引入了额外的编码开销。你想想看,每一层都要依赖下层,编码器得做更多计算。
13.3 Simulcast vs SVC:怎么选?
| 维度 | Simulcast | SVC |
|---|---|---|
| 编码效率 | 高(独立编码) | 中(层间预测有开销) |
| 带宽占用 | 高(多份完整流) | 低(一份流,按需取层) |
| CPU负载 | 高(多个编码器) | 中(一个编码器,但层数多) |
| 接收端灵活性 | 高(直接切换流) | 高(逐层叠加) |
| 浏览器支持 | 广泛(Chrome、Firefox) | 有限(Chrome支持VP9 SVC) |
我个人习惯:如果目标用户主要是桌面端、带宽充足,优先Simulcast。如果移动端用户多、网络波动大,SVC更合适。当然,两者也可以结合——我就做过「Simulcast + 每流内部SVC」的方案,效果不错,但复杂度也上去了。
13.4 在WebRTC中启用Simulcast
启用Simulcast,核心是在创建RTCPeerConnection时,通过编码参数告诉浏览器:「我要发多份流」。
看代码:
const pc = new RTCPeerConnection();
// 获取本地视频流
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// 添加轨道,并指定Simulcast编码参数
const transceiver = pc.addTransceiver(stream.getVideoTracks()[0], {
direction: 'sendonly',
sendEncodings: [
{ rid: 'h', maxBitrate: 900000, scaleResolutionDownBy: 1.0 }, // 720p
{ rid: 'm', maxBitrate: 300000, scaleResolutionDownBy: 2.0 }, // 360p
{ rid: 'l', maxBitrate: 100000, scaleResolutionDownBy: 4.0 } // 180p
]
});
这里的关键参数:
- rid:流的标识,接收端靠它来区分
- maxBitrate:每个流的最大码率
- scaleResolutionDownBy:分辨率缩放因子。1.0是原始分辨率,2.0是缩小一半
避坑指南:我曾经把scaleResolutionDownBy写成了0.5,结果浏览器直接报错。这个值必须≥1.0,表示「缩小多少倍」,不是「保留多少比例」。
发送端搞定后,接收端怎么知道有三个流?答案是:通过RTP头部扩展。每个流在RTP包中携带自己的rid,接收端根据rid来区分。
13.5 接收端选择流
接收端收到多个流后,需要根据自身条件选一个。WebRTC提供了两种方式:
方式一:手动切换
通过设置接收端的编码参数,指定要解码哪个rid:
// 假设接收端有一个transceiver
const receiver = pc.getReceivers()[0];
// 手动选择要解码的流
await receiver.setCodecPreferences([
{ rid: 'm' } // 只解码中等质量的流
]);
这种方式适合「用户手动选择画质」的场景。比如视频会议中,用户点击「流畅模式」,我们就切到低码率流。
方式二:自适应切换
WebRTC内置了带宽估计机制,可以自动切换流。你只需要在发送端设置好编码参数,接收端会基于REM B(带宽估计)动态选择。
具体做法:
// 发送端设置编码参数时,不指定rid,让浏览器自动选择
const transceiver = pc.addTransceiver(track, {
sendEncodings: [
{ maxBitrate: 900000, scaleResolutionDownBy: 1.0 },
{ maxBitrate: 300000, scaleResolutionDownBy: 2.0 },
{ maxBitrate: 100000, scaleResolutionDownBy: 4.0 }
]
});
浏览器会根据接收端的带宽估计,自动在三个流之间切换。当带宽充足时,接收端收到高质量流;带宽下降时,自动切到低质量流。
我的建议:自适应切换虽然方便,但切换过程可能有短暂的花屏或卡顿。如果你对体验要求极高,可以自己实现「平滑切换」——先缓冲几帧新流,再切换解码器。我在直播项目中就是这么做的,切换几乎无感。
13.6 核心逻辑图
下面这张图展示了Simulcast的完整流程:发送端编码三个流,接收端根据带宽选择其中一个解码。
13.7 实战中的坑与技巧
- 流数不要贪多:我见过有人设了5个流,结果编码器直接崩溃。一般3个流足够覆盖大多数场景。
- 码率设置要合理:三个流的码率不要等差。比如720p给900kbps,360p给300kbps,180p给100kbps。这样切换时码率变化平滑。
- 注意浏览器差异:Chrome对Simulcast支持最好,Firefox次之,Safari目前只支持SVC。如果你要兼容Safari,建议用VP9 SVC。
- 监控带宽变化:接收端可以通过
pc.getReceivers()[0].getStats()获取当前流的码率,结合带宽估计,决定是否切换。
总结一下:Simulcast是「多流备选」,SVC是「单流分层」。选哪个,取决于你的目标平台和网络环境。我个人更倾向Simulcast,因为浏览器支持好、实现简单。但如果你追求极致的带宽效率,SVC值得深入研究。
嗯,这一章的内容就到这里。记住:多流不是目的,让每个用户都能看到流畅的视频才是。