webrtc代码走读五(JitterBuffer)

一、 什么是JitterBuffer

        Jitter Buffer也叫做抖动缓冲区,它是实时音视频里面的一个重要模块,它对数据包丢失、乱序、延迟到达等情况进行处理,平滑的向解码模块输出数据包/帧,抵抗各种弱网情况对播放/渲染造成的影响,降低卡顿,提高用户的观看体验。

二、JitterBuffer在音视频系统中的位置

        JitterBuffer在实时音视频系统中的位置如下所示:

 三、 视频JitterBuffer的工作原理

1. JitterBuffer的核心思想

        Jitter buffer的核心思想是用时间换空间,以增大端到端的延迟为代价来换取视频通话的流畅性。当网络不稳定时(抖动发生),增加buffer的长度,多缓存一些数据,以应对将来可能发生的抖动;当网络稳定下来时,减小buffer的长度,少缓存一些数据,降低视频端到端的延迟,提高实时性。因此jitter buffer的运行过程是一个根据抖动来动态调整buffer长度的过程。好的jitter buffer能够在保证尽量不卡的前提下降低端到端的延迟,即它能够在延迟和卡顿率之间取得较好的平衡。

2. 产生抖动的原因

        1) 网络传输路径改变。例如,当前的传输路径是A,但是下一刻路径A上的某个路由器出现了故障,这时候数据包的路径就会发生改变,导致端到端的传输时长发生变化。

        2) 网络自身的抖动。很多情况下网络有噪声,产生抖动是很正常的。

        3) 网络发生拥塞。拥塞发生的时候,数据包会在路由器上排队,导致端到端延迟变大。

        4) 抗丢包手段带来的额外抖动。网络出现丢包的时候,我们一般会使用nack/arq去重传数据,重传会带来额外的延迟。
 3. 计算抖动的方法

        数据包传输时长的变化就是抖动,假设相邻的两个数据包packet1和packet2,它们发送时间戳是send_timestamp1和send_ timestamp2,接收时间戳是recv_ timestamp1和recv_ timestamp2,那么它们之间的抖动可以按照下面的方法计算:

这是最简单的计算方法,要想准确计算出网络抖动还需要考虑很多因素,这里不再赘述。

4. JitterBuffer的工作原理

1) 接收侧收到数据包,开始组帧,这一步是必须的,帧不完整会导致花屏。

2) 每个帧组好之后,放进buffer里,然后按照帧序号进行排序。

3) 检查帧的参考关系。对于解码器来说,如果一个帧的参考帧丢失了,那么这个帧将解码失败或者花屏,所以参考关系必须要满足之后才能把数据送进解码器里。

4) 根据每一帧的时间戳(采集时间戳或者发送时间戳)以及接收时间戳计算抖动。这里的难点在于如何精确计算抖动。

5) 根据抖动计算buffer的长度。

6) 根据抖动自适应的调整buffer长度。抖动越大,预留的buffer长度越大,这样可以利用增加延迟的方式来降低卡顿;抖动越小,预留的buffer长度越小,这样可以降低延迟。

四、浅析webrtc里的视频JitterBuffer

 1.WebRTC里视频JitterBuffer的运行机制

        Jitterbuffer被两个线程操作,写线程负责组帧完成之后把数据写入JitterBuffer里,读线程负责从JitterBuffer里读取数据然后解码。

写线程:

1) 判断当前视频帧是否有效,把帧插入buffer里,然后移除buffer里过期的、无效的帧;

2) 判断帧之间的参考关系是否已经满足;

3) 如果当前帧可以解码,那么激活解码线程(读线程)。

读线程:

1) 找到buffer中第一个可以解码的帧(假设它是frame):如果这个帧的渲染时间戳是无效的,那么根据当前的抖动(开始的时候抖动值是0,它在步骤3中被更新)计算每个帧的渲染时间戳(render timestamp),并保存在帧信息中,然后根据这个帧的渲染时间戳和当前时间计算最大需要等待的时间(最大的等待时间不会超过200毫秒),然后休眠等待;

2) 如果在等待的时间内还有新的可以解码的帧到来,那么重复步骤2,直到超时;

