五:数据的接收和发送

1、 发送过程:

应用发送数据时调用接口 rtp_session_send_with_ts 完成。参数为会话句柄,数据缓冲区地址,数据长度以及应用当前的时间戳。在该接口中,会先调用 rtp_session_create_packet 接口,根据缓冲区地址及数据长度,构造一个新的消息块,并根据会话信息初始化 rtp 头信息。完了将缓冲区中的数据拷贝到消息块中。最后以消息块为参数,调用 rtp_session_sendm_with_ts 接口进行数据发送。rtp_session_sendm_with_ts调用更底层的函数 __rtp_session_sendm_with_ts,在该函数中完成具体的发送处理。下面具体分析该函数的实现:

如果发送还没有启动,也就是说当前是第一次启动,则snd_ts_offset 变量首先被设置为应用当前开始的值。如果启动了调度,则snd_time_offset 设置为调度器运行到现在的时间。这应该算是时间戳的初始化。

如果调度被启用了,则对部分时间戳做一些调整,如下:

首先计算包应该发送的时间,就是packet time。计算方法为在发送第一个数据包时的调度器时间加上包的发送间隔,这个间隔根据应用当前给的时间与第一次的发送给的时间的差 值除以payload 的时钟速率计算得到,比如第一次发送的时间为 100,当前为 300,也就是说发送经过了 200 个单位,如果 payload 的 clock rate 为 10,则说明经过了 20 个时间戳单位, 也就是说当前包的时间戳为调度器时间加 20。(packet time 实际上应该是将下一个包的发送时间转换为调度器时间,交给调度器让调度器来调度)如果计算的packet time与调度器当前的运行时间的差值小于2 的 31 此方,并且二者不相等,则设置该等待点在 packet time唤醒。(关于该比较,参见其他说明部分)

在发送数据前,RTP的时间戳设置为应用传进来的当前的时间戳。snd_last_ts时间戳也 设置为应用当前给的时间戳。

之后就调用实际的发送接口 rtp_session_rtp_send 进行发送。该接口具体会调用 send 系统调用将数据包发送到网络的另一端。

发送完成后调用rtp_session_rtcp_process_send 查看是否需要发送 rtcp 包,依据的原则是:

如果由应用程序询问的最后的时间戳减去以接收单位计算的最后一个rtcp 包发送的时间大于 rtcp 报告包应该发送的时间间隔,或者最后发送数据包的时间戳与按照发送时间戳单位计算的最后一个 rtcp 报告包发送的时间的差值大于 rtcp 应该发送的间隔,就构造 rtcp 的发送者报告包发送。

在构造 rtcp 控制包的过程中,ssrc 源同步描述符采用session 上的源同步描述信息,NTP时间戳使用系统当前的时间加上 1900 到 1970 年间的秒数,实际上这个时间就是 1900 年当当前的秒数了(参见时间戳说明部分)。RTP 时间戳使用snd_last_ts,也就是最后发送的流的时间戳。发送的包数和包字节计数使用session 上RTP 流上统计的计数。另外,如果数据包有被收到,则包含一个报告块,目前的设计也仅只包含一个报告块。数据包构造完成后直 接发送。

如果会话当前的模式为 send-only,则调用 rtp_session_rtcp_recv接收处理 rtcp 包。如果会话支持接收模式,则rtcp 包的接收会在 rtp 接收过程中处理。

2、 接收过程。

数据包的接收是通过调用rtp_session_recv_with_ts 接口完成的。该接口实际上是调用rtp_session_recvm_with_ts 从底层接收数据,将返回的消息块中的有效数据(不包含rtp 头) 拷贝到用户的buffer 中。下面具体看 rtp_session_recvm_with_ts 的实现:

如果接收还没有启动,rcv_query_ts_offset设置为应用给定的初始时间,也就是应用询问的时间,记录了一个开始时间偏移。如果发送没有启动或者为 recv-only 模式,则 session 的 last_rcv_time 设置为系统当前的时间。如果设置了调度器,那么rcv_time_offset 设置为调度器启动后运行到当前所用的时间,这个作为接收的时间偏移。如果接收已经启动了,为了避免针对同一个时间戳连续多次接收,这里判断如果当前应用参数给的时间等于rcv_last_app_ts 也即应用程序最近一次询问的时间戳,那么read_socket 变量设置为 FALSE, 避免连续接收。

