NACK则在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端发送缓冲区的支持,RFC5104[2]定义NACK数据包的格式。
1 NACK框架
1.1 NACK介绍
与NACK对应的是ACK,ACK是到达通知技术。以TCP为例,他可靠因为接收方在收到数据后会给发送方返回一个“已收到数据”的消息(ACK),告诉发送方“我已经收到了”,确保消息的可靠。
NACK也是一种通知技术,只是触发通知的条件刚好的ACK相反,在未收到消息时,通知发送方“我未收到消息”,即通知未达。
在rfc4585协议中定义可重传未到达数据的类型有二种:
1 RTPFB:rtp报文丢失重传。
2 PSFB:指定净荷重传,指定净荷重传里面又分如下三种:
1) PLI(Picture Loss Indication)视频帧丢失重传。
2)SLI(Slice Loss Indication)slice丢失重转。
3)RPSI(Reference Picture Selection Indication)参考帧丢失重传。
在创建视频连接的SDP协议里面,会协商以上述哪种类型进行NACK重转。以webrtc为例,会协商两种NACK,一个rtp报文丢包重传的nack(nack后面不带参数,默认RTPFB)、PLI 视频帧丢失重传的nack。
class NackStructor {
public:
uint32_t version :2; //协议版本号(2bit)
uint32_t padding :1; //是否有填充(1bit)可理解为是否有其他跟随或补充信息在消息中
uint32_t fmt :5; //(5bit)在不同消息中叫法不同,也叫blockcount或RC,此处指格式
//format,在NACK消息中此值为1,之所以同时具有fmt和packettype
//是因为某些消息存在多级类型,比如当前的NACK消息属于Feedback
//类型。因此fmt=1(Feedback).
uint32_t packettype :8; //包类型(8bit),NACK消息类型值为205,Feedback子类型
uint32_t length :16; //(16bit)消息长度
uint32_t ssrc; //构造发送当前消息包的端的SSRC,若本端是此NACK包的发送者,此
//SSRC值就是本端的SSRC值(源标识符)
uint32_t ssrcsource; //NACK消息部分,也是SSRC值,在此可称为媒体源标识符,用来指明
//此消息要反鐀谁的情况。比如我是接收端,我知道发送端的音频流
//(SSRC:1234)给我发来的数据包有丢包,我要反鐀对方音频流的
//情况,那这个地方就应该填写1234(对方音频流SSRC)。
uint32_t pid:16; //packet id(sequence number)详见下面PID-BLP图
uint32_t blp:16;
};
在一个消息的一个公用头后面的NACK块部分可以有多个,每个NACK块可以反馈1个流(SSRC)的情况。当然 //1个NACK块中的blp部分也可以有多个。
NACK作为RTP层反馈参数,和Video Codec联系在一起。WebRTC在初始化阶段,创建call会话时,会收集本端支持的所有Video Codec,NACK作为Codec的属性被一起收集。在接下来的SDP协商过程中,NACK属性被协商到Offer/Answer中。
webRTC在评估到收发端之间RTT延迟比较小的时候会采用NACK来进行丢包补偿,NACK是一个请求重发过程,其流程如下图所示。这个过程有一个问题是在网络抖动和丢包很厉害的情况下有可能造成同一时刻收到很多NACK的重传请求,发送端瞬间把这些重传请求放入pacer中进行重发,这样pacer的延迟会增大,而且pace的参考码率会随着pace queue的延迟控制变的很大而出现间歇性网络风暴。WebRTC在处理NACK重传时设计了一个重传码率控制器,其设计原理是通过统计单位时间窗口周期中发送的字节数据来限流,如果这个时间窗内发送的数据的码率大于estimator评估的码率,不进行当前NACK请求的重传,等待下一个NACK。
1.4 WebRTC NACK算法实现
新的WebRTC的NACK算法构建NACKList的方式不同。
1)call启动,并接收rtp包;
2 )rtp包头部解析,并转发给具体的编码格式解析具体编解码参数信息;
3) vi_channel 开启线程,jitterbuffer对数据包进行处理、数据帧获取、数据包插入,数据包重排等操作;jitterbuffer进行插包处理,同时进行NACKList构建;
每次进行插包处理时,都将接收到数据包序列号和之前已经保存的最新序列号latest_received_sequence_number进行比对:如果不是新的数据包,则从missing_sequence_numbers中剔除;如果是新的数据包,则进行NACK构建,以上次保存的序列号+1为起始值,以新收到的序列号为结束,将之间的序列号先缓存到missing_sequence_numbers中;
插入该序列号完成后,需要判断当前missing_sequence_numbers是否过大、是否收到的序列号是否太旧,并进行NACKList过大、序列号过久等处理;完成NACKList瘦身;
判断并更新当前序列号为latest_received_sequence_number;
这样每次接受到一个数据包,都可以形成一个较小的NACK List;
4) vi_channel开启重传线程,jitterbuffer进行NACK获取,得到NACKlist通过rtcp反馈给发送端,进行数据包重发;
5)这时只需要从missing_sequence_numbers中获取当前的NACK列表即可,可以保证是最新的NACK请求列表。
webrtc支持RTPFB和PLI FB两种重传方式。
RTPFB在JB里面实现。通过RTP报文的序列号和时间戳,判断是否出现丢包异常。参考NackTracker类实现。
-->PlatformThread::StartThread
->PlatformThread::Run
->ProcessThreadImpl::Run
->ProcessThreadImpl::Process
->PacedSender::Process
->PacedSender::SendPacket
->PacketRouter::TimeToSendPacket
->ModuleRtpRtcpImpl::TimeToSendPacket
->RTPSender::TimeToSendPacket
->RtpPacketHistory::GetPacketAndSetSendTime
->RtpPacketHistory::GetPacket
接收端有两种方式驱动方式NACK
1)收包驱动
DeliverPacket
->DeliverRtp
->RtpStreamReceiverController::OnRtpPacket
->RtpDemuxer::OnRtpPacket
->RtpVideoStreamReceiver::OnRtpPacket
->RtpVideoStreamReceiver::ReceivePacket
->RtpReceiverImpl::IncomingRtpPacket
->RTPReceiverVideo::ParseRtpPacket
->RtpVideoStreamReceiver::OnReceivedPayloadData
->NackModule::OnReceivedPacket
->VideoReceiveStream::SendNack
->RtpVideoStreamReceiver::RequestPacketRetransmit
->ModuleRtpRtcpImpl::SendNack
2)定时驱动
NackModule::Process
PLI FB在webrtc里面实现的是请求关键帧。当连续出现解码失败,或者长期没有解码输入,就通过RTCP报文发送请求IDR帧命令。参考VideoReceiveStream::Decode、RequestKeyFrame这两个函数实现。