3) 根据frame的时间信息以及帧大小计算新的抖动值,并用这个抖动更新当前的抖动。

2. 计算抖动延迟

        抖动延迟由网络抖动延迟、解码延迟、渲染延迟构成。其中,解码延迟和渲染延迟比较稳定,网络抖动延迟是动态变化的。计算网络抖动是Jitterbuffer的核心之一。webrtc认为网络抖动由两个部分构成: 

1) 网络噪声带来的抖动延迟,也叫做网络排队延迟。

2) 传输大的视频帧(特别是关键帧)对网络造成冲击带来的抖动延迟。

        为了准确估算出抖动延迟,必须要估算出网络排队延迟和信道速率(通过信道速率可以计算大的视频帧对网络造成的冲击所带来的延迟) 。webrtc使用卡尔曼滤波估算网络排队延迟和信道速率。卡尔曼滤波是一种预测的算法,它以协方差为标准,根据上一时刻的系统状态估算当前时刻系统的状态,然后根据当前的测量值调整当前时刻系统的状态,最后得到当前最优的系统状态。它认为估算出来的值和测量出来值都是有偏差的,因此要根据一个偏好因子(卡尔曼滤波增益系数)来判断我们最后需要的值更加偏向于估计值还是测量值。由于卡尔曼滤波比较复杂,这里并不打算深入探讨,下面介绍一下使用卡尔曼滤波计算网络抖动延迟的大致流程:

        1) 抖动的计算与信道速率、网络排队延迟有关,因此要计算抖动,就必须先计算信道速度和网络排队延迟。

        2) 把信道速率和网络排队延迟当作系统状态,算法的目标就是估算出最优的信道速度和网络排队延迟。假设系统是一个线性系统,如果网络非常好,那么很容易估算出当前系统的状态等于上一个时刻的系统状态,也就是说信道速度和网络排队延迟保持不变。

        3) 但是实际上网络是动态变化的,因此需要对估算出的这个系统状态(即信道速度和网络排队延迟)进行调整。

        4)调整的具体方式:

        5)根据抖动延迟的观测值(两帧传输时长的变化值)和预测值(根据上一个系统状态推导出来),计算它们的残差;

        6)利用残差计算网络噪声;

        7) 根据抖动延迟观测值、前后两帧大小差值、网络噪声、系统误差协方差等计算卡尔曼增益系数。

        8)利用卡尔曼增益系数更新系统状态(即信道速率和网络排队延迟)。

        9)根据更新后的系统状态计算抖动延迟:

        3. 根据抖动延迟计算视频帧的渲染时间 

        得到网络抖动延迟之后,计算总的抖动延迟:jitter_delay = net_jitter_delay + decode_delay + render_delay。然后根据抖动延迟和当前的时间,计算什么时候渲染当前的视频帧,然后根据渲染时间和当前时间确定当前帧在解码之前需要等待的时间(wait_time),通过wait_time保证了各个视频之间是平滑的,减少了卡顿。另外在等待的时间内也可以缓存更多的视频帧,避免了下一次遇到弱网时再次卡顿。

  • PacketBuffer:负责帧的完整性,保证组成帧的每个包序列号连续,并且有一个包标识帧的开始,有一个包标识帧的结束;

         

  • RtpFrameReferenceFinder:负责给每个帧设置好参考帧,同时兼顾GOP内各帧的连续性;

  • FrameBuffer:负责帧的连续性和可解码性,这里帧的连续性是指某帧的所有参考帧都已经收到,帧的可解码性是指某帧的所有参考帧都已经被解码;

 

  • VCMJitterEstimator:计算抖动(googJitterbufferMS),用于计算目标延迟(googTargetDelayMs),用于音视频同步;

         

  • VCMTiming:计算当前延迟(googCurrentDelayMs),用于计算渲染时间。

 

 五、 JitterBuffer结构和基本流程

         RtpVideoStreamReceiver类收到RTP包后,交给PacketBuffer类缓存、排序。

        PacketBuffer收集满1个完整的帧后,交还给RtpVideoStreamReceiver类。

        RtpVideoStreamReceiver类将一个完整的帧交给RtpFrameReferenceFinder。

        RtpFrameReferenceFinder类缓存最近的GOP,每个完整帧落在一个GOP中会填充好该帧的参考帧,交还给RtpVideoStreamReceiver。

        RtpVideoStreamReceiver将填充好参考帧的完整帧交给FrameBuffer,FrameBuffer判断某帧的所有参考帧都收到认为该帧连续,在某帧的所有参考帧都解码后认为该帧可以解码,从而可以交给解码器。

        可以认为JitterBuffer的这些模块分三个层次分别做了RTP包的排序、GOP内帧的排序、GOP之间的排序:

  • 包的排序:PacketBuffer;
  • 帧的排序:RtpFrameReferenceFinder;
  • GOP的排序:FrameBuffer。

