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获取的核心流程
  1. 接收端生成RR报告:接收端收到RTP数据(音视频帧)后,生成RTCP的RR报告,包含“接收时间戳”“延迟since最后一个SR”等计算RTT必需的字段。

  2. 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
}

img

图片解释:

阶段1:在RTCP RR报告中计算RTT延时(“RTT是怎么算出来的”)

这是RTT的“诞生环节”——接收端通过解析RTCP协议的RR报告(接收者报告),从底层协议一路解析到最终计算出RTT:

  • BaseChannel::ProcessPacket 开始接收RTCP报文,经过 WebRtcVideoChannelCallVideoSendStream 等模块层层转发,最终由 RTCPReceiver 负责解析报文。
  • 核心逻辑在 RTCPReceiver::HandleReportBlock 函数中:它提取RR报告里的 LSR(最后一次收到发送端“SR报告”的时间戳)和 DLSR(接收端处理延迟),通过公式算出RTT,然后更新到 last_round_trip_time_ms 这个关键参数中。

阶段2:获取更新到的RTT信息(“RTT怎么被上层模块拿到的”)

计算好的RTT需要被上层模块感知,才能用来调整网络策略:

  • SendSideCongestionController::MaybeTriggerOnNetworkChanged 开始(这是一个定时触发的网络状态更新逻辑),通过 BitrateControllerImplSendSideBandwidthEstimation 等模块,最终拿到最新的 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))=原帧率/2
  float 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的“缓存时间”

img

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++; // 重发次数+1
      it->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计算、VCMNackFecMethodNackModule应用,扮演着“眼睛”和“指挥棒”的角色:

  • 作为“眼睛”:通过RR报告反映网络延迟状况,让WebRTC实时感知网络快慢。
  • 作为“指挥棒”:
    1. 决定NACK/FEC的选择(低RTT用NACK,高RTT用FEC,中RTT混合用);
    2. 配置FEC的最大保护帧数(RTT越大,保护帧数越多);
    3. 设定NACK的缓存重发时间(超过1个RTT判定丢包,发起重发)。
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