接下来进入正常的处理流程,首先将rcv_last_app_ts 设置为当前应用时间,也就是更新当前最后一次接收的时间。如果read_socket 设置了,调用 rtp_session_rtp_recv和 rtp_session_rtcp_recv 接口实际的从底层 socket接收数据。

在 rtp_session_rtp_recv 中接收到数据后会调用 rtp_session_rtp_parse 对数据包进行解析。在rtp_session_rtp_parse 中如果发现数据包是 telephone event 包,则会创建一个事件,将其发送到事件队列上,具体处理参见事件部分的说明。Jitter中相关变量的更新也是在该接口中 进行处理的,通过调用jitter_control_new_packet接口完成。最后将数据包放到接收队列上等

待进一步的处理。 从rtp_session_rtp_recv出来后,会检查会话的telephone event队列,如果不为空,则说

明收到了拨号包,一方面需要调用注册的回调函数,另一方面则需要将其发送给事件队列。之后接收就返回了。如果该队列上没有包,则继续处理:

如果设置了接收同步标识,rcv_ts_offset被设置为当前收到的 RTP 数据包中的时间戳。这作为流的第一个时间戳。rcv_last_ret_ts变量则设置为当前应用给出的时间。这里仅仅是给一个初始的值。之后清掉同步标识。因此之前的偏移 rcv_ts_offset 记录了第一个 rtp 数据包的时间戳。后续到达的数据包将不再经过这里的处理逻辑。

调用接口 jitter_control_get_compensated_timestamp 计算流的时间戳。具体参见 jitter 模块说明。 如果 rtp上的jitter 控制是使能的,那么就会利用 jitter buffer 机制对数据包进行流控, 否则,就直接从队列上取一个新的数据包。在 jitter 使能的情况下,如果 session 的 permissive 算法被启用了,那么就调用 rtp_getq_permissive接口获取数据。在该接口中,判断如果计算出的流的时间戳与 rtp 数据包中记录的时间戳的差值小于 2的31 次方,就从队列中弹出一个包返回,否则返回空。如果没有启用permissive 算法则调用 rtp_getq 接口按照正常方式接收数据包。在该接口中,我们返回时间戳等于或者早于计算的时间戳的数据包,如果这样的数据包不止一个,那么扔掉更老的包,也就是从队列上最先取出来的包,最后返回的就是最近一次取出的数据包。如果有两个数据包有相同的时间戳,那么只返回一个。另外,在该接口中如果有数据包也就是更老的包被丢弃了,那么会把丢弃的包数目记载到reject 参数中返回。

如果上一步确有数据包返回,那么会对数据包中的时间戳进行更新,这部分参见对jitter_control_update_corrective_slide接口的说明。随后将 rcv_last_ts 时间戳更新为包原始到达时的时间戳值,即未更新前的值。接着调用rtp_session_rtcp_process_recv 接口进行 rtcp 的接收处理。(之前是发送处理)触发条件和触发后时间量的修改同发送部分。如果最后一次rtcp 的 sr 报告中的发送计数小于统计量中的发送包数的统计,则调用 make_sr 构造 sr 报告包,同时将之前的统计计数更新为统计量中保存的值。如果该值不小于,则说明不需要发送rtcp 的 sr 报告包,但是如果同时接收的包数大于零,就是说有数据包被接收到,则调用 make_rr 构造 rtcp 的 rr 包。如果包构造成功,则调用 rtp_session_rtcp_send发送包。

之后如果没有启动调度,则直接将包返回给上层,不需要再进行特殊处理,否则进行调度的处理。类似与发送部分,同样是根据应用给定的时间和应用第一次调用接收时的时间差值作为参数,调用rtp_session_ts_to_time 接口计算出包的调度时间间隔。这个间隔加上应用询问第一个包时调度器运行的时间作为包的下次调度时间。如果这个时间在调度器当前的时间之后,则就将这个时间作为唤醒点,等待调度器调度。

