webrtc源码走读(七)核心引擎层——Qos模块

1、Qos整体架构与设计哲学

WebRTC 的 QoS(服务质量)机制是一套端到端闭环反馈系统,运行在应用层,不依赖网络设备。其核心目标是:

  • 保障实时性:音视频通信对延迟极其敏感(语音 >100ms 就会影响可懂度)
  • 最大化可用带宽利用率:在不造成拥塞的前提下尽可能提高画质/音质
  • 容忍丢包与抖动:互联网环境无法保证可靠传输,必须具备容错能力

整个系统通过接收端反馈 → 发送端决策 → 媒体调整的闭环,动态适应网络变化。

为什么必须闭环?

因为网络状态瞬息万变(如从 WiFi 切到 4G),只有基于接收端真实反馈的决策才可靠。预测模型(如仅靠丢包率判断拥塞)在真实网络中极易误判。


2、QoS 模块与其他组件的交互关系

接收端
发送端
原始帧
分层编码
RTP包
带宽估计
层码率建议
重传包
RTP包
解码帧
丢包列表
NACK RTCP
到达时间
抖动值
Transport-CC RTCP
RTCP反馈
Video Encoder
SVC Controller
Pacer
Network
BWE
NACK Resender
Jitter Buffer
Video Decoder
Packet Loss Detector
NACK Generator
Transport Feedback
BWE
Jitter Estimator

关键交互说明:

交互方向 触发条件 数据内容 作用
接收端 → 发送端 每 50~100ms Transport Feedback(包到达时间) BWE 计算可用带宽
接收端 → 发送端 检测到丢包 NACK RTCP(丢失序列号) 请求重传关键视频包
BWE → Pacer 带宽更新 目标比特率(bps) 控制发送节奏,避免突发
BWE → SVC Controller 带宽变化 各层最大码率 动态开启/关闭增强层
Jitter Buffer → BWE 抖动过大 网络波动信号 辅助 BWE 判断拥塞

3、核心机制详解

3.1. NACK:丢包重传(仅视频)

作用

当接收端发现 RTP 包丢失时,主动请求发送端重传。适用于低 RTT(<300ms)且丢包率中等(5%~15%) 的场景。

工作流程与时序图
接收端 网络 发送端 接收端 网络 发送端 包 发送 RTP 包 收到 检测到 发送 NACK RTCP (seq=101) 从历史缓存查找 重传 RTP 包 收到重传包 完整帧组装 → 解码

源码(接收端生成 NACK)

cpp 复制代码
// 文件: modules/rtp_rtcp/source/rtcp_receiver.cc
void RtcpReceiver::OnReceivedRtcpReportBlock(
    const RtcpReportBlock& report_block,
    uint32_t ssrc) {

  uint16_t last_seq = report_block.last_sequence_number;
  std::vector<uint16_t> nack_list;

  // 回溯最多 2000 毫秒内的包(默认窗口)
  for (uint16_t seq = last_seq - 1; 
       seq != last_seq && nack_list.size() < kMaxNackListSize; 
       --seq) {

    if (packet_receiver_->IsPacketLost(seq)) {
      nack_list.push_back(seq);
    }
    
    // 防止无限循环(序列号回绕)
    if (nack_list.size() >= 2000) break;
  }

  if (!nack_list.empty()) {
    rtp_rtcp_->SendNack(ssrc, nack_list); // 发送 NACK RTCP
  }
}

源码(发送端重传)

cpp 复制代码
// 文件: modules/rtp_rtcp/source/rtp_packet_history.cc
bool RtpPacketHistory::GetPacket(uint16_t sequence_number, 
                                RtpPacketToSend* packet) {
  // 在历史缓存中查找对应序列号的包
  for (const auto& item : packet_history_) {
    if (item.packet.sequence_number() == sequence_number) {
      *packet = item.packet;
      return true;
    }
  }
  return false; // 包已过期(超过 2000ms),无法重传
}

3.2. RTT:往返延迟测量

作用

RTT 是判断是否启用 NACK 的关键阈值(通常 <100ms 才有效),也用于 Jitter Buffer 的目标延迟计算。

工作流程
Receiver Sender Receiver Sender 发送 Sender Report (SR),含 LSR 时间戳 记录 LSR,并等待一段时间(DLSR) 发送 Receiver Report (RR),含 DLSR 计算 RTT = 当前时间 - LSR - DLSR

源码(RTT 计算)

