在WebRTC中,前向纠错(FEC)和丢包重传(NACK)是抵抗网络错误的重要手段。FEC在发送端将数据包添加冗余纠错码,纠错码连同数据包一起发送到接收端;接收端根据纠错码对数据进行检查和纠正。RFC5109[1]定义FEC数据包的格式。NACK则在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端发送缓冲区的支持,RFC5104[2]定义NACK数据包的格式。
本文在研究WebRTC源代码的基础上,以Video数据包的发送和接收为例,深入分析ANCK丢包重传机制的实现。主要内容包括:SDP协商NACK,接收端丢包判定,NACK报文构造、发送、接收和解析,RTP数据包重传。
1 WebRTC NACK框架
1.1 NACK介绍
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。
1.2 基本流程
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。
2 NACK算法框架
2.1 MediaEngine 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 接收新的数据包,将完整的数据发送给解码端进行解码;
– 每次一个完整图像帧接收到后,更新一下解码状态标志中最小值;以便后续NACK获取时最小值的更新。
6 将解码后的图像数据发送给render进行渲染。
2.2 WebRTC 最新NACK算法
新的WebRTC的NACK算法和MediaEngine中的大致相同,但是构建NACKList的方式不同。
1 MediaEngine启动,并接收rtp包;
2 rtp包头部解析,并转发给具体的编码格式解析具体编解码参数信息;
3 vi_channel 开启线程,jitterbuffer对数据包进行处理、数据帧获取、数据包插入,数据包重排等操作;<font color=”red”>**jitterbuffer进行插包处理,同时进行NACKList构建;**</font>
– 每次进行插包处理时,都将接收到数据包序列号和之前已经保存的最新序列号latest_received_sequence_number进行比对:如果不是新的数据包,则从missing_sequence_numbers中剔除;如果是新的数据包,则进行NACK构建,以上次保存的序列号+1为起始值,以新收到的序列号为结束,将之间的序列号先缓存到missing_sequence_numbers中;
– 插入该序列号完成后,需要判断当前missing_sequence_numbers是否过大、是否收到的序列号是否太旧,并进行NACKList过大、序列号过久等处理;完成NACKList瘦身;
– 判断并更新当前序列号为latest_received_sequence_number;
<font color=”red”>这样每次接受到一个数据包,都可以形成一个较小的NACK List</font>
4 vi_channel开启重传线程,jitterbuffer进行NACK获取,得到NACKlist通过rtcp反馈给发送端,进行数据包重发;
– 这时只需要从missing_sequence_numbers中获取当前的NACK列表即可,可以保证是最新的NACK请求列表。
3 音视频流畅度优化问题汇总
之前Linky进行NACK优化:包括RecycleFramesUntilKeyFrame函数、NACK其他参数优化等,但是出现一个问题:建立通话后有7~15s左右时间黑屏现象;
3.1 马赛克
分析原因如下:
– 由于建立通话时出现黑屏现象,因此尝试将decodable属性放开,可以保证所有图像帧在一个RTT时间后可以解码,而不是出现循环等待现象,这样黑屏现象可以解决,但是出现马赛克。原因就是每帧图象帧只等待有限RTT时间就送去解码,没有等待跟多次NACK请求,这时数据帧是不完整的因此会出现马赛克现象。
解决办法
– 将decodable属性关闭,并参考黑屏现象解决办法:
c++
void VCMSessionInfo::UpdateDecodableSession(const FrameData& frame_data) {
if (complete_ || decodable_)
return;
if (frame_type_ == kVideoFrameKey ||
!HaveFirstPacket())
return;
decodable_ = true;
}
3.2 黑屏
分析原因如下:
– 由于原MediaEngine中构建NACKList时都需要获取最大值和最小值,但该最大值和最小值又依赖获取得到一帧完整图像帧所有数据,因此在建立通话伊始阶段,一直获取不到完整图像帧所有的包,导致MediaEngine一直请求关键帧,但没有完整数据帧可以解码。
解决办法
– 尝试在接受到一帧图像帧数据后,如果该帧图像已经有起始码标志位、同时有结束码标志,虽然不是完整帧,但是可以设置为NACK标志位,并在获取最大最小值时进行判断,用于最小值一直获取不到的破解办法:
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;
}
}
3.3 卡顿
对比iOS和Android版本和E10通话流程,发现iOS通话过程更加流畅,抓包分析发现NACK请求数据比较少,但是更加敏捷,反应比较迅速。而Android版本,前几次需要发送多个关键帧请求,然后再进行NACK请求,同时每次NACK请求相比iOS数据量都比较大。
分析原因如下:
– 如第二节列举的NACK请求算法的差异,导致NACK请求后NACKList数据的差异,影响到是否快速响应的效率,同时也影响NACKList数据大小问题。
解决办法
– 尝试将新的NACK计算算法移植到MediaEngine中,正在调优中。
碰到的问题:
– 查看log和 抓包发现:存在一种可能,每一帧关键帧或者IDR帧的第一个包(sps包)可能丢包,但是新的旧的NACK算法都无法判断出来,进行请求,导致I帧和P帧都解码失败。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。