音视频同步事关多媒体产品的最直观用户体验,是音视频媒体数据传输和渲染播放的最基本质量保证。音视频如果不同步,有可能造成延迟、卡顿等非常影响用户体验的现象。因此,它非常重要。一般说来,音视频同步维护媒体数据的时间线顺序,即发送端在某一时刻采集的音视频数据,接收端在另一时刻同时播放和渲染。</br>
本文在深入研究WebRTC源代码的基础上,分析其音视频同步的实现细节,包括RTP时间戳的产生,RTCP SR报文的构造、发送和接收,音视频同步的初始化和同步过程。RTP时间戳是RTP数据包的基石,而RTCP SR报文是时间戳和NTP时间之间进行转换的基准。下面详细描述之。</br>
1 RTP时间戳的产生
</br>
个人认为,RTP时间戳和序列号是RTP协议的精华所在:前者定义媒体负载数据的采样时刻,描述负载数据的帧间顺序;后者定义RTP数据包的先后顺序,描述媒体数据的帧内顺序。关于RTP时间戳,摘录RFC3550定义如下[1]:</br>
“The timestamp reflects the sampling instant of the first octet in the RTP data packet. The sampling instant must be derived from a clock that increments monotonically and linearly in time to allow synchronization and jitter calculations. The resolution of the clock must be sufficient for the desired synchronization accuracy and for measuring packet arrival jitter (one tick per video frame is typically not sufficient). ”</br>
由以上定义可知,RTP时间戳反映RTP负载数据的采样时刻,从单调线性递增的时钟中获取。时钟的精度由RTP负载数据的采样频率决定,比如视频的采样频率一般是90khz,那么时间戳增加1,则实际时间增加1/90000秒。</br>
下面回到WebRTC源代码内部,以视频采集为例分析RTP时间戳的产生过程,如图1所示。</br>
视频采集线程以帧为基本单位采集视频数据,视频帧从系统API被采集出来,经过初步加工之后到达VideoCaptureImpl::IncomingFrame()函数,设置render_time_ms_为当前时间(其实就是采样时刻)。</br>
执行流程到达VideoCaptureInput::IncomingCapturedFrame()函数后,在该函数设置视频帧的timestamp,ntp_time_ms和render_time_ms。其中render_time_ms为当前时间,以毫秒为单位;ntp_time_ms为采样时刻的绝对时间表示,以毫秒为单位;timestamp则是采样时间的时间戳表示,是ntp_time_ms和采样频率frequency的乘积,以1/frequency秒为单位。由此可知,timestamp和ntp_time_ms是同一采样时刻的不同表示。</br>
接下来视频帧经过编码器编码之后,发送到RTP模块进行RTP打包和发送。构造RTP数据包头部时调用RtpSender::BuildRTPheader()函数,确定时间戳的最终值为rtphdr->timestamp = start_timestamp + timestamp,其中start_timestamp是RtpSender在初始化时设置的初始时间戳。RTP报文构造完毕之后,经由网络发送到对端。</br>
2 SR报文构造和收发
</br>
由上一节论述可知,NTP时间和RTP时间戳是同一时刻的不同表示,区别在于精度不同。NTP时间是绝对时间,以毫秒为精度,而RTP时间戳则和媒体的采样频率有关。因此,我们需要维护一个NTP时间和RTP时间戳的对应关系,该用以对两种时间的进行转换。RTCP协议定义的SR报文维护了这种对应关系,下面详细描述。</br>
2.1 时间戳初始化
</br>
在初始化阶段,ModuleRtpRtcpImpl::SetSendingStatus()函数会获取当前NTP时间的时间戳表示(ntp_time * frequency),作为时间戳初始值分别设置RTPSender和RTCPSender的start_timestamp参数(即上节在确定RTP数据包头部时间戳时的初始值)。</br>
视频数据在编码完之后发往RTP模块构造RTP报文时,视频帧的时间戳timestamp和本地时间capture_time_ms通过RTCPSender::SetLastRtpTime()函数记录到RTCPSender对象的last_rtp_timestamp和last_frame_capture_time_ms参数中,以将来将来构造RTCP SR报文使用。</br>
2.2 SR报文构造及发送
</br>
WebRTC内部通过ModuleProcessThread线程周期性发送RTCP报文,其中SR报文通过RTCPSender::BuildSR(ctx)构造。其中ctx中包含当前时刻的NTP时间,作为SR报文[1]中的NTP时间。接下来需要计算出此刻对应的RTP时间戳,即假设此刻有一帧数据刚好被采样,则其时间戳为:</br>
rtp_timestamp = start_timestamp_ + last_rtp_timestamp_ +
(clock_->TimeInMilliseconds() - last_frame_capture_time_ms_) *
(ctx.feedback_state_.frequency_hz / 1000);</br>
至此,NTP时间和RTP时间戳全部齐活儿,就可以构造SR报文进行发送了。</br>
2.3 SR接收
</br>
接收端在收到SR报文后,把其中包含的NTP时间和RTP时间戳记录在RTCPSenderInfo对象中,供其他模块获取使用。比如通过RTCPReceiver::NTP()或者SenderInfoReceived()函数获取。</br>
3 音视频同步
</br>
前面两节做必要的铺垫后,本节详细分析WebRTC内部的音视频同步过程。</br>
3.1 初始化配置
</br>
音视频同步的核心就是根据媒体负载所携带RTP时间戳进行同步。在WebRTC内部,同步的基本对象是AudioReceiveStream/VideoReceiveStream,根据sync_group进行相互配对。同步的初始化设置过程如图2所示。</br>
Call对象在创建Audio/VideoReceiveStream时,调用ConfigureSync()进行音视频同步的配置。配置参数为sync_group,该参数在PeerConnectionFactory在创建MediaStream时指定。在ConfigureSync()函数内部,通过sync_group查找得到AudioReceiveStream,然后再在video_receive_streams中查找得到VideoReceiveStream。得到两个媒体流,调用VideoReceiveStream::SetSyncChannel同步,在ViESyncModule::ConfigureSync()函数中把音视频参数进行保存,包括音频的voe_channel_id、voe_sync_interface, 和视频的video_rtp_receiver、video_rtp_rtcp。</br>
3.2 同步过程
</br>
音视频的同步过程在ModuleProcessThread线程中执行。ViESyncModule作为一个模块注册到ModuleProcessThread线程中,其Process()函数被该线程周期性调用,实现音视频同步操作。</br>
音视频同步的核心思想就是以RTCP SR报文中携带的NTP时间和RTP时间戳作为时间基准,以AudioReceiveStream和VideoReceiveStream各自收到最新RTP时间戳timestamp和对应的本地时间receive_time_ms作为参数,计算音视频流的相对延迟,然后结合音视频的当前延迟计算最终的目标延迟,最后把目标延迟发送到音视频模块进行设置。目标延迟作为音视频渲染时的延迟下限值。整个过程如图3所示。</br>
首先,从VideoReceiver获得当前视频延迟current_video_delay,即video_jitter_delay,decode_delay和render_delay的总和。然后从VoEVideoSyncImpl获得当前音频延迟current_audio_delay,即audio_jitter_delay和playout_delay的总和。</br>
然后,音视频分别以各自的rtp_rtcp和rtp_receiver更新各自的measure。其基本操作包括:从rtp_receiver获取最新接收到的RTP报文的RTP时间戳latest_timestamp和对应的本地接收时刻latest_receive_time_ms,从rtp_rtcp获取最新接收的RTCP SR报文中的NTP时间和RTP时间戳。然后把这些数据都存储到measure中。注意measure中保存最新两对RTCP SR报文中的NTP时间和RTP时间戳,用来在下一步计算媒体流的采样频率。</br>
接下来,计算最新收到的音视频数据的相对延迟。其基本流程如下:首先得到最新收到RTP时间戳latest_timestamp对应的NTP时间latest_capture_time。这里用到measure中存储的latest_timestamp和RTCP SR的NTP时间和RTP时间戳timestamp,利用两对数值计算得到采样频率frequency,然后有latest_capture_time = latest_timestamp / frequency,得到单位为毫秒的采样时间。最后得到音视频的相对延迟:</br>
relative_delay = video_measure.latest_receive_time_ms -
audio_measure.latest_receive_time_ms -
(video_last_capture_time - audio_last_capture_time); </br>
至此,我们得到三个重要参数:视频当前延迟current_video_delay, 音频当前延迟current_audio_delay和相对延迟relative_delay。接下来用这三个参数计算音视频的目标延迟:首先计算总相对延迟current_diff = current_video_delay – current_audio_delay + relative_delay,根据历史值对其求加权平均值。如果current_diff > 0,表明当前视频延迟比音频延迟长,需要减小视频延迟或者增大音频延迟;反之如果current < 0,则需要增大视频延迟或者减小音频延迟。经过此番调整之后,我们得到音视频的目标延迟audio_target_delay和video_target_delay。</br>
最后,我们把得到的目标延迟audio_target_delay和video_target_delay分别设置到音视频模块中,作为将来渲染延迟的下限值。到此为止,一次音视频同步操作完成。该操作在ModuleProcessThread线程中会周期性执行。</br>
4 总结
</br>
本文详细分析了WebRTC内部音视频同步的实现细节,包括RTP时间戳的产生,RTCP SR报文的构造、发送和接收,音视频同步的初始化和同步过程。通过本文,对RTP协议、流媒体通信和音视频同步有更深入的认识。</br>
</br>
</br>
参考文献
</br>
[1] RFC3550 - RTP: A Transport Protocol for Real-Time Applications