接收和发送过程中各个时间戳值的关系如下图所示:

图5-1

六: 防抖动的实现 

关于 jitter 结构体中部分变量的说明:(关于该结构体参见图 2-8) 其中 jitt_comp 为用户定义的防抖动补偿时间,jitt_comp_ts为将其转化为时间戳单位的值,adapt_jitt_comp_ts为使用自适应算法计算后的补偿时间值。 slide 为包期望接收时间和应用接收时间的差值的平均值,prev_slide 为上一次保存的 slide值。 jitter 为 diff(最新得到的包的时间戳值与本地接收时间值的差,用于计算slide)与更新后的 slide 的差值的平均值,olddiff 为上一次计算出的diff 值,inter_jitter为间隔抖动,参见 rtcp 协议(参考1)。

corrective_step和 corrective_slide 为校正步进值和校正滑动值,在更新包里带的时间戳时会用到。

adaptive和 enabled 在介绍 jitter 结构体时已做说明。

在会话初始化的时候,会调用 rtp_session_set_jitter_buffer_params,该接口设置 jitter buffer 的参数。代码中实际上将默认的 jitter时间设置为了 80 毫秒,也就是四个数据块的间隔(针对8KHZ 音频采样数据而言),输入数据包的队列长度设置为了 100,也就是可以缓冲 100 个数据包。同时,打开了jitter 的自适应(adaptive)特性,也就是jitter 自适应补偿(adaptive compensation)。在实际中,用户也可以单独调用rtp_session_set_jitter_compensation 设置 jitter 补偿时间,可以调用rtp_session_enable_adaptive_jitter_compensation 单独设置是否打开自适应补偿功能。

在设置jitter buffer 的时候,会调用接口 jitter_control_init完成 jitter 的初始化。在该接口中,jitt_comp 设置为用户设置的值,该值就是补偿值。另外,调用jitter_control_set_payload 将该补偿值转换为时间戳单位的值,设置给jitter_comp_ts,转换依赖于 payload 的时钟采样率。校正步进值(corrective_step)设置为(160 * 8000 )/pt->clock_rate;大部分音频采样率都是8KHZ,所以应该是按照 160 的时间戳单位来校正。

要使用jitter,需要使能 enabled 变量,要使用 adaptive,需要打开 adaptive 变量。

数据发送过程不需要 jitter 做什么控制,关键是在接收中。数据接收完后并不是直接交给上层应用,而是放到 buffer 中,其实就是队列。Buffer 的大小在 jitter 初始化部分设置,默认为 100(队列的长度),也就是可以缓冲 100 个包,这对一秒钟动辄百十来个网络包的媒体流来讲,其实也缓冲不了多少数据。另外,接收到的包只要解析通过,都先缓冲到队列中, 如果包数目超过了队列大小,则移除最老的包,这也符合常理。后续为应用传递的包都是从队列上取出来的,所以取的也就是最老的包。在数据包是否需要取出来上传给应用就需要jitter 来控制了。

对于已经缓冲到本地的数据包,没有 jitter buffer 控制的情况下我们直接将其返回,如果有控制,则需要判断包的时间戳,只有比给定时间戳老的包(早于给定时间戳到达)才上传给应用。那么这里控制包是否上传给应用,关键的因素就在于给定的时间戳值,这个值是怎么来的呢?在程序中,通过调用jitter_control_get_compensated_timestamp接口计算得到。基本计算式为:

Ts = user_ts + slide –adapt_jitt_comp_ts

为了更好的理解上面的计算式,我们来看上述几个值是如何计算出来的。首先,user_ts, 这是应用程序接收流时给出的时间戳,是基于应用接收的速度和payload 类型计算出来的, 典型的,对于采样率为 8KHZ 的音频数据来说,该时间戳的增加步进值为 160.(按照每20 毫秒采样一次来算)。如果网络传输没有延迟,数据包处理不需要消耗时间,那么user_ts 应该和 rtp 包里带的时间戳值是一致的。

