webrtc代码走读(八)-QOS-FEC-flexfec rfc8627

WebRTC 实现媒体传输冗余的方式主要有三种,分别针对不同场景设计:

  • UlpFEC(RFC5109):早期视频冗余方案,仅支持部分视频编码格式
  • FlexFEC(RFC8627):当前主流冗余方案,兼容性更强、效率更高
  • InbandFEC:专为 Opus 音频设计的内置冗余机制

1 FEC 核心工作原理

UlpFEC 与 FlexFEC 的底层逻辑一致:将 M 个媒体报文 通过异或运算生成 N 个 FEC 冗余报文(N 即冗余度),形成一个保护包组。在网络传输中,即使该包组丢失不超过 N 个报文,接收端也能通过"剩余媒体包 + FEC 冗余包"反向计算,恢复出完整的媒体数据。

发送端打包示意图(冗余度=2)

以 4 个媒体包(D1-D4)为例,生成 2 个冗余包(R1-R2),形成"4+2"的包组结构。示意图中按行列排布媒体包与冗余包,明确冗余包对媒体包的保护范围。

好比发送 4 份重要文件时,额外生成 2 份"混合备份文件"(非简单复制,而是通过异或整合原文件信息),确保文件传输的安全性,冗余包与媒体包的对应关系在图中清晰可见。

网络丢包示意图(×表示丢包)

包组传输过程中,D2、D3 两个媒体包位置标记"×"表示丢失,仅 D1、D4 媒体包与 R1、R2 冗余包保留,直观展示丢包后的包组状态。

原本 6 个包(4 原 + 2 备)中,2 份原文件在网络中丢失,仅剩 2 份原文件和 2 份备份文件,图中丢失包的位置一目了然,便于理解后续恢复逻辑。

报文恢复示意图(⊕=异或运算)

标注"D1⊕x⊕y⊕""D1⊕x⊕D4"等异或运算过程,展示接收端通过剩余媒体包(D1、D4)与冗余包(R1、R2)反向推导,逐步恢复丢失的 D2、D3 的过程。

利用备份文件的"混合信息",结合剩余原文件反向推导,图中清晰标注运算步骤,最终找回所有丢失的原文件,确保接收内容完整。

2、FlexFEC VS UlpFEC:核心差异对比

对比维度 UlpFEC FlexFEC 关键影响
媒体包打包格式 RFC2198 + RFC5109 RFC8627 FlexFEC 是专为 WebRTC 优化的新协议,兼容性更强
FEC SSRC 与媒体报文共享 SSRC 独立 SSRC UlpFEC 无法区分媒体包与 FEC 包;FlexFEC 可快速识别,避免冗余处理
FEC Sequence 与媒体报文共享 Sequence 独立 Sequence UlpFEC 易导致序号混淆;FlexFEC 序号独立,逻辑更清晰
适用 Codec 仅支持 VP8/VP9(需 PictureId) 无 Codec 限制 UlpFEC 无法用于 H264 等无帧边界标识的编码;FlexFEC 通用性更强
NACK 重传问题 FEC 包会触发 NACK 重传 FEC 包不触发 NACK UlpFEC 导致带宽浪费;FlexFEC 减少无效重传,节省带宽
单帧媒体包限制 最大 48 个(超过不参与保护) 理论无限制(源码未完全适配) UlpFEC 对大帧场景保护不足;FlexFEC 适配性更好
2.1 UlpFEC 的 Codec 支持

WebRTC 仅允许 VP8/VP9 等带帧边界信息的 Codec 使用 UlpFEC,核心原因是 H264 仅靠 Sequence 序号判断帧完整性,若 FEC 包插入帧中间会导致判断逻辑失效。相关逻辑通过 MaybeCreateFecGenerator->ShouldDisableRedAndUlpfec->PayloadTypeSupportsSkippingFecPackets 调用链实现,默认只有 VPX 系列编码支持 UlpFEC 冗余编码,具体源码如下:

