在WebRTC中,前向纠错(FEC)和丢包重传(NACK)是抵抗网络错误的重要手段。FEC在发送端将数据包添加冗余纠错码,纠错码连同数据包一起发送到接收端;接收端根据纠错码对数据进行检查和纠正。RFC5109[1]定义FEC数据包的格式。NACK则在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端发送缓冲区的支持,RFC5104[2]定义NACK数据包的格式。
本文在研究WebRTC源代码的基础上,以Video数据包的发送和接收为例,深入分析ANCK丢包重传机制的实现。主要内容包括:SDP协商NACK,接收端丢包判定,NACK报文构造、发送、接收和解析,RTP数据包重传。
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。
NACK作为RTP层反馈参数,和Video Codec联系在一起。WebRTC在初始化阶段,创建MediaEngine时,会收集本端支持的所有Video Codec,NACK作为Codec的属性被一起收集。在接下来的SDP协商过程中,NACK属性被协商到Offer/Answer中。
webRTC在评估到收发端之间RTT延迟比较小的时候会采用NACK来进行丢包补偿,NACK是一个请求重发过程,其流程如下图所示。这个过程有一个问题是在网络抖动和丢包很厉害的情况下有可能造成同一时刻收到很多NACK的重传请求,发送端瞬间把这些重传请求放入pacer中进行重发,这样pacer的延迟会增大,而且pace的参考码率会随着pace queue的延迟控制变的很大而出现间歇性网络风暴。WebRTC在处理NACK重传时设计了一个重传码率控制器,其设计原理是通过统计单位时间窗口周期中发送的字节数据来限流,如果这个时间窗内发送的数据的码率大于estimator评估的码率,不进行当前NACK请求的重传,等待下一个NACK。
MediaEngine中NACK算法思想:
1 MediaEngine启动,并接收rtp包;
2 rtp包头部解析,并转发给具体的编码格式解析具体编解码参数信息;
3 vi_channel 开启线程,jitterbuffer对数据包进行处理、数据帧获取、数据包插入,数据包重排等操作;
GetFrame:获取empty类型图像帧,用于保存当前的数据包;
依据数据包的时间戳计算图像帧render时间戳;
jitterbuffer查找对应的图像帧,并完成数据包插入、重排操作;
4 vi_channel开启重传线程,jitterbuffer进行NACK构建;并将构建完成NACKlist通过rtcp反馈给发送端,进行数据包重发;
首先获取到当前序列号的最大值和最小值,用于构建初始NACK列表nack_seq_nums_internal;如果最大值和最小值都为-1,则进行KeyFrame请求,而不是NACK重传;
如果最大值和最小值都大于0;则计算NACKList大小;如果NACKList大于NACK阈值,则需要进行NACKList瘦身,查找FrameList中的关键帧,比将小于该关键帧之前的数据帧丢弃,重复该过程,完成瘦身;
获取到符合要求的NACKList初始列表nack_seq_nums_internal后,以最小值+1为起始值,并依次递加的方式对NACKList列表nack_seq_nums_internal进行初始化;
将初始化后的nack_seq_nums_internal和FrameList各个帧中的数据包序列号依次比对,如果存在则NACKList置为-1;同时处理NACKList中的空包数据,置为-2;
对nack_seq_nums_internal中数据进行压缩:将已经接收到的包(已置为-1)和空数据包(已置为-2)进行过滤;
依次将NACKList初始列表nack_seq_nums_internal中的数据添加到NACKList nack_seq_nums中,完成NACKList构建;
将构建完成的NACKlist反馈给发送端,进行重传;
5 接收新的数据包,将完整的数据发送给解码端进行解码;
6 将解码后的图像数据发送给render进行渲染。
新的WebRTC的NACK算法和MediaEngine中的大致相同,但是构建NACKList的方式不同。
1 MediaEngine启动,并接收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反馈给发送端,进行数据包重发;
之前Linky进行NACK优化:包括RecycleFramesUntilKeyFrame函数、NACK其他参数优化等,但是出现一个问题:建立通话后有7~15s左右时间黑屏现象;
分析原因如下:
解决办法
void VCMSessionInfo::UpdateDecodableSession(const FrameData& frame_data) {
if (complete_ || decodable_)
return;
if (frame_type_ == kVideoFrameKey ||
!HaveFirstPacket())
return;
decodable_ = true;
}
分析原因如下:
解决办法
void VCMSessionInfo::UpdateCompleteSession() {
if (packets_.front().isFirstPacket && packets_.back().markerBit) {
// Do we have all the packets in this session?
bool complete_session = true;
PacketIterator it = packets_.begin();
PacketIterator prev_it = it;
++it;
for (; it != packets_.end(); ++it) {
if (!InSequence(it, prev_it)) {
complete_session = false;
break;
}else{
session_nack_ = true;
}
prev_it = it;
}
complete_ = complete_session;
}
}
if(*low_seq_num == -1 && frame_buffers_[i]->IsRetransmitted()){
*low_seq_num = last_decoded_state_.sequence_num();
}
对比iOS和Android版本和E10通话流程,发现iOS通话过程更加流畅,抓包分析发现NACK请求数据比较少,但是更加敏捷,反应比较迅速。而Android版本,前几次需要发送多个关键帧请求,然后再进行NACK请求,同时每次NACK请求相比iOS数据量都比较大。
分析原因如下:
解决办法
碰到的问题: