webrtc源码走读(二)-QOS-RTT
1、什么是RTT?
RTT的全称是Round-Trip Time,翻译过来就是“环路延时”,简单说就是数据从发送端发出去,到接收端收到后返回确认信息,这一整个过程花费的时间,单位通常是毫秒(ms)。
它为什么重要?因为WebRTC要根据RTT判断网络状况:
- RTT小(比如20ms):说明网络通畅,数据传得又快又稳。
- RTT大(比如500ms):说明网络有延迟,数据传输慢,可能需要调整策略来保证通信质量。
2、RTT是怎么来的?------WebRTC中的获取流程
WebRTC通过解析RTCP协议中的RR报告(Receiver Report,接收者报告)计算RTT,整个流程需多个模块协作,以下结合核心类与函数逻辑拆解:
2.1 RTT获取的核心流程
-
接收端生成RR报告:接收端收到RTP数据(音视频帧)后,生成RTCP的RR报告,包含“接收时间戳”“延迟since最后一个SR”等计算RTT必需的字段。
-
RR报告回传与模块处理:RR报告通过网络回传给发送端,发送端各模块按如下路径处理,最终计算RTT:
BaseChannel::ProcessPacket(接收RR报文) → WebRtcVideoChannel::OnRtcpReceived(转发RTCP事件) → Call::DeliverRtcp(Call模块分发RTCP任务) → VideoSendStreamImpl::DeliverRtcp(视频发送流接收RTCP) → ModuleRtpRtcpImpl::IncomingRtcpPacket(RTP/RTCP模块解析报文) → RTCPReceiver::ParseCompoundPacket(解析复合RTCP包,识别RR报告) → RTCPReceiver::HandleReceiverReport(处理RR报告,调用计算逻辑) → RTCPReceiver::HandleReportBlock(提取报告块中的延迟字段) → 计算RTT并更新到SendSideBandwidthEstimation::last_round_trip_time_ms(核心参数存储)
2.2 核心类RTCPReceiver的RTT计算逻辑
RTCPReceiver是计算RTT的核心类,其HandleReportBlock函数会提取RR报告中的字段,结合发送端的SR(Sender Report)时间戳,计算环路延时,以下是关键逻辑:
// RTCPReceiver.cpp 中处理RR报告块的函数
void RTCPReceiver::HandleReportBlock(const ReportBlock& report_block,const RtcpPacket& rtcp_packet) {// 1. 获取接收端记录的“最后一次收到发送端SR的时间戳”(LSR,Last Sender Report)uint32_t lsr = report_block.last_sender_report_timestamp();// 2. 获取接收端“从收到SR到发送当前RR的延迟”(DLSR,Delay Since Last Sender Report)uint32_t dlsr = report_block.delay_since_last_sender_report();// 3. 从发送端的SR缓存中,获取LSR对应的“发送端实际发送SR的时间”(sender_send_time_ms)auto it = sender_report_map_.find(lsr);if (it == sender_report_map_.end()) {// 若未找到对应的SR,无法计算RTT,直接返回return;}int64_t sender_send_time_ms = it->second;// 4. 计算RTT:公式为“当前时间 - 发送端发SR时间 - 接收端DLSR延迟”// 当前时间(接收端收到RR的时间)- 发送端发SR的时间 - 接收端处理延迟 = 环路延时int64_t now_ms = clock_->TimeInMilliseconds();int64_t rtt_ms = now_ms - sender_send_time_ms - (dlsr * 1000) / 65536;// 注:dlsr单位是1/65536秒,需转换为毫秒(×1000/65536)// 5. 将计算出的RTT更新到带宽估计模块(供NACK/FEC使用)bandwidth_observer_->OnReceivedRtcpReceiverReport(rtt_ms);// 最终更新到SendSideBandwidthEstimation::last_round_trip_time_ms
}

图片解释:
阶段1:在RTCP RR报告中计算RTT延时(“RTT是怎么算出来的”)
这是RTT的“诞生环节”——接收端通过解析RTCP协议的RR报告(接收者报告),从底层协议一路解析到最终计算出RTT:
- 从
BaseChannel::ProcessPacket开始接收RTCP报文,经过WebRtcVideoChannel、Call、VideoSendStream等模块层层转发,最终由RTCPReceiver负责解析报文。 - 核心逻辑在
RTCPReceiver::HandleReportBlock函数中:它提取RR报告里的LSR(最后一次收到发送端“SR报告”的时间戳)和DLSR(接收端处理延迟),通过公式算出RTT,然后更新到last_round_trip_time_ms这个关键参数中。
阶段2:获取更新到的RTT信息(“RTT怎么被上层模块拿到的”)
计算好的RTT需要被上层模块感知,才能用来调整网络策略:
- 从
SendSideCongestionController::MaybeTriggerOnNetworkChanged开始(这是一个定时触发的网络状态更新逻辑),通过BitrateControllerImpl、SendSideBandwidthEstimation等模块,最终拿到最新的last_round_trip_time_ms(也就是当前的RTT值)。
阶段3:线程定时调用,更新保护机制(“RTT怎么指导抗丢包策略的”)
RTT最终用来决定是用**NACK(重传)还是FEC(前向纠错)**来抗丢包,这部分是“策略执行环节”:
- 定时线程由
PlatformThread::StartThread启动,触发ProcessThreadImpl的处理逻辑。 - 最终调用
VCMNackFecMethod::ProtectionFactor函数——根据RTT的大小,决定是只开NACK、只开FEC,还是混合使用,从而动态调整音视频传输的抗丢包策略。
3、RTT的核心作用:决定NACK和FEC怎么工作
WebRTC的NACK(否定确认)和FEC(前向纠错)是抗丢包的核心机制,RTT通过VCMNackFecMethod等类直接决定两种机制的选择、参数配置,以下结合关键源码拆解。
3.1 影响NACK和FEC的“选择”:低/中/高RTT对应不同策略
VCMNackFecMethod::ProtectionFactor是决定NACK/FEC选择的核心函数,它根据RTT大小切换“NACK-only”“FEC-only”“混合模式”,以下是带注释的源码片段:
// VCMNackFecMethod.cpp 中决定保护机制的函数
bool VCMNackFecMethod::ProtectionFactor(const VCMProtectionParameters* parameters) {// parameters->rtt:当前最新的RTT值(从last_round_trip_time_ms获取)// _lowRttNackMs:低RTT阈值(默认~100ms,可配置)// _highRttNackMs:高RTT阈值(默认~300ms,可配置)// 1. 低RTT场景:仅用NACK,关闭FEC(FEC冗余因子设为0)if (_lowRttNackMs == -1 || parameters->rtt < _lowRttNackMs) {// 调用父类FEC方法,将FEC的delta帧保护因子设为0(不发FEC冗余)VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);_protectionFactorD = 0; // FEC冗余量为0,仅用NACK}// 2. 中RTT场景:混合模式(FEC+NACK),调整FEC冗余量else if (_highRttNackMs == -1 || parameters->rtt < _highRttNackMs) {// adjustRtt:RTT调整因子(范围0~1,RTT越大,因子越小,FEC冗余越少,依赖NACK补充)float adjustRtt = 1.0f; // 注:原代码暂禁用动态调整,实际场景会根据RTT查表计算// 按RTT调整FEC的delta帧冗余因子(RTT越大,FEC冗余越少,减少带宽浪费)_protectionFactorD = static_cast<uint8_t>(adjustRtt * static_cast<float>(_protectionFactorD));// 更新调整后的FEC参数VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);}// 3. 高RTT场景:仅用FEC(NACK重发会迟到,无效)else {// 不调整FEC冗余因子,保持默认值(靠FEC独立抗丢包)VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);}return true;
}
3.2 影响FEC的“冗余量”:RTT决定最大保护帧数
前情提要(术语解释):
①时间分层:
概念:把视频拆成“基础层+增强层”,分层传
时间分层是WebRTC为了适配不同网络带宽的一种技术:
- 它把原本连续的视频帧,拆成多个“层”。最底层叫“基础层”,画面最核心(比如能保证基本流畅);上面的“增强层”则是补充细节(让画面更清晰、更流畅)。
- 比如,把30帧/秒的视频拆成2层:基础层可能只有15帧/秒(保证基本流畅),增强层再补充15帧/秒(让画面更丝滑)。
作用:网络差就只传基础层,网络好就传多层
- 当用户网络很差时,只发基础层,保证“能看”;
- 当用户网络变好时,再发增强层,让画面“更清晰/更流畅”。
这样既避免了“网络差时视频直接卡住”,又能在网络好时提供更好的体验。
实现逻辑:分层后,各层帧间隔发送
还是以“30帧/秒拆成2层”为例:
- 基础层的帧:第1、3、5…帧(间隔1帧发);
- 增强层的帧:第2、4、6…帧(补全基础层的间隔)。
接收端收到后,把两层的帧合起来,就能还原30帧/秒的流畅画面;如果只收到基础层,也能以15帧/秒的帧率播放,不至于完全看不了。
简单来说,时间分层就是**“视频版的‘低配/高配切换’”**——网络差就用低配(基础层),网络好就用高配(基础层+增强层),从而在各种网络环境下都能保证视频能看、好看。
FEC的“最大保护帧数”指一次为多少帧数据准备冗余,RTT越大,需保护的帧数越多(避免多帧在传输中同时丢包),核心逻辑在VCMNackFecMethod::ComputeMaxFramesFec:
// VCMNackFecMethod.cpp 中计算FEC最大保护帧数的函数
int VCMNackFecMethod::ComputeMaxFramesFec(const VCMProtectionParameters* parameters) {// parameters->rtt:当前RTT(ms)// parameters->frameRate:视频帧率(fps)// parameters->numLayers:时间分层数(用于适配不同网络)// 1. 特殊场景:时间分层数>2时,仅保护1帧(多层数据间隔大,多帧保护意义小)if (parameters->numLayers > 2) {return 1; }// 2. 计算基础层帧率:时间分层会降低基础层帧率(分层数越多,基础层帧率越低)// 例:2层分层时,基础层帧率=原帧率/(2^(2-1))=原帧率/2float base_layer_framerate = parameters->frameRate / static_cast<float>(1 << (parameters->numLayers - 1));// 3. 核心公式:计算最大保护帧数(RTT越大,帧数越多)// 逻辑:2 × 基础层帧率 × (RTT/1000) → 确保1个RTT内传输的帧都被FEC覆盖int max_frames_fec = std::max(static_cast<int>(2.0f * base_layer_framerate * parameters->rtt / 1000.0f + 0.5f), // +0.5f用于四舍五入1 // 最少保护1帧,避免帧数为0);// 4. 限制最大帧数(避免FEC冗余过多,浪费带宽)// kUpperLimitFramesFec:默认是10帧(WebRTC内置常量)if (max_frames_fec > kUpperLimitFramesFec) {max_frames_fec = kUpperLimitFramesFec;}return max_frames_fec; // 返回最终的FEC最大保护帧数
}
VCMNackFecMethod::ComputeMaxFramesFec 代码运算示例表
| 场景编号 | 输入参数(代码中parameters值) | 代码执行关键步骤 | 最终FEC最大保护帧数 |
|---|---|---|---|
| 1 | - 时间分层数(numLayers):1 - 视频帧率(frameRate):30fps - RTT:100ms | 1. 分层数=1≤2,不触发特殊场景; 2. 基础层帧率=30/(2^(1-1))=30/1=30fps; 3. 核心公式计算:2×30×(100/1000)+0.5=6.5→取整6,且6≥1; 4. 6≤10(默认上限),不触发限制。 | 6帧 |
| 2 | - 时间分层数(numLayers):2 - 视频帧率(frameRate):60fps - RTT:200ms | 1. 分层数=2≤2,不触发特殊场景; 2. 基础层帧率=60/(2^(2-1))=60/2=30fps; 3. 核心公式计算:2×30×(200/1000)+0.5=12.5→取整12,且12≥1; 4. 12>10(默认上限),触发限制,取10。 | 10帧 |
| 3 | - 时间分层数(numLayers):3 - 视频帧率(frameRate):24fps - RTT:500ms | 1. 分层数=3>2,触发特殊场景,直接返回1; 2-4步不执行(因第一步已返回结果)。 | 1帧 |
| 4 | - 时间分层数(numLayers):1 - 视频帧率(frameRate):15fps - RTT:50ms | 1. 分层数=1≤2,不触发特殊场景; 2. 基础层帧率=15/(2^(1-1))=15/1=15fps; 3. 核心公式计算:2×15×(50/1000)+0.5=1.5→取整1,且1≥1; 4. 1≤10,不触发限制。 | 1帧 |
| 5 | - 时间分层数(numLayers):2 - 视频帧率(frameRate):45fps - RTT:150ms | 1. 分层数=2≤2,不触发特殊场景; 2. 基础层帧率=45/(2^(2-1))=45/2=22.5fps; 3. 核心公式计算:2×22.5×(150/1000)+0.5=7.25→取整7,且7≥1; 4. 7≤10,不触发限制。 | 7帧 |
4、RTT的另一个作用:配置NACK的“缓存时间”