cpp 复制代码
/**
 * @brief 判断当前 Payload 类型是否支持跳过 FEC 包(仅用于 UlpFEC 场景)
 * @param payload_name:Payload 名称(如 "VP8"、"H264")
 * @param trials:WebRTC 实验配置(用于开启特定功能)
 * @return true:支持;false:不支持
 * @说明:UlpFEC 依赖 Codec 的帧边界标识(如 VP8/VP9 的 PictureId),H264 无此信息故不支持
 */
bool PayloadTypeSupportsSkippingFecPackets(const std::string& payload_name, 
                                           const WebRtcKeyValueConfig& trials) {  
  // 将 Payload 名称转为对应的 Codec 类型
  const VideoCodecType codecType = PayloadStringToCodecType(payload_name);  

  // 1. 直接支持 VP8/VP9(自带 PictureId,可准确区分帧边界)
  if (codecType == kVideoCodecVP8 || codecType == kVideoCodecVP9) {    
    return true;  
  }  

  // 2. 通用 Codec(如 H264)需开启 "WebRTC-GenericPictureId" 实验配置才支持
  if (codecType == kVideoCodecGeneric &&      
      absl::StartsWith(trials.Lookup("WebRTC-GenericPictureId"), "Enabled")) {    
    return true;  
  }  

  // 3. 其他 Codec(如默认 H264)不支持 UlpFEC
  return false;  
}
2.2 UlpFEC 的带宽浪费问题

由于 UlpFEC 的 SSRC 与媒体报文共享,且 Sequence 与媒体报文共用,接收端无法区分媒体包与 FEC 包。在开启 NACK(负确认)机制时,FEC 冗余包会被误判为媒体包,触发 NACK 重传,导致不必要的带宽消耗。而 FlexFEC 凭借独立 SSRC 和独立 Sequence,可精准区分包类型,避免该问题。因此,目前 WebRTC 场景中更优选 FlexFEC 冗余编码,UlpFEC 应用逐渐减少。

3 、FlexFEC 原理深度解析

3.1 冗余模式(RFC8627 定义)

FlexFEC 支持三种异或编码模式,可根据网络丢包类型(随机丢包/突发丢包)选择适配模式,但 WebRTC 源码仅实现了部分模式。

1D 行异或模式

模式逻辑

示意图以表格形式展示媒体包分组,如第一行包含 S₁、S₂、S₃、S_L,第二行包含 S_L+1、S_L+2、S_L+3,以此类推,每行媒体包通过异或运算生成 1 个行冗余包(R)。
通俗解释 :将 15 个媒体包分成 3 行(每行 5 个),每行生成 1 个"行备份包"。若某行丢失 1 个媒体包,可通过该行剩余包 + 行备份包恢复。
适用场景:随机丢包(单一行内少量丢包易恢复)。

1D 列异或模式

模式逻辑

示意图中媒体包按列排布,如第一列包含 S₁、S_L+1、S_(D-1)×L+1,每列媒体包下方标注"XOR"运算符号,最终生成列冗余包(C₁、C₂、C₃)。
通俗解释 :将 15 个媒体包分成 3 列(每列 5 个),每列生成 1 个"列备份包"。若某列丢失 1 个媒体包,可通过该列剩余包 + 列备份包恢复。
适用场景 :突发丢包(同一列内连续丢包易恢复)。
WebRTC 实现 :源码默认使用该模式,通过 MaskRandomMaskBursty 算法选择列分组方式,例如:

  • kMaskBursty12_4:对应的掩码值包括 0x8a80、0xc540、0x6220、0x3910 等
  • kMaskRandom12_4:对应的掩码值包括 0x8b20、0x14b0、0x22d0、0x4550 等
    系统根据初始配置选择 MaskRandomMaskBursty 冗余模式,当包组报文个数 > 12 时,自动启用 1D 列异或冗余模式,具体逻辑可参考 PacketMaskTable::LookUp 函数实现。

