1. 网络JB抖动缓冲的主要功能
视频会议网络抖动缓冲接收网络包并对其进行排序组帧后发给混频器解编码,所以整个过程可以分为暂存、排序、组帧和丢包处理四个部分。为了使模块更清晰,媒体处理更高效,我们的抖动缓冲采用二级缓冲策略。一级缓冲为RTP处理缓冲,二级缓冲为音视频通用缓冲。
视频会议网络抖动缓冲接收网络包并对其进行排序组帧后发给混频器解编码,所以整个过程可以分为暂存、排序、组帧和丢包处理四个部分。为了使模块更清晰,媒体处理更高效,我们的抖动缓冲采用二级缓冲策略。一级缓冲为RTP处理缓冲,二级缓冲为音视频通用缓冲。
![](https://i-blog.csdnimg.cn/blog_migrate/6cb58ab43541da78f4c450d872ee0385.gif)
图 1. 网络抖动缓冲工作流程
1.1 暂存
因为数据写入RTP缓冲区是在RTP回调中进行的,所以在RTP数据读取的过程中需要把RTP数据包一次性读出来。为了提高RTP回调处理效率并减少大块数据拷贝次数,在RTP数据写入缓冲区的过程中不进行排序,而是按照数据包到来的顺序直接写入一级缓冲区(见图1(b))。当缓冲满以后,缓冲主动丢包。
因为数据写入RTP缓冲区是在RTP回调中进行的,所以在RTP数据读取的过程中需要把RTP数据包一次性读出来。为了提高RTP回调处理效率并减少大块数据拷贝次数,在RTP数据写入缓冲区的过程中不进行排序,而是按照数据包到来的顺序直接写入一级缓冲区(见图1(b))。当缓冲满以后,缓冲主动丢包。
1.2 排序
为减少数据拷贝,二级缓冲不再对一级缓冲进行数据缓冲,而对数据相关的信息(主要是该包对应于一级缓冲的地址和RTP包头信息)进行缓冲(图中绿色为数据包,蓝色表示数据包对应的信息包)。RTP接收任务读取二级缓冲的数据包(可能存在抖动、丢包、乱序),按照"插空排序"方法将信息写入二级缓冲的适当位置(见图1(c))。这种"插空排序"方法按序号将包分成正常包、早到包、晚到包和错误包四种包。因此,这四种包插入二级缓冲的方法略有不同。对于正常包和错误包,它们将被插入到上一个正常包或早到包的后面;对于早到包和晚到包则根据序号计算被插入到相应的位置。正常包、早到包、晚到包的插入位置统一计算公式如下:
nIndex = (((nWrite-1) + (nCurrSeq-nPrevSeq)+ MAX_SEQUENCE)
% MAX_SEQUENCE) % MAX_RING_BUFFERS;
下面来举例说明一帧三个数据包插入的具体计算方法(插包过程见图1(b)到图(c))。
a.插入第一个包
一级缓冲的第一个包(序号为1)放入二级缓冲之前,有已知初始条件:
MAX_SEQUENCE:65536 /*数据包序号不能超过的最大值*/
MAX_RING_BUFFERS:150 /*缓冲环的最大缓冲单元数目 */
nPrevSeq:0; /*先前插入包的序号*/
nWrite:0; /*下一个插入包的缓冲索引位置*/
那么,在第一个包(序号nCurrSeq为1)放入二级缓冲时,按公式计算当前插入位置有:
nIndex = ((0-1+(1-0)+65536)%65536)%150 ; /*当前插入包的缓冲索引位置*/
即nIndex等于0。所以这个包插入到二级缓冲的第一个缓冲(缓冲索引位置nIndex为0)。
另外,nPrevSeq刷新为nCurrSeq的值,即1;nWrite刷新为nIndex+1的值。
b.插入第二个包
一级缓冲的第一个包插入到二级缓冲之后有:
nPrevSeq:1; /*nPrevSeq=nCurrSeq=1*/
nWrite:1; /* nWrite=nIndex+1=0+1=1*/
那么,第二个包(早到包,序号nCurrSeq为3)放入二级缓冲时,按公式计算当前插入位置有:
nIndex = (((1-1+(3-1)+65536)%65536)%150 ; /*当前插入包的缓冲索引位置*/
即nIndex=2。所以这个包插入到二级缓冲的第三个缓冲(缓冲索引位置nIndex为2)。
c.插入第三个包
一级缓冲的第二个包插入到二级缓冲之后有:
nPrevSeq:3; /*nPrevSeq=nCurrSeq=3*/
nWrite:3; /* nWrite=nIndex+1=2+1=3*/
那么,第三个包(晚到包,序号nCurrSeq为3)放入二级缓冲时,按公式计算当前插入位置有:
nIndex =((3-1+(2-3)+65536)%65536)%150; /*当前插入包的缓冲索引位置*/
即nIndex=1。所以这个包插入到二级缓冲的第二个缓冲(缓冲索引位置nIndex为1)。
特别要声明,晚到包不刷新nPrevSeq和nWrite。
1.3 组帧及丢包错序处理
在组帧之前,缓冲需要等待I帧,丢掉这个I帧之前的所有P帧。特别一点,如果缓冲来的第一个包刚好是I帧包,那么该I帧所有包都要丢掉。
在读取二级缓冲进行组帧时,需要对即将组帧的包进行检测,看是否存在丢包或错序。
丢包检测就是查看缓冲中当前读取位置nRead到该帧最后一个包的位置nRead+x总共x个包中是否存在空闲缓冲单元(即到目前为止,该缓冲单元还没有包插入过)。错序检测就是检测从nRead开始的第一个包到该帧的最后一个包是否存在错误序列号。判断是否是错误序列号的依据是根据理论估计值和实际值比较是否相等,若不等则说明是错误序列号。
目前,丢包和错序的处理一样,就是丢掉当前帧。如果当前帧是I帧的话,还需要设置I帧等待标志为TRUE。
[NOTE:这中错序检测算法不同于终端的检测算法,终端只对一帧中的第一个包做错序检测而不是所有的包。显然,错序检测必须是针对一帧中所有包的。]
2. 二级缓冲的允许读取的最低包数
从二级缓冲中读取数据包进行组帧必须满足以下条件:
缓冲中已经至少有一帧的数据并且数据包总数要大于一定的值(这里称其为阀值,threshold)。这个阀值的设置具有一定意义。其值不能设置过小也不能设置过大,因为过小,效果就不明显(这个效果后面会提到),过大则导致时滞加大(这里的时滞指缓冲上一次读走的一帧和最近存入的一帧之间的时间差,这种时间差越大,则让人感觉运动图象的时滞也越强)。目前视频会议数据派发模块设置的二级缓冲阀值为8个包。由于阀值的存在,实际上二级缓冲已经被分化为两个区域。根据这两个区域的不同特性,我们命名它们为活跃区域和稳定区域,即ACTIVE REGION和INACTIVE REGION两个区域。在INACTIVE REGION这个区域,因为这些包处在缓冲区的下层,所以相对稳定,新的包(可能乱序)插入该区域的可能性较小,另外,本该到达的包还未到达的可能性也较少。而在ACTIVE REGION中,新的包不断插入该区域,另外,乱序和有包未到达的可能性较大。显然,ACTIVE REGION中的数据包不宜读取。
将缓冲分成两个区域的好处如下:
a. INACTIVE REGION区域比较稳定,组帧操作从该区域读取数据包时出现未到包的可能性较小,所以,该操作流产的可能性也较小。
b. 减少组帧操作过于频繁地查询缓冲中的数据包。
![](https://i-blog.csdnimg.cn/blog_migrate/c7096ac9445ca4d705fcc753322b3637.gif)
图 2. 二级缓冲的活跃区域和稳定区域
将冲分成两个区域的坏处就是INACTIVE REGION的包在缓冲快满时需要保护。因为在乱序比较厉害的时候,如果不保护,乱序包可能会不小心插入该区域。
3. 二级缓冲组帧后的数据传递
前面说到,二级缓冲是对一级缓冲数据包的信息存储和非数据存储,所以二级缓冲通过组帧必须能够传递有效信息给下一层,并使下一层模块能够根据传递的信息读取一级缓冲中的数据。整个过程由信息包到数据包,相对于先前的操作刚好是一个反向的过程(抽象的过程如图3)。
图 3. 下一层模块根据信息包读取数据包的过程
前面说到,二级缓冲是对一级缓冲数据包的信息存储和非数据存储,所以二级缓冲通过组帧必须能够传递有效信息给下一层,并使下一层模块能够根据传递的信息读取一级缓冲中的数据。整个过程由信息包到数据包,相对于先前的操作刚好是一个反向的过程(抽象的过程如图3)。
![](https://i-blog.csdnimg.cn/blog_migrate/b56f7f518fb71dc7a32263bc878bd10c.gif)
图 3. 下一层模块根据信息包读取数据包的过程
显然下一层模块要通过二级缓冲信息包中的部分信息去读取数据,所以信息包中必须包含数据包的地址和长度等信息。那么如何将一帧中多个包的信息同时传递给下一层模块呢?这里的做法是利用UB(unit buffer,单元缓冲,一种我们公司设计的内参管理方法。在后面的文章中会详细分析这种设计目的和优缺点)分配一段小内存,然后将各个信息包中的地址信息逐个存入该段内存。数据包的长度信息存放在数据包地址的头2个字节里。下一层模块通过上层传递的UB内存读取数据包的过程如图4所示。
[NOTE:为了省拷贝,第二级缓冲在排序时不搬移一级缓冲中的数据包,而是存储数据包对应的经过排序的信息包。这样的设计最终导致了必须要传递地址给下一层模块才能实现数据的间接传递。]
优点:这种结构可以了减少一次数据拷贝造成的CPU消耗。另外,组帧和取帧操作由两个不同的任务完成,并且每帧数据均由链表上的一段相连连续的节点组成(见图4 链表上的一帧数据),所以,缓冲任务不需要定时地逐个包地去查询缓冲中是否已经有一帧完整的数据到达,只要查看帧到达标志就可以,所以,这样做可以在一定程度上提高取帧的效率。
缺点:在排序插入数据包时,特别是在有乱序丢包环境下,这种链表结构的缓冲存储信息包的效率显得有点低。还有,在一些临界条件下,链表操作容易出错;而且,一旦出错后,不易恢复。
新抖动缓冲的二级缓冲比较简洁,没有采用任务链表方式来存储信息包,只是采用一个循环环状缓冲来存储所有信息包,其优缺点如下。
优点:结构简单,逻辑少,不易出错,即使出错,也比较容易恢复现场。采用"插空"排序方法效率较高,且不容易出错。
缺点:在排序插入数据包时,特别是在有乱序丢包环境下,这种链表结构的缓冲存储信息包的效率显得有点低。还有,在一些临界条件下,链表操作容易出错;而且,一旦出错后,不易恢复。
新抖动缓冲的二级缓冲比较简洁,没有采用任务链表方式来存储信息包,只是采用一个循环环状缓冲来存储所有信息包,其优缺点如下。
优点:结构简单,逻辑少,不易出错,即使出错,也比较容易恢复现场。采用"插空"排序方法效率较高,且不容易出错。
缺点:因为组帧和取帧操作交由一个任务完成,所以这个任务需要定时地逐个包地去查询缓冲中是否已经有一帧完整的数据到达,所以在这一点上,取帧效率偏低。
在一般情况下,这两种缓冲方法均可以实现对多路数据的缓冲。那么,在这里,缓冲机制的优越主要体现在乱序和丢包处理上。新抖动缓冲采用"插空"排序来处理乱序的方式相对于原来链表排序的方式要好一些,其优势主要体现在逻辑简单效率高。在丢包处理上,新抖动缓冲和原来缓冲的最大区别在与新抖动缓冲的丢包处理更全面而且安全,这主要体现在以下几点:
a. 新抖动缓冲区分I帧和非I帧包(即P帧包)。当网络丢掉I帧包时,二级缓冲置I帧等待标志,等待下一个I帧。如果网络丢失P帧包时,那么二级缓冲仅仅扔掉这个P帧就可以,不用设置I帧等待标志。扔掉P帧包不会造成明显的图象花和停顿现象。
b. 当抖动缓冲组帧做检查时,如果发现有错序现象(意味着网络大规模丢包现象存在),那么需要清整个缓冲并置I帧等待标志(在一个缓冲中存在两段时间相隔较旧的数据,传到底层容易使解码设备解死,这一点已经通过实验得到验证。)。
[NOTE:不清缓冲,久而久之,经过多次之后,最终会使底层BSP15(韩国Equator公司的专业煤体处理器)设备挂起。这一点已经经过丢包乱序测试得以验证。现在的终端和视频会议MCU在经过几百次的丢包模拟测试后,底层BSP15设备均挂起。现在的终端仅仅只对一帧中的第一个包做错序检测,而不是全部,而且,对错序的处理仅仅是设置I帧等待标志而没有清空缓冲。MCU的抖动缓冲则将错序和丢包等同,也没有清空缓冲。]