NACK调用栈
-
RtpDemuxer :
-
在音视频引擎模块中, 将从网络中收到的RTP包分发给不同的channel, 音频包分发给voice Channel, 视频包分发给videochannel
-
这里通过RtpDemuxer将RTP分发给不同的流, 底层每个流对应不同的编解码器, 接收流对应解码器, 发送流对应编码器
-
如果这里收到的是RTX包, 就会分发给RtxReceiveSream去处理, 如果这里收到的是RTP包, 就会分发给RtpVideoStreamReceiver去处理(RTX处理完之后,同样会执行这里的流程)
-
ReceivePacket : 包含复杂且重要的逻辑
-
OnReceivedPayloadData : 如果这次收到的包与上次收到的包之间有间隔, 就会调用OnReceivedPacket
-
OnReceivedPacket : 计算出到底丢了哪些包, 然后生成NACK消息发给发送端
ReceivePacket()
-
如果开启了fec, 会首先执行fec逻辑会对nack逻辑有干扰, 这里以关闭fec功能为前提来分析nack逻辑
-
跳过fec逻辑, 首先会通过payload_type_map根据pt找到rtp解包器, 使用解包器对rtp包进行解析, 从而得到rtp的payload
-
将解析得到的payload作为参数传给OnReceivedPayloadData()
OnReceivePayloadData()
- 首先对header中进行域的填充, 例如: 视频方向、视频类型、是否最后一个包
- 从扩展头中获取信息, 并更新到header中, 至此一个(视频)帧的基本情况就搞清楚了
- 后面的三个if语句逻辑尤为重要
- if (video_header.is_last_packet_in_frame) : 从扩展头中获取色彩空间信息
- if (loss_notification_controller) : 处理丢失找回的包
- if (nack_module) : 检测是否发生丢包, 并将丢失的包记录下来
-
rtcp_feedback_buffer_.SendBufferedRtcpFeedback() : 发送nack消息
-
最后将该packet插入到packet_buffer_中为组帧做准备
下面来看nack_module分支的逻辑
-
这个包是否属于关键帧
-
调用nack_module的OnReceivedPacket()判断是否发生丢包
OnReceivedPacket()
- 首先判断是否初始化, 初始化完成后退出
-
将这次收到的包的seqNum赋值给newest_seq_num
-
判断是否为关键帧, 并插入keyframe_list中
- 判断这个包是否跟上一个收到的包重复, 重复包不处理
- 如果已经完成初始化, 并且不是重复包, 则通过AheadOf()判断这个包是不是newest_seq_num前面的包
- 如果true, 说明这个包是一个晚到达的乱序包, 不需要NACK处理, 需要到nack列表里查找是否存在这个包, 并且删除
- 通过了上面逻辑的筛选, 说明这次收到的是一个新的包
- 判断并保存关键帧的第一个包, 这里要注意: 只有关键帧的第一个包才能插入keyframe_list_中, 该判断逻辑是在OnReceivedPayloadData()中的if(nack_module)分支中执行的
- keyframe_list_容量为10000, 把超出容量的老旧的包删除. 这里要注意: kMaxPacketAge并不是keyframe_list_的长度, 而是keyframe_list_中最大的seq_num和最小的seq_num的差值, 实际中keyframe_list_中的数据量远小于10000. 这里做这个最大值的限制原因是: 实时通讯系统中注重实时性, 数据的时效性尤为重要, 当缓冲的数据过于老旧的时候会影响通讯质量
- 判断是否找回来的包, 即is_recovered, recovered_list_的逻辑同keyframe_list_
- 此时来到代码的173行, 经过以上逻辑, 说明这个包符合一下几种情况
-
不是第一个包
-
不是重复包
-
不是迟到的乱序包
-
不是找回的包
-
这个包的seq_num正好是newest_seq_num+1, 正常顺序包,不需要处理
-
这个包的seq_num是newest_seq_num+N, 即可能出现了丢包
-
丢了N个包, 这里调用**AddPacketsToNack()**将这N个丢失的包记录下来
-
这N个包可能因为网络抖动出现了乱序没有按时到达, 不属于丢包, 同样记录下来
- 通过**GetNackBatch()**函数判断是否发生了真正的丢包
- 最后调用nack_sender_->SendNack()将真正丢包的nack消息发送给对端
- 这里要注意 : SendNack的第二个参数为true, 原因下文讲解
AddPacketsToNack()
这个函数的逻辑是将包序号丢失的seq_Num插入到nack_list_列表中, 作为可疑丢包等待下一步处理
-
这个函数同样本着数据实效性的原则, 限制缓存长度
-
这里有个极端情况, 就是275行, 当清理列表后仍然达不到长度要求时, 就将nack_list_清空, 并向对端请求一帧IDR帧以重启不工作的解码器
-
再次循环recovered_list_, 看是否已经被找到, 否则视为可疑的丢失的包插入nack_list_中
GetNackBatch()
这个函数的逻辑只是甄别出可疑丢包列表中的真实丢包
- 定义的两个布尔变量: consider_seq_num是以seqNum作为丢包判断条件、consider_timestamp是以时间戳作为丢包判断条件
- 定义了一个数组nack_batch, 所有被判定为丢包的数据都会插入到该数组中
- delay_timed_out : 现在的时间减去nackInfo创建的时间的差值是否大于send_nack_delay_ms_(默认为0), 所以通常情况下delay_timed_out = true.
- nack_on_rtt_passed : 从上次发送开始到现在, 是否超过了一个RTT时长
-
如果在同一个RTT之内连续发送nack消息, 在对端将丢失的包发过来之后, 本端还没有收到, 继而又发送了nack消息, 就会导致:
-
给发送端增加了不必要的工作量
-
导致发送端重复发送相同的数据, 增加了带宽负担
- nack_on_seq_num_passed : nackInfo的send_at_time属性=-1 ,即第一次发送, 且通过判断包的位置确定这个包是newest_seq_num前面的包
- 代码第350行通过综合上述三个参数去执行甄别是否真的丢包的逻辑
-
这里if判断中可以通过seqNum作为丢包判断条件, 也可以通过consider_timestamp作为丢包判断条件
-
这里是选择的通过seqNum作为丢包判断条件
-
关于这个逻辑, 有一个疑问: 符合if条件的数据已经直接插入到nack_batch中了,然后判断nack_list中的下一个nackInfo, 直到遍历完整个列表即可, 那么这里的retry是在尝试做什么? (找到答案后会在这里作出解答)
-
答: 就是每次执行这个函数的时候, 发现这个丢失的包是个常客(一直没有被重传过来), 次数达到一定数量后就不再浪费资源和时间了, 直接remove掉
SendNack()
发送nack信息
-
将丢包重新放入一个列表中
-
在OnReceivedPacket()中调用SendNack(), 参数buffering_allowed = true, 就会在SendNack()执行的时候, 不发送nack消息
-
这么做的原因是: 为了应对包乱序的情况, 在上述一系列处理逻辑执行中,或者执行后, 乱序的包已经收到了, 但是这个包已经被当作了丢失的包, 此时发送nack消息就会不那么合理
-
所以真正的发送nack消息的函数是ProcessNacks()
ProcessNacks()
-
这是一个周期执行的函数
-
这里以时间戳作为是否丢包的判断条件来找到真正的丢包
-
调用SendNack()真正的将nack消息发送出去
-
关于这个函数同样有个疑问: 既然这个函数是周期执行的, 而且在OnReceivedPacket()中调用SendNack()也并没有把nack消息发送出去, 那么为什么还需要在OnReceivedPacket()函数中调用GetNackBatch()函数?
-
答: webrtc 这块有点乱,应该是多次修改后的结果,实际在OnReceivePacket 里调用_SendNack()是不必要的_