2D 数组异或模式

模式逻辑

示意图展示媒体包以二维数组形式排列,同时对"行"和"列"执行异或运算,生成"行冗余包(R)"和"列冗余包(C)",形成二维保护网。例如数组中包含 xR、R2、xX12R3 等标识,标注行与列的运算关系。
通俗解释 :15 个媒体包既按行生成备份,又按列生成备份。即使某区域同时丢失多个包(如 1 行 + 1 列),仍可通过双重冗余恢复。
WebRTC 现状:源码未实现该模式,仅支持 1D 模式,且标注"2-D Parity FEC Protection Fails Error Recovery"场景,说明该模式在特定丢包情况下也存在恢复失败的可能。

补充说明

Webrtc源码仅实现了MaskRandom、MaskBursty或1D列异或冗余模式。

3.2 RTP 包协议格式

完整 RTP 报文结构

FlexFEC 的 RTP 报文需在标准 RTP 结构基础上插入 FEC 头,完整结构如下,示意图中以分层形式展示各字段的包含关系:

复制代码
IP Header(IP 层头)
├─ Transport Header(传输层头,如 UDP 头)
└─ RTP Header(标准 RTP 头,12 字节)
   ├─ FEC Header(FlexFEC 自定义头,最小 20 字节)
   └─ Repair Payload(FEC 冗余数据载荷)

FEC Header 定义(WebRTC 自定义实现)

WebRTC 未完全遵循 RFC8627 的 FEC 头格式,而是在 flexfec_header_reader_writer.h 头文件中定义了自定义结构,示意图以二进制位和字节偏移标注字段位置,核心字段及偏移如下:

偏移(字节) 字段名称 长度(字节) 说明
0-1 标志位(R/F/P/X) 2 保留位(R)、FEC 类型位(F)、保护位(P)、扩展位(X)
2-3 PT Recovery 2 对应的媒体包 PT 类型,用于关联 FEC 包与媒体包
4-7 Length Recovery 4 保护的媒体包总长度,用于恢复媒体包数据长度
8-11 TS Recovery 4 保护的媒体包时间戳,确保媒体包时序正确
12-13 SSRC Count 2 保护的媒体流 SSRC 数量(通常为 1),支持多流保护场景
14-17 SSRC_i 4 对应的媒体流 SSRC,用于绑定 FEC 包与特定媒体流
18-19 SN Base_i 2 保护的媒体包起始序列号,作为计算保护包序列号的基准
20-21 k + Mask [0-14] 2 保护的媒体包数量(k) + 掩码(前 15 位),掩码 bit=1 表示对应包被保护
22-27 Mask [15-45] 6(可选) 掩码(中间 31 位),k>16 时启用,扩展保护包数量范围
28-41 Mask [46-108] 14(可选) 掩码(后 63 位),k>48 时启用,进一步扩展保护范围

掩码(Mask)核心作用与计算

掩码用于标记"当前 FEC 包保护哪些媒体包",1 个 bit 对应 1 个媒体包(bit=1 表示该媒体包被保护)。WebRTC 定义了掩码长度表,根据保护的媒体包数量(k)自动选择,源码中通过常量数组定义:

cpp 复制代码
/**
 * @brief FlexFEC 掩码的字节长度表(根据保护的媒体包数量 k 确定)
 * @说明:1 个 bit 对应 1 个媒体包,故长度随 k 增加而扩展
 * - k ≤ 16:掩码占 2 字节(16 bit)
 * - 17 ≤ k ≤ 48:掩码占 6 字节(48 bit)
 * - 49 ≤ k ≤ 112:掩码占 14 字节(112 bit)
 * WebRTC 实际限制 k ≤ 48(即单 FEC 包最多保护 48 个媒体包)
 */
constexpr size_t kFlexfecPacketMaskSizes[] = {2, 6, 14};

