目录
一. 前言
二. FEC基本原理
三. ULPFEC
1. ULPFEC基础理论
2. ULPFEC报文格式
RTP Header
FEC Header
FEC Level Header
FEC Level Payload
四. WebRTC ULPFEC实现源码剖析
1. 创建FEC对象
2. 生成FEC包的调用流程
3. 冗余包数量规则
4. 保护分配规则
5. XOR生成FEC包负载
6. 以RED格式打包发送
7. 接收FEC包并恢复丢失的媒体包
五. 参考资料
网络中传输实时音视频数据通常使用 UDP,由于传输中存在丢包的问题可能使听到的声音有断续或者看到的视频卡顿,针对弱网场景可以通过添加适当的冗余包来对抗丢包引起的卡顿问题。
FEC 基本原理是通过原始数据生成一定的冗余数据,如果原始数据在传输过程中有丢失,接收端也可以通过冗余数据恢复原始数据。
本文介绍 ULPFEC 的基础理论,报文格式,并介绍 WebRTC ULPFEC 相关的源码实现。
FEC 基本原理是通过原始数据根据一定的运算产生冗余数据,可以通过异或产生冗余数据的,也可以通过 Reed-Solomon 算法产生冗余数据,以 ULPFEC 为例,它使用异或运算来生成冗余数据,异或的特性是相异为 1,相同为 0。
如下图所示,将 Data1 和 Data2 逐位异或生成冗余数据 R,由于 0 ^ 0 = 0, 1 ^ 1 = 0, 0 ^ 1 = 1, 1 ^ 0 = 1,因此 R 的值如下所示。
原先发送端将 Data1, Data2 传输给接收端,如果传输过程中 Data1 或者 Data2 丢失了,那么接收端无法收齐完整数据,只能等待重传,现在发送端将 Data1, Data2, R 同时发送给接收端,假设 Data1 或者 Data2 丢失了,通过 R 与另外一个未丢失的 Data 异或,即可还原出丢失的 Data,无需等待发送端重传。
这里只是以 Data1, Data2 进行举例,多个数据包也是同理,实际场景下具体多少个数据包为一组以及生成的冗余包数通常需要结合丢包率,码率,帧率等信息决定。
FEC 有 RedFec,ULPFec 和 FlexFec 等,WebRTC 使用了 ULPFec,并借助了 Red 封装格式封装冗余报文。
ULPFEC(Uneven Level Protection,非均等保护)能够针对不同数据包提供不同级别的保护,以如下为例进行说明。
原始数据包为 Packet A, B, C, D,冗余包为 ULPFEC Packet #1 和 #2,冗余包为原始数据包提供保护。
ULPFEC Packet #1 为 Packet A 和 B 的 L0 长度提供 0 级保护(注意不是对 Packet A 和 B 整个包进行保护),ULPFEC Packet #2 具有两个保护级别,0 级保护是对 Packet C 和 D 的 L0 长度提供保护,1 级保护是对 Packet A, B, C, D 提供保护。
假设 Packet A,B 和 ULPFEC Packet #1 传输过程中 Packet A 丢失了,根据 ULPFEC Packet #1 和 Packet B 即可还原出 Packet A 的前 L0 长度的数据,注意 Packet A L0 长度之后的数据不能恢复(这里暂时先不考虑到 ULPFEC Packet #2),因为 ULPFEC Packet #1 仅对 Packet A 和 B 提供了 0 级别保护,且 L0 < min(len(Packet A), len(Packet B)),如果你觉得不完整的数据包对于应用程序而言是无用的,你也可以调整 L0 的长度为 max(len(Packet A), len(Packet B)),这通常是带宽和保护力度的权衡,冗余越大意味着抗丢包能力越强,但同时也会消耗更多的带宽。
对于 Packet C, D 和 ULPFEC Packet #2 的 0 级别保护与 Packet A, B 和 ULP FEC Packet #1 同理,不同之处在于对于 ULP FEC Packet #2,它为 Packet A, B, C, D 同时提供了 1 级保护(即 L1 长度部分),假设 A, B, C, D 在传输过程中任意一个包丢失,ULPFEC Packet #2 没有丢失,那么丢失包的 L1 长度数据部分也能得到恢复。
对于 L0 的保护,两个原始数据包对应一个冗余包,对于 L1 的保护,四个原始数据包对应一个冗余包,很明显 L0 的冗余度更高,保护力度更强。
FEC 报文格式如上所示,首先是 RTP Header,接下来是固定 10 字节的 FEC Header,然后是一层或多层的 FEC Level Header 和 Payload。
ULPFEC 的 RTP Header 仅在 FEC 报文通过单独的数据流发送才使用到,其格式和字段含义仍然遵循 RFC3550 的定义,如果你对 RTP Header 字段含义不太熟悉,请阅读这篇文章,下面仅对一些字段做进一步说明。
M:Mark 标记位,在 ULPFEC RTP Header 中应设置为 0
PT:FEC 报文的负载类型,可以动态选择确定
SSRC:与被保护媒体流的 SSRC 相同
SN (sequence number):它必须比先前传输的 FEC 报文的序列号大 1
TS (timestamp):时间戳必须设置为传输 FEC 报文时的媒体 RTP 时间戳
E:保留的扩展位,目前设置为 0
L:指示是否使用长掩码,如果 L 设置为 0 表示 FEC Level Header 的 mask 为 2 字节,如果 L 设置为 1 表示 FEC Level Header 的 mask 为 6 字节
P / X / CC / M / PT recovery:通过 FEC 包关联保护的所有媒体包的 RTP 头部的 P / X / CC / M / PT 位异或运算得到
SN base:本 FEC 包所保护的所有 RTP 媒体包中最小的 sequence number
TS recovery:本 FEC 包所保护的所有 RTP 媒体包的 timestamp 经过异或运算后得到该值
length recovery:本 FEC 包所保护的所有媒体数据包负载长度经过异或运算后得到该值
Protection Length:保护级别对应的保护长度
mask:掩码,如果 mask 中的第 i 位设置为 1,表示 SN base + i 序号的媒体包与此 FEC 包关联,注意 mask 的最高有效位的位置对应 i = 0,即上图第 16 bit 的位置,最低有效位对应 i = 15 或者 i = 47(取决于 FEC Header 中的 L 为 0 还是 1)
我们以两个例子来说明 FEC 报文的各个字段是如何确定的,假设 FEC 流是通过单独的 RTP 会话发送的,并且我们准备发送四个媒体包 A, B, C, D(SSRC=2),它们的序列号分别为 8, 9, 10, 11,时间戳分别为 3, 5, 7, 9,Packet A 和 C 的 Payload Type=11,Packet B 和 D 的 Payload Type=18,A 的载荷长度为 200,B 为 140,C 为 100,D 为 340。
示例一:使用单一保护级别保护四个 Packet 的全长(即整个包的保护),Packet A/B/C/D 任何一个丢失都能通过 FEC Packet 恢复。
FEC RTP Header 见如下 Figure 7,FEC Header 见如下 Figure 8,FEC Level Header 见如下 Figure 9。
Version 固定为 2,Padding 为 0 表示无填充,Extension 为 0 表示无 RTP 扩展头部,Marker 固定为 0,PT 为 127(我们假设 Payload Type=127 来表示 FEC 包),起始序列号为 1,TS 为发送 FEC 包时的媒体 RTP 时间戳(D 时间戳为 9,因此 TS = 9),SSRC 与保护的媒体流的 SSRC 相同,因此为 2。
E 为保留扩展位,值固定为 0,L 为 0 表示使用短长度掩码,P / X / CC / M / PT 的值是根据其保护的所有 RTP 包的头部中 P / X / CC / M / PT 异或得到,SN base 为保护的所有 RTP 包的最小序号 min(8, 9, 10, 11),TS recovery 值是根据所保护的所有 RTP 包的时间戳经过异或运算得到,length recovery 为所保护的所有媒体数据包负载长度经过异或运算后得到。
FEC Level Header 部分如上所示,Protection Length 为对应级别的保护长度,L0 提供了 4 个 Packet 最大长度的保护,因此值为 340(Packet D 的载荷长度),mask 值为 0b1111000000000000,因为该 FEC 关联的媒体包序号为 8,9,10,11,且 SN base = 8。
示例二:具有两个保护级别的示例,L0=70,L1=90
如上产生了两个 FEC 包 ULP #1 和 #2,ULP #1 保护 Packet A 和 B 的 L0 长度,ULP #2 保护 Packet C 和 D 的 L0 长度,同时保护 Packet A, B, C, D 的 L1 长度。
ULP #1 的 RTP Header 见如下 Figure 11,FEC Header 见如下 Figure 12,FEC Level Header 见如下 Figure 13。
ULP #2 的 RTP Header 见如下 Figure 14,FEC Header 见如下 Figure 15,FEC Level Header (L0) 见如下 Figure 16,FEC Level Header (L1) 见如下 Figure 17。
FEC Level Payload 是通过受保护包 Level 对应长度的数据按位异或生成,如果某些包长度不足对应保护等级的覆盖范围,则在末尾使用 0 进行填充。
WebRTC 视频默认启用 ULPFEC 功能(h264 不开启),如下是 FEC 相关对象的创建流程,FEC 有两个重要类,一是 FecController,它是 FEC 的控制器,用于计算 FEC 保护因子等参数,二是 VideoFecGenerator,它是 FEC 的操作对象,用于对媒体包生成 FEC 冗余,它使用了 FecController,WebRTC 也包含了 FlexFec 的实现,但默认使用的是 ULPFEC。
编码器完成帧图片编码后会回调 RtpVideoSender::OnEncodedImage 函数,然后调用 RTPSenderVideo::SendEncodedImage,再调用 RTPSenderVideo::SendVideo,在 SendVideo 函数中,如果 VideoFecGenerator 对象不为空,则会调用 AddPacketAndGenerateFec,之后进行 EncodeFec 的动作。
在 EncodeFec 函数中会调用 NumFecPackets 获取需要对应生成的冗余包数量,该数量根据当前需要保护的媒体包数以及保护因子确定。
protection_factor 是在调用 EncodeFec 函数时传入的,其值为 CurrentParams().fec_rate,CurrentParams 会根据当前处理的帧是否为关键帧选择 keyframe_params 还是 delta_params,而 current_params_ 又是通过 *pending_params_ 赋值的。
pending_params_ 的值在 UlpfecGenerator::SetProtectionParameters 中更新,回溯 SetProtectionParameters 的调用栈,调用流程为:RtpVideoSender::OnBitrateUpdated -> FecControllerDefault::UpdateFecRates -> RtpVideoSender::ProtectionRequest -> UlpfecGenerator::SetProtectionParameters。
FecControllerDefault::UpdateFecRates 会根据码率,丢包率等信息计算所需要的保护因子,具体逻辑见 VCMFecMethod::ProtectionFactor。
在介绍 FEC 保护因子的计算逻辑前,我们先介绍 kFecRateTable,它是一个用于确定保护因子的一维数组表,长度为 6450,通常把它看做是一个 50*129 的二维数组表。如果知道了帧的有效码率以及当前丢包率,就可以确定一个下标位置,获取对应的保护因子。所在的行是通过帧有效码率计算获取的,行之间的跨度为 5kbps,所在的列是由丢包率确定的,第 128 列表示 50% 的丢包率。
如下是计算保护因子的代码逻辑,如果 packetLoss == 0 表示无丢包,protectionFactor 为 0,不需要打冗余包,如果丢包率大于 0,则计算帧的有效码率并结合丢包率,从 kFecRateTable 取出保护因子。
假设确定了 M 个媒体包需要 N 个 FEC 包保护,那么保护分配规则应该如何确定呢,哪些 FEC 包应该保护哪些媒体包正是此小节要讨论的内容。
WebRTC 中有两个掩码表,kPacketMaskRandomTbl 和 kPacketMaskBurstyTbl,这两个表分别用于随机丢包和突发丢包情况下 M 个媒体包需要 N 个 FEC 包保护时的保护分配规则,WebRTC 默认使用的是随机丢包的模型,没有检查当前是随机丢包还是突发丢包进行动态选择。
随机丢包掩码表如下所示,它定义了 1-12 个媒体包受保护时的掩码规则。
kPacketMaskRandomM 里面包含 kMaskRandomM_1...kMaskRandomM_N 项(N <= M),我们以 1 个 FEC 包保护 1 个媒体包为例,其掩码为 0b1000000000000000(0x80, 0x00),也就是该 FEC 包保护 SN+0 序号的媒体包,如果是 1 个 FEC 包保护 2 个媒体包,其掩码为 0b1100000000000000(0xc0, 0x00),也就是该 FEC 包保护 SN+0 和 SN+1 序号的媒体包,如果是 2 个 FEC 包保护 3 个媒体包,第一个 FEC 包的掩码为 0b1100000000000000,第二个 FEC 包的掩码为 0b1010000000000000,也就是第一个 FEC 包保护 SN+0 和 SN+1 序号的媒体包,第二个 FEC 包保护 SN+0 和 SN+2 序号的媒体包,其他组合情况详见掩码表。
确定好冗余包数量以及保护分配规则后,就需要根据媒体包异或生成冗余包,代码流程如下,GenerateFecPayloads 用于生成 FEC 包,但是这个步骤的 FEC 不是最终状态,需要再调用 FinalizeFecHeaders 调整 FEC 头部的一些信息。
GenerateFecPayloads 流程分析如下,首先判断当前媒体包是否受本 FEC 包保护,如果不是则直接跳到下一个媒体包处理,如果是需要受保护的,如果本 FEC 包还没有填充过媒体包数据则直接做一些拷贝(具体从媒体包怎么拷贝可以查看如下代码),如果本 FEC 包已经有填入过媒体包内容,则用媒体包数据与 FEC 包中已有的数据做异或操作。
之后调整 FEC Header 头部的一些字段值,规则与 RFC 文档描述的一致,此处不进行赘述。
RED 编码是一种冗余编码的方式,它允许在同一个 RTP 包塞入不同编码的数据,格式如下,具体可见 RFC2198。
WebRTC 启用 red, ulpfec 后,对于媒体包,它将媒体包数据塞入 RED RTP 包,RTP Header 的 PT 为 RED 使用的 Payload Type,block PT 设置为媒体包使用的 Payload Type,对于 FEC 包,它把 FEC 包数据塞入 RED RTP 包,RTP Header 的 PT 为 RED 使用的 Payload Type,block PT 设置为 ulpfec 使用的 Payload Type。注意 WebRTC 中媒体包和 FEC 包是分开打包的,不是打在同一个 RED 包里,下图展示的是 RED 同时塞入了两种 PT 的格式。
利用 FEC 包恢复丢失的媒体包就是使用媒体包生成 FEC 包的逆操作。
首先接收 RTP 包后会进入 RtpVideoStreamReceiver::ReceivePacket,判断如果是 red 包的 payloadType 则进入 ParseAndHandleEncapsulatingHeader 中处理。
ParseAndHandleEncapsulatingHeader 首先执行 AddReceivedRedPacket,它把收到的媒体包和 FEC 都塞到 received_packets_ 中,并打上对应区分的标记,之后执行 ProcessReceivedFec 逻辑,它一方面将媒体包回调给 RtpVideoStreamReceiver::OnRecoveredPacket,相当于执行正常接收非 RED 格式媒体包的处理流程,之后执行 DecodeFec 尝试根据 FEC 包恢复丢失的媒体包。
DecodeFec 逻辑如下,首先执行 InsertPacket 将媒体包和 FEC 包插入合适的列表中,然后执行 AttemptRecovery 尝试恢复丢失的数据包。
对于媒体包则插入到 recovered_packets 列表,对于 FEC 包,它会根据 mask 列表解析该 FEC 包保护的媒体包,然后把需要受保护的媒体包挂到自己的 protected_packets 列表下,然后把 FEC 包塞入 received_fec_packets_。
真正尝试恢复媒体包的逻辑在 AttemptRecovery 函数中,它首先遍历 received_fec_packets_,判断本 FEC 包所保护的媒体包的缺失情况做以下不同处理。
1. 如果 packets_missing 为 0 说明接收到的媒体包已经完整了,可以删除本 FEC 包
2. 如果 packets_missing 大于 1,说明条件还不成熟,需要继续等待,先跳过处理(因为根据异或原理,一个 FEC 包只能恢复媒体包组里丢失的一个媒体包)
3. 如果 packets_missing 等于 1,说明已具备恢复条件,调用 RecoverPacket 异或恢复媒体包即可,之后把恢复的媒体包塞入 recovered_packets 中
对于恢复的媒体包,再回调给 RtpVideoStreamReceiver::OnRecoveredPacket,即按照接收到非 RED 格式的媒体包正常处理即可。
至此,WebRTC 如何根据媒体包打 FEC 包,包数量如何确定,保护规则如何确定,发送时的打包格式以及接收后如何根据 FEC 包还原媒体包已经介绍完毕。
RTC3550(RTP: A Transport Protocol for Real-Time Applications)
RFC2198(RTP Payload for Redundant Audio Data)
RFC5109(RTP Payload Format for Generic Forward Error Correction)