六:帧完整性 - PacketBuffer

          6.1 包缓存

        PacketBuffer类有两个类型的包缓存:

  • std::vector data_buffer_,数据缓存,保存包原始数据,用于拼接整帧原始数据;
  • std::vector sequence_buffer_,排序缓存,保存包连续性信息,用于缓存包序列号等信息并排序成完整的帧。 

         6.2  帧的开始和结束 

         

        在这里重点强调一帧第一个包的标识是因为该标识对判断帧的完整性有重要作用,另外,一帧的最后一个包就是简单根据RTP头中的marker位来标识,只有在第一个包、最后一个包都取到并且中间的所有包都连续的情况下,才认为是一个完整的帧。

        6.3  插入RTP数据包 - PacketBuffer::InsertPacket 

          数据缓存、排序缓存这两个包缓存都是初始长度为size_(512)的数组,一旦缓存满会倍增容量,直到达到最大长度max_size_(2048)。

        插入包的过程就是把数据填入这两个缓存的过程,同时会判断是否出现丢包,如果出现丢包则等待,在没有出现丢包的情况下,会判断是否已经获得了完整的帧,如果已经组装好了若干完整的帧,则通过OnAssembledFrame回调通知RtpVideoStreamReceiver。

        

        

         6.4  丢包检测 - PacketBuffer::UpdateMissingPackets

        PacketBuffer维护一个丢包缓存missing_packets_,主要用于在PacketBuffer::FindFrames中判断某个已经完整的P帧前面是否有未完整的帧,如果有,该帧可能是I帧,也可能是P帧,这里并不会立刻把这个完整的P帧向后传递给RtpFrameReferenceFinder,而是暂时清除状态,等待前面的所有帧完整后才重复检测操作,所以这里实际上也发生了帧的排序,并产生了一定的帧间依赖。         

           6.5 连续包检测 - PacketBuffer::PotentialNewFrame

PacketBuffer::PotentialNewFrame(uint16_t seq_num)函数用于检测seq_num前的所有包是连续的,只有包连续,才进入完整帧的检测,所以叫“潜在的新帧检测”。 

         

        6.6 帧完整性检测 - PacketBuffer::FindFrames 

        

         PacketBuffer::FindFrames函数会遍历排序缓存中连续的包,检查一帧的边界,但是这里对VPX和H264的处理做了区分:

        1)对VPX,这个函数认为包的frame_begin可信,这样VPX的完整一帧就完全依赖于检测到frame_begin和frame_end这两个包

        2)另外这里对H264的P帧做了一些特殊处理,虽然P帧可能已经完整,但是如果该P帧前面仍然有丢包空洞,不会立刻向后传递,会等待直到所有空洞被填满,因为P帧前面可能有I帧,如果I帧还不完整,即使向后传递也无法解码。

        

        七:总结

        1)PacketBuffer::InsertPacket向包缓存插入RTP数据,并触发帧完整性检查;

        2)PacketBuffer::PaddingReceived处理空包,并触发帧完整性检查;

        3)PacketBuffer::UpdateMissingPackets,更新丢包信息,用于检查P帧前面的空洞;

        4)PacketBuffer::PotentialNewFrame,判断包的连续性,只有连续的包才检查帧完整性;

        5)PacketBuffer::FindFrames,帧完整性检查,如果得到完整帧,则通过OnAssembledFrame回调上报。

