NACK机制
发送端实现NACK的三个重点流程:
发送RTP报文,实时存储报文到packet_history_队列
处理接收到的RTCP NACK报文
把nack包里的序号放到nack_sequence_numbers丢包队列
重发NACK反馈的RTP报文
重发报文这里有三点需要注意:
1)会判断上次重传报文时间和当前时间差是否大于RTT,若小于则不重传。
2)NACK重新发送媒体数据有两种方式:单独RTX通道发送、与媒体数据混在一起发送
两种形式对单纯的NACK抗性影响不太大,但是与媒体数据混在一起发送模式,接收端无法区分是NACK重传报文,还是正常媒体数据,会导致接收端反馈的丢包率低于实际值,影响gcc算法探测码率,及发送端FEC冗余度配置。所以建议还是以RTX通道单独发送。
3)RTPSender::ReSendPacket在将重传数据加入pacer队列,会设置报文优先级,为了保证实时性,NACK重传报文需要按照高优先级重传。
优先级配置在set_packet_type,发送报文时,会根据kRetransmission获取发送优先级。
接收端NACK
webrtc接收端触发发送NACK报文有两处:
1、接收RTP报文,对序列号进行检测,发现有丢包,立即触发发送NACK报文。(NackModule::OnReceivedPacket->NackModule::GetNackBatch)
2、定时检查nack_list_队列,发现丢包满足申请重传条件,立即触发发送NACK报文。(其中NackModule::Process是挂载在接收RTP报文线程的一个定时任务,调用NackModule::GetNackBatch,调度周期是kProcessIntervalMs(默认20ms))
重点函数:
核心函数
NackModule::AddPacketsToNack、NackModule::GetNackBatch是NACK核心函数。
NackModule::AddPacketsToNack
决定是否将该报文放入NACK队列
1、nack_list的最大长度为kMaxNackPackets,多的包会循环清空nack_list中关键帧之前的包
2、nack_list中的最新加入的包和最老的包序号之间不超过kMaxPacketAge,以保证nack请求列表中不会有太老旧的包号。
NackModule::GetNackBatch
决定是否发生NACK请求重传该报文。两种触发方式都是调用这个函数决定是否发送NACK请求重传。
该函数的中心思想是:
1、因为报文有可能出现乱序抖动情况,不能说检测出丢包就立即重传,需要等待send_nack_delay_ms_,当等待时间大于send_nack_delay_ms_,申请重传。
2、发送NACK报文有两种实现方式:一种是基于序列号,判断丢包的序列号小于当前序列号,另一种基于时间,发送端重发丢失的包,当两次发送NACK重传请求时间大于rtt_ms_时,才会申请再次重传。
3、视频会议场景对实时性要求很高,当报文一直处于丢包状态,不能持续申请重传,最大重传次数为kMaxNackRetries,超过最大重传次数,放弃该报文。不再重传。
pacer实现
背景介绍
一帧音频数据本身不大,不会超过以太网的最大报文长度。一个RTP报文可以搞定,按照打包时长的节奏发送就可以。但视频数据不能按照音频数据的思路发送,一帧视频可能很大,远大于以太网的1500byte,需要分别封装在几个RTP报文中,若这些视频帧RTP报文一起发送到网络上,必然会导致网络瞬间拥塞。产生丢包抖动等异常。
大多数编解码格式下,一帧音频数据长度固定,音频码率持续平稳。码率不会出现忽高忽低现象。但是一帧视频数据长度受内容影响严重。I、P、B帧间的长度相差非常大。直接发送网络波动幅度很大。因为编码器特别是视频编码出来的码率是波动的,但是发送速率需要保持稳定
Pacer的目的就是让视频数据按照评估码率均匀的在各个时间片发送出去。
设计PACER模块主要解决三个问题:怎么发送报文、什么时候发报文、每次发多少数据量。
怎么发送报文
音频、视频、NACK、FEC、Padding报文都要统一从Pacer模块发送。所以音视频编码数据RTP切分打包后,首先将RTP报文存在pace queue队列,并将报文元数据(packet id, size, timestamp, 重传标示)送到pacer queue进行排队等待发送,插入队列的元数据会进行优先级排序。
pace queue是一个基于优先级排序的多维链表,它并不是一个先进先出的fifo,而是一个按优先级排序的list。报文优先级规则是:
优先级高的报文排在fifo的前面,低的排在后面;
首先判断报文的priority等级,等级越小的优先级越高(priority等级根据报文类型进行分类);
根据报文类型确定数据优先级处理函数如下:(音频>重传>视频>fec>padding)
int GetPriorityForType(RtpPacketMediaType type) {
// Lower number takes priority over higher.
switch (type) {
case RtpPacketMediaType::kAudio:
// Audio is always prioritized over other packet types.
return kFirstPriority + 1;
case RtpPacketMediaType::kRetransmission:
// Send retransmissions before new media.
return kFirstPriority + 2;
case RtpPacketMediaType::kVideo:
case RtpPacketMediaType::kForwardErrorCorrection:
// Video has "normal" priority, in the old speak.
// Send redundancy concurrently to video. If it is delayed it might have a
// lower chance of being useful.
return kFirstPriority + 3;
case RtpPacketMediaType::kPadding:
// Packets that are in themselves likely useless, only sent to keep the
// BWE high.
return kFirstPriority + 4;
}
RTC_CHECK_NOTREACHED();
}
Pacer每次触发发送事件时先从queue的最前面取出优先级最高的报文进行发送,这样做的目的是让视频在传输的过程中延迟尽量小,重传的报文尽快能到达防止等待卡顿。pace queue还可以设置最大延迟,如果超过最大延迟,会计算queue中数据发送所需要的码率,并且会把这个码率替代target bitrate作为budget参考码率来加速发送。
什么时候发报文
PacingController::NextSendTime、PacingController::ProcessPackets是PACER模块两个核心函数,PacingController::ProcessPackets按照PacingController::NextSendTime控制的节奏周期调用。完成PACER平滑发送功能。
PacingController::NextSendTime在控制发送节奏上,有两种模式kPeriodic、kDynamic。kDynamic还没理解透,这里先记录kPeriodic实现方式。kPeriodic模式下,固定每隔5ms调用一次发送报文任务。
每次发多少数据量
media_budget_算出当前时间片网络可以发送多少数据,然后从pacer queue当中取出报文元数据进行网络发送。
media_budget_根据评估出来的目标码率和时间差计算这次定时事件能发送多少字节:
delta time:上次检查时间点和这次检查时间点的时间差。
target bitrate:pacer的参考码率,是由probe模块根据网络探测带宽评估出来的。
remain_bytes:每次触发发包时会减去发送报文的长度size,如果remain_bytes > 0,继续从pace queue中取下一个报文进行发送,直到remain_bytes <=0 或者 pace queue没有更多的报文。如果pacer queue没有更多待发送的报文,但media_budget_计算出还可以发送更多的数据,这个时候pacer会进行padding报文补充
Pacer模块引入延时规避方法
max_pacing_delay:
PacingController::ProcessPackets会实时计算当前处理方式会引入的系统延时,当延时大于设定目标上限值(max_pacing_delay),需要及时调整Pacer目标码率,保证当前数据都能及时发送出去,保证Pacer模块引入延时时间可控。
编码算法码控模块配合:
Pacer模块实现不复杂,但是要想真正做好Pacer功能,仅仅靠一个Pacer模块是玩不转的,需要与视频编码器的码控模块配合:
首先探测模块配置码率给编码器,编码器一定要保证在可控周期内码率收敛到配置的码率参数值以内,否则会给Pacer模块造成的累计延时越来越大压力;
另外IP帧rate也要在合理范围内。若I帧超大,势必导致关键帧传输延时变大,影响端到端系统延时。
拥塞算法--googCC算法(webrtc中使用)
控制发送端的发送码率
WebRTC防止拥塞的根基是有准确的带宽评估方法。它提供了两种带宽评估方法,一种是基于丢包的带宽评估,另一种是基于延时的带宽评估。而基于延时的评估方法又分为接收端(Goog-REMB)和发送端(TCC,transportCC)的带宽评估方法,目前默认采用的是Goog-TCC方法,因为其相对来说更为精准。
发送端基于丢包率的码率控制
接收端基于延迟的码率控制
基于延时的带宽评估方法比基于丢包的评估更好一些,因为它可以提前预估是否发生了拥塞。基于丢包的评估丢包率一旦超过10%就说明可能已经发生拥塞了,而网络一旦拥塞,再想恢复回原来的状态,需要花费一段时间,而这段时间就会影响音视频的服务质量。
而基于延时的带宽评估就不会产生这种情况。它的基本原理是,如果接收到的数据包的网络传输时延在持续增长,就说明网络变差了,当达到一定程度时,就要将评估的带宽值降下来,以防止发生网络拥塞。它的计算公式是根据状态机来的(状态机比较复杂,我这里就不讲了),当状态非常好时,需要增加带宽,同丢包增加带宽一样,每次增加8%;如果延时一直累加,则需要降低带宽,带宽降为原来85%,其它情况就保持当前带宽,无增无减。
为什么TCC比REMB准确?
TCC和REMB主要有两个区别。第一是计算的端不同,REMB是在接收端计算的,接收端计算后再将结果返回给发送端进行控制,而在回传结果时,可能网络又发生了新的变化,这就造成了REMB的及时性不够;TCC是将所有数据都交给发送端做计算和控制,因此及时性和准确度会更高。第二是滤波器不同,REMB是卡尔曼滤波器(Kalman),TCC是最小二乘法滤波器(Trend line)。最小二乘法滤波器在网络延时评估这方面比卡尔曼滤波器效果更好一些。
FEC算法
发送端将负载数据加上一定的冗余纠错码一起发送,接收端根据接收到的纠错码对数据进行差错检测,如果发现差错,则利用纠错码进行纠错。buildRedPacket()
基础理论很简单,利用异或的原理进行数据恢复,当然有一些代价,就是需要多占用一些带宽,用来发送冗余数据。
一、概述
webrtc冗余打包方式有三种:Red(rfc2198)、Ulpfec(rfc5109)、Flexfec(草案)。其中Red和Ulpfec要成对使用。
二、RedFEC
简单将老报文打包到新包上。如下图所示,冗余度为1时,RFC2198打包情况:
这种方法在音视频领域几乎不使用,因为冗余包只能保护特定一个报文,这种方法带宽占用量很大,恢复能力有限,性价比很低。只是早期的T38传真、RFC2833收号会使用该协议,因为传真和收号的数据量比较小。
webrtc里面说使用了RFC2198冗余,实际上仅仅是借用该协议的封装格式,封装FEC冗余报文。
三、UlpFEC
将一组M个报文进行异或,生成N(N就是FEC的冗余度)个FEC报文,打包出去。这组报文任意丢其中的N个,都可以通过这组(M-N)个报文+FEC冗余包恢复回来,比简单的RFC2198保护的范围扩大了很多。例如下面示意图:D为媒体包,R为冗余包,该图所示的冗余度为1。
若UlpFEC异或所有报文,带宽占用量也比较大,在实际应用会根据网络情况进行适当取舍。
四.FlexFEC
五.FEC算法汇总
FEC是无线传输领域的一个前向纠错的算法。网上搜资料的时候经常把无线的算法看的云里雾里的,研究半天都不知道这个和视频传输有什么关系。
无线传输领域的FEC算法主要有TURBO、LDPC、POLAR这三种。
音视频传输领域的FEC算法有如下几种:
1、webrtc的opus音频使用的是inband FEC和交织编码
2、webrtc的视频ulpfec使用的是异或XOR
3、Reed Solomon算法比较复杂,理论上数据恢复能力比较强。
六、webrtc代码分析
webrtc默认使能Red+Ulp的FEC。Flex仅在实验阶段,还不能正式使用。
将UlpFEC编码数据封装为RED包
原文:WebRTC-NACK、Pacer和拥塞控制和FEC - 资料 - 我爱音视频网 - 构建全国最权威的音视频技术交流分享论坛
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