保护的媒体包序列号计算逻辑

通过"起始序列号(SN Base_i) + 掩码 bit 置 1 的位置"确定被保护媒体包的序列号,核心源码如下(参考 ForwardErrorCorrection::InsertFecPacket 函数):

cpp 复制代码
/**
 * @brief 解析 FEC 包的掩码,生成该 FEC 包保护的媒体包序列号列表
 * @param fec_packet:当前 FEC 包(含掩码信息)
 * @param protected_media_ssrc:对应的媒体流 SSRC(绑定 FEC 与媒体包)
 * @param fec_packet->protected_packets:输出参数,存储保护的媒体包信息
 */
for (uint16_t byte_idx = 0; byte_idx < fec_packet->packet_mask_size; ++byte_idx) {  
  // 1. 逐字节读取掩码(1 字节 = 8 bit,对应 8 个媒体包)
  uint8_t packet_mask = fec_packet->pkt->data[fec_packet->packet_mask_offset + byte_idx];  

  // 2. 逐 bit 解析掩码(从高位到低位,bit=1 表示该媒体包被保护)
  for (uint16_t bit_idx = 0; bit_idx < 8; ++bit_idx) {  
    if (packet_mask & (1 << (7 - bit_idx))) {  
      // 3. 创建保护的媒体包对象,绑定 SSRC
      std::unique_ptr<ProtectedPacket> protected_packet(new ProtectedPacket());  
      protected_packet->ssrc = protected_media_ssrc_;  

      // 4. 计算被保护媒体包的序列号:起始序列号 + 偏移(byte_idx*8 + bit_idx)
      // 示例:SN Base_i=100,byte_idx=0,bit_idx=2 → 序列号=100+0*8+2=102
      protected_packet->seq_num = static_cast<uint16_t>(  
          fec_packet->seq_num_base + (byte_idx << 3) + bit_idx);  

      // 5. 存储到保护列表(后续用于丢包恢复)
      protected_packet->pkt = nullptr;  // 暂不存储包数据,仅记录序列号
      fec_packet->protected_packets.push_back(std::move(protected_packet));  
    }  
  }  
}

3.3 SDP 协商(FlexFEC 启用流程)

FlexFEC 需通过 SDP 协商确定媒体流与 FEC 流的绑定关系,核心参数包括 SSRC 分组、PT 类型、恢复窗口等。以下是典型的 SDP 示例,展示单媒体流(SSRC:1234)与单 FEC 流(SSRC:2345)的协商配置:

sdp 复制代码
v=0
o=ali 1122334455 1122334466 IN IP4 fec.example.com
s=2-D Parity FEC with no in band signaling Example
t=0 0
m=video 30000 RTP/AVP 100 110  # 媒体流 PT=100,FlexFEC 流 PT=110
c=IN IP4 192.0.2.0/24
a=rtpmap:100 MP2T/90000        # 媒体流编码格式:MP2T,时钟频率 90000Hz
a=rtpmap:110 flexfec/90000     # FlexFEC 编码格式,时钟频率与媒体流一致(90000Hz)
a=fmtp:110; repair-window:200000  # FlexFEC 恢复窗口:200ms(单位:微秒)
a=ssrc:1234                     # 媒体流 SSRC 标识:1234
a=ssrc:2345                     # FlexFEC 流 SSRC 标识:2345
a=ssrc-group:FEC-FR 1234 2345   # 绑定媒体流与 FEC 流,FEC-FR 表示前向错误恢复

PT 类型与 SSRC 绑定(源码实现)

  1. PT 类型确定
    WebRTC 通过 GetPayloadTypesAndDefaultCodecs 函数分配 FlexFEC 的 PT 类型,优先使用 [35,63] 低区间(适配新 Codec),若低区间用尽则使用 [96,127] 高区间。恢复窗口默认设为 10 秒(10000000 微秒),该参数必须在 SDP 中声明,源码逻辑如下:
cpp 复制代码
// 定义 FlexFEC 相关常量
static const int kFirstDynamicPayloadTypeLowerRange = 35;  // 低区间 PT 起始值
static const int kLastDynamicPayloadTypeLowerRange = 63;   // 低区间 PT 结束值
static const int kFirstDynamicPayloadTypeUpperRange = 96;  // 高区间 PT 起始值
static const int kLastDynamicPayloadTypeUpperRange = 127;  // 高区间 PT 结束值

bool GetPayloadTypesAndDefaultCodecs(const std::vector<webrtc::SdpVideoFormat>& supported_formats,
                                     const WebRtcKeyValueConfig& trials,
                                     std::vector<VideoCodec>* output_codecs,
                                     bool is_decoder_factory) {
  std::vector<webrtc::SdpVideoFormat> formats = supported_formats;
  int payload_type_lower = kFirstDynamicPayloadTypeLowerRange;
  int payload_type_upper = kFirstDynamicPayloadTypeUpperRange;

  // 检查是否启用 FlexFEC,若启用则添加 FlexFEC 格式到支持列表
  if ((!is_decoder_factory && IsEnabled(trials, "WebRTC-FlexFEC-03-Advertised")) ||
      (!IsDisabled(trials, "WebRTC-FlexFEC-03-Advertised"))) {
    webrtc::SdpVideoFormat flexfec_format(kFlexfecCodecName);
    // 设置恢复窗口为 10 秒(单位:微秒),该参数必须在 SDP 中存在
    flexfec_format.parameters.insert({kFlexfecFmtpRepairWindow, "10000000"});
    formats.push_back(flexfec_format);
  }

  // 为每个支持的格式分配 PT 类型
  for (const webrtc::SdpVideoFormat& format : formats) {
    VideoCodec codec(format);
    bool isFecCodec = absl::EqualsIgnoreCase(codec.name, kULpfecCodecName) ||
                      absl::EqualsIgnoreCase(codec.name, kFlexfecCodecName);
    bool isAv1Codec = absl::EqualsIgnoreCase(codec.name, kAv1CodecName);
    bool isCodecValidForLowerRange = isFecCodec || isAv1Codec;

    // 检查 PT 类型是否用尽
    if (payload_type_lower > kLastDynamicPayloadTypeLowerRange) {
      RTC_LOG(LS_ERROR) << "Out of dynamic payload types [35, 63] after fallback from [96, 127], skipping the rest.";
      break;
    }

    // 低区间用于新 Codec(如 FlexFEC、AV1)或高区间用尽时
    if (isCodecValidForLowerRange || payload_type_upper >= kLastDynamicPayloadTypeUpperRange) {
      codec.id = payload_type_lower++;
    } else {
      codec.id = payload_type_upper++;
    }

    // 添加默认反馈参数(如 NACK、TWCC 等)
    AddDefaultFeedbackParams(&codec, trials);
    output_codecs->push_back(codec);
  }

  return true;
}
  1. SSRC 绑定与查询
    WebRTC 通过 AddFecFrSsrc 函数将 FEC 流 SSRC 与媒体流 SSRC 绑定为"FEC-FR"组,通过 GetFecFrSsrc 函数查询媒体流对应的 FEC 流 SSRC,源码如下:
cpp 复制代码
/**
 * @brief 为已添加的主 SSRC(媒体流)添加 FEC-FR 类型的从 SSRC(FEC 流)
 * @param primary_ssrc:主 SSRC(媒体流 SSRC)
 * @param fecfr_ssrc:从 SSRC(FEC 流 SSRC)
 * @return true:添加成功;false:添加失败
 */
bool RtpParameters::AddFecFrSsrc(uint32_t primary_ssrc, uint32_t fecfr_ssrc) {
  return AddSecondarySsrc(kFecFrSsrcGroupSemantics, primary_ssrc, fecfr_ssrc);
}

