26、带宽自适应:动态调整视频码率、Simulcast(联播)技术、SVC(可伸缩视频编码)

做音视频通话,最头疼的问题是什么?

我个人觉得,不是编码选什么,也不是信令怎么走。而是——网络突然变差了,画面卡成PPT,声音断断续续,对方问你“你刚才说什么?”

嗯,这种场景我遇到过太多次了。早期做一款在线教育产品时,学生家里WiFi不稳定,老师那边的视频码率还飙在2Mbps,结果就是学生端疯狂丢包,整节课体验极差。

所以,带宽自适应能力,是WebRTC实战中绕不开的核心话题。说白了,就是让视频流学会“看人下菜碟”——网络好就高清,网络差就降质,保证通话不中断。

一、为什么需要带宽自适应?

我们先想一个问题:视频通话的码率是固定的好,还是动态的好?

固定码率,实现简单。但网络波动时,要么卡顿,要么浪费带宽。我见过不少项目,上线后才发现,固定码率在移动网络下根本扛不住。

动态调整,才是正解。WebRTC内部有一套完整的带宽估计机制,叫做GCC(Google Congestion Control)。它会根据丢包率、RTT(往返时延)、接收端反馈等信息,实时估算当前可用带宽。

有了这个估算值,我们就可以做三件事:

  • 动态调整视频码率——最直接的方式,带宽降了,码率跟着降。
  • Simulcast(联播)——同时发送多路不同分辨率的流,接收端按需选择。
  • SVC(可伸缩视频编码)——只发一路流,但编码时分层,接收端按能力解码。

这三种方案,各有各的适用场景。下面我一个个拆开讲。

二、动态调整视频码率

这是最基础的做法。WebRTC的RTCRtpSender对象上有一个setParameters()方法,我们可以用它来动态修改编码器的码率上限。

核心思路:监听带宽估计事件,拿到可用带宽后,按比例设置视频编码器的目标码率。

代码示例:

// 获取视频发送者
const sender = pc.getSenders().find(s => s.track.kind === 'video');

// 监听带宽估计(需要浏览器支持)
// 实际项目中,可以通过 stats 轮询或自定义反馈来获取带宽信息
function adjustBitrate(availableBitrate) {
  if (!sender) return;

  const params = sender.getParameters();
  if (!params.encodings) return;

  // 设置目标码率为可用带宽的 80%,留点余量
  const targetBitrate = Math.floor(availableBitrate * 0.8);
  params.encodings[0].maxBitrate = targetBitrate;

  sender.setParameters(params)
    .then(() => {
      console.log(`码率已调整为: ${targetBitrate} bps`);
    })
    .catch(e => console.error('设置码率失败', e));
}

我在项目中遇到过一个问题:频繁调用setParameters()会导致编码器重置,反而造成短暂卡顿。后来我加了一个防抖逻辑——带宽变化超过20%才触发调整,效果好了很多。

小技巧:不要直接设成可用带宽的100%。留20%给音频和重传,视频占80%左右比较稳。

三、Simulcast(联播)技术

动态调整码率有个局限:它只能改变同一路流的清晰度。如果接收端网络差异很大(比如一个在WiFi下,一个在4G下),单路流很难同时满足所有人。

Simulcast的思路很简单:发送端同时编码多路不同分辨率的视频流,比如同时发720p、360p、180p三路。接收端根据自身网络状况,选择其中一路来接收。

WebRTC中启用Simulcast,需要在addTransceiver时指定编码参数:

const transceiver = pc.addTransceiver('video', {
  direction: 'sendonly',
  sendEncodings: [
    { rid: 'high',  maxBitrate: 2_000_000, scaleResolutionDownBy: 1.0 },
    { rid: 'mid',   maxBitrate: 500_000,   scaleResolutionDownBy: 2.0 },
    { rid: 'low',   maxBitrate: 100_000,   scaleResolutionDownBy: 4.0 }
  ]
});

这里rid是流的标识,scaleResolutionDownBy表示分辨率缩放比例。1.0是原图,2.0是缩小到1/2,4.0是1/4。

接收端怎么选?可以通过RTCRtpReceiversetParameters()来切换:

// 接收端选择 mid 流
const receiver = pc.getReceivers().find(r => r.track.kind === 'video');
const params = receiver.getParameters();
params.encodings.forEach(enc => {
  enc.active = (enc.rid === 'mid');
});
receiver.setParameters(params);

注意:Simulcast会显著增加发送端的编码负载和带宽消耗。三路流同时编码,CPU和上行带宽都要翻倍。我建议只在服务端转发的场景(SFU)中使用,P2P场景慎用。