slide,根据前面的介绍,为数据包期望接收的时间和实际本地接收的时间差值的平均值。每次接收到新的rtp包后该值即进行更新,新的 slide 值为之前值乘以 0.99 加上新计算的值乘以0.01。

adapt_jitt_comp_ts的计算依赖于 jitter 的值。Jitter按前面介绍,为 diff 与更新后的 slide 的差值的平均值。同样是已计算得到的值乘以 0.99加上新的到的值乘以 0.01 得到。如果数据包均匀到达,那么diff 的值和slide 的值应该就是相等的,这样 jitter 的值就为零。相反, 如果 jitter 的值变化比较大,那么说明数据包每次到来的间隔参差不一,一定程度上反映抖动比较大。

inter_jitter为间隔抖动,含义和计算参见 rtcp 发送间隔分析(参考3)。从上面计算可以看出,inter-jitter反映了两次间隔的抖动情况,而 slide 则反映了一个比较长期的均匀的抖动情况。

如果打开了adaptive,slide 的值就会不断更新,并且每当收到 50 个包后,adapt_jitt_comp_ts就会被更新。新值为 jitt_comp_ts 和 2*jitter 中较大的一个。

上述计算过程可参见接口jitter_control_new_packet。

现在回过头来再看上面的计算式,slide实际上是个小于零的值,因此我们实际上是努力将时间戳靠近包里自带的时间戳值上去的,补偿值在一定程度上起到了缓冲的作用。

关于数据包时间戳值的更新,如果从队列中取出了数据包,并且当前数据包的时间戳值与之前收到的数据包的时间戳值不一致,在打开adaptive 的情况下,将对数据包的时间戳值进行校正,算法如下:

当前 slide 减去之前的 slide,如果差值大于校正步进值correction_step,则校正滑动值 correction_slide 加上一个 correction_setp,prev_slide更新为 slide 加上 correction_step 的值。如果差值小于 correction_step的负值,则将其转换为正值后按照之前的方式进行相同的更新。之后将包里的时间戳修改为加上 correction_slide 后的值。(如此修改后后续还有用否?数据包已经交给上层了。)

从上面描述的机制来看,jitter buffer机制利用缓冲在一定程度上能保证数据包以比较均 匀的速度上传给应用。

七: 事件的处理

在 rtp 会话上,保存了 signal table、表中的各个事件的回调函数以及事件队列。如果接收到 signal table 中所注册的事件信息,则调用注册的回调函数进行处理,而对于 telephone event 包,除了调用回调函数外,还需要将其发送到事件队列,以便上层应用进行进一步的处理。

八: 其他需要说明的

Rtp 这块有关时间戳的比较计算,主要通过几个宏来完成。

RTP_TIMESTAMP_IS_NEWER_THAN(ts1,ts2)比较 ts1 小 于 等于ts2 , RTP_TIMESTAMP_IS_STRICTLY_NEWER_THAN(ts1,ts2)则比较ts1 小于 ts2,没有等于关系。但是实际实现中,差值都是与2 的 31 次方来比较,原理如下:

就是将差值结果强制转换为 uinit 后于 0x8000000 进行比较,这样大于等于零返回成功, 小于零返回失败。还是简单的比较关系而已。

关于 rtcp 同步 rtp 流的问题:

从 rtp 传输过来的媒体流可能为音频流和视频流,二者采用了不同的编码方式和不同的分采样率,同时这两个流中都带有时间戳信息。如何将这两个流进行同步,这可以通过rtcp 的报告包来完成。在 rtcp 的报告包中带有具有绝对的基于 NTP 的时间戳信息,同时也带有与 rtp 流中相同的采样时间戳信息,依据这两个时间戳信息就可以对同一个媒体流进行同步。Rtcp 中带有的唯一的 cname 信息可以将同一个媒体的音频和视频流信息关联起来,虽然这两个流使用不同的源描述符 ssrc。

关于 rtcp 发送的间隔:

