> 技术文档 > 【音视频】 WebRTC GCC 拥塞控制算法

【音视频】 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 的初代版本,采用 “接收端 + 发送端” 分工的架构,核心流程分为接收端延时梯度预估发送端丢包率预估两部分,最终发送端取两者最小值作为实际发送码率。

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

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

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

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

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

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

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

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

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

字段 含义 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 ( t i ) = [ t i − t i − 1] − [ T i − T i − 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(自适应阈值) 动态调整。

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

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

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

t h r e s h o l d ( t i ) = t h r e s h o l d ( t i − 1) + k ⋅ Δ t ⋅ ( ∣ d ( t i ) ∣ − t h r e s h o l d ( t i − 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:当前组与上一组的时间间隔(毫秒);
  • k k k:调整系数(动态变化):
    • ∣ d ( t i ) ∣ < t h r e s h o l d ( t i − 1 ) ( N o r m a l 状态): k = 0.00018 |d(t_i)| < threshold(t_{i-1})(Normal 状态):k=0.00018 d(ti)<threshold(ti1)Normal状态):k=0.00018(阈值缓慢减小,提高灵敏度);
    • ∣ d ( t i ) ∣ ≥ t h r e s h o l d ( t i − 1 ) ( O v e r u s e / U n d e r u s e 状态): k = 0.01 |d(t_i)| \\geq threshold(t_{i-1})(Overuse/Underuse 状态):k=0.01 d(ti)threshold(ti1)Overuse/Underuse状态):k=0.01(阈值快速增大,避免频繁切换状态)。

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

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

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

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

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

作用:根据 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 GCC 拥塞控制算法

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

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

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

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

(1)丢包率获取

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

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

RR报文格式如下:
【音视频】 WebRTC GCC 拥塞控制算法

(2)码率调整策略

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

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

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

(3)最终码率确定

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

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

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

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

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

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

对比维度 REMB-GCC TCC-GCC 带宽预估位置 接收端(延时梯度)+ 发送端(丢包率) 仅发送端(统一处理延时梯度 + 丢包率) 接收端反馈内容 仅 “最大带宽(REMB 值)” 详细收包时序(TCC 报文:包的发送 / 接收时间) 反馈报文 RTCP REMB RTCP 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(重传)配合,平衡延迟与可靠性。

昭通新闻