18、Simulcast与SVC:分层编码原理、空间/时间分层、接收端选择策略
聊到WebRTC的多人通信,有个问题一直很头疼——带宽适配。
你想想看,一个房间里有人用5G手机,有人还在用3G网络,还有人连着WiFi但信号差得要命。如果我只发一路视频流,那要么高清用户觉得糊,要么低带宽用户卡成PPT。怎么办?
分层编码就是干这个的。说白了,就是把视频拆成几层,接收端根据自己的能力挑着收。
我最早接触这个,是在做一个跨国视频会议项目的时候。那时候我们用的是单流方案,结果东南亚的同事天天抱怨卡顿,而北美这边又嫌画质不够。后来切了Simulcast,问题才真正解决。
Simulcast:多路独立编码
Simulcast的思路很直接——发送端同时编码多路不同分辨率的视频流,然后一起发出去。
举个例子,我开一个1080p的摄像头,Simulcast会同时生成三路流:
- 高清流:1080p,码率2~4Mbps
- 标清流:480p,码率500k~1Mbps
- 低清流:180p,码率100~300kbps
这三路流是独立编码的,互不依赖。接收端根据自己的带宽和屏幕尺寸,选一路最合适的来收。
关键点:Simulcast的每一路都是完整的视频流,可以独立解码。这意味着接收端不需要任何额外处理,直接拉流就能播放。
我在项目中遇到过一个问题——有些开发者以为Simulcast是编码器自动生成的。其实不是。你需要手动配置编码参数,告诉编码器要生成几路流,每路的分辨率和码率是多少。
代码层面,大概是这样:
// 创建三个编码器配置
const encodings = [
{ rid: 'high', maxBitrate: 2500000, maxFramerate: 30, scaleResolutionDownBy: 1.0 },
{ rid: 'mid', maxBitrate: 500000, maxFramerate: 30, scaleResolutionDownBy: 2.0 },
{ rid: 'low', maxBitrate: 150000, maxFramerate: 15, scaleResolutionDownBy: 4.0 }
];
// 添加到RTCRtpSender
const sender = pc.addTrack(track, stream);
await sender.setParameters({ encodings });
嗯,这里要注意——rid是流的标识,接收端通过它来区分不同层。我见过有人把rid写成中文,结果浏览器直接报错。rid只能用字母数字和下划线。
SVC:可伸缩视频编码
SVC的思路和Simulcast完全不同。它不是生成多路独立流,而是把一帧画面编码成一个基础层加多个增强层。
- 基础层:最低质量,可以独立解码
- 增强层:依赖基础层,叠加后提升质量
打个比方,基础层就像一张黑白素描,增强层一层层往上加颜色、加细节。接收端如果带宽不够,只收基础层也能看,就是画质差点。
SVC支持三种分层方式:
| 分层类型 | 说明 | 典型场景 |
|---|---|---|
| 空间分层 | 不同分辨率,如360p + 720p + 1080p | 大屏小屏切换 |
| 时间分层 | 不同帧率,如7.5fps + 15fps + 30fps | 网络抖动时降帧 |
| 质量分层 | 不同量化参数,如QP 40 + QP 30 + QP 20 | 带宽波动时平滑降质 |
我个人习惯把时间分层用在移动端场景。手机网络经常波动,降分辨率不如降帧率来得自然。你想想看,画面突然变模糊很突兀,但帧率从30降到15,人眼其实不太敏感。
小技巧:SVC的时间分层可以通过调整GOP结构来实现。比如用IPPP...结构,每4帧一个关键帧,这样丢帧时最多丢3帧,不会影响后续解码。
Simulcast vs SVC:怎么选?
这个问题我经常被问到。说实话,没有绝对的好坏,看场景。
我画了一张对比图,帮你快速理解:
从图上能看出来,Simulcast是「多路并行」,SVC是「层层叠加」。两者各有优劣:
- Simulcast优势:实现简单,浏览器支持好,接收端解码压力小
- Simulcast劣势:带宽浪费(同时发多路流),编码器压力大
- SVC优势:带宽利用率高,平滑切换,编码器只需编一次
- SVC劣势:解码复杂度高,浏览器支持有限(Chrome不支持SVC)
注意:截至2024年,Chrome和Firefox只支持Simulcast,不支持SVC。Safari支持SVC但不支持Simulcast。如果你要做跨浏览器方案,建议优先考虑Simulcast。
接收端选择策略
分层编码只是手段,真正重要的是接收端怎么选。
我见过最粗暴的做法——固定选最高清流。结果呢?带宽一波动,视频直接卡死。后来我们改成了动态选择策略。
核心逻辑就三条:
- 带宽探测:实时估算可用带宽,用GCC(Google Congestion Control)算法
- 层级匹配:根据带宽选择能承载的最高层
- 平滑切换:避免频繁切换,设置滞回区间
代码实现大概是这样:
function selectLayer(bandwidthKbps) {
// 定义各层的带宽需求
const layers = [
{ rid: 'low', required: 150 },
{ rid: 'mid', required: 500 },
{ rid: 'high', required: 2000 }
];
// 从高到低匹配
for (let i = layers.length - 1; i >= 0; i--) {
if (bandwidthKbps >= layers[i].required * 1.2) {
return layers[i].rid; // 留20%余量
}
}
return 'low'; // 保底
}
嗯,这里有个坑——不要只看瞬时带宽。我曾经遇到过网络抖动,带宽在500k和2M之间来回跳,结果接收端在mid和high之间反复切换,用户体验反而更差。
解决方案是加一个滞回区间:
- 从low切到mid:带宽需持续高于600k超过2秒
- 从mid切到low:带宽需持续低于400k超过1秒
这样能有效避免「乒乓切换」。
个人经验:在移动端,我还会结合屏幕尺寸做选择。比如手机屏幕小,即使带宽够,也只收到mid层就够了。省下来的带宽可以留给其他视频流。
实际项目中的避坑指南
最后分享几个我踩过的坑:
- 编码器负载:Simulcast需要同时编码多路流,CPU和GPU压力很大。我曾在低端手机上测试,三路Simulcast直接让CPU飙到90%。后来我们限制只在WiFi下开启Simulcast,4G下只用单流。
- 关键帧对齐:Simulcast的多路流最好在关键帧上对齐,否则接收端切换时会有短暂花屏。设置
keyFrameInterval为相同值可以解决。 - 带宽估算不准:GCC算法在丢包率高时容易误判。我建议结合接收端REMB反馈和发送端丢包率综合判断。
分层编码这东西,说起来简单,做起来全是细节。但一旦调好了,效果立竿见影。我那个跨国会议项目,切了Simulcast之后,东南亚同事的卡顿率从30%降到了5%以下。
嗯,这就是分层编码的核心思路。你可以在自己的项目里试试,从Simulcast入手,先跑通三路流,再慢慢优化选择策略。
公众号:蓝海资料掘金营,微信deep3321