NACK会将“待重发的数据”存在NackModule的“NACK列表”中,RTT决定“等多久判断数据丢包、发起重发”,核心逻辑在NackModule::GetNackBatch,源码注释如下:
// NackModule.cpp 中获取NACK重发列表的函数
std::vector<uint16_t> NackModule::GetNackBatch(int64_t now_ms) {std::vector<uint16_t> nack_batch; // 存储待重发的序列号auto it = nack_list_.begin();while (it != nack_list_.end()) {// it->second:NACK列表中的条目(含sent_at_time:上次发送时间,retries:重发次数)// delay_timed_out:是否超过“等待RTT”的时间(判断是否丢包)bool delay_timed_out = now_ms - it->second.sent_at_time >= rtt_ms_;// consider_timestamp:是否满足时间戳判断条件(辅助丢包检测)bool consider_timestamp = it->second.sent_at_time == 0 || delay_timed_out;// 核心逻辑:若超过1个RTT未收到确认,判定为丢包,加入重发列表if (delay_timed_out && consider_timestamp) {nack_batch.emplace_back(it->second.seq_num); // 加入待重发列表it->second.retries++; // 重发次数+1it->second.sent_at_time = now_ms; // 更新本次重发时间// 若重发次数超过上限(kMaxNackRetries:默认3次),从列表删除(避免无限重发)if (it->second.retries >= kMaxNackRetries) {LOG(LS_WARNING) << "Sequence number " << it->second.seq_num << " removed from NACK list due to max retries.";it = nack_list_.erase(it); // 删除超次数条目continue;}}++it;}return nack_batch; // 返回待重发列表,供发送端重发
}
关键逻辑解读:
now_ms - it->second.sent_at_time >= rtt_ms_:若数据发送后,超过1个RTT未收到确认,判定为丢包,发起重发。kMaxNackRetries:默认3次重发上限,超过后删除条目,避免因网络极差导致的无限重发,浪费带宽。
5、总结:RTT是WebRTC QoS的“核心指挥棒”
整个WebRTC的QoS(服务质量)保障里,RTT通过RTCPReceiver计算、VCMNackFecMethod和NackModule应用,扮演着“眼睛”和“指挥棒”的角色:
- 作为“眼睛”:通过RR报告反映网络延迟状况,让WebRTC实时感知网络快慢。
- 作为“指挥棒”:
- 决定NACK/FEC的选择(低RTT用NACK,高RTT用FEC,中RTT混合用);
- 配置FEC的最大保护帧数(RTT越大,保护帧数越多);
- 设定NACK的缓存重发时间(超过1个RTT判定丢包,发起重发)。
