1. 程式人生 > >FFmpeg學習6:視音訊同步

FFmpeg學習6:視音訊同步

轉載自:http://www.cnblogs.com/wangguchangqing/p/5900426.html     謝謝版主

在上一篇文章中,視訊和音訊是各自獨立播放的,並不同步。本文主要描述瞭如何以音訊的播放時長為基準,將視訊同步到音訊上以實現視音訊的同步播放的。主要有以下幾個方面的內容

  • 視音訊同步的簡單介紹
  • DTS 和 PTS
  • 計算視訊中Frame的顯示時間
  • 獲取Audio clock(audio的播放時長)
  • 將視訊同步到音訊上,實現視音訊同步播放

視音訊同步簡單介紹

一般來說,視訊同步指的是視訊和音訊同步,也就是說播放的聲音要和當前顯示的畫面保持一致。想象以下,看一部電影的時候只看到人物嘴動沒有聲音傳出;或者畫面是激烈的戰鬥場景,而聲音不是槍炮聲卻是人物說話的聲音,這是非常差的一種體驗。
在視訊流和音訊流中已包含了其以怎樣的速度播放的相關資料,視訊的幀率(Frame Rate)指示視訊一秒顯示的幀數(影象數);音訊的取樣率(Sample Rate)表示音訊一秒播放的樣本(Sample)的個數。可以使用以上資料通過簡單的計算得到其在某一Frame(Sample)的播放時間,以這樣的速度音訊和視訊各自播放互不影響,在理想條件下,其應該是同步的,不會出現偏差。但,理想條件是什麼大家都懂得。如果用上面那種簡單的計算方式,慢慢的就會出現音視訊不同步的情況。要不是視訊播放快了,要麼是音訊播放快了,很難準確的同步。這就需要一種隨著時間會線性增長的量

,視訊和音訊的播放速度都以該量為標準,播放快了就減慢播放速度;播放快了就加快播放的速度。所以呢,視訊和音訊的同步實際上是一個動態的過程,同步是暫時的,不同步則是常態。以選擇的播放速度量為標準,快的等待慢的,慢的則加快速度,是一個你等我趕的過程。

播放速度標準量的的選擇一般來說有以下三種:

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

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指示顯示順序。所以流中可以是這樣的:

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)。

/**
    * 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

ret = avcodec_receive_frame(video->video_ctx, frame);
if (ret < 0 && ret != AVERROR_EOF)
    continue;

if ((pts = av_frame_get_best_effort_timestamp(frame)) == AV_NOPTS_VALUE)
    pts = 0;

pts *= av_q2d(video->stream->time_base);

pts = video->synchronize(frame, pts);

frame->opaque = &pts;

注意,這裡的pts是double型,因為將其乘以了time_base,代表了該幀在視訊中的時間位置(秒為單位)。有可能存在呼叫av_frame_get_best_effort_timestamp得不到一個正確的PTS,這樣的情況放在函式synchronize中處理。

double VideoState::synchronize(AVFrame *srcFrame, double pts)
{
    double frame_delay;

    if (pts != 0)
        video_clock = pts; // Get pts,then set video clock to it
    else
        pts = video_clock; // Don't get pts,set it to video clock

    frame_delay = av_q2d(stream->codec->time_base);
    frame_delay += srcFrame->repeat_pict * (frame_delay * 0.5);

    video_clock += frame_delay;

    return pts;
}

video_clock是視訊播放到當前幀時的已播放的時間長度。在synchronize函式中,如果沒有得到該幀的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)
{
    audio_state->audio_clock = av_q2d(audio_state->stream->time_base) * pkt.pts;
}

由於一個packet中可以包含多個幀,packet中的PTS比真正的播放的PTS可能會早很多,可以根據Sample Rate 和 Sample Format來計算出該packet中的資料可以播放的時長,再次更新Audio clock 。

// 每秒鐘音訊播放的位元組數 sample_rate * channels * sample_format(一個sample佔用的位元組數)
audio_state->audio_clock += static_cast<double>(data_size) / (2 * audio_state->stream->codec->channels *            
        audio_state->stream->codec->sample_rate);

上面乘以2是因為sample format是16位的無符號整型,佔用2個位元組。
有了Audio clock後,在外面獲取該值的時候卻不能直接返回該值,因為audio緩衝區的可能還有未播放的資料,需要減去這部分的時間

double AudioState::get_audio_clock()
{
    int hw_buf_size = audio_buff_size - audio_buff_index;
    int bytes_per_sec = stream->codec->sample_rate * audio_ctx->channels * 2;

    double pts = audio_clock - static_cast<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的播放速度來調整這個延遲時間,以實現視音訊的同步播放。
具體實現:

// 將視訊同步到音訊上,計算下一幀的延遲時間
// 使用要播放的當前幀的PTS和上一幀的PTS差來估計播放下一幀的延遲時間,並根據video的播放速度來調整這個延遲時間
double current_pts = *(double*)video->frame->opaque;
double delay = current_pts - video->frame_last_pts;
if (delay <= 0 || delay >= 1.0)
    delay = video->frame_last_delay;

video->frame_last_delay = delay;
video->frame_last_pts = current_pts;

// 根據Audio clock來判斷Video播放的快慢
double ref_clock = media->audio->get_audio_clock();

double diff = current_pts - ref_clock;// diff < 0 => video slow,diff > 0 => video quick

double threshold = (delay > SYNC_THRESHOLD) ? delay : SYNC_THRESHOLD;

// 調整播放下一幀的延遲時間,以實現同步
if (fabs(diff) < NOSYNC_THRESHOLD) // 不同步
{
    if (diff <= -threshold) // 慢了,delay設為0
        delay = 0;
    else if (diff >= threshold) // 快了,加倍delay
        delay *= 2;
}
video->frame_timer += delay;
double actual_delay = video->frame_timer - static_cast<double>(av_gettime()) / 1000000.0;
if (actual_delay <= 0.010)
    actual_delay = 0.010; 

// 設定一下幀播放的延遲
schedule_refresh(media, static_cast<int>(actual_delay * 1000 + 0.5));

frame_last_ptsframe_last_delay是上一幀的PTS以及設定的播放上一幀時的延遲時間。

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

Summary

本文主要描述如何利用audio的播放時長作為基準,將視訊同步到音訊上以實現視音訊的同步播放。視音訊的同步過程是一個動態過程,快者等待,慢則加快播放,在這樣的你等我趕的過程過程中實現同步播放。
本文程式碼:https://github.com/brookicv/FSplayer