FEC报文构建、FEC掩码构造和丢失数据包恢复
ForwardErrorCorrection::EncodeFec()
主要:
- 如2.2.1中所介绍的,随机丢包和突发丢包下fec组包模式不同,所以webrtc准备了两张mask表 kFecMaskRandom(随机丢包), kFecMaskBursty(突发丢包)去生成mask, 调用internal::GeneratePacketMasks()去生成mask
- 根据mask和packet, 调用GenerateFecPayloads()生成fec包
- 调用FinalizeFecHeaders()填入ssrc
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
本节在深度分析WebRTC 60 Codebase中ULPFEC的相关代码,总结出其实现算法和细节。
下面以Video为例,从FEC报文构建、FEC掩码构造和丢失数据包恢复三个方面分析ULPFEC在WebRTC中的实现。
3.1 ULPFEC报文构建
WebRTC中ULPFEC报文构建的流程如图8所示:
图8 FEC报文构建和发送流程.png
FEC报文构建开始于编码线程编码完一帧数据的后处理,控制流程经PayloadRouter到达RtpSenderVideo::SendVideo()函数。
如果当前会话配置了red和ulpfec,则调用SendVideoPacketAsRedMaybeWithUlpfec(),该函数主要做四件事:
1)把本rtp数据包送入UlpfecGenerator,并尝试构造FEC包;
2)获取1)构造的FEC包(已经封装为RED包)列表;
3)把本RTP数据包封装为RED包并发送到网络;
4)把FEC包列表发送到网络。
其中2)构造RED包的过程很简单,按照RFC2198的定义填充字段即可,3)和4)发送RED包到网络也很简单,在此不再过多论述。接下来重点分析2)的FEC包的构造过程。
步骤2)会触发ForwardErrorCorrection::EncodeFec()函数,该函数流程伪代码如下所示:
ForwardErrorCorrection::EncodeFec(media_packets,protection_factor,
num_important_packets, use_unequal_protection, fec_mask_type, fec_packets) {
// step 1, 根据媒体数据包个数和保护因子,确定需要生成的fec数据包个数并初始化。
int num_fec_packets = NumFecPackets(num_media_packets, protection_factor);
for (int i = 0; i < num_fec_packets; ++i) {
memset(generated_fec_packets_[i].data, 0, IP_PACKET_SIZE);
fec_packets->push_back(&generated_fec_packets_[i]);
}
// step2, 构建fec掩码表,并从中获取构造fec包需要的掩码,存储在packet_mask_中。
// 这一步是最关键的,packet_masks_决定媒体数据包在FEC包中受保护的分布情况。
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_);
// 步骤3,以packet_masks_和packet_mask_size_,media_packets和num_fec_packets
// 为输入,生成FEC包集合generated_fec_packets_,包括半成品的头部和成型的负载。
GenerateFecPayloads(media_packets, num_fec_packets);
// 步骤4,填充并修正生成生成FEC数据包的头部,这很简单,按照RFC填充即可。
FinalizeFecHeaders(num_fec_packets, media_ssrc, seq_num_base);
}
step1: 根据媒体数据包个数和保护因子,确定需要生成的fec数据包个数并初始化。
step2: 构建fec掩码表,并从中获取构造fec包需要的掩码,存储在packet_mask_中。
step3: 以packet_masks_和packet_mask_size_,media_packets和num_fec_packets
为输入,生成FEC包集合generated_fec_packets_,包括半成品的头部和成型的负载。
step4: 填充并修正生成生成FEC数据包的头部,这很简单,按照RFC填充即可。
FEC构造过程:
1) 根据输入媒体数据包的个数m和保护因子factor,确定需要生成的FEC包的个数num_fec = m * factor / 256.
2) 初始化这num_fec个FEC包。
3) 创建掩码表并获取num_fec个FEC包所需要的掩码数据,存储在packet_masks_。
掩码表是预先定义好的一张三维表格,用以模拟不同情况下媒体数据包在FEC包中的保护分布情况,3.2节会针对该问题进一步分析。
4) 函数以掩码信息packet_masks_,媒体数据包media_packets和fec包个数num_fec为输入调用GenerateFecPayloads()函数生成FEC数据包,此时FEC包中的负载部分已经成型,但是FEC头部需要进一步修正
5) 调用FinalizeFecHeaders()解决。
至此,FEC包的构造过程分析完毕。需要注意的是,WebRTC仅仅使用ULPFEC的Level 0对媒体数据包进行保护,也即FEC包中只有一个FEC Level。
另外,在WebRTC内部,保护RTP头部的FEC头部称之为Level 0,而保护RTP负载的FEC Level部分称之为Level 1。这里和FEC5109文档中所定义的稍有不同,需要注意一下。
3.2 ULPFEC掩码表和掩码
根据RFC5109中相关定义可知,一个媒体数据包可以由多个FEC包保护,而一个FEC包也可以多保护多个媒体数据包。
假设m个媒体数据包需要n个FEC数据包保护,则可以定义一个二维的m * n零一矩阵来描述媒体数据包在fec包中的保护分布情况:矩阵中元素m[i, j]置1表示第i个媒体数据包需要第j个FEC包保护。从行角度讲,第i行元素表示第i个FEC包保护的媒体数据包的集合;
从列角度讲,第j列元素表示保护第j个媒体数据包的FEC包的集合。
由于该矩阵是零一矩阵,因此在存储上可以采用掩码来存储。
这个掩码也就是FEC Level Header中所定义的掩码。
那么掩码中的1该如何分布。我们知道,现实世界中网络丢包分为随机丢包和突发丢包两种情况,FEC包需要能够针对这两种情况对媒体数据包进行保护。
WebRTC预先构造两个掩码表kPacketMaskRandomTbl和kPacketMaskBurstyTbl,以模拟在随机情况和突发情况下媒体数据包在FEC包中的保护分配情况。
假设在随机丢包场景下,对于m * n的情况,我们只需要从kPacketMaskRandomTbl[m][n]就可以获取FEC包所需要的全部掩码. 然后该掩码为基础,构造FEC数据包。
根据ULP思想,FEC包可以对媒体数据包集合中的不同数据包实施不同的保护力度。
这源于一帧视频数据编码后生成的一系列RTP数据包中,其重要性是不一样的,比如开始几个RTP包的重要性更大一些.因此,WebRTC在构造FEC包的掩码时,有均匀保护和非均匀保护两种策略。
对于均匀保护,所有RTP包的重要性一样,FEC包对他们进行平等的均匀的保护。
对于m * n,FEC包使用的掩码即为kPacketMaskRandomTbl[m][n]。
对于非均匀保护,RTP包集合被非为重要数据包集合S1和普通数据包集合S2,分配较多个FEC包来保护S1,较少个FEC包保护S2。
WebRTC定义了三种模式针对实现非均匀保护:
1)kModeNoOverlay:非叠加保护,保护S1的掩码和S2的掩码相互分离。
2)kModeOverlay:叠加保护,保护S1的掩码和S2的掩码叠加在一起。
3)kModeBiasFirstPacket:在均匀保护的基础上,所有FEC包都保护第一个包。
假设保护场景为(m,n),其中重要数据包为前k个,分配给重要数据包的FEC包个数为t,掩码表为mask_table.
则三种场景下最终掩码的确定如下:
1)kModeNoOverlay:mask_table[k][t]和mask_table[m-k][n-t]的移位组合。
2)kModeOverlay: mask_table[k][t]和mask_table[m][n-t]的拼接。
3)kModeBiasFirstPacket:mask_table[m][n],再第一列全部置1。
至此,关于ULPFEC的掩码表和掩码分析完毕。
3.3 ULPFEC报文接收和数据包恢复
ULPFEC报文接收和丢失数据包恢复是ULPFEC报文构造和发送的逆过程,该过程分为三个子过程:
1)把接收到的RED包进行解包得到RTP包或FEC包。
2)把RTP/FEC包插入到FEC处理模块的合适列表中。
3)发起数据包恢复尝试。图9描述这个过程。
图 9 ULPFEC接收数据包和丢失数据包恢复.png
1) RTP数据包到达RtpStreamReceiver::ReceivePacket(),判断出该RTP包为RED包后,调用ParseAndHandleEncapsulatingHeader()。
该函数主要做了两件事:RED解包和处理包。前者很简单,就是按照RFC2198去掉RED头部,得到纯RTP报文或者FEC报文。后者深入到UlpfecReceiver中进一步处理RTP/FEC报文。
在ProcessReceivedFec()函数中,若接收到的是RTP包,则
(1) 调用callback的OnRecoveredPacket()把RTP包发送到VCM模块,以不耽误解码过程。
(2) 调用DecodeFec()执行FEC恢复数据包过程。
(3) 把新恢复的数据包送到VCM模块进行解码。
DecodeFec()函数做两件事情:把数据包插入到合适列表,并发起丢失包恢复过程。
FEC包插入到Fec列表,RTP包插入到Media列表。
对于FEC包,还要根据其掩码遍历Media列表,把所有本FEC包保护的RTP包挂在自己名下的列表中.
对于RTP包,还要根据自己的序列号遍历Fec列表,把本RTP包挂在所有保护自己的FEC包名下的列表中
2)尝试恢复丢失数据包过程AttemptRecovery:对于Fec列表中的每一个FEC包,判断其目前的丢包数:
如果未丢包则无需恢复操作,删除本FEC包继续下一个;
如果丢包数大于1,则条件还不成熟,跳过本FEC包继续下一个;
如果丢包数等于1,则调用RecoveryPacket()恢复一个RTP数据包。
3) 把该恢复包插入到Media列表中,并根据该RTP包的序列号遍历Fec列表,把该RTP包挂在所有保护自己的FEC包名下的列表中。
4) 是真正执行恢复数据包过程的RecoveryPacket()。流程走到这里,所有条件都已经成熟,按照RFC5109的定义按部就班恢复数据包即可:
(1)准备阶段:把本FEC包中的头部数据和payload数据拷贝到recover_packet中作为基准数据。(2)恢复阶段:针对本FEC包保护的每个已收到RTP包,recover_packet包与之执行XOR操作,结果仍然存储在recover_packet中。全部RTP包执行完XOR操作以后,recover_packet中的负载部分即为待恢复RTP包的负载数据。
3)收尾阶段:修正recover_packet的RTP头部,得到最终恢复的RTP包。
至此,一个完整的接收端恢复丢失数据包的过程分析完毕。