所有的基于网络传输的音视频采集播放系统都会存在音视频同步的问题,作为现代互联网实时音视频通信系统的代表,WebRTC 也不例外。本文将对音视频同步的原理以及 WebRTC 的实现做深入分析。
1、时间戳 (timestamp)
同步问题就是快慢的问题,就会牵扯到时间跟音视频流媒体的对应关系,就有了时间戳的概念。
时间戳用来定义媒体负载数据的采样时刻,从单调线性递增的时钟中获取 , 时钟的精度由 RTP 负载数据的采样频率决定。音频和视频的采样频率是不一样的,一般音频的采样频率有 16KHz、44.1KHz、48KHz 等,而视频反映在采样帧率上,一般帧率有 25fps、29.97fps、30fps 等。
习惯上音频的时间戳的增速就是其采样率,比如 16KHz 采样,每 10ms 采集一帧,则下一帧的时间戳,比上一帧的时间戳,从数值上多 16 x10=160,即音频时间戳增速为 16/ms。而视频的采样频率习惯上是按照 90KHz 来计算的,就是每秒 90K 个时钟 tick,之所以用 90K 是因为它正好是上面所说的视频帧率的倍数,所以就采用了 90K。所以视频帧的时间戳的增长速率就是 90/ms。
2、时间戳的生成
音频帧时间戳的生成
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 字段,即使是扩展字段里,我们也没有放这个值,如下面视频的时间相关的扩展字段。
3、音视频同步核心依据
从上面可以看出,RTP 包里面只包含每个流的独立的、单调递增的时间戳信息,也就是说音频和视频两个时间戳完全是独立的,没有关系的,无法只根据这个信息来进行同步,因为无法对两个流的时间进行关联,我们需要一种映射关系,将两个独立的时间戳关联起来。
这个时候 RTCP 包里面的一种发送端报告分组 SR(SenderReport) 包就上场了,详情请参考RFC3550。
SR 包的其中一个作用就是来告诉我们每个流的 RTP 包的时间戳和 NTP 时间的对应关系的。靠的就是上边图片中标出的 NTP 时间戳和 RTP 时间戳,通过 RFC3550 的描述,我们知道这两个时间戳对应的是同一个时刻,这个时刻表示此 SR 包生成的时刻。这就是我们对音视频进行同步的最核心的依据,所有的其它计算都是围绕这个核心依据来展开的。
4、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 包的时间信息是通过下面的函数进行更新的:
5、音视频同步的计算
因为同一台机器上音频流和视频流的本地系统时间是一样的,也就是系统时间对应的 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 中音视频同步的原理。如果有什么遗漏或者错误,欢迎大家一起交流!