这块程序中只是按照 5 秒的间隔来进行计算,是固定的。存在巨大的局限性,但是对于点对点的通信来讲,问题不是很大。另外测试 eyebeam 程序,发现是按 3 秒左右来发送rtcp 包的,不知是计算得到还是固定值。

这块可以按照协议要求对 oRTP 进行改进。

关于 rtcp 包的接收发送规则:

目前从代码来看,每次发送一个 rtp 包后检查是否需要发送 rtcp 包。每次接收rtp 包时同时检查是否可以接收 rtcp 包,完了后检查是否需要发送 rtcp 包。但是协议的规则不是这样。

关于 rtcp 包的构建问题:

Rtcp包必须以复合包的形式向网络上传输,而且必须至少包含两个基本的包,第一个必须是发送者报告包或者接受者报告包,另外包括 sdes 源描述项包。

关于 rtcp 协议方面的问题,可以参考 rtcp 协议的说明文档,参考资料 1

九: 使用 oRTP 库

oRTP 提供了测试程序来测试 oRTP 库,同时测试程序也是如何使用 oRTP 最好的例子。

测试程序在源代码的 test 目录下,包括发送测试 rtpsend.c,接收测试rtprecv.c,并行发送测试 mrtpsend.c,并行接收测试 mrtprecv.c,还有有关telephone event 相关的测试。这里的说明 只针对上述四个有关接收发送测试程序的测试结果。

程序中提供的测试例子,大部分都是针对音频数据的,时间戳增加值也都是设置为160 了(如何计算得来,参见时间戳部分的说明),这在接收和发送视频数据时存在问题,需要做些修改,具体见问题列表。

海思3716 平台上 oRTP 代码的编译:. /configure --host=arm-hisiv200-linux【指定交叉环境】--prefix=【指定安装目录】 Make Make install完了之后会在指定的目录下创建 include、lib以及 share 三个文件夹。include 包含了我们要使用 oRTP 库的头文件,lib 目录包含了编译完的库,share下是文档说明。可以把 test 目录下的文件拿到安装目录下,跟库一起编译出来可执行文件进行测试。

测试问题列表:

1、 测试程序默认为测试音频流,没有包含视频流,时间戳步进值为 160,就是按照20ms 采样周期加 8KHZ 采样率计算出来的。这就导致接收和发送视频流的速度都特别慢。因此接收视频数据时需要调整步进值为3600,即 90000 采样率加 25 帧每秒计算得来。实际使用过程中可根据测试效果进行调整。

2、单独调整第一条还不能完全解决速率问题,还需要调整 payload type。接收过程可以根据接收的类型发生改变(即会产生payload type changed 事件),从而对此作出相应调整,故对接收过程影响不大,但是发送过程因为默认payload 类型设置为了 payload_type_pcmu8000,导致发送速度很慢,(基于 8KHZ 采样率)调整为33 (payload_type_mp2v)即可。

3、 33在 oRTP 中默认并没有添加支持,可仿照 avprofile.c 文件中的实现单独添加。

4、 测试中发现 multiple recv 测试程序接收不到数据,将文件操作接口换为fopen,及 ascii 标准类型的文件读写接口即可,原因待查。

5、 多会话接收目前是按照端口号不同来进行的。

6、 关闭调度模式和 block 模式可以加速视频流的推送

7、 在没有调度器的情况下,控制视频流发送速度,可以改善马赛克情况。其实发送过快

也不行,用 vlc 播放测试来看。

8、 在启动调度器的情况下,设置应用时间戳增量值(user_ts)或者调整该变量也可以调

整视频流的发送速度,调整马赛克情况。

9、 数据发送速率与采样率,user_ts 时间戳值增加以及buffer 大小均有关系。

10、 在并行发送和接收测试时(msend、mrecv),需要设置为非阻塞模式,否则程序可能会被卡死。

十: 参考

1 RFC3550RTP: A Transport Protocol for Real-Time Applications 2 RFC3551RTP Profile for Audio and Video Conferences with Minimal Control 3 RTCP 发送间隔分析

posted on 2014-10-16 18:08  xianbing  阅读(2951)  评论(0编辑  收藏  举报