全部的基于网络传输的音视频采集播放系统都会存在音视频同步的问题,做为现代互联网实时音视频通讯系统的表明,WebRTC 也不例外。本文将对音视频同步的原理以及 WebRTC 的实现作深刻分析。html
做者:良逸网络
审校:泰一tcp
同步问题就是快慢的问题,就会牵扯到时间跟音视频流媒体的对应关系,就有了时间戳的概念。ide
时间戳用来定义媒体负载数据的采样时刻,从单调线性递增的时钟中获取,时钟的精度由 RTP 负载数据的采样频率决定。音频和视频的采样频率是不同的,通常音频的采样频率有 16KHz、44.1KHz、48KHz 等,而视频反映在采样帧率上,通常帧率有 25fps、29.97fps、30fps 等。函数
习惯上音频的时间戳的增速就是其采样率,好比 16KHz 采样,每 10ms 采集一帧,则下一帧的时间戳,比上一帧的时间戳,从数值上多 16 x10=160,即音频时间戳增速为 16/ms。而视频的采样频率习惯上是按照 90KHz 来计算的,就是每秒 90K 个时钟 tick,之因此用 90K 是由于它正好是上面所说的视频帧率的倍数,因此就采用了 90K。因此视频帧的时间戳的增加速率就是 90/ms。ui
WebRTC 的音频帧的时间戳,从第一个包为 0,开始累加,每一帧增长 = 编码帧长 (ms) x 采样率 / 1000,若是采样率 16KHz,编码帧长 20ms,则每一个音频帧的时间戳递增 20 x 16000/1000 = 320。这里只是说的未打包以前的音频帧的时间戳,而封装到 RTP 包里面的时候,会将这个音频帧的时间戳再累加上一个随机偏移量(构造函数里生成),而后做为此 RTP 包的时间戳,发送出去,以下面代码所示,注意,这个逻辑一样适用于视频包。阿里云
WebRTC 的视频帧,生成机制跟音频帧彻底不一样。视频帧的时间戳来源于系统时钟,采集完成后至编码以前的某个时刻(这个传递链路很是长,不一样配置的视频帧,走不一样的逻辑,会有不一样的获取位置),获取当前系统的时间 timestamp_us_
,而后算出此系统时间对应的 ntp_time_ms_
,再根据此 ntp 时间算出原始视频帧的时间戳 timestamp_rtp_
,参看下面的代码,计算逻辑也在 OnFrame
这个函数中。
为何视频帧采用了跟音频帧不一样的时间戳计算机制呢?个人理解,通常状况音频的采集设备的采样间隔和时钟精度更加准确,10ms 一帧,每秒是 100 帧,通常不会出现大的抖动,而视频帧的帧间隔时间较大采集精度,每秒 25 帧的话,就是 40ms 一帧。若是还采用音频的按照采样率来递增的话,可能会出现跟实际时钟对不齐的状况,因此就直接每取一帧,按照取出时刻的系统时钟算出一个时间戳,这样能够再现真实视频帧跟实际时间的对应关系。编码
跟上面音频同样,在封装到 RTP 包的时候,会将原始视频帧的时间戳累加上一个随机偏移量(此偏移量跟音频的并非同一个值),做为此 RTP 包的时间戳发送出去。值得注意的是,这里计算的 NTP 时间戳根本就不会随着 RTP 数据包一块儿发送出去,由于 RTP 包的包头里面没有 NTP 字段,即便是扩展字段里,咱们也没有放这个值,以下面视频的时间相关的扩展字段。3d
从上面能够看出,RTP 包里面只包含每一个流的独立的、单调递增的时间戳信息,也就是说音频和视频两个时间戳彻底是独立的,没有关系的,没法只根据这个信息来进行同步,由于没法对两个流的时间进行关联,咱们须要一种映射关系,将两个独立的时间戳关联起来。code
这个时候 RTCP 包里面的一种发送端报告分组 SR (SenderReport) 包就上场了,详情请参考 RFC3550。
SR 包的其中一个做用就是来告诉咱们每一个流的 RTP 包的时间戳和 NTP 时间的对应关系的。靠的就是上边图片中标出的 NTP 时间戳和 RTP 时间戳,经过 RFC3550 的描述,咱们知道这两个时间戳对应的是同一个时刻,这个时刻表示此 SR 包生成的时刻。这就是咱们对音视频进行同步的最核心的依据,全部的其它计算都是围绕这个核心依据来展开的。
由上面论述可知,NTP 时间和 RTP 时间戳是同一时刻的不一样表示,只是精度和单位不同。NTP 时间是绝对时间,以毫秒为单位,而 RTP 时间戳则和媒体的采样频率有关,是一个单调递增数值。生成 SR 包的过程在 RTCPSender::BuildSR(const RtcpContext& ctx)
函数里面,老版本里面有 bug,写死了采样率为 8K,新版本已经修复,下面截图是老版本的代码:
首先,咱们要获取当前时刻(即 SR 包生成时刻)的 NTP 时间。这个直接从传过来的参数 ctx 中就能够得到:
其次,咱们要计算当前时刻,应该对应的 RTP 的时间戳是多少。根据最后一个发送的 RTP 包的时间戳 last_rtp_timestamp_
和它的采集时刻的系统时间 last_frame_capture_time_ms_
,和当前媒体流的时间戳的每 ms 增加速率 rtp_rate
,以及从 last_frame_capture_time_ms_
到当前时刻的时间流逝,就能够算出来。注意,last_rtp_timestamp_
是媒体流的原始时间戳,不是通过随机偏移的 RTP 包时间戳,因此最后又累加了偏移量 timestamp_offset_
。其中最后一个发送的 RTP 包的时间信息是经过下面的函数进行更新的:
由于同一台机器上音频流和视频流的本地系统时间是同样的,也就是系统时间对应的 NTP 格式的时间也是同样的,是在同一个坐标系上的,因此能够把 NTP 时间做为横轴 X,单位是 ms,而把 RTP 时间戳的值做为纵轴 Y,画在一块儿。下图展现了计算音视频同步的原理和方法,其实很简单,就是使用最近的两个 SR 点,两点肯定一条直线,以后给任意一个 RTP 时间戳,均可以求出对应的 NTP 时间,又由于视频和音频的 NTP 时间是在同一基准上的,因此就能够算出二者的差值。
上图以音频的两个 SR 包为例,肯定出了 RTP 和 NTP 对应关系的直线,而后给任意一个 rtp_a,就算出了其对应的 NTP_a,同理也能够求任意视频包 rtp_v 对应的 NTP_v 的时间点,两个的差值就是时间差。
下面是 WebRTC 里面计算直线对应的系数 rate 和偏移 offset 的代码:
在 WebRTC 中计算的是最新收到的音频 RTP 包和最新收到的视频 RTP 包的对应的 NTP 时间,做为网络传输引入的不一样步时长,而后又根据当前音频和视频的 JitterBuffer 和播放缓冲区的大小,获得了播放引入的不一样步时长,根据两个不一样步时长,获得了最终的音视频不一样步时长,计算过程在 StreamSynchronization::ComputeRelativeDelay()
函数中,以后又通过了 StreamSynchronization::ComputeDelays()
函数对其进行了指数平滑等一系列的处理和判断,得出最终控制音频和视频的最小延时时间,分别经过 syncable_audio_->SetMinimumPlayoutDelay(target_audio_delay_ms)
和 syncable_video_->SetMinimumPlayoutDelay(target_video_delay_ms)
应用到了音视频的播放缓冲区。
这一系列操做都是由定时器调用 RtpStreamsSynchronizer::Process()
函数来处理的。
另外须要注意一下,在知道采样率的状况下,是能够经过一个 SR 包来计算的,若是没有 SR 包,是没法进行准确的音视频同步的。
WebRTC 中实现音视频同步的手段就是 SR 包,核心的依据就是 SR 包中的 NTP 时间和 RTP 时间戳。最后的两张 NTP 时间-RTP 时间戳
坐标图若是你能看明白(其实很简单,就是求解出直线方程来计算 NTP),那么也就真正的理解了 WebRTC 中音视频同步的原理。若是有什么遗漏或者错误,欢迎你们一块儿交流!
「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。