cpp 复制代码
// 文件: modules/rtp_rtcp/source/rtcp_receiver.cc
void RtcpReceiver::OnRtcpReceived(const RtcpPacket* packet) {
  if (packet->type() == kRtcpRr) {
    const RtcpRrPacket* rr = static_cast<const RtcpRrPacket*>(packet);
    
    // 提取 LSR(Sender Report 时间戳)和 DLSR(Delay since LSR)
    uint32_t lsr = rr->lsr();
    uint32_t dlsr = rr->dlsr();

    // 转换为毫秒(高16位是秒,低16位是小数)
    int64_t lsr_ms = (lsr >> 16) * 1000;
    int64_t dlsr_ms = (dlsr >> 16) * 1000;

    // RTT = 当前时间 - LSR - DLSR
    int64_t rtt_ms = clock_->TimeInMilliseconds() - lsr_ms - dlsr_ms;

    // 指数加权平均平滑(α=0.1)
    if (rtt_ms > 0) {
      estimated_rtt_ms_ = 0.1 * rtt_ms + 0.9 * estimated_rtt_ms_;
    }
  }
}

3.3. FEC:前向纠错(仅视频)

作用

通过冗余包恢复丢失数据,适用于高丢包率(>15%)或高 RTT(>300ms) 场景,避免 NACK 延迟过高。

与 NACK 的协同策略

网络条件 策略 原因
丢包率 < 5%,RTT < 100ms 仅 NACK 带宽效率最高
丢包率 5%15%,RTT 100300ms NACK + 低冗余 FEC 混合容错
丢包率 > 15% 或 RTT > 300ms 仅 FEC 避免 NACK 延迟

源码(FEC 生成)

cpp 复制代码
// 文件: modules/rtp_rtcp/source/fec_generator.cc
void FecGenerator::GenerateFecPackets(
    const std::vector<RtpPacketToSend>& packets,
    std::vector<RtpPacketToSend>* fec_packets) {

  if (packets.empty()) return;

  // 计算 FEC 包数量(例如冗余度 20%)
  size_t num_fec = (packets.size() * fec_rate_ + 99) / 100;

  for (size_t i = 0; i < num_fec; ++i) {
    RtpPacketToSend fec;
    fec.SetPayloadType(kFlexfecPayloadType);
    fec.SetSequenceNumber(packets.back().sequence_number() + i + 1);

    // 简化版:对所有包 payload 异或生成 FEC
    std::vector<uint8_t> fec_payload;
    for (const auto& pkt : packets) {
      const uint8_t* p = pkt.payload();
      size_t sz = pkt.payload_size();
      if (fec_payload.empty()) {
        fec_payload.assign(p, p + sz);
      } else {
        for (size_t j = 0; j < std::min(fec_payload.size(), sz); ++j) {
          fec_payload[j] ^= p[j];
        }
      }
    }
    fec.SetPayload(fec_payload.data(), fec_payload.size());
    fec_packets->push_back(std::move(fec));
  }
}

3.4. BWE:带宽估计(Transport-CC)

作用

实时估算可用上行带宽,为 Pacer 和 SVC 提供决策依据。

工作流程与时序图
SVC Pacer Receiver Network Sender SVC Pacer Receiver Network Sender 发送 RTP 包(带 TCC 序列号) RTP 包(可能延迟/乱序) 记录每个包的到达时间 Transport Feedback RTCP(含序列号+到达时间) 计算排队延迟 → 更新带宽估计 设置新目标比特率 调整各层码率

源码(BWE 更新)

cpp 复制代码
// 文件: modules/congestion_controller/goog_cc/goog_cc_bandwidth_estimator.cc
void GoogCcBandwidthEstimator::OnTransportFeedback(
    const TransportFeedback& feedback) {

  for (const auto& packet : feedback.packets) {
    int64_t delay_ms = packet.arrival_time_ms - packet.send_time_ms;

    // 卡尔曼滤波器平滑延迟
    double predicted = kalman_filter_.Predict();
    double residual = delay_ms - predicted;
    kalman_filter_.Update(residual);

    // 简化带宽计算(实际使用 AIMD 算法)
    if (delay_ms > 0) {
      int bitrate = (packet.size_bytes * 8000) / delay_ms;
      current_estimate_ = bitrate;
    }
  }

  // 通知 Pacer 和 SVC
  observer_->OnBandwidthEstimate(current_estimate_);
}

3.5. SVC:可伸缩视频编码

作用

将视频分为基础层(BL)和增强层(EL),根据带宽动态调整质量。

层级结构示例(VP9 S1T3 模式)

层级 分辨率 码率占比 作用
BL 320×180 30% 必需层,保证基本可用
EL1 640×360 40% 中等质量
EL2 1280×720 30% 高清质量

