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 实现 :源码默认使用该模式,通过 MaskRandom 或 MaskBursty 算法选择列分组方式,例如:
kMaskBursty12_4:对应的掩码值包括 0x8a80、0xc540、0x6220、0x3910 等kMaskRandom12_4:对应的掩码值包括 0x8b20、0x14b0、0x22d0、0x4550 等
系统根据初始配置选择MaskRandom或MaskBursty冗余模式,当包组报文个数 > 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 绑定(源码实现)
- 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;
}
- 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 头(用于带宽估计)后发送 |
编码核心流程
-
初始化 FEC 编码句柄
通过
FlexfecSender构造函数初始化 FEC 编码参数,如冗余度、掩码类型等,依赖UlpfecGenerator和ForwardErrorCorrection模块实现编码逻辑。 -
媒体包入队与 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;
}
- FEC 包生成关键步骤
- 确定冗余包数量 :通过
ForwardErrorCorrection::NumFecPackets函数,根据冗余度(fec_rate)和媒体包数量计算需生成的 FEC 包数量。 - 生成掩码数据 :调用
internal::GeneratePacketMasks函数,根据掩码类型(如kMaskBursty、kMaskRandom)生成掩码,确定 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 | 将恢复的媒体包交给上层模块(如解码器)处理 |
解码核心流程(源码解析)
- 包类型区分与存储
FlexfecReceiver::AddReceivedPacket函数根据 SSRC 判断包类型:
- 媒体包 :完整保存 RTP 头和载荷,调用
ForwardErrorCorrection::InsertMediaPacket加入媒体包队列,用于后续恢复验证。 - FEC 包 :去除 RTP 头,仅保存 FEC 载荷和 FEC 头,调用
ForwardErrorCorrection::InsertFecPacket解析掩码和保护列表,加入 FEC 包队列。
- 丢包恢复核心逻辑
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;
}