八  查找参考帧 - RtpFrameReferenceFinder

        

        上图描述了RtpFrameReferenceFinder的基本工作原理,顾名思义,RtpFrameReferenceFinder就是要找到每个帧的参考帧。I帧是GOP起始帧自参考,后续GOP内每个帧都要参考上一帧。 

         RtpFrameReferenceFinder维护最近的GOP表,收到P帧后,RtpFrameReferenceFinder找到P帧所属的GOP,将P帧的参考帧设置为GOP内该帧的上一帧,之后传递给FrameBuffer。

        RtpFrameReferenceFinder还保证GOP内帧的输出连续,对H264来说,每收到一帧都判断该帧的第一个包的序列号是否与之前GOP收到的最后一个包序列号连续,是则输出连续帧,否则缓存等待直到连续。对VPX,只需要简单判断PID是否连续即可。这种连续传递的依赖关系会导致GOP内任一帧丢失则GOP内的剩余时间都处于卡顿状态。

       8.1 图像ID - PID

         PID(Picture ID)是每帧图像的唯一标识,VPX定义了PID,但是H264没有这个概念,RtpFrameReferenceFinder使用每帧的最后一个包的序列号作为H264帧的PID。

        在一个GOP内,除了I帧、P帧之外,可能还有WebRTC为补偿发送码率填充的空包,也会占用一个序列号。I帧是GOP的开始,没有连续性问题,但是要判断当前收到的P帧是否连续则需要判断该P帧的第一个包序列号-1是否等于该GOP当前收到的最后一个包序列号,可能是上一帧的最后一个包,也可能是一个填充包。

        RtpFrameReferenceFinder定义的的GOP表结构:

        keyvaluelast_seq_num:I帧最后一个包序列号,PIDlast_picture_id_gop:GOP内最新的一个帧的最后一个包的序列号, 用于设置为下一个帧的参考帧。last_picture_id_with_padding_gop:GOP内最新一个包的序列号,有可能是last_picture_id_gop,也有可能是填充包,用于检查帧的连续性。

        8.2 设置参考帧 - RtpFrameReferenceFinder::ManageFramePidOrSeqNum

        该函数用于检查输入帧的连续性,并且设置其参考帧。

         

        8.4 处理填充包 - RtpFrameReferenceFinder::PaddingReceived       

        该函数缓存填充包,并更新填充包状态,假如该填充包刚好填补了当前GOP的序列号空洞,则有可能有缓存的P帧进入连续状态,所以尝试处理一次缓存的P帧。

         

        8.5 更新填充包状态 - RtpFrameReferenceFinder::UpdateLastPictureIdWithPadding 

         8.6 处理缓存的包 - RtpFrameReferenceFinder::RetryStashedFrames            8.7 总结

        RtpFrameReferenceFinder缓存GOP信息,每个帧(以及填充包)进入GOP排序,如果某个帧连续,则设置其参考帧为GOP内上一帧并输出,I帧不需要参考帧,P帧需要参考帧 。

        9 有序输出 - FrameBuffer

        上节的RtpFrameReferenceFinder为了设置P帧的参考帧为上一帧,保证了GOP内帧的有序,但是不保证GOP的有序,这个保证是由FrameBuffer来实现。   

         如上图所示,FrameBuffer按照帧的先后顺序向解码器输出帧。FrameBuffer按顺序输出“可解码”的帧,这里的“可解码”意思是某帧“连续”、并且其所有参考帧都已经被解码,这里“连续”的意思是指某个帧的所有参考帧都已经收到。I帧是自参考的,所以直接是可解码的,但是P帧则需要等待所有参考帧,也就是上一帧被收到。

        这样,因为PacketBuffer、RtpFrameReferenceFinder这两个类只是保证帧的完整、GOP内帧的有序,一旦当前GOP的P帧还未完整,下个GOP的I帧提前进入FrameBuffer,则会直接丢弃当前GOP的所有后续P帧。

9.1 插入帧 - FrameBuffer::InsertFrame

        该函数将当前帧插入帧缓存,如果该帧的所有参考帧都已经收到,那么认为该帧是连续的,那么通过同步事件通知解码线程取待解码帧,同时通知参考该帧的所有帧,检查他们的未连续参考帧数量是否已经为0,是则连续。

   ​​​​​ 

