ffmpeg音視頻同步---視頻同步到音頻時鐘

做者:Huatiangit

github:https://github.com/Huatiangithub

郵箱: 773512457@qq.com緩存

平臺:Fedora 25 (64bit)bash

音視頻同步簡單介紹

通常來講,視頻同步指的是視頻和音頻同步,也就是說播放的聲音要和當前顯示的畫面保持一致。想象如下,看一部電影的時候只看到人物嘴動沒有聲音傳出;或者畫面是激烈的戰鬥場景,而聲音不是槍炮聲倒是人物說話的聲音,這是很是差的一種體驗。
在視頻流和音頻流中已包含了其以怎樣的速度播放的相關數據,視頻的幀率(Frame Rate)指示視頻一秒顯示的幀數(圖像數);音頻的採樣率(Sample Rate)表示音頻一秒播放的樣本(Sample)的個數。可使用以上數據經過簡單的計算獲得其在某一Frame(Sample)的播放時間,以這樣的速度音頻和視頻各自播放互不影響,在理想條件下,其應該是同步的,不會出現誤差。但,理想條件是什麼你們都懂得。若是用上面那種簡單的計算方式,慢慢的就會出現音視頻不一樣步的狀況。要不是視頻播放快了,要麼是音頻播放快了,很難準確的同步。這就須要一種隨着時間會線性增加的量,視頻和音頻的播放速度都以該量爲標準,播放快了就減慢播放速度;播放快了就加快播放的速度。因此呢,視頻和音頻的同步其實是一個動態的過程,同步是暫時的,不一樣步則是常態。以選擇的播放速度量爲標準,快的等待慢的,慢的則加快速度,是一個你等我趕的過程。ide

播放速度標準量的的選擇通常來講有如下三種:函數

  • 將視頻同步到音頻上,就是以音頻的播放速度爲基準來同步視頻。視頻比音頻播放慢了,加快其播放速度;快了,則延遲播放。
  • 將音頻同步到視頻上,就是以視頻的播放速度爲基準來同步音頻。
  • 將視頻和音頻同步外部的時鐘上,選擇一個外部時鐘爲基準,視頻和音頻的播放速度都以該時鐘爲標準。

DTS和PTS

上面提到,視頻和音頻的同步過程是一個你等我趕的過程,快了則等待,慢了就加快速度。這就須要一個量來判斷(和選擇基準比較),究竟是播放的快了仍是慢了,或者正以同步的速度播放。在視音頻流中的包中都含有DTS和PTS,就是這樣的量(準確來講是PTS)。DTS,Decoding Time Stamp,解碼時間戳,告訴解碼器packet的解碼順序;PTS,Presentation Time Stamp,顯示時間戳,指示從packet中解碼出來的數據的顯示順序。
視音頻都是順序播放的,其解碼的順序不該該就是其播放的順序麼,爲啥還要有DTS和PTS之分呢。對於音頻來講,DTS和PTS是相同的,也就是其解碼的順序和顯示的順序是相同的,但對於視頻來講狀況就有些不一樣了。
視頻的編碼要比音頻複雜一些,特別的是預測編碼是視頻編碼的基本工具,這就會形成視頻的DTS和PTS的不一樣。這樣視頻編碼後會有三種不一樣類型的幀:工具

  • I幀 關鍵幀,包含了一幀的完整數據,解碼時只須要本幀的數據,不須要參考其餘幀。
  • P幀 P是向前搜索,該幀的數據不徹底的,解碼時須要參考其前一幀的數據。
  • B幀 B是雙向搜索,解碼這種類型的幀是最複雜,不但須要參考其一幀的數據,還須要其後一幀的數據。

