1、FEC冗余度配置的核心逻辑
-
接收端算丢包率,发丢包报告"
接收端统计收到的数据包,算出"丢了多少比例"(比如10个包丢1个,丢包率10%),然后通过RTCP_RR(一种专门的反馈报文)把这个"丢包率"告诉发送端。
-
发送端接"报告",预判未来丢包"
发送端收到丢包率后,不是直接用这个数值,而是用算法"猜"未来一小段时间(比如接下来1秒)的丢包率。比如现在丢包率10%,但最近3次反馈分别是8%、9%、10%,趋势在上升,就可能预判未来丢包率是11%。这一步是为了"提前准备",避免等丢包严重了再调整,反应太慢。
-
按预判丢包率,查表
发送端有个预设的表,里面记录了"不同丢包率对应多少冗余度"。比如预判丢包率10%,查表格知道I帧(关键帧,重要性高)需要30%冗余,P帧(普通帧,依赖关键帧)需要15%冗余。就像预判中雨,选一把中等大小的伞。
-
按冗余度打包FEC,发送
FEC模块根据确定的冗余度,生成对应的冗余包,和原始媒体包一起发送。比如15%冗余度意味着每发100个媒体包,额外发15个冗余包。这样即使丢了15个以内的包,接收端也能靠冗余包恢复。
2、冗余度从计算到生效的完整链路
上面4步是核心逻辑,实际在WebRTC代码里,这个过程是通过一系列函数"接力"完成的,分为两大链路:"丢包率→冗余度"的计算链路 和 "冗余度→FEC包"的封装链路。
2.1 链路1:从丢包率到冗余度的计算
这条链路的核心是"定时触发+反馈处理",简单说就是"接收端发丢包率→发送端定时收、算、调"。用"流程图+通俗解释"拆解:
| 函数调用流程(从上到下是执行顺序) | 通俗解释 |
|---|---|
| 线程定时启动:PlatformThread::StartThread → Run | WebRTC有个专门的"工作线程",定时(比如每200ms)触发一次"检查网络状况"的任务 |
| ProcessThreadImpl::Process → SendSideCongestionController::Process | 线程触发后,先到"发送端拥塞控制器",这里会接收从接收端发来的RTCP_RR报文,提取里面的丢包率数据(比如"最近丢包率10%")。 |
| SendSideCongestionController::MaybeTriggerOnNetworkChanged | 拥塞控制器判断:"丢包率变化够大了,需要调整冗余度",就触发"网络变化"事件,把丢包率传给下一个模块。 |
| BitrateAllocator::OnNetworkChanged → VideoSendStreamImpl::OnBitrateUpdated | "比特率分配器"收到丢包率后,告诉"视频发送流"模块:"网络丢包变了,你得调整FEC了"。 |
| ProtectionBitrateCalculator::SetTargetRates | 专门负责"保护策略计算"的模块接收到消息,开始准备计算冗余度。 |
| media_optimization::VCMLossProtectionLogic::FilteredLoss | 这里是"预判丢包率"的核心!用三种算法(后面讲)算出未来的丢包率,比如把当前10%丢包率预判成11%。 |
| VCMFecMethod::ProtectionFactor / VCMNackFecMethod::ProtectionFactor | 用预判的丢包率"查表",算出I帧和P帧各自的冗余度(比如I帧30%,P帧15%)。 |
| VideoSendStreamImpl::ProtectionRequest → ModuleRtpRtcpImpl::SetFecParameters | 把算好的冗余度(分成key_fec_params关键帧参数、delta_fec_params普通帧参数)传给"RTP/RTCP模块",告诉它"接下来按这个冗余度打包FEC"。 |
补充说明:除了"定时触发",还有很多场景会触发这个流程,比如突然收到"丢包率从10%涨到30%"的反馈,会立刻触发调整,不用等定时,避免"反应滞后"。
2.2 链路2:从冗余度到FEC包的封装
这条链路的核心是"编码完帧→按冗余度加FEC",简单说就是"视频帧编码好→按之前算的冗余度加冗余包→一起发送"。同样用"流程图+通俗解释"拆解:
| 函数调用流程(从上到下是执行顺序) | 通俗解释 |
|---|---|
| VideoStreamEncoder::EncodeTask::Run → EncodeVideoFrame | 视频编码器(比如H264、VP8)把原始视频帧编码成"压缩后的帧"(比如I帧、P帧),就像把大照片压缩成小文件。 |
| H264EncoderImpl::Encode → CMEncodedFrameCallback::OnEncodedImage | 编码完成后,触发"编码完成回调",告诉后续模块"帧准备好了,可以加FEC了"。 |
| VideoSendStreamImpl::OnEncodedImage → PayloadRouter::OnEncodedImage | "视频发送流"模块把编码帧传给"负载路由器",这里会判断"这是I帧还是P帧",然后选对应的冗余度参数(I帧用key_fec_params,P帧用delta_fec_params)。 |
| ModuleRtpRtcpImpl::SendOutgoingData → RTPSenderVideo::SendVideo | 把选好的冗余度参数传给"RTP发送模块",告诉它"按这个参数生成FEC包"。 |
| RTPSenderVideo::SendVideoPacketAsRedMaybeWithUlpfec | 这里是"Red+Ulpfec"的核心逻辑,把原始RTP包和FEC冗余包打包在一起(Red协议负责封装,Ulpfec负责生成冗余包)。 |
| UlpfecGenerator::AddRtpPacketAndGenerateFec → ForwardErrorCorrection::NumFecPackets | "Ulpfec生成器"根据冗余度(比如15%)计算需要生成多少个冗余包,比如100个媒体包对应15个冗余包。 |
| ForwardErrorCorrection::EncodeFec → GenerateFecPayloads → XorPayloads | 最终生成FEC冗余包的"内容"(用异或运算把媒体包数据混合生成冗余数据),然后和原始包一起发送。 |
3、WebRTC如何"预判未来丢包率"
前面提到,发送端会"预判未来丢包率",WebRTC提供了3种算法,分别对应不同场景,各有优缺点。
3.1 算法1:直接用当前丢包率(最简单,适合稳定网络)
- 逻辑:不做任何预判,直接把接收端最新反馈的丢包率当"未来丢包率"。比如接收端刚说"现在丢包率10%",就认为未来也会是10%。
- 适用场景:网络非常稳定,丢包率长时间不变(比如一直5%左右),不需要复杂计算。
- 缺点:网络波动时反应慢,比如突然丢包率从5%涨到20%,还按5%准备冗余,会导致大量包丢了无法恢复。
3.2 算法2:一阶指数平滑算法(最常用,适合波动网络)
- 逻辑:把"过去的预判值"和"当前的实际值"按比例混合,新数据权重高,旧数据权重低。就像"今天的天气预报=0.3×昨天的预报+0.7×今天的实际天气",让预报更贴近最新情况,又不会忽高忽低。
- 公式 :
St+1 = α×xt + (1-α)×St
其中:St+1:下一次的预判丢包率(比如未来1秒的);α:平滑系数(WebRTC里通常设为0.1~0.3,值越大,越依赖当前数据,反应越快);xt:当前收到的实际丢包率(比如接收端刚反馈的10%);St:上一次的预判丢包率(比如上次算的9%)。
- 例子:α=0.2,St=9%,xt=10% → St+1=0.2×10% + 0.8×9% = 9.2%,比直接用10%更平缓,避免"一波动就乱调"。
- 适用场景:网络有小波动(比如5%→7%→10%),需要平滑过渡,避免冗余度频繁调整导致带宽浪费。
3.3 算法3:窗口期最大丢包率(最保守,适合突发丢包)
- 逻辑:记录最近一段时间(比如最近5秒)内的所有丢包率,取其中最大的值作为"未来丢包率"。比如最近5次反馈是8%、10%、12%、9%、11%,最大是12%,就按12%准备冗余。
- 逻辑:就像"看到最近一周下过一次大雨,就按大雨的情况准备雨伞",属于"宁可多准备,也不冒险"的保守策略。
- 适用场景:网络容易突发丢包(比如WiFi突然断一下、4G信号跳变),需要提前按最坏情况准备,避免突发丢包导致画面卡顿。
4、核心函数注释与解读
前面讲了原理和流程,现在结合博文中的源码,把关键函数拆解开,加上详细注释,让你能看懂"代码里到底怎么实现这些逻辑"。
4.1 预判丢包率
(VCMLossProtectionLogic::FilteredLoss)这个函数是"预判未来丢包率"的入口,根据选择的"过滤模式"(3种算法),返回最终的预判丢包率。
cpp
/**
* @brief 计算预判的丢包率(核心函数)
* @param nowMs 当前时间(毫秒),用于判断历史数据是否过期
* @param filter_mode 过滤模式(对应3种预判算法:kNoFilter=直接用当前值,kAvgFilter=指数平滑,kMaxFilter=窗口期最大)
* @param lossPr255 当前收到的实际丢包率(注意:单位是"以255为分母的比例",比如10%丢包率对应25(25/255≈10%))
* @return uint8_t 预判后的丢包率(同样以255为分母)
*/
uint8_t VCMLossProtectionLogic::FilteredLoss(int64_t nowMs,
FilterPacketLossMode filter_mode,
uint8_t lossPr255) {
// 1. 更新"窗口期最大丢包率"的历史记录(不管用哪种算法,都先记录历史数据,以备后续使用)
UpdateMaxLossHistory(lossPr255, nowMs);
// 2. 更新"指数平滑算法"的历史值(为kAvgFilter模式做准备)
// _lastPrUpdateT:上一次更新指数平滑值的时间
// 计算"距离上次更新的时间差",用于调整平滑系数(时间越久,当前数据权重越高)
_lossPr255.Apply(rtc::saturated_cast<float>(nowMs - _lastPrUpdateT),
rtc::saturated_cast<float>(lossPr255));
_lastPrUpdateT = nowMs; // 更新"上次更新时间"为当前时间
// 3. 根据选择的过滤模式,计算最终的预判丢包率
uint8_t filtered_loss = lossPr255; // 默认值:直接用当前丢包率(kNoFilter模式)
switch (filter_mode) {
case kNoFilter:
// 模式1:直接用当前丢包率,不做任何处理
break;
case kAvgFilter:
// 模式2:用指数平滑算法的结果(取整时+0.5是为了四舍五入,比如9.2→9,9.6→10)
filtered_loss = rtc::saturated_cast<uint8_t>(_lossPr255.filtered() + 0.5);
break;
case kMaxFilter:
// 模式3:用窗口期内的最大丢包率
filtered_loss = MaxFilteredLossPr(nowMs);
break;
}
return filtered_loss; // 返回预判后的丢包率
}
4.2 指数平滑算法
(ExpFilter::Apply)这个函数是"算法2"的具体实现,负责计算"平滑后的丢包率",对应公式St+1 = α×xt + (1-α)×St。
cpp
/**
* @brief 一阶指数平滑算法的核心实现
* @param exp 时间因子(距离上次更新的时间差,用于调整平滑系数α)
* @param sample 当前的实际丢包率(即公式中的xt)
* @return float 平滑后的丢包率(即公式中的St+1)
*/
float ExpFilter::Apply(float exp, float sample) {
if (filtered_ == kValueUndefined) {
// 第.一次调用:还没有历史平滑值,直接把当前样本作为初始平滑值
// 比如第一次收到丢包率10%,平滑值就设为10%
filtered_ = sample;
} else if (exp == 1.0f) {
// 时间因子为1.0:表示距离上次更新刚好是"一个平滑周期",直接用标准公式计算
// alpha_是预设的平滑系数(比如0.2)
filtered_ = alpha_ * filtered_ + (1 - alpha_) * sample;
} else {
// 时间因子不是1.0:根据时间差调整平滑系数(时间越久,alpha越大,当前样本权重越高)
// 公式:alpha = alpha_^exp(比如alpha_=0.2,exp=0.5,alpha≈0.447,当前样本权重更高)
float alpha = std::pow(alpha_, exp);
filtered_ = alpha * filtered_ + (1 - alpha) * sample;
}
// 如果设置了"最大平滑值",确保平滑后的结果不超过这个最大值(避免预判值过高)
if (max_ != kValueUndefined && filtered_ > max_) {
filtered_ = max_;
}
return filtered_; // 返回平滑后的丢包率
}
4.3 更新窗口期最大丢包率的历史
(UpdateMaxLossHistory)这个函数负责"记录最近一段时间的丢包率",为"算法3(窗口期最大)"提供数据支持,比如记录最近5次的丢包率。
cpp
/**
* @brief 更新"窗口期最大丢包率"的历史记录
* @param lossPr255 当前的实际丢包率(以255为分母)
* @param now 当前时间(毫秒),用于判断历史数据是否过期
*/
void VCMLossProtectionLogic::UpdateMaxLossHistory(uint8_t lossPr255, int64_t now) {
// kLossPrShortFilterWinMs:窗口期长度(比如1000毫秒,即1秒)
// 检查"最早的历史记录"是否还在窗口期内:如果没过期,且当前丢包率比之前的最大值大,就更新最大值
if (_lossPrHistory[0].timeMs >= 0 && now - _lossPrHistory[0].timeMs < kLossPrShortFilterWinMs) {
if (lossPr255 > _shortMaxLossPr255) {
_shortMaxLossPr255 = lossPr255; // 更新窗口期内的最大值
}
} else {
// 最早的历史记录已过期,需要"移除旧数据,加入新数据"(类似队列的"先进先出")
// 1. 先判断是否是第一次添加数据
if (_lossPrHistory[0].timeMs == -1) {
// 第一次添加:直接把当前丢包率作为初始最大值
_shortMaxLossPr255 = lossPr255;
} else {
// 不是第一次:把历史数据"往后挪一位"(移除最旧的,空出第一位)
for (int32_t i = (kLossPrHistorySize - 2); i >= 0; i--) {
_lossPrHistory[i + 1].lossPr255 = _lossPrHistory[i].lossPr255; // 丢包率数据后移
_lossPrHistory[i + 1].timeMs = _lossPrHistory[i].timeMs; // 时间戳后移
}
// 重置最大值:如果之前的最大值是"被移除的旧数据",需要重新计算最大值
if (_shortMaxLossPr255 == 0) {
_shortMaxLossPr255 = lossPr255;
}
}
// 2. 在第一位加入当前的新数据(最新的丢包率和时间戳)
_lossPrHistory[0].lossPr255 = lossPr255;
_lossPrHistory[0].timeMs = now;
}
}
4.4 获取窗口期内的最大丢包率
(MaxFilteredLossPr)这个函数是"算法3"的核心,从历史记录中找出"窗口期内最大的丢包率",作为预判值。
cpp
/**
* @brief 从历史记录中获取"窗口期内的最大丢包率"
* @param nowMs 当前时间(毫秒),用于过滤过期的历史数据
* @return uint8_t 窗口期内的最大丢包率(以255为分母)
*/
uint8_t VCMLossProtectionLogic::MaxFilteredLossPr(int64_t nowMs) const {
uint8_t maxFound = _shortMaxLossPr255; // 初始值:当前记录的最大值
// 如果没有任何历史数据(第一次调用),直接返回初始最大值
if (_lossPrHistory[0].timeMs == -1) {
return maxFound;
}
// 遍历所有历史记录,找出"在窗口期内"的最大丢包率
for (int32_t i = 0; i < kLossPrHistorySize; i++) {
// 1. 如果当前历史记录未初始化(timeMs=-1),跳出循环(后面的都是空的)
if (_lossPrHistory[i].timeMs == -1) {
break;
}
// 2. 过滤过期数据:如果历史记录的时间距离现在超过"窗口期总长度"(比如5秒),跳出循环
// kLossPrHistorySize:历史记录的最大数量(比如5条)
// kLossPrShortFilterWinMs:单条记录的窗口期(比如1秒),总窗口期=5×1=5秒
if (nowMs - _lossPrHistory[i].timeMs > kLossPrHistorySize * kLossPrShortFilterWinMs) {
break;
}
// 3. 如果当前历史记录的丢包率比"已找到的最大值"大,更新最大值
if (_lossPrHistory[i].lossPr255 > maxFound) {
maxFound = _lossPrHistory[i].lossPr255;
}
}
return maxFound; // 返回窗口期内的最大丢包率
}
5、总结:WebRTC FEC冗余度配置的核心要点
- 核心思想:"按需调整,不浪费不不足"------根据网络丢包率动态改冗余度,平衡抗丢包能力和带宽消耗。
- 关键流程:接收端算丢包率→发送端预判丢包率→查表格定冗余度→按冗余度封FEC包,四步闭环。
- 算法选择 :
- 稳定网络用"直接用当前丢包率"(简单);
- 波动网络用"指数平滑"(平衡反应速度和平滑度);
- 突发丢包用"窗口期最大"(保守,避免风险)。
- 代码核心 :关键函数是
FilteredLoss(选算法算预判丢包率)、ExpFilter::Apply(指数平滑)、MaxFilteredLossPr(窗口期最大),理解这几个函数就能掌握冗余度计算的核心。
通过这套机制,WebRTC能在复杂的网络环境中,自动调整FEC的"力度",既保证音视频不卡顿,又不浪费过多带宽,这也是它能成为主流实时音视频框架的重要原因之一。