webrtc代码走读(五)-QOS-FEC原理
在WebRTC音视频传输中,前向纠错(FEC)作为关键的抗丢包技术,通过主动生成冗余数据,让接收端在部分数据包丢失时仍能恢复完整内容。
1、WebRTC FEC概述
WebRTC的FEC机制核心目标是通过冗余数据对抗网络丢包,其本质是“用带宽换可靠性”。目前WebRTC支持三种冗余打包协议,各有适配场景,其中Red与Ulpfec需成对使用,Flexfec为实验性协议(需手动开启)。
| 协议名称 | 核心标准 | 适用场景 | 核心特点 |
|---|---|---|---|
| Red | RFC 2198 | 小数据量传输(传真、RFC2833收号) | 简单拼接老报文,冗余度固定,恢复能力弱 |
| Ulpfec | RFC 5109 | 音视频主流场景(VP8/VP9) | 异或生成冗余包,保护范围广,支持动态冗余度 |
| Flexfec | 草案阶段 | 复杂丢包场景(需实验性开启) | 引入交织算法,支持1D行/列、2D行列异或,灵活性最强 |
2、三种FEC冗余打包方式原理详解
2.1 Red(RFC 2198):简单冗余拼接
Red是WebRTC中最基础的FEC方式,核心逻辑是将旧媒体包直接打包到新包中,形成“新包+旧包”的冗余结构。例如冗余度为1时,打包序列如下:
- D1(仅原始媒体包)
- D2+D1(新包D2携带旧包D1)
- D3+D2(新包D3携带旧包D2)
- …
- Dn+Dn-1(新包Dn携带旧包Dn-1)
优缺点与应用场景
- 优点:实现简单,无需复杂运算;
- 缺点:冗余包仅能保护1个旧包,恢复能力有限;且冗余度固定(如1:1),带宽占用高,性价比极低;
- 应用场景:仅用于小数据量传输(如T38传真、RFC2833电话按键信号),音视频传输中几乎不使用;
- WebRTC适配:WebRTC仅借用RFC2198的封装格式,实际未使用其原始冗余逻辑,而是用于封装其他FEC冗余包。
2.2 Ulpfec(RFC 5109):异或冗余生成
Ulpfec(Uneven Level Protection FEC)是WebRTC音视频传输的主流FEC方案,核心通过异或(XOR)运算生成冗余包,大幅扩大保护范围。其核心逻辑是:将M个媒体包作为一组,生成N个冗余包(N为冗余度),该组中任意丢失N个包,均可通过剩余(M-N)个媒体包+冗余包恢复。
1. 核心工作流程
以“4个媒体包(D1-D4)+2个冗余包(R1-R2)”为例(冗余度2):
- 发送端打包:对D1-D4执行异或运算,生成R1和R2(具体异或规则由掩码表定义);
- 网络丢包:假设传输中D2、D3丢失,仅D1、D4、R1、R2到达接收端;
- 丢包恢复:接收端通过异或反向计算:
D2 = D1 ⊕ D4 ⊕ R1 ⊕ R2
D3 = D1 ⊕ D2 ⊕ D4 ⊕ R1
2. 关键技术:PacketMaskTable掩码表
Ulpfec通过PacketMaskTable定义“哪些媒体包参与异或生成冗余包”,支持两种丢包模型:
- kFecMaskBursty(突发丢包掩码):针对连续丢包场景,掩码规则让冗余包覆盖更多连续媒体包,提升突发丢包恢复能力;
- kFecMaskRandom(随机丢包掩码):针对分散丢包场景,掩码规则让冗余包均匀覆盖媒体包,适配随机丢包;
现状:WebRTC理论上可通过网络反馈(丢包类型)自适应选择掩码,但目前功能缺失,默认使用随机丢包模型。
3. 优缺点与应用场景
- 优点:保护范围广(一组包可恢复多个丢失包),冗余度可动态调整,带宽利用率高;
- 缺点:仅支持1D行异或,对复杂丢包(如部分行+部分列丢失)适配不足;
- 应用场景:WebRTC视频传输默认方案(VP8/VP9),适合大多数网络场景(随机丢包、轻度突发丢包)。
2.3 Flexfec:灵活交织异或
Flexfec是WebRTC中实验性FEC方案,核心改进是引入交织算法,突破Ulpfec仅1D行异或的限制,支持1D行、1D列、2D行列三种异或模式,适配更复杂的丢包场景。
1. 三种异或模式解析
-
1D行异或:与Ulpfec逻辑一致,将媒体包按行排列(如S1-S4为一行),对每行执行异或生成冗余包(R1);
[S1, S2, S3, S4] → XOR运算 → R1(行冗余包) -
1D列异或:将媒体包按矩阵排列(如2行4列),对每列执行异或生成冗余包(C1-C4);
[S1, S2, S3, S4] [S5, S6, S7, S8]↓ ↓ ↓ ↓ [C1, C2, C3, C4](列冗余包,每列异或生成) -
2D行列异或:同时对矩阵的“行”和“列”执行异或,生成行冗余包+列冗余包,形成双重保护;
[S1, S2, S3] → R1(行冗余) [S4, S5, S6] → R2(行冗余)↓ ↓ ↓ [C1, C2, C3](列冗余)
2. 关键注意事项与应用场景
- 开启条件:需同时使能两个字段试验参数:
WebRTC-FlexFEC-03/Enabled和WebRTC-FlexFEC-03-Advertised/Enabled,否则可能出现死机异常; - 现状:目前仍处于草案阶段,异或模式选择逻辑尚未完善,未正式大规模应用;
- 应用场景:适用于复杂丢包场景(如无线传输中的混合丢包),未来可能成为WebRTC FEC的主流方案。
3、WebRTC FEC算法与 codec 适配
FEC算法并非单一技术,而是根据传输内容(音频/视频)和 codec 特性选择适配方案。WebRTC中不同 codec 的FEC适配逻辑如下:
| 传输类型 | codec | 所用FEC算法 | 核心逻辑 |
|---|---|---|---|
| 音频 | Opus | InBand FEC + 交织编码 | 1. InBand FEC:将冗余数据嵌入音频帧内,无需额外FEC包; 2. 交织编码:打乱音频帧顺序传输,避免连续丢包导致音质突变; |
| 视频 | VP8/VP9 | Ulpfec(异或XOR) | 默认使用Ulpfec,通过异或生成冗余包,动态调整冗余度; |
| 视频 | H264 | Flexfec(可选) | 默认关闭Ulpfec,需手动开启Flexfec(实验性); |
| 通用 | - | ReedSolomon | 算法复杂,恢复能力强,但计算开销高,WebRTC中未大规模使用; |
4、WebRTC FEC源码核心逻辑解析
基于WebRTC源码,拆解FEC的使能、封装、冗余度动态调整三大核心流程
4.1 FEC使能:注册支持的编码格式
WebRTC通过InternalEncoderFactory构造函数注册FEC相关编码,默认使能Red+Ulpfec,Flexfec需条件开启。
// InternalEncoderFactory:WebRTC内部编码器工厂,负责注册FEC编码
InternalEncoderFactory::InternalEncoderFactory() {// 1. 注册基础视频编码(H264/VP8/VP9)if (webrtc::H264Encoder::IsSupported()) {cricket::VideoCodec codec(kH264CodecName);codec.SetParam(kH264FmtpLevelAsymmetryAllowed, "1"); // 允许非对称级别codec.SetParam(kH264FmtpProfileLevelId, kH264ProfileLevelConstrainedBaseline); // 约束基线规格supported_codecs_.push_back(std::move(codec));}supported_codecs_.push_back(cricket::VideoCodec(kVp8CodecName)); // VP8默认支持if (webrtc::VP9Encoder::IsSupported()) {supported_codecs_.push_back(cricket::VideoCodec(kVp9CodecName)); // VP9按需支持}// 2. 使能Red和Ulpfec(默认成对开启)supported_codecs_.push_back(cricket::VideoCodec(kRedCodecName));supported_codecs_.push_back(cricket::VideoCodec(kUlpfecCodecName));// 3. 条件使能Flexfec(需通过字段试验判断)if (IsFlexfecAdvertisedFieldTrialEnabled()) {cricket::VideoCodec flexfec_codec(kFlexfecCodecName);// 设置修复窗口:10秒(单位:微秒),SDP必须包含该参数flexfec_codec.SetParam(kFlexfecFmtpRepairWindow, "10000000");// 添加RTCP反馈:支持Transport CC(拥塞控制)和REMB(最大比特率估计)flexfec_codec.AddFeedbackParam(FeedbackParam(kRtcpFbParamTransportCc, kParamValueEmpty));flexfec_codec.AddFeedbackParam(FeedbackParam(kRtcpFbParamRemb, kParamValueEmpty));supported_codecs_.push_back(std::move(flexfec_codec));}
}
4.2 FEC封装:视频数据包发送逻辑
RTPSenderVideo::SendVideo是视频FEC封装的核心入口,负责判断是否对数据包进行FEC保护,并分路径调用Flexfec/Red+Ulpfec逻辑。
// RTPSenderVideo::SendVideo:视频数据包发送与FEC封装
// 参数:packet-待发送RTP包;storage-数据包存储(用于重传/FEC恢复);temporal_id-时间分层ID
void RTPSenderVideo::SendVideo(std::unique_ptr<RtpPacketToSend> packet, RtpPacketStorage* storage, int temporal_id) {// 1. 确定是否保护:仅保护时间分层0(基础层)或无分层的包(上层依赖基础层,保护基础层性价比高)bool protect_packet = temporal_id == 0 || temporal_id == kNoTemporalIdx;// 2. 处理时间戳扩展:含扩展的包暂不支持FEC(WebRTC issue #7859)if (packet->HasExtension<VideoTimingExtension>()) {packet->set_packetization_finish_time_ms(clock_->TimeInMilliseconds()); // 设置打包时间protect_packet = false; // 关闭FEC保护}// 3. 分路径发送:Flexfec → Red+Ulpfec → 直接发送if (flexfec_enabled()) {// 路径1:开启Flexfec,调用专用发送逻辑(后续将集成到PacedSender)SendVideoPacketWithFlexfec(std::move(packet), storage, protect_packet);} else if (red_enabled()) {// 路径2:开启Red,调用Red+Ulpfec发送逻辑SendVideoPacketAsRedMaybeWithUlpfec(std::move(packet), storage, protect_packet);} else {// 路径3:无FEC,直接发送SendVideoPacket(std::move(packet), storage);}
}
4.3 冗余度动态调整:基于丢包率的自适应逻辑
WebRTC通过“丢包率反馈→调整保护因子→计算冗余包数量”的流程,动态适配网络状况。核心涉及三个关键函数:
1. 计算最大保护帧数
VCMNackFecMethod::ComputeMaxFramesFec根据时间分层、基础层帧率和RTT,确定FEC可保护的最大帧数,避免过度保护。
// 计算FEC最大保护帧数:确保一个RTT内可恢复完整帧,且不超过上限
int VCMNackFecMethod::ComputeMaxFramesFec(const VCMProtectionParameters* parameters) {// 分层>2时,仅保护基础层,强制最大帧数为1if (parameters->numLayers > 2) {return 1;}// 计算基础层帧率:总帧率 / 2^(分层数-1)(如30fps、2分层→15fps)float base_layer_framerate = parameters->frameRate / static_cast<float>(1 << (parameters->numLayers - 1));// 理论最大帧数:2 * 基础层帧率 * RTT / 1000(确保一个RTT内覆盖2倍帧数)int max_frames_fec = std::max(static_cast<int>(2.0f * base_layer_framerate * parameters->rtt / 1000.0f + 0.5f),1 // 最小保护1帧);// 限制最大帧数(避免带宽浪费)if (max_frames_fec > kUpperLimitFramesFec) {max_frames_fec = kUpperLimitFramesFec;}return max_frames_fec;
}
2. 计算冗余包数量
ForwardErrorCorrection::NumFecPackets根据“媒体包数量”和“保护因子”,计算实际需要生成的冗余包数量。
// 计算FEC冗余包数量:按保护因子比例生成,确保至少1个冗余包
int ForwardErrorCorrection::NumFecPackets(int num_media_packets, int protection_factor) {// 比例计算:(媒体包数 * 保护因子 + 128) >> 8(+128用于四舍五入,>>8等价于/256)int num_fec_packets = (num_media_packets * protection_factor + (1 << 7)) >> 8;// 兜底:保护因子>0时,至少生成1个冗余包if (protection_factor > 0 && num_fec_packets == 0) {num_fec_packets = 1;}// 断言:冗余包数不超过媒体包数(避免过度冗余)RTC_DCHECK_LE(num_fec_packets, num_media_packets);return num_fec_packets;
}
3. 调整保护因子
VCMFecMethod::ProtectionFactor根据网络丢包率,动态调整保护因子(I帧和P帧分别处理,I帧需要更高保护)。
核心逻辑:
- 限制丢包率范围(最大50%,因FEC表仅支持到50%丢包);
- 对P帧:根据丢包率从
kFecRateTable中查询基础保护因子,若丢包率过高则提升; - 对I帧:在P帧基础上“boost”(提升保护因子),确保关键帧不丢失;
- 限制最大保护因子(50%,避免带宽占用过高)。
4.4 FEC编码核心:生成冗余包 payload
ForwardErrorCorrection::EncodeFec是FEC编码的核心函数,负责生成掩码、适配丢失包、生成冗余包 payload。
// 生成FEC冗余包:初始化→生成掩码→适配丢包→填充payload
int ForwardErrorCorrection::EncodeFec(const PacketList& media_packets,int protection_factor,FecMaskType fec_mask_type,int num_important_packets,bool use_unequal_protection,std::vector<Packet*>* fec_packets) {// 1. 计算冗余包数量,无冗余则返回int num_media_packets = static_cast<int>(media_packets.size());int num_fec_packets = NumFecPackets(num_media_packets, protection_factor);if (num_fec_packets == 0) {return 0;}// 2. 初始化冗余包:填充0(异或中0不影响结果),加入输出列表fec_packets->clear();for (int i = 0; i < num_fec_packets; ++i) {memset(generated_fec_packets_[i].data, 0, IP_PACKET_SIZE);generated_fec_packets_[i].length = 0; // 标记未处理fec_packets->push_back(&generated_fec_packets_[i]);}// 3. 生成掩码:根据丢包类型(随机/突发)确定异或规则packet_mask_size_ = internal::PacketMaskSize(num_media_packets);memset(packet_masks_, 0, num_fec_packets * packet_mask_size_);const internal::PacketMaskTable mask_table(fec_mask_type, num_media_packets);internal::GeneratePacketMasks(num_media_packets, num_fec_packets, num_important_packets, use_unequal_protection,mask_table, packet_masks_);// 4. 适配丢失包:在掩码中插入0,标记丢失包不参与异或int num_mask_bits = InsertZerosInPacketMasks(media_packets, num_fec_packets);if (num_mask_bits < 0) {return -1;}packet_mask_size_ = internal::PacketMaskSize(num_mask_bits);// 5. 生成冗余包payload:通过异或填充数据GenerateFecPayloads(media_packets, num_fec_packets);return 0;
}
5、WebRTC FEC实践注意事项
- H264的FEC适配:WebRTC中H264默认关闭Ulpfec,需手动开启Flexfec(实验性),且需确保字段试验参数正确;
- Flexfec开启风险:必须同时使能
WebRTC-FlexFEC-03/Enabled和WebRTC-FlexFEC-03-Advertised/Enabled,否则可能导致客户端死机; - 冗余度平衡:冗余度越高,抗丢包能力越强,但带宽占用越高;建议根据实际网络丢包率动态调整(如丢包率<5%时冗余度10%,丢包率10%-20%时冗余度30%);
- 时间分层与FEC:仅对时间分层0(基础层)进行FEC保护,上层分层依赖基础层,避免过度消耗带宽;
- Payload类型限制:仅VP8/VP9/Generic(开启PictureId)支持“跳过FEC包”,H264等编码需等待FEC包才能解码,需注意延迟控制。
6、总结
WebRTC的FEC机制通过Red、Ulpfec、Flexfec三种方案,覆盖了从简单小数据到复杂音视频的抗丢包需求:
- Red:仅用于小数据量,音视频中不推荐;
- Ulpfec:音视频主流方案,适配大多数网络场景,性价比高;
- Flexfec:实验性方案,灵活性最强,未来潜力大;
在实际开发中,需根据 codec 类型(VP8/VP9/H264)、网络丢包特征(随机/突发)和带宽预算,选择合适的FEC方案,并通过动态冗余度调整,平衡“可靠性”与“带宽消耗”,最终提升WebRTC音视频通话质量。