I幀的解碼是最簡單的,只須要本幀的數據;P幀也不是很複雜,值須要緩存上一幀的數據便可,整體來講都是線性,其解碼順序和顯示順序是一致的。B幀就比較複雜了,須要先後兩幀的順序,而且不是線性的,也是形成了DTS和PTS的不一樣的「元兇」,也是在解碼後有可能得不到完整Frame的緣由。(更多I,B,P幀的信息可參考
假如一個視頻序列,要這樣顯示I B B P,可是須要在B幀以前獲得P幀的信息,所以幀可能以這樣的順序來存儲I P B B,這樣其解碼順序和顯示的順序就不一樣了,這也是DTS和PTS同時存在的緣由。DTS指示解碼順序,PTS指示顯示順序。因此流中能夠是這樣的:ui

Stream : I P B B
DTS      1 2 3 4
PTS      1 4 2 3

一般來講只有在流中含有B幀的時候,PTS和DTS纔會不一樣。編碼

計算frame的顯示時間

在計算某一幀的顯示時間以前,現來弄清楚FFmpeg中的時間單位:時間基(TIME BASE)。在FFmpeg中存在這多個不一樣的時間基,對應着視頻處理的不一樣的階段(分佈於不一樣的結構體中)。在本文中使用的是 AVStream 的時間基,來指示Frame顯示時的時間戳(timestamp)。spa

/**
    * This is the fundamental unit of time (in seconds) in terms
    * of which frame timestamps are represented.
    *
    */
AVRational time_base;

能夠看出,AVStream 中的time_base是以秒爲單位,表示frame顯示的時間,其類型爲 AVRational 。AVRational 是一個分數,其聲明以下:

/**
 * rational number numerator/denominator
 */
typedef struct AVRational{
    int num; ///< numerator
    int den; ///< denominator
} AVRational;

num爲分子,den爲分母。PTS爲一個 uint64_t 的整型,其單位就是 time_base 。表示視頻長度的 duration 也是一個 uint64_t ,那麼使用以下方法就能夠計算出一個視頻流的時間長度:

time(second) = st->duration * av_q2d(st->time_base);

st爲一個AVStream的指針,av_q2d 將一個 AVRational 轉換爲雙精度浮點數。一樣的方法也能夠獲得視頻中某幀的顯示時間:

timestamp(second) = pts * av_q2d(st->time_base);

也就是說,獲得了Frame的PTS後,就能夠獲得該frame顯示的時間戳。

獲得frame的pts

經過上面的描述知道,若是有了Frame的PTS就計算出幀的顯示的時間。下面的代碼展現了在從packet中解碼出frame後,如何獲得frame的PTS:

avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);

        if(packet->dts == AV_NOPTS_VALUE && packet->pts && packet->pts != AV_NOPTS_VALUE){
            pts = packet->pts;
        }else if(packet->dts != AV_NOPTS_VALUE){
            pts = packet->dts;
        }else{
            pts = 0;
        }
        pts *= av_q2d(is->video_st->time_base);

        if(frameFinished){
            pts = synchronize_video(is, pFrame, pts);
            if(queue_picture(is, pFrame, pts) < 0)
                break;
        }

注意,這裏的pts是double型,由於將其乘以了time_base,表明了該幀在視頻中的時間位置(秒爲單位)。有可能dts和pts都沒有獲得正確的,這種狀況放到synchronize_video中處理:

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts){

    double frame_delay;

    if(pts != 0){
        is->video_clock = pts;
    }else{
        pts = is->video_clock;
    }

    frame_delay = av_q2d(is->video_st->codec->time_base);

    frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
    is->video_clock += frame_delay;

    return pts;
}

video_clock 是視頻播放到當前幀時的已播放的時間長度。在 synchronize_video 函數中,若是沒有獲得該幀的PTS就用當前的 video_clock 來近似,而後更新video_clock的值。到這裏已經知道了video中frame的顯示時間了(秒爲單位),下面就描述若是獲得Audio的播放時間,並以此時間爲基準來安排video中顯示時間。

獲取 audio clock

Audio Clock,也就是Audio的播放時長,能夠在Audio時更新Audio Clock。在函數 audio_decode_frame 中解碼新的packet,這是能夠設置Auddio clock爲該packet的PTS:

if(pkt->pts != AV_NOPTS_VALUE) {
          is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
        }

因爲一個packet中能夠包含多個幀,packet中的PTS比真正的播放的PTS可能會早不少,能夠根據Sample Rate 和 Sample Format來計算出該packet中的數據能夠播放的時長,再次更新Audio clock 。

is->audio_clock += (double)resampled_data_size /
                (double)(2 * is->audio_st->codec->channels * is->audio_st->codec->sample_rate);

上面乘以2是由於sample format是16位的無符號整型,佔用2個字節。
有了Audio clock後,在外面獲取該值的時候卻不能直接返回該值,由於audio緩衝區的可能還有未播放的數據,須要減去這部分的時間:

double get_audio_clock(VideoState *is){
    double pts;
    int hw_buf_size, bytes_per_sec, n;

    pts = is->audio_clock;
    hw_buf_size = is->audio_buf_size - is->audio_buf_index;
    bytes_per_sec = 0;
    n = is->audio_st->codec->channels * 2;

    if(is->audio_st){
        bytes_per_sec = is->audio_st->codec->sample_rate * n;
    }

    if(bytes_per_sec){
        pts -= (double)hw_buf_size / bytes_per_sec;
    }

    return pts;
}

用audio緩衝區中剩餘的數據除以每秒播放的音頻數據獲得剩餘數據的播放時間,從Audio clock中減去這部分的值就是當前的audio的播放時長。

同步

如今有了video中Frame的顯示時間,而且獲得了做爲基準時間的音頻播放時長Audio clock ,能夠將視頻同步到音頻了。

  • 用當前幀的PTS - 上一播放幀的PTS獲得一個延遲時間
  • 用當前幀的PTS和Audio Clock進行比較,來判斷視頻的播放速度是快了仍是慢了
  • 根據上一步額判斷結果,設置播放下一幀的延遲時間。

 使用要播放的當前幀的PTS和上一幀的PTS差來估計播放下一幀的延遲時間,並根據video的播放速度來調整這個延遲時間,以實現視音頻的同步播放。

相關代碼:

void video_refresh_timer(void *userdata)
{

    VideoState *is = (VideoState *)userdata;
    VideoPicture *vp;
    double actual_delay, delay, sync_threshold, ref_clock, diff;

    if(is->video_st)
    {
        if(is->pictq_size == 0)
        {
            schedule_refresh(is, 1);
        }
        else
        {
            vp = &is->pictq[is->pictq_rindex];

            //設置延遲,首先和上次的pts對比得出延遲,更新延遲和pts;
            //經過與音頻時鐘比較,獲得更精確的延遲
            //最後與外部時鐘對比,得出最終可用的延遲,並刷新視頻
            delay = vp->pts - is->frame_last_pts;
            if(delay <= 0 || delay >= 1.0){
                delay = is->frame_last_delay;//若是延遲不正確,咱們使用上一個延遲
            }

            is->frame_last_delay = delay;
            is->frame_last_pts = vp->pts;

            ref_clock = get_audio_clock(is);
            diff = vp->pts - ref_clock;

            sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
            if(fabs(diff) < AV_NOSYNC_THRESHOLD){
                if(diff <= -sync_threshold){//音頻快於視頻
                    delay = 0;
                }else if(diff >= sync_threshold){//視頻快於音頻
                    delay = 2 * delay;
                }
            }

            is->frame_timer += delay;

            actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
            if(actual_delay < 0.010){
                actual_delay = 0.010;
            }

            schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));

            video_display(is);

            if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE)
            {
                is->pictq_rindex = 0;
            }

            SDL_LockMutex(is->pictq_mutex);
            is->pictq_size--;
            SDL_CondSignal(is->pictq_cond);
            SDL_UnlockMutex(is->pictq_mutex);
        }
    }
    else
    {
        schedule_refresh(is, 100);
    }
}

frame_last_pts 和 frame_last_delay 是上一幀的PTS以及設置的播放上一幀時的延遲時間。

  • 首先根據當前播放幀的PTS和上一播放幀的PTS估算出一個延遲時間。
  • 用當前幀的PTS和Audio clock相比較判斷此時視頻播放的速度是快仍是慢了
  • 視頻播放過快則加倍延遲,過慢則將延遲設置爲0
  • frame_timer保存着視頻播放的延遲時間總和,這個值和當前時間點的差值就是播放下一幀的真正的延遲時間
  • schedule_refresh 設置播放下一幀的延遲時間。

本文代碼下載地址:

https://github.com/Huatian/ffmpeg-tutorial 

相關文章
相關標籤/搜索