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,然后渲染線程和聲音播放線程根據本地時鍾決定是否要播放當前音視頻數據。

----------------------------------------------------------------------------------------

播放器內部的音視頻同步

以上是流媒體的音視頻同步,純點播播放器同步還會略有不同。

一種同步思路是,視頻向音頻同步。

因為音頻的渲染速率一定,視頻只需同步至音頻即可。

基於機器的本地時間,創建一個播放器基准時間類,由接收到的音頻幀pts不斷更新該類的時間。

接收到視頻幀時,攜帶的pts為理論到達時間,根據該pts和此時的本機時間可以計算實際到達時間,根據兩者的差值決定該視頻幀的具體操作:

1. 實際到達過早:進行等待;

2. 實際到達適中:則計算diff,usleep diff;

3. 實際到達晚一點:則打印日志,計算diff,usleep diff;

4. 實際到達過晚:打印日志,返回結果為丟幀; 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM