WebRTC中NACK的处理流程

421 阅读7分钟

NACK调用栈

  • RtpDemuxer :

  • 在音视频引擎模块中, 将从网络中收到的RTP包分发给不同的channel, 音频包分发给voice Channel, 视频包分发给videochannel

  • 这里通过RtpDemuxer将RTP分发给不同的流, 底层每个流对应不同的编解码器, 接收流对应解码器, 发送流对应编码器

  • 如果这里收到的是RTX包, 就会分发给RtxReceiveSream去处理, 如果这里收到的是RTP包, 就会分发给RtpVideoStreamReceiver去处理(RTX处理完之后,同样会执行这里的流程)

  • ReceivePacket : 包含复杂且重要的逻辑

  • OnReceivedPayloadData : 如果这次收到的包与上次收到的包之间有间隔, 就会调用OnReceivedPacket

  • OnReceivedPacket : 计算出到底丢了哪些包, 然后生成NACK消息发给发送端

ReceivePacket()

  1. 如果开启了fec, 会首先执行fec逻辑会对nack逻辑有干扰, 这里以关闭fec功能为前提来分析nack逻辑

  2. 跳过fec逻辑, 首先会通过payload_type_map根据pt找到rtp解包器, 使用解包器对rtp包进行解析, 从而得到rtp的payload

  3. 将解析得到的payload作为参数传给OnReceivedPayloadData()

OnReceivePayloadData()

  1. 首先对header中进行域的填充, 例如: 视频方向、视频类型、是否最后一个包
  2. 从扩展头中获取信息, 并更新到header中, 至此一个(视频)帧的基本情况就搞清楚了
  3. 后面的三个if语句逻辑尤为重要
  • if (video_header.is_last_packet_in_frame) : 从扩展头中获取色彩空间信息
  • if (loss_notification_controller) : 处理丢失找回的包
  • if (nack_module) : 检测是否发生丢包, 并将丢失的包记录下来
  1. rtcp_feedback_buffer_.SendBufferedRtcpFeedback() : 发送nack消息

  2. 最后将该packet插入到packet_buffer_中为组帧做准备

下面来看nack_module分支的逻辑

  1. 这个包是否属于关键帧

  2. 调用nack_module的OnReceivedPacket()判断是否发生丢包

OnReceivedPacket()

  1. 首先判断是否初始化, 初始化完成后退出
  •  将这次收到的包的seqNum赋值给newest_seq_num

  • 判断是否为关键帧, 并插入keyframe_list中

  1. 判断这个包是否跟上一个收到的包重复, 重复包不处理
  2. 如果已经完成初始化, 并且不是重复包, 则通过AheadOf()判断这个包是不是newest_seq_num前面的包
  • 如果true, 说明这个包是一个晚到达的乱序包, 不需要NACK处理, 需要到nack列表里查找是否存在这个包, 并且删除
  1. 通过了上面逻辑的筛选, 说明这次收到的是一个新的包
  • 判断并保存关键帧的第一个包, 这里要注意: 只有关键帧的第一个包才能插入keyframe_list_中, 该判断逻辑是在OnReceivedPayloadData()中的if(nack_module)分支中执行的
  • keyframe_list_容量为10000, 把超出容量的老旧的包删除. 这里要注意: kMaxPacketAge并不是keyframe_list_的长度, 而是keyframe_list_中最大的seq_num和最小的seq_num的差值, 实际中keyframe_list_中的数据量远小于10000. 这里做这个最大值的限制原因是: 实时通讯系统中注重实时性, 数据的时效性尤为重要, 当缓冲的数据过于老旧的时候会影响通讯质量
  1. 判断是否找回来的包, 即is_recovered, recovered_list_的逻辑同keyframe_list_
  2. 此时来到代码的173行, 经过以上逻辑, 说明这个包符合一下几种情况
  • 不是第一个包

  • 不是重复包

  • 不是迟到的乱序包

  • 不是找回的包

  • 这个包的seq_num正好是newest_seq_num+1, 正常顺序包,不需要处理

  • 这个包的seq_num是newest_seq_num+N, 即可能出现了丢包

  • 丢了N个包, 这里调用**AddPacketsToNack()**将这N个丢失的包记录下来

  • 这N个包可能因为网络抖动出现了乱序没有按时到达, 不属于丢包, 同样记录下来

  1. 通过**GetNackBatch()**函数判断是否发生了真正的丢包
  2. 最后调用nack_sender_->SendNack()将真正丢包的nack消息发送给对端
  • 这里要注意 : SendNack的第二个参数为true, 原因下文讲解

AddPacketsToNack()

这个函数的逻辑是将包序号丢失的seq_Num插入到nack_list_列表中, 作为可疑丢包等待下一步处理

  • 这个函数同样本着数据实效性的原则, 限制缓存长度

  • 这里有个极端情况, 就是275行, 当清理列表后仍然达不到长度要求时, 就将nack_list_清空, 并向对端请求一帧IDR帧以重启不工作的解码器

  • 再次循环recovered_list_, 看是否已经被找到, 否则视为可疑的丢失的包插入nack_list_中

GetNackBatch()

这个函数的逻辑只是甄别出可疑丢包列表中的真实丢包

  1. 定义的两个布尔变量: consider_seq_num是以seqNum作为丢包判断条件、consider_timestamp是以时间戳作为丢包判断条件
  2. 定义了一个数组nack_batch, 所有被判定为丢包的数据都会插入到该数组中
  3. delay_timed_out : 现在的时间减去nackInfo创建的时间的差值是否大于send_nack_delay_ms_(默认为0), 所以通常情况下delay_timed_out = true. 
  4. nack_on_rtt_passed :  从上次发送开始到现在, 是否超过了一个RTT时长
  • 如果在同一个RTT之内连续发送nack消息, 在对端将丢失的包发过来之后, 本端还没有收到, 继而又发送了nack消息, 就会导致:

  • 给发送端增加了不必要的工作量

  • 导致发送端重复发送相同的数据, 增加了带宽负担

  1. nack_on_seq_num_passed : nackInfo的send_at_time属性=-1 ,即第一次发送, 且通过判断包的位置确定这个包是newest_seq_num前面的包
  2. 代码第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()是不必要的_