/**
 * @brief 根据主 SSRC(媒体流)查询对应的 FEC-FR 从 SSRC(FEC 流)
 * @param primary_ssrc:主 SSRC(媒体流 SSRC)
 * @param fecfr_ssrc:输出参数,存储查询到的 FEC 流 SSRC
 * @return true:查询成功;false:查询失败(主 SSRC 不存在或无对应 FEC 流)
 */
bool RtpParameters::GetFecFrSsrc(uint32_t primary_ssrc, uint32_t* fecfr_ssrc) const {
  return GetSecondarySsrc(kFecFrSsrcGroupSemantics, primary_ssrc, fecfr_ssrc);
}

4、FlexFEC 编码与解码

4.1 FlexFEC 编码实现

编码调用栈

FlexFEC 编码的核心流程是"媒体包入队 → 生成 FEC 包 → 加入 pacing 队列 → 调度发送",调用栈如下表所示,清晰展示各模块的调用关系:

流程阶段 涉及函数/模块 功能说明
媒体包入队 FlexfecSender::AddPacketAndGenerateFec、UlpfecGenerator::AddPacketAndGenerateFec 接收媒体包,满足条件(如包数达标、帧结束)时触发 FEC 生成
FEC 包生成 ForwardErrorCorrection::EncodeFec、ForwardErrorCorrection::GenerateFecPayloads 对媒体包进行异或运算,生成 FEC 冗余数据,封装 FEC 头
FEC 包入队 FlexfecSender::GetFecPackets、PacingController::EnqueuePacket 获取生成的 FEC 包,加入 pacing 队列(控制发送速率,避免网络拥塞)
FEC 包发送 PacedSender::Process、PacketRouter::SendPacket 调度 pacing 队列中的 FEC 包,封装 TWCC 头(用于带宽估计)后发送

编码核心流程

  1. 初始化 FEC 编码句柄

    通过 FlexfecSender 构造函数初始化 FEC 编码参数,如冗余度、掩码类型等,依赖 UlpfecGeneratorForwardErrorCorrection 模块实现编码逻辑。

  2. 媒体包入队与 FEC 触发
    UlpfecGenerator::AddPacketAndGenerateFec 函数负责接收媒体包,当媒体包数量达到 kUlpfecMaxMediaPackets(48 个)或当前帧结束时,触发 FEC 生成。需注意,WebRTC 限制单帧媒体包数不超过 48,超过部分不加入 media_packets_ 队列,不参与 FEC 保护,源码如下:

cpp 复制代码
/**
 * @brief 向 UlpFEC 生成器添加媒体包,满足条件时生成 FEC 包
 * @param packet:待添加的媒体包(RTP 包)
 * @param complete_frame:是否为当前帧的最后一个媒体包
 * @说明:单帧媒体包数超过 48 时,后续包不参与 FEC 保护;不支持跨帧冗余
 */
bool UlpfecGenerator::AddPacketAndGenerateFec(const RtpPacket& packet, bool complete_frame) {
  // 限制:单帧媒体包数不超过 48,超过部分不加入队列
  if (media_packets_.size() < kUlpfecMaxMediaPackets) {
    // 创建 FEC 包对象,复制媒体包数据
    auto fec_packet = std::make_unique<ForwardErrorCorrection::Packet>();
    fec_packet->data = packet.Buffer();
    media_packets_.push_back(std::move(fec_packet));

    // 保存最后一个媒体包的 RTP 头,用于生成 FEC 包时复用头信息
    RTC_DCHECK_GE(packet.headers_size(), kRtpHeaderSize);
    last_media_packet_ = packet;
  }

  // 标记当前帧是否结束,统计已保护的帧数
  if (complete_frame) {
    ++num_protected_frames_;
  }

  // 满足以下条件时生成 FEC 包:
  // 1. 当前帧结束;
  // 2. 已保护帧数达到最大限制(params.max_fec_frames);
  // 3. 媒体包数达到最小限制,且带宽开销低于阈值
  auto params = CurrentParams();
  if (complete_frame &&
      (num_protected_frames_ >= params.max_fec_frames ||
       (media_packets_.size() >= params.min_media_packets &&
        OverheadBelowMax(params.fec_rate)))) {
    // 不使用不等保护机制,无重要包优先级区分
    constexpr int kNumImportantPackets = 0;
    constexpr bool kUseUnequalProtection = false;

    // 调用 FEC 编码接口生成 FEC 包
    fec_->EncodeFec(media_packets_, params.fec_rate, kNumImportantPackets,
                    kUseUnequalProtection, params.fec_mask_type, &generated_fec_packets_);

    // 若生成的 FEC 包为空,重置状态(准备下一组编码)
    if (generated_fec_packets_.empty()) {
      ResetState();
    }
  }

  return true;
}
  1. FEC 包生成关键步骤
  • 确定冗余包数量 :通过 ForwardErrorCorrection::NumFecPackets 函数,根据冗余度(fec_rate)和媒体包数量计算需生成的 FEC 包数量。
  • 生成掩码数据 :调用 internal::GeneratePacketMasks 函数,根据掩码类型(如 kMaskBurstykMaskRandom)生成掩码,确定 FEC 包保护的媒体包列表。
  • 异或运算生成载荷ForwardErrorCorrection::GenerateFecPayloads 函数对媒体包的 RTP 头(仅前 12 字节,不含扩展头)和载荷进行异或运算,生成 FEC 载荷。
  • 封装 FEC 头ForwardErrorCorrection::FinalizeFecHeaders 函数根据自定义格式封装 FEC 头,填充 SSRC、SN Base、掩码等信息。
4.2 FlexFEC 解码实现

解码调用栈

FlexFEC 解码的核心流程是"区分包类型 → 存储包数据 → 恢复丢失包 → 回调上层",调用栈如下表所示:

流程阶段 涉及函数/模块 功能说明
接收包入口 FlexfecReceiver::OnRtpPacket、FlexfecReceiver::ProcessReceivedPacket 接收 RTP 包,触发包处理逻辑
包类型区分 FlexfecReceiver::AddReceivedPacket 根据 SSRC 区分媒体包与 FEC 包:媒体包保存完整 RTP 数据,FEC 包仅保存载荷
FEC 包解析 ForwardErrorCorrection::InsertFecPacket 解析 FEC 包的掩码和保护列表,加入 received_fec_packets 队列
媒体包存储 ForwardErrorCorrection::InsertMediaPacket 解析媒体包序列号,加入 recovered_packets 队列
丢包恢复 ForwardErrorCorrection::DecodeFec、ForwardErrorCorrection::AttemptRecovery 检查丢失包,用 FEC 包恢复,仅支持"N+1"模式(1 个 FEC 包恢复 1 个丢失包)
恢复包回调 RtpVideoStreamReceiver::OnRecoveredPacket 将恢复的媒体包交给上层模块(如解码器)处理

解码核心流程(源码解析)

  1. 包类型区分与存储
    FlexfecReceiver::AddReceivedPacket 函数根据 SSRC 判断包类型:
  • 媒体包 :完整保存 RTP 头和载荷,调用 ForwardErrorCorrection::InsertMediaPacket 加入媒体包队列,用于后续恢复验证。
  • FEC 包 :去除 RTP 头,仅保存 FEC 载荷和 FEC 头,调用 ForwardErrorCorrection::InsertFecPacket 解析掩码和保护列表,加入 FEC 包队列。
  1. 丢包恢复核心逻辑
    WebRTC 仅支持"N+1"恢复模式(1 个 FEC 包最多恢复 1 个丢失的媒体包),核心通过 ForwardErrorCorrection::AttemptRecovery 函数实现,步骤如下:
    • 遍历 FEC 包队列,获取每个 FEC 包的保护列表;
    • 统计保护列表中"已接收媒体包数"和"丢失媒体包数";
    • 若仅丢失 1 个媒体包,且已接收包数 = 保护总数 - 1,调用 ForwardErrorCorrection::RecoverPacket 函数通过异或运算恢复丢失包;
    • 将恢复的媒体包加入 recovered_packets 队列,通过 RtpVideoStreamReceiver::OnRecoveredPacket 回调给上层。