9.2 更新参考帧信息 - FrameBuffer::UpdateFrameInfoWithIncomingFrame 

        该函数检查某帧的参考帧是否已经连续,初始化未连续参考帧计数器num_missing_continuous、未解码参考帧计数器num_missing_decodable,同时反向建立被参考帧与依赖帧之间的关系,方便状态(连续、可解码)传播。 

9.3 FrameBuffer::NextFrame

        该函数从帧缓存中获取一个可以解码的帧,该帧必须是连续的(所有参考帧都已经收到),并且其所有参考帧都已经被解码。对I帧来说本身是连续的且自参考,可以直接被取走,P帧则需要依赖参考帧的连续、解码状态。 

 ​​​​​​​        

         

        可解码性传播: 

 

        9.4 总结

         FrameBuffer缓存即将进入解码器的帧,按照顺序向解码器输出连续的、所有参考帧都已经被解码的帧。

     10 抖动与延迟

        JitterBuffer包含Jitter与Buffer,上面几节讲了Buffer,主要用于缓存、排序、组帧、有序输出,起到抗抖动的作用。但是网络的具体抖动指标是多少,网络的延迟是多少,需要其他的一些工具计算。

         10.1 抖动计算             ​​​​

  • VCMInterFrameDelay:计算帧间延迟 = 两帧的接收时间差 - 两帧的发送时间差;

  • VCMJitterEstimator:通过VCMInterFrameDelay计算的帧间延迟计算出最优抖动值。

        上图描述了帧间延迟(抖动)观测值的计算方法:jitter = tr_delta - ts_delta = (tr2 - tr1) - (ts2 - ts1),也就是两帧的接收时间差 - 两帧的发送时间差。

        计算最优抖动的算法和GCC中使用到达时间滤波器(InterArrival)计算到达时间增量、使用过载估计器(OveruseEstimator)计算最优的到达间隔增量的算法基本一样,都是利用卡尔曼滤波器,综合帧间延迟的观测值、预测值,获得最优的帧间延迟(也就是网络抖动),只是数据采样的形式不太相同,GCC使用5ms的包簇(也可以称为帧),这里直接使用视频帧,这里不再详述。 

        10.2 延迟 - VCMTiming 

         VCMTiming可以输出接收端的以下参数,这些参数可以在使用浏览器拉流时在chrome://webrtc-internals页面中看到。

名字含义
googDecodeMs最近一次解码耗时.
googMaxDecodeMs最大解码耗时,实际上是第95百分位数,也就是大于采样集合95%的解码延迟.
googRenderDelayMs渲染耗时,固定为10ms.
googJitterBufferMs网络抖动,见上节.
googMinPlayoutDelayMs最小播放时延,音视频同步器输出的视频帧播放应该延迟的时长.
googTargetDelayMs目标时延,googCurrentDelayMs会逼近目标延迟.
googCurrentDelayMs当前时延,用于计算视频帧渲染时间.

 10.2.1 目标延迟 - googTargetDelayMs

        很明显,目标延迟基本上就是抖动+解码时间+渲染时间,与播放延迟的最大者,也就是播放当前帧总体的期望延迟,作为当前延迟googCurrentDelayMs的参考值,并最终用于音视频同步。

10.2.2 当前延迟 - googCurrentDelayMs 

        FrameBuffer每获得一个可解码帧会调用一次,更新当前延迟,最终用于计算渲染时间。

10.2.2 平滑渲染时间 - TimestampExtrapolator 

         FrameBuffer每获得一个可解码帧,都要更新其渲染时间,渲染时间通过TimestampExtrapolator类获得。TimestampExtrapolator也是一个卡尔曼滤波器,其输入为输入帧的时间戳,TimestampExtrapolator会根据输入帧的时间戳的间隔计算输出渲染时间,目标是平滑输出帧的时间间隔。

        视频帧的最终渲染时间 = 帧平滑时间 + 当前延迟。

 

11 总结 

        RTP包进入JitterBuffer后,最终输出了完整、连续、可解码的视频帧,并携带了可用于最终播放的渲染时间。

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值