所有的基于网络传输的音视频采集播放系统都会存在音视频同步的问题,作为现代互联网实时音视频通信系统的代表,WebRTC 也不例外。本文将对音视频同步的原理以及 WebRTC 的实现做深入分析。
同步问题就是快慢的问题,就会牵扯到时间跟音视频流媒体的对应关系,就有了时间戳的概念。
时间戳用来定义媒体负载数据的采样时刻,从单调线性递增的时钟中获取,时钟的精度由 RTP 负载数据的采样频率决定。音频和视频的采样频率是不一样的,一般音频的采样频率有 16KHz 、44.1KHz 、48KHz 等,而视频反映在采样帧率上,一般帧率有 25fps 、29.97fps 、30fps 等。
习惯上音频的时间戳的增速就是其采样率,比如 16KHz 采样,每 10ms 采集一帧,则下一帧的时间戳,比上一帧的时间戳,从数值上多 16 x10=160,即音频时间戳增速为 16/ms 。而视频的采样频率习惯上是按照 90KHz 来计算的,就是每秒 90K 个时钟 tick,之所以用 90K 是因为它正好是上面所说的视频帧率的倍数,所以就采用了 90K 。所以视频帧的时间戳的增长速率就是 90/ms 。
WebRTC 的音频帧的时间戳,从第一个包为 0,开始累加,每一帧增加 = 编码帧长 (ms) x 采样率 / 1000,如果采样率 16KHz,编码帧长 20ms,则每个音频帧的时间戳递增 20 x 16000/1000 = 320 。这里只是说的未打包之前的音频帧的时间戳,而封装到 RTP 包里面的时候,会将这个音频帧的时间戳再累加上一个随机偏移量(构造函数里生成),然后作为此 RTP 包的时间戳,发送出去,如下面代码所示,注意,这个逻辑同样适用于视频包。
WebRTC 的视频帧,生成机制跟音频帧完全不同。视频帧的时间戳来源于系统时钟,采集完成后至编码之前的某个时刻(这个传递链路非常长,不同配置的视频帧,走不同的逻辑,会有不同的获取位置),获取当前系统的时间 timestamp_us_
,然后算出此系统时间对应的 ntp_time_ms_
,再根据此 ntp 时间算出原始视频帧的时间戳 timestamp_rtp_
,参看下面的代码,计算逻辑也在 OnFrame
这个函数中。
跟上面音频一样,在封装到 RTP 包的时候,会将原始视频帧的时间戳累加上一个随机偏移量(此偏移量跟音频的并不是同一个值),作为此 RTP 包的时间戳发送出去。值得注意的是,这里计算的 NTP 时间戳根本就不会随着 RTP 数据包一起发送出去,因为 RTP 包的包头里面没有 NTP 字段,即使是扩展字段里,我们也没有放这个值,如下面视频的时间相关的扩展字段。
从上面可以看出,RTP 包里面只包含每个流的独立的、单调递增的时间戳信息,也就是说音频和视频两个时间戳完全是独立的,没有关系的,无法只根据这个信息来进行同步,因为无法对两个流的时间进行关联,我们需要一种映射关系,将两个独立的时间戳关联起来。
这个时候 RTCP 包里面的一种发送端报告分组 SR (SenderReport) 包就上场了,详情请参考 RFC3550。
由上面论述可知,NTP 时间和 RTP 时间戳是同一时刻的不同表示,只是精度和单位不一样。NTP 时间是绝对时间,以毫秒为单位,而 RTP 时间戳则和媒体的采样频率有关,是一个单调递增数值。生成 SR 包的过程在 RTCPSender::BuildSR(const RtcpContext& ctx)
函数里面,老版本里面有 bug,写死了采样率为 8K,新版本已经修复,下面截图是老版本的代码:
首先,我们要获取当前时刻(即 SR 包生成时刻)的 NTP 时间。这个直接从传过来的参数 ctx 中就可以获得:
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 时间是在同一基准上的,所以就可以算出两者的差值。
下面是 WebRTC 里面计算直线对应的系数 rate 和偏移 offset 的代码:
StreamSynchronization::ComputeRelativeDelay()
函数中,之后又经过了 StreamSynchronization::ComputeDelays()
函数对其进行了指数平滑等一系列的处理和判断,得出最终控制音频和视频的最小延时时间,分别通过 syncable_audio_->SetMinimumPlayoutDelay(target_audio_delay_ms)
和 syncable_video_->SetMinimumPlayoutDelay(target_video_delay_ms)
应用到了音视频的播放缓冲区。
这一系列操作都是由定时器调用 RtpStreamsSynchronizer::Process()
函数来处理的。
另外需要注意一下,在知道采样率的情况下,是可以通过一个 SR 包来计算的,如果没有 SR 包,是无法进行准确的音视频同步的。
NTP 时间-RTP 时间戳
坐标图如果你能看明白(其实很简单,就是求解出直线方程来计算 NTP ),那么也就真正的理解了 WebRTC 中音视频同步的原理。如果有什么遗漏或者错误,欢迎大家一起交流!
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.