说实话,Simulcast的优点是切换非常平滑——接收端可以瞬间切到不同质量的流,没有解码延迟。缺点是浪费带宽,发送端要同时推三路。

四、SVC(可伸缩视频编码)

SVC是另一种思路。它只编码一路流,但这路流内部是分层的。比如:

  • 时间层(Temporal):控制帧率。底层只有关键帧,高层加上插值帧。
  • 空间层(Spatial):控制分辨率。底层是低分辨率,高层叠加高分辨率细节。
  • 质量层(Quality):控制画质。底层是粗糙画质,高层提升信噪比。

接收端可以根据自己的带宽和解码能力,只解码部分层。比如网络差时只解码底层,得到低分辨率但流畅的画面;网络好时解码所有层,得到全高清画质。

WebRTC对SVC的支持,目前主要依赖VP9编码器。启用方式:

const transceiver = pc.addTransceiver('video', {
  direction: 'sendonly',
  sendEncodings: [
    {
      rid: 'svc',
      scalabilityMode: 'L3T3',  // 3层空间 + 3层时间
      maxBitrate: 2_000_000
    }
  ]
});

scalabilityMode的格式是L<空间层数>T<时间层数>。比如L3T3表示3层空间分辨率、3层帧率。

接收端解码时,浏览器会自动根据网络状况丢弃部分层数据。开发者不需要手动干预——这是SVC相比Simulcast最大的优势。

我的经验:SVC在Chrome上对VP9支持较好,但H264的SVC支持有限。如果你需要兼容Safari或老版本浏览器,Simulcast可能是更稳妥的选择。

五、Simulcast vs SVC:怎么选?

我整理了一个对比表,方便你决策:

维度 Simulcast SVC
编码负载 高(多路同时编码) 低(单路编码)
带宽消耗 高(同时发送多路) 低(只发一路,分层)
切换延迟 极低(接收端直接选流) 低(丢弃部分层即可)
浏览器兼容 好(Chrome/Firefox/Safari均支持) 一般(VP9支持较好,H264有限)
实现复杂度 中等(需管理多路流切换) 低(浏览器自动处理)
适用场景 SFU架构,接收端网络差异大 P2P或小规模会议,希望节省带宽

我个人习惯是:新项目优先考虑SVC,因为它更节省带宽和CPU。但如果目标用户中有大量Safari用户,或者需要精细控制每路流的质量,Simulcast更靠谱。

六、知识体系总览

下面这张图,帮你把本章的核心逻辑串起来:

带宽自适应方案对比 带宽估计 (GCC) 动态调整码率 setParameters() Simulcast(联播) 多路流,接收端选择 SVC(可伸缩编码) 单路流,分层解码 ✅ 实现简单 ✅ 兼容性好 ❌ 切换有延迟 ❌ 无法多端适配 ✅ 切换极快 ✅ 多端适配好 ❌ 编码负载高 ❌ 上行带宽浪费 ✅ 带宽节省 ✅ 编码负载低 ❌ 浏览器兼容有限 ❌ 控制粒度较粗

七、避坑指南

最后,分享几个我踩过的坑:

  • 不要过度调整码率。我曾经写过一个逻辑,每100ms根据带宽调整一次码率。结果编码器频繁重置,画面反而更卡。后来改成每2秒评估一次,带宽变化超过15%才调整,稳多了。
  • Simulcast的rid命名要规范。有些浏览器对rid大小写敏感。我建议统一用小写字母,比如highmidlow,别用HighHIGH
  • SVC的VP9编码器在移动端可能不支持。我在Android Chrome上遇到过VP9 SVC解码失败的情况。建议在移动端降级到Simulcast或单流动态码率。
  • 带宽估计不准时,可以自己实现反馈。WebRTC内置的GCC在弱网下有时会过于激进。我习惯在应用层加一个基于接收端统计的带宽反馈通道,双保险。

嗯,带宽自适应这块,说白了就是让视频流学会“弹性伸缩”。没有银弹,只有根据场景选对方案。Simulcast适合多端差异大的SFU场景,SVC适合追求带宽效率的P2P场景,动态码率则是保底方案。

你在实际项目中,可以根据用户设备和网络环境,甚至做动态切换——网络好时用SVC全层解码,网络差时切到Simulcast的低流。灵活一点,总没错。

最后一个小建议:上线前一定要做弱网模拟测试。用Chrome的DevTools模拟3G、2G网络,看看你的自适应逻辑能不能扛住。我吃过这个亏,希望你不用再吃。