首页 > 代码库 > vlc源码分析(五) 流媒体的音视频同步
vlc源码分析(五) 流媒体的音视频同步
vlc播放流媒体时实现音视频同步,简单来说就是发送方发送的RTP包带有时间戳,接收方根据此时间戳不断校正本地时钟,播放音视频时根据本地时钟进行同步播放。
首先了解两个概念:stream clock和system clock。stream clock是流时钟,可以理解为RTP包中的时间戳;system clock是本地时钟,可以理解为当前系统的Tick数。
第一个RTP包到来时:
fSyncTimestamp = rtpTimestamp;// rtp时间戳赋值为本地记录的时间戳 fSyncTime = timeNow;// 本地同步时钟直接赋值为本地当前时钟,注意这样赋值是错误的,但随后就会被RTCP的SR包修正
之后有RTP包到来,则根据上一次RTP包的时间戳差值计算得到真实的时间差值:
// Divide this by the timestamp frequency to get real time: double timeDiff = timestampDiff/(double)timestampFrequency;// 差值除以90KHz得到真实时间
当RTCP的Sender Report(SR)包到来时,会对fSyncTime进行重置,直接赋值为NTP时间戳
fSyncTime.tv_sec = ntpTimestampMSW - 0x83AA7E80; // 1/1/1900 -> 1/1/1970 double microseconds = (ntpTimestampLSW*15625.0)/0x04000000; // 10^6/2^32 fSyncTime.tv_usec = (unsigned)(microseconds+0.5);
然后以此差值更新fSyncTime,也就是说live555接收部分的时钟fSyncTime既由RTP包时间戳不断的校正,也由RTCP的SR包不断的赋值修改。
在RTSP的Session建立时会创建解码器的本地时钟,本地时钟是一对时钟,包括stream clock和system clock,初始值均为INVALID。
static inline clock_point_t clock_point_Create( mtime_t i_stream, mtime_t i_system ) { clock_point_t p; p.i_stream = i_stream;// VLC_TS_INVALID p.i_system = i_system;// VLC_TS_INVALID return p; }
当RTP数据到来的时候,不仅会更新VLC接收部分的时钟,VLC解码部分的时钟也会通过input_clock_Update()函数更新。当解码部分根据判定stream clock出现较大延迟时,还会重置本地时钟对,重置时设置system clock为当前本机时钟Tick数。
live555接收完RTP数据后,存入BufferedPacket中。由于RTP封装H264是按照RFC3984来封装的,所以解析的时候按照该协议解析H264数据,解析时发现NALU起始,就会放入一个block_t中,然后该block_t就被推入以block_t为单位的数据fifo(src\misc\block.c)中,等待解码线程解码。block_t带有pts和dts,均为RTPSource的pts。
解码时如果视频音频都有的话,会创建两个Decoder,每个Decoder包含一个fifo,同时会创建两个解码线程(视频和音频),分别从各自的fifo中取出数据解码。视频和音频解码入口都是DecoderThread,从fifo中取出数据数据进入视频或者音频的解码分支。视频解码线程在解码时会将block_t的pts和dts传递给AVPacket(modules/codec/avcodec/video.c):
pkt.pts = p_block->i_pts;
pkt.dts = p_block->i_dts;
FFmpeg解码视频后,AVFrame将带有时间戳,但是这个时间戳是stream clock,之后会把stream clock转换为system clock,转换函数如下:
static mtime_t ClockStreamToSystem( input_clock_t *cl, mtime_t i_stream ) { if( !cl->b_has_reference ) return VLC_TS_INVALID; return ( i_stream - cl->ref.i_stream ) * cl->i_rate / INPUT_RATE_DEFAULT + cl->ref.i_system; }
同理,音频解码完后,也会进行stream clock到system clock的转换。音频的解码后的数据会直接播放,视频解码完的图像帧会放入图像fifo(src\misc\picture_fifo.c)中,等待渲染线程渲染。渲染线程会根据解码后图像的显示时间,决定是否播放:
......
decoded = picture_fifo_Pop(vout->p->decoder_fifo); if (is_late_dropped && decoded && !decoded->b_force) { const mtime_t predicted = mdate() + 0; /* TODO improve */ const mtime_t late = predicted - decoded->date; if (late > VOUT_DISPLAY_LATE_THRESHOLD) {// 延迟大于20ms,则不予播放,直接释放该图像 msg_Warn(vout, "picture is too late to be displayed (missing %d ms)", (int)(late/1000)); picture_Release(decoded); lost_count++; continue; } else if (late > 0) {// 延迟大于0小于20ms,打印日志并播放 msg_Dbg(vout, "picture might be displayed late (missing %d ms)", (int)(late/1000)); } }
......
整个接收流程的框图如下, 可以看出两个解码线程其实并没有直接联系,它们之间的联系是通过音视频数据包的的stream clock转换为system clock,然后渲染线程和声音播放线程根据本地时钟决定是否要播放当前音视频数据。
vlc源码分析(五) 流媒体的音视频同步