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 高清流 (1080p) 独立编码,2~4Mbps 标清流 (480p) 独立编码,500k~1Mbps 低清流 (180p) 独立编码,100~300kbps 接收端选择一路 带宽高→收高清流 带宽低→收低清流 SVC 增强层2 (细节/高帧率) 依赖下层 增强层1 (中等质量) 依赖下层 基础层 (最低质量) 可独立解码 接收端叠加层数 带宽高→收全部层 带宽低→只收基础层

从图上能看出来,Simulcast是「多路并行」,SVC是「层层叠加」。两者各有优劣:

  • Simulcast优势:实现简单,浏览器支持好,接收端解码压力小
  • Simulcast劣势:带宽浪费(同时发多路流),编码器压力大
  • SVC优势:带宽利用率高,平滑切换,编码器只需编一次
  • SVC劣势:解码复杂度高,浏览器支持有限(Chrome不支持SVC)

注意:截至2024年,Chrome和Firefox只支持Simulcast,不支持SVC。Safari支持SVC但不支持Simulcast。如果你要做跨浏览器方案,建议优先考虑Simulcast。

接收端选择策略

分层编码只是手段,真正重要的是接收端怎么选。

我见过最粗暴的做法——固定选最高清流。结果呢?带宽一波动,视频直接卡死。后来我们改成了动态选择策略。

核心逻辑就三条:

  1. 带宽探测:实时估算可用带宽,用GCC(Google Congestion Control)算法
  2. 层级匹配:根据带宽选择能承载的最高层
  3. 平滑切换:避免频繁切换,设置滞回区间

代码实现大概是这样:

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