源码(SVC 控制)

cpp 复制代码
// 文件: modules/video_coding/svc/scalable_video_controller.cc
void ScalableVideoController::OnBandwidthEstimate(int bitrate_bps) {
  const float weights[3] = {0.3f, 0.4f, 0.3f}; // BL, EL1, EL2
  int layer_bitrates[3] = {0};

  for (int i = 0; i < num_layers_; ++i) {
    layer_bitrates[i] = static_cast<int>(bitrate_bps * weights[i]);
    // 不超过该层上限
    if (layer_bitrates[i] > max_bitrate_[i]) {
      layer_bitrates[i] = max_bitrate_[i];
    }
  }

  encoder_->SetLayerBitrates(layer_bitrates); // 通知编码器
}

3.6. Pacer:发送调度器

作用

将 BWE 限制的总带宽均匀分配到时间片,避免突发流量导致拥塞。

工作流程
BWE: 1000kbps
Pacer
每5ms发送 625 字节
网络

源码(Pacer 调度)

cpp 复制代码
// 文件: modules/pacing/paced_sender.cc
void PacedSender::Process() {
  int target_bps = congestion_controller_->GetTargetBitrate();
  // 5ms 时间片应发送的字节数
  int bytes_to_send = (target_bps * 5) / (1000 * 8);

  int sent = 0;
  while (sent < bytes_to_send && !packet_queue_.empty()) {
    RtpPacketToSend pkt = packet_queue_.Pop();
    network_->SendPacket(pkt);
    sent += pkt.size();
  }

  // 5ms 后再次调度
  task_queue_.PostDelayedTask([this]() { Process(); }, 5);
}

4、全链路工作流程示例(网络恶化场景)

场景:用户从 WiFi 切换到 4G,丢包率从 2% 升至 15%,RTT 从 50ms 升至 250ms。
JitterBuffer BWE Receiver Network Pacer SVC控制器 编码器 应用层 JitterBuffer BWE Receiver Network Pacer SVC控制器 编码器 应用层 用户切换到4G,网络恶化 输入 1080p 视频帧 编码为 BL+EL1+EL2 三层 RTP 包(1000kbps) 均匀发送 正常接收(丢包率2%) 丢包率升至15%,RTT=250ms 检测丢包 → 生成 NACK Transport Feedback(延迟增大) 带宽估计降至 800kbps 建议降低码率 新目标速率 800kbps 关闭 EL2,保留 BL+EL1 降速至 800kbps 发送 NACK 请求 重传丢失包 发送 FEC 冗余包 恢复完整帧 组装帧 → 解码播放


5、不同场景关键参数调整

参数 默认值 可调整的值(仅举例) 适用场景 说明
kNackWindowMs 2000ms 1500ms 移动网络 减少内存占用,避免重传过期包
kBandwidthEstimationWindowMs 1000ms 500ms 高抖动网络 提升 BWE 响应速度
FEC 冗余度 动态 15%(固定) 4G/弱网 稳定抗丢包
SVC 层权重 BL:0.3, EL1:0.4, EL2:0.3 BL:0.4, EL1:0.4, EL2:0.2 会议场景 优先保证基础画质清晰
相关推荐
她说彩礼65万9 分钟前
C语言 const的使用
java·服务器·c语言
redfred12 分钟前
HyperFrames 部署教程:5.6 万 Star 开源「写 HTML 出视频」框架,Windows 从零上手(CLI 三条命令 + 7 个避坑)
ffmpeg·开源·自动化·html·音视频
JEECG低代码平台17 分钟前
不买服务器也能异地组网:开源 P2WLAN 把两地电脑“插“进同一台路由器
运维·服务器·开源·开源软件
martindelophy33 分钟前
用自然语言剪视频:我们在 Timeline Studio 中实现了 ChatCut 对话剪辑
android·音视频
cehuishi952733 分钟前
测绘外业调查如何正确的开展
运维·服务器·网络
特视界说40 分钟前
没有完整 3D 模型,能做产品三维动画吗?
3d·微信·音视频·新浪微博
lisanmengmeng41 分钟前
NRPE 添加命令(三)
java·linux·服务器
人工智能培训1 小时前
数字孪生驱动大模型工业知识库:为具身机器人植入领域专业经验
大数据·linux·服务器·前端·人工智能
特视界说1 小时前
产品宣传动画和企业宣传片有什么区别,应该先做哪个?
微信·音视频·新浪微博·新人首发
bluebonnet271 小时前
【docker】容器内映射外部音频驱动
docker·容器·音视频