ortp使用详解2

五:数据的接收和发送 linux

一、 发送过程:算法

应用发送数据时调用接口 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 设置为调度器运行到如今的时间。这应该算是时间戳的初始化。session

若是调度被启用了,则对部分时间戳作一些调整,以下:app

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

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

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

发送完成后调用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 接收过程当中处理。

二、 接收过程。

数据包的接收是经过调用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 目录下的文件拿到安装目录下,跟库一块儿编译出来可执行文件进行测试。

测试问题列表:

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

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

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

四、 测试中发现 multiple recv 测试程序接收不到数据,将文件操做接口换为fopen,及 ascii 标准类型的文件读写接口便可,缘由待查。

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

六、 关闭调度模式和 block 模式能够加速视频流的推送

七、 在没有调度器的状况下,控制视频流发送速度,能够改善马赛克状况。其实发送过快

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

八、 在启动调度器的状况下,设置应用时间戳增量值(user_ts)或者调整该变量也能够调

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

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

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

十: 参考

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

相关文章
相关标签/搜索