cpp 复制代码
/**
 * @brief 尝试用 FEC 包恢复丢失的媒体包(核心解码函数)
 * @param media_packets:已接收的媒体包队列
 * @param fec_packets:已接收的 FEC 包队列
 * @param recovered_packets:输出参数,存储恢复成功的媒体包
 * @return true:至少恢复 1 个包;false:未恢复任何包
 * @说明:仅支持"N+1"模式,即 1 个 FEC 包最多恢复 1 个丢失的媒体包
 */
bool ForwardErrorCorrection::AttemptRecovery(
    const std::vector<Packet*>& media_packets,
    const std::vector<Packet*>& fec_packets,
    std::vector<std::unique_ptr<Packet>>* recovered_packets) {
  RTC_DCHECK(recovered_packets);
  bool recovery_succeeded = false;

  // 遍历所有 FEC 包,检查是否可用于恢复
  for (const auto* fec_pkt : fec_packets) {
    const auto& protected_list = fec_pkt->protected_packets;
    if (protected_list.empty()) {
      continue;  // 无保护的媒体包,跳过该 FEC 包
    }

    // 统计已接收媒体包数和丢失媒体包信息
    size_t received_count = 0;
    Packet* lost_pkt = nullptr;
    uint16_t lost_seq = 0;

    for (const auto& protected_pkt : protected_list) {
      // 查找该保护包是否已在媒体包队列中(通过序列号匹配)
      auto it = std::find_if(media_packets.begin(), media_packets.end(),
                             [&](const Packet* p) {
                               return p->seq_num == protected_pkt->seq_num &&
                                      p->ssrc == protected_pkt->ssrc;
                             });

      if (it != media_packets.end()) {
        received_count++;  // 已接收,计数加 1
      } else {
        lost_pkt = const_cast<Packet*>(protected_pkt.get());  // 标记丢失的包
        lost_seq = protected_pkt->seq_num;
      }
    }

    // 仅当"丢失 1 个包"且"已接收包数 = 保护总数 - 1"时,尝试恢复
    if (lost_pkt != nullptr && received_count == protected_list.size() - 1) {
      // 调用 RecoverPacket 函数恢复丢失的媒体包
      auto recovered_pkt = RecoverPacket(media_packets, fec_pkt, lost_seq);
      if (recovered_pkt != nullptr) {
        recovered_packets->push_back(std::move(recovered_pkt));
        recovery_succeeded = true;
        // 一次仅恢复 1 个包,跳出循环(可优化为批量恢复)
        break;
      }
    }
  }

  return recovery_succeeded;
}
相关推荐
惊讶的猫4 小时前
c++基础
开发语言·c++
mohesashou4 小时前
HCIP第二次作业(VRRP/STP/VLAN/Eth-trunk/NAT)
网络
Code_Shark8 小时前
AtCoder Beginner Contest 426 题解
数据结构·c++·算法·数学建模·青少年编程
仰泳的熊猫9 小时前
LeetCode:698. 划分为k个相等的子集
数据结构·c++·算法·leetcode
xlq223229 小时前
7(内存管理)(上)(了解)
c++
h7997109 小时前
wsl使用代理网络
网络
WBluuue10 小时前
数据结构与算法:摩尔投票算法
c++·算法·leetcode
ghie909010 小时前
Reactor 模式结合 epoll
网络
柯一梦10 小时前
深入解析C++ String类的实现奥秘
c++