当前位置: 首页 > java >正文

【音视频】 WebRTC GCC 拥塞控制算法

参考文章:https://blog.csdn.net/weixin_38102771/article/details/127780907

WebRTC GCC 拥塞控制算法详解

一、GCC 概述

GCC(Google Congestion Control)是 Google 为实时音视频通信(RTC)设计的拥塞控制算法,是 WebRTC 框架默认的拥塞控制方案。其核心目标是在避免网络拥塞的前提下,最大化利用网络带宽,同时保证音视频传输的低延迟和低丢包率 —— 这对实时场景(如视频会议、直播)至关重要。

1.1 GCC 的核心设计思路

传统 TCP 拥塞控制(如 Reno、CUBIC)依赖 “丢包即拥塞” 的判断逻辑,且重传机制会引入额外延迟,不适合实时音视频。GCC 则采用双指标融合的策略:

  • 延时梯度(Delay Gradient):提前感知拥塞(网络队列堆积会导致延时增加,早于丢包发生);
  • 丢包率(Packet Loss Rate):验证拥塞是否已发生(丢包是拥塞的直接结果)。

通过两种指标的结合,GCC 既能快速响应网络变化,又能避免单一指标的误判(如瞬时延时波动、偶发丢包)

1.2 GCC 的两个版本

GCC 经历了两次架构演进,核心差异在于带宽预估逻辑的部署位置

版本核心特点反馈报文适用场景
REMB-GCC接收端计算延时梯度并预估带宽,发送端基于丢包率预估带宽,取两者最小值RTCP REMB早期 WebRTC 版本,简单场景
TCC-GCC接收端仅反馈收包时序信息,发送端统一处理延时梯度 + 丢包率,集中式预估带宽RTCP TCC主流版本(替代 REMB-GCC),复杂网络

二、REMB-GCC 算法原理

REMB-GCC 是 GCC 的初代版本,采用 “接收端 + 发送端” 分工的架构,核心流程分为接收端延时梯度预估发送端丢包率预估两部分,最终发送端取两者最小值作为实际发送码率。

在这里插入图片描述

2.1 接收端:基于延时梯度的带宽预估

接收端通过分析数据包的 “发送 - 接收时序差”,计算延时梯度,判断网络是否拥塞,并预估最大可用带宽,再通过 RTCP REMB 报文反馈给发送端。核心模块分为 5 个:

在这里插入图片描述

(1)Arrival-time Filter(到达时间滤波)

作用:避免单包时序波动的干扰,基于 “分组” 计算稳定的时间差(而非单包)。
WebRTC 中,发送端会将 RTP 包按5ms 间隔分组发送(由 Pacer 模块控制),接收端同样按 “组” 统计时序信息,核心逻辑如下:

  • 分组依据:通过 RTP 扩展字段 abs-send-time(24bit,精度 3.8us)获取包的发送时间,若某包的发送时间与当前组首包的发送时间差 >5ms,则归为新组。

  • 核心数据结构TimestampGroup(存储每组的关键信息):

在这里插入图片描述

字段含义
first_timestamp组内首包的 RTP 发送时间戳
first_arrival_ms组内首包的接收时间(毫秒)
complete_time_ms组内最后一包的接收时间(毫秒,代表 “组接收完成时间”)
size组内所有包的总大小(字节)
last_system_time_ms组内最后一包的系统接收时间(用于校准时钟偏移)
  • 时间差计算:当新组完成接收后,计算当前组与上一组的:

    • 接收时间差:arrival_delta = current.complete_time_ms - prev.complete_time_ms
    • 发送时间差:send_delta = current.first_timestamp - prev.first_timestamp
    • 包大小差:size_delta = current.size - prev.size

这些差值将传入下一个模块,用于计算延时梯度。

下面是 WebRTC 计算延时梯度的代码:

bool InterArrival::ComputeDeltas(uint32_t timestamp,int64_t arrival_time_ms,int64_t system_time_ms,size_t packet_size,uint32_t* timestamp_delta,int64_t* arrival_time_delta_ms,int* packet_size_delta) {assert(timestamp_delta != NULL);assert(arrival_time_delta_ms != NULL);assert(packet_size_delta != NULL);bool calculated_deltas = false;if (current_timestamp_group_.IsFirstPacket()) {// We don't have enough data to update the filter, so we store it until we// have two frames of data to process.current_timestamp_group_.timestamp = timestamp;current_timestamp_group_.first_timestamp = timestamp;current_timestamp_group_.first_arrival_ms = arrival_time_ms;} else if (!PacketInOrder(timestamp)) {return false;} else if (NewTimestampGroup(arrival_time_ms, timestamp)) {// First packet of a later frame, the previous frame sample is ready.if (prev_timestamp_group_.complete_time_ms >= 0) {*timestamp_delta =current_timestamp_group_.timestamp - prev_timestamp_group_.timestamp;*arrival_time_delta_ms = current_timestamp_group_.complete_time_ms -prev_timestamp_group_.complete_time_ms;// Check system time differences to see if we have an unproportional jump// in arrival time. In that case reset the inter-arrival computations.int64_t system_time_delta_ms =current_timestamp_group_.last_system_time_ms -prev_timestamp_group_.last_system_time_ms;if (*arrival_time_delta_ms - system_time_delta_ms >=kArrivalTimeOffsetThresholdMs) {RTC_LOG(LS_WARNING)<< "The arrival time clock offset has changed (diff = "<< *arrival_time_delta_ms - system_time_delta_ms<< " ms), resetting.";Reset();return false;}if (*arrival_time_delta_ms < 0) {// The group of packets has been reordered since receiving its local// arrival timestamp.++num_consecutive_reordered_packets_;if (num_consecutive_reordered_packets_ >= kReorderedResetThreshold) {RTC_LOG(LS_WARNING)<< "Packets are being reordered on the path from the ""socket to the bandwidth estimator. Ignoring this ""packet for bandwidth estimation, resetting.";Reset();}return false;} else {num_consecutive_reordered_packets_ = 0;}assert(*arrival_time_delta_ms >= 0);*packet_size_delta = static_cast<int>(current_timestamp_group_.size) -static_cast<int>(prev_timestamp_group_.size);calculated_deltas = true;}prev_timestamp_group_ = current_timestamp_group_;// The new timestamp is now the current frame.current_timestamp_group_.first_timestamp = timestamp;current_timestamp_group_.timestamp = timestamp;current_timestamp_group_.first_arrival_ms = arrival_time_ms;current_timestamp_group_.size = 0;} else {current_timestamp_group_.timestamp =LatestTimestamp(current_timestamp_group_.timestamp, timestamp);}// Accumulate the frame size.current_timestamp_group_.size += packet_size;current_timestamp_group_.complete_time_ms = arrival_time_ms;current_timestamp_group_.last_system_time_ms = system_time_ms;return calculated_deltas;
}
(2)Overuse Estimator(过载估计器)

作用:通过卡尔曼滤波平滑延时梯度,减少网络噪声(如瞬时抖动)的影响。 核心公式(延时梯度定义):
(d(ti)=[ti−ti−1]−[Ti−Ti−1])(d(t_i) = [t_i - t_{i-1}] - [T_i - T_{i-1}])(d(ti)=[titi1][TiTi1])

  • (t_i):第 i 组的接收完成时间;(T_i):第 i 组的首包发送时间;
  • 理想网络中 (d(t_i) = 0);网络拥塞时,队列堆积导致 (t_i) 增大,(d(t_i) > 0);网络空闲时 (d(t_i) < 0)。

卡尔曼滤波的作用是对连续多组的 (d(t_i)) 进行平滑处理,输出更稳定的 “拥塞趋势值”,避免单组波动导致误判。

OveruseEstimator::Update 函数用于估计延时梯度,它并不是直接使用上一次计算得到的延时梯度值,而是将延时梯度传入该函数,通过卡尔曼滤波算法后得到延时梯度值:

void OveruseEstimator::Update(int64_t t_delta,double ts_delta,int size_delta,BandwidthUsage current_hypothesis,int64_t now_ms) {// 代码省略,详见 src/modules/remote_bitrate_estimator/overuse_estimator.cc
}
(3)Overuse Detector(过载检测器)

作用:根据平滑后的延时梯度,判断当前网络状态(3 种状态),判断依据是 “延时梯度与动态阈值的关系”。

网络状态判断条件含义
Overuse(拥塞)1. 平滑后的延时梯度 > 动态阈值;
2. 该状态持续时间 > 阈值(如 100ms);
3. 延时梯度呈增长趋势
网络队列持续堆积,需立即降低码率
Underuse(空闲)平滑后的延时梯度 < - 动态阈值网络带宽未被充分利用,可维持或小幅提升码率
Normal(正常)延时梯度在 [- 动态阈值,动态阈值] 之间网络处于平衡状态,可缓慢提升码率以探测更大带宽

关键:阈值并非固定值,而是通过 Adaptive Threshold(自适应阈值) 动态调整。

在这里插入图片描述

(4)Adaptive Threshold(自适应阈值)

作用:解决 “固定阈值太敏感 / 不敏感” 的问题,根据历史延时梯度动态调整阈值。 核心更新公式:

threshold(ti)=threshold(ti−1)+k⋅Δt⋅(∣d(ti)∣−threshold(ti−1)threshold(t_i) = threshold(t_{i-1}) + k \cdot \Delta t \cdot (|d(t_i)| - threshold(t_{i-1})threshold(ti)=threshold(ti1)+kΔt(d(ti)threshold(ti1)

  • Δt\Delta tΔt:当前组与上一组的时间间隔(毫秒);
  • kkk:调整系数(动态变化):
    • ∣d(ti)∣<threshold(ti−1)(Normal状态):k=0.00018|d(t_i)| < threshold(t_{i-1})(Normal 状态):k=0.00018d(ti)<threshold(ti1)Normal状态):k=0.00018(阈值缓慢减小,提高灵敏度);
    • ∣d(ti)∣≥threshold(ti−1)(Overuse/Underuse状态):k=0.01|d(t_i)| \geq threshold(t_{i-1})(Overuse/Underuse 状态):k=0.01d(ti)threshold(ti1)Overuse/Underuse状态):k=0.01(阈值快速增大,避免频繁切换状态)。

通过该逻辑,阈值能自适应网络波动,平衡 “检测灵敏度” 和 “稳定性”。

在这里插入图片描述

(5)Remote Rate Controller(远端速率控制器)

在这里插入图片描述

在这里插入图片描述

作用:根据 Overuse Detector 输出的网络状态,计算接收端可承受的最大带宽(REMB 值),调整策略如下:

网络状态码率调整策略
Overuse降低码率:新码率 = 过去 500ms 内 “已确认接收带宽(acked_bitrate)” × 0.85
Normal提升码率:新码率 = 当前码率 × 1.08(缓慢提升,避免突然拥塞)
Underuse维持码率:不调整(避免盲目提升导致后续拥塞)

acked_bitrate:接收端通过 RTCP 反馈的 “已成功接收的数据包总大小 / 时间” 计算,反映实际可用带宽

WebRTC 对应的 Rate Controller 调整最大码率的代码如下

void AimdRateControl::ChangeBitrate(const RateControlInput& input,Timestamp at_time) {absl::optional<DataRate> new_bitrate;DataRate estimated_throughput =input.estimated_throughput.value_or(latest_estimated_throughput_);if (input.estimated_throughput)latest_estimated_throughput_ = *input.estimated_throughput;// An over-use should always trigger us to reduce the bitrate, even though// we have not yet established our first estimate. By acting on the over-use,// we will end up with a valid estimate.if (!bitrate_is_initialized_ &&input.bw_state != BandwidthUsage::kBwOverusing)return;ChangeState(input, at_time);// We limit the new bitrate based on the troughput to avoid unlimited bitrate// increases. We allow a bit more lag at very low rates to not too easily get// stuck if the encoder produces uneven outputs.const DataRate troughput_based_limit =1.5 * estimated_throughput + DataRate::KilobitsPerSec(10);switch (rate_control_state_) {case kRcHold:break;case kRcIncrease:if (estimated_throughput > link_capacity_.UpperBound())link_capacity_.Reset();// Do not increase the delay based estimate in alr since the estimator// will not be able to get transport feedback necessary to detect if// the new estimate is correct.// If we have previously increased above the limit (for instance due to// probing), we don't allow further changes.if (current_bitrate_ < troughput_based_limit &&!(send_side_ && in_alr_ && no_bitrate_increase_in_alr_)) {DataRate increased_bitrate = DataRate::MinusInfinity();if (link_capacity_.has_estimate()) {// The link_capacity estimate is reset if the measured throughput// is too far from the estimate. We can therefore assume that our// target rate is reasonably close to link capacity and use additive// increase.DataRate additive_increase =AdditiveRateIncrease(at_time, time_last_bitrate_change_);increased_bitrate = current_bitrate_ + additive_increase;} else {// If we don't have an estimate of the link capacity, use faster ramp// up to discover the capacity.DataRate multiplicative_increase = MultiplicativeRateIncrease(at_time, time_last_bitrate_change_, current_bitrate_);increased_bitrate = current_bitrate_ + multiplicative_increase;}new_bitrate = std::min(increased_bitrate, troughput_based_limit);}time_last_bitrate_change_ = at_time;break;case kRcDecrease: {DataRate decreased_bitrate = DataRate::PlusInfinity();// Set bit rate to something slightly lower than the measured throughput// to get rid of any self-induced delay.decreased_bitrate = estimated_throughput * beta_;if (decreased_bitrate > current_bitrate_ && !link_capacity_fix_) {// TODO(terelius): The link_capacity estimate may be based on old// throughput measurements. Relying on them may lead to unnecessary// BWE drops.if (link_capacity_.has_estimate()) {decreased_bitrate = beta_ * link_capacity_.estimate();}}if (estimate_bounded_backoff_ && network_estimate_) {decreased_bitrate = std::max(decreased_bitrate, network_estimate_->link_capacity_lower * beta_);}// Avoid increasing the rate when over-using.if (decreased_bitrate < current_bitrate_) {new_bitrate = decreased_bitrate;}if (bitrate_is_initialized_ && estimated_throughput < current_bitrate_) {if (!new_bitrate.has_value()) {last_decrease_ = DataRate::Zero();} else {last_decrease_ = current_bitrate_ - *new_bitrate;}}if (estimated_throughput < link_capacity_.LowerBound()) {// The current throughput is far from the estimated link capacity. Clear// the estimate to allow an immediate update in OnOveruseDetected.link_capacity_.Reset();}bitrate_is_initialized_ = true;link_capacity_.OnOveruseDetected(estimated_throughput);// Stay on hold until the pipes are cleared.rate_control_state_ = kRcHold;time_last_bitrate_change_ = at_time;time_last_bitrate_decrease_ = at_time;break;}default:assert(false);}current_bitrate_ = ClampBitrate(new_bitrate.value_or(current_bitrate_));
}
(6)Remb Processing(REMB 报文处理)

接收端计算出最大带宽后,通过 RTCP REMB 报文 反馈给发送端,核心细节:

  • 报文类型:RTCP 扩展报文,PT(Payload Type)=206,FMT(Format)=15;
  • 关键字段
    • Unique Identifier:固定为 0x52454D42(即 “REMB” 的 ASCII 码);
    • BR Exp + BR Mantissa:共同表示最大带宽(单位:bps),计算公式:带宽 = Mantissa × 2^Exp
    • SSRC feedback:需要限制码率的发送端 SSRC 列表(支持多流场景);
  • 发送频率:默认每 200ms 发送一次;若检测到新带宽 < 上一次的 97%(拥塞加剧),则立即发送。

具体报文格式如下:

在这里插入图片描述

WebRTC 中 RTCP REMB 报文一般是每 200ms 反馈一次,但是当检测到可用带宽小于上次预估的 97% 时则会立刻反馈

在这里插入图片描述

2.2 发送端:基于丢包率的带宽预估

发送端除了接收 REMB 反馈的带宽,还会独立基于丢包率预估带宽,最终取两者的最小值作为实际发送码率(双重保险)。

(1)丢包率获取

发送端通过 RTCP RR 报文(Receiver Report)的 fraction lost 字段获取丢包率:

  • fraction lost:8bit 无符号数,表示 “从上一次 RR 到本次 RR 期间,丢失的数据包占总发送包的比例”;
  • 计算方式:丢包率 = fraction lost / 256 × 100%(精度约 0.39%)。

RR报文格式如下:
在这里插入图片描述

(2)码率调整策略

根据丢包率判断网络状态,调整发送码率:

丢包率范围网络状态码率调整策略
>10%严重拥塞大幅降码率:新码率 = 当前码率 × 0.5(快速减少发送量,缓解拥塞)
2% ~ 10%轻度拥塞小幅降码率:新码率 = 当前码率 × 0.85(缓慢调整,平衡带宽与稳定性)
<2%网络空闲提升码率:新码率 = 当前码率 × 1.1(探测更大带宽,避免浪费)
对于目标码率的调整方式,WebRTC 处理代码如下所示

在这里插入图片描述

(3)最终码率确定

发送端将 “接收端 REMB 带宽” 与 “本地丢包率预估带宽” 比较,取较小值作为最终发送码率,并同步给:

  • Encoder(编码器):调整视频分辨率、帧率或码率因子(如 H.264 的 CRF);
  • Pacer(发送 pacing 模块):控制数据包的发送节奏,避免突发发送导致队列堆积;
  • FEC(前向纠错):根据码率调整 FEC 冗余度,提升抗丢包能力。

三、TCC-GCC 算法原理(主流版本)

REMB-GCC 存在明显缺陷:接收端需承担带宽预估计算,且 REMB 报文仅反馈 “最大带宽”,缺乏精细化时序信息。因此 Google 推出 TCC-GCC(Transport-wide Congestion Control),作为当前 WebRTC 的默认方案。

在这里插入图片描述

3.1 TCC-GCC 与 REMB-GCC 的核心差异

对比维度REMB-GCCTCC-GCC
带宽预估位置接收端(延时梯度)+ 发送端(丢包率)仅发送端(统一处理延时梯度 + 丢包率)
接收端反馈内容仅 “最大带宽(REMB 值)”详细收包时序(TCC 报文:包的发送 / 接收时间)
反馈报文RTCP REMBRTCP TCC(Transport-wide CC)
计算复杂度接收端 / 发送端均有复杂度接收端无复杂度,发送端集中计算
精度较低(依赖接收端预估,信息有限)较高(发送端掌握全量时序,可做更精细判断)

3.2 TCC-GCC 的核心流程

  1. 发送端打标:为每个 RTP 包分配一个全局唯一的 Transport Sequence Number(TSN),并记录包的发送时间(send_time);
  2. 接收端反馈:接收端按 TSN 顺序统计收包情况,生成 RTCP TCC 报文,包含:
    • 每个 TSN 的接收时间(recv_time);
    • 丢包标记(哪些 TSN 未收到);
  3. 发送端计算:发送端根据 TCC 报文的 send_timerecv_time,计算延时梯度(逻辑与 REMB-GCC 一致),同时结合丢包率,统一预估带宽;
  4. 码率调整:发送端直接根据预估带宽调整 Encoder、Pacer 等模块,无需与接收端带宽取最小值。

3.3 TCC-GCC 的优势

  • 减少接收端负担:尤其适合弱终端(如手机、IoT 设备),接收端仅需统计时序,无需复杂计算;
  • 更高精度:发送端掌握全量包的发送 / 接收时序,可更精准判断拥塞趋势(如区分 “网络抖动” 和 “持续拥塞”);
  • 支持多流统一控制:同一 Transport 下的多 RTP 流(如视频 + 音频)可共享 TCC 反馈,实现全局拥塞控制。

四、GCC 的应用与优势

4.1 适用场景

GCC 专为实时音视频设计,核心适用场景包括:

  • 视频会议(如 Zoom、Teams、WebRTC 会议系统);
  • 实时直播(如游戏直播、互动直播);
  • 低延迟互动场景(如在线教育、远程控制)。

4.2 核心优势

  1. 低延迟优先:通过延时梯度提前感知拥塞,避免丢包(丢包会导致重传,增加延迟);
  2. 带宽利用率高:Normal 状态下缓慢提升码率,最大化利用空闲带宽;
  3. 自适应网络:动态阈值和双指标融合,适应复杂网络(如 4G/5G、WiFi、公网);
  4. 与实时场景适配:不依赖 TCP 重传,通过 FEC/RTX(重传)配合,平衡延迟与可靠性。
http://www.xdnf.cn/news/19701.html

相关文章:

  • 整理期初数据用到的EXCEL里面的函数操作
  • 【专栏升级】大模型应用实战并收录RAG专题,Agent专题,LLM重构数据科学流程专题,端侧AI专题,累计63篇文章
  • Xcode 编译速度慢是什么原因?如何提高编译速度?
  • MyBatis-Plus 实现用户分页查询(支持复杂条件)
  • Ansible循环与判断实战指南
  • SQL Server--提取性能最差的查询
  • Redisson分布式锁会发生死锁问题吗?怎么发生的?
  • 嵌入式系统与51单片机全解析
  • 20.Linux进程信号(一)
  • 深入浅出 RabbitMQ - SpringBoot2.X整合RabbitMQ实战
  • 数据结构——顺序表和单向链表(1)
  • WPF 开发必备技巧:TreeView 自动展开全攻略
  • 豪华酒店品牌自营APP差异对比分析到产品重构
  • Qt6实现绘图工具:12种绘图工具全家桶!这个项目满足全部2D场景
  • 国产化部署的it运维平台:功能全面,操作便捷
  • OpenCV Python
  • 新手也能轻松选!秒出PPT和豆包AI PPT优缺点解析
  • 《Python Flask 实战:构建一个可交互的 Web 应用,从用户输入到智能响应》
  • 企业如何实现零工用工零风险?盖雅全自动化合规管控
  • 2024 年 AI 产业格局复盘:头部企业竞逐方向与中小玩家生存破局点
  • K8s HPA自动扩缩容实战指南
  • 广东某地非金属矿山自动化监测服务项目
  • Android 16k页面大小适配
  • Rust SQLx 开发指南:利用 Tokio 进行性能优化
  • VUE基础
  • 代码随想录---动态规划篇
  • 机器学习辅助的Backtrader资产配置优化策略
  • 人脸识别在智能安防中的实践路径
  • nodejs文件读写操作完整版
  • 国标落地!中小学生午休课桌椅迎来规范,聚焦舒适与耐用