1. 程式人生 > >vlc原始碼分析(五) 流媒體的音視訊同步

vlc原始碼分析(五) 流媒體的音視訊同步

轉載地址:https://www.cnblogs.com/jiayayao/p/6890882.html

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,然後渲染執行緒和聲音播放執行緒根據本地時鐘決定是否要播放當前音視訊資料。