音視頻同步原理

簡(jiǎn)介

本文主要描述如何以音頻的播放時(shí)長(zhǎng)為基準(zhǔn),將視頻同步到音頻上以實(shí)現(xiàn)音頻的同步播放。音視頻同步指的是視頻和音頻同步,也就是說(shuō)播放的聲音和視頻顯示的畫(huà)面保持一致。

正文

  • 視頻幀率(Frame Rate):指的是一秒顯示的幀數(shù)
  • 音頻采樣率(Sample Rate):指的是一秒播放的樣本個(gè)數(shù)

我們可以通過(guò)簡(jiǎn)單的計(jì)算得出one Frame 的播放時(shí)間或者是 one Sample的播放時(shí)間。以這樣的速度音頻和視頻各自進(jìn)行播放,在理想情況下,音視頻是會(huì)同步的。但是,實(shí)際情況下,音視頻會(huì)慢慢出現(xiàn)不同步的。不是視頻快了,就是音頻快了,很難準(zhǔn)確的同步。這就需要一種隨著時(shí)間會(huì)線性增長(zhǎng)的量,視頻和音頻的播放速度都以此為準(zhǔn)。播放快了就減慢播放速度,播放快了就加快播放速度。所以,視頻和音頻的同步實(shí)際上是一個(gè)動(dòng)態(tài)同步的過(guò)程,同步是暫時(shí)的,不同步是常態(tài)。以選擇的播放速度量為基準(zhǔn),快的等待慢點(diǎn),慢得則加快速度。

播放速度標(biāo)準(zhǔn)量的選擇一般來(lái)說(shuō)有三種:

  • 將視頻同步到音頻上,就是以音頻的播放速度為基準(zhǔn)來(lái)同步視頻。視頻比音頻慢了,加快其播放速度;快了,則減慢其播放速度。
  • 將音頻同步到視頻上,就是以視頻的播放速度為基準(zhǔn)來(lái)同步音頻。
  • 將視頻和音頻同步外部的時(shí)鐘上,選擇一個(gè)外部時(shí)鐘為基準(zhǔn),視頻和音頻的播放速度都以該時(shí)鐘為標(biāo)準(zhǔn)。

DTS和PTS

在音視頻流中的包都含有DTSPTS,我們以此作為選擇基準(zhǔn),到底是播放快了還是慢了,或者正以同步的速度播放。

  • DTS:Decoding Time Stamp 解碼時(shí)間戳——告訴解碼器packet解碼順序
  • PTS:Presenting Time Stamp 顯示時(shí)間戳——指示從packet中解碼出來(lái)的數(shù)據(jù)的顯示順序

計(jì)算視頻Frame的顯示時(shí)間

在計(jì)算某一幀的顯示時(shí)間之前,我們要先了解一下FFMpeg中的時(shí)間單位:Time Base。AVStream的TimeBase用來(lái)顯示的時(shí)間戳。

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

可以看出,AVStream是以秒(s)為單位來(lái)表示Frame的顯示時(shí)間,其類型是AVRational。

PTS為一個(gè)uint64_t的整型,其單位就是time_base。表示視頻長(zhǎng)度的duration也是一個(gè)uint64_t,st為一個(gè)AVStream的指針,av_q2d將一個(gè)AVRational轉(zhuǎn)換為雙精度浮點(diǎn)數(shù)。那么使用如下方法就可以計(jì)算出一個(gè)視頻流的時(shí)間長(zhǎng)度:

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

同樣的方法也可以得到視頻中某幀的顯示時(shí)間:

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

也就是說(shuō),得到了Frame的PTS后,就可以得到該frame顯示的時(shí)間戳。

得到Frame的PTS

通過(guò)上面的描述知道,如果有了Frame的PTS就計(jì)算出幀的顯示的時(shí)間。下面的代碼展示了在從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型,因?yàn)閷⑵涑艘粤藅ime_base,代表了該幀在視頻中的時(shí)間位置(秒為單位)。有可能存在調(diào)用av_frame_get_best_effort_timestamp得不到一個(gè)正確的PTS,這樣的情況放在函數(shù)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;
}

獲取Audio Clock

Audio Clock,也就是Audio的播放時(shí)長(zhǎng),可以在Audio時(shí)更新Audio Clock。在函數(shù)audio_decode_frame中解碼新的packet,這是可以設(shè)置Auddio clock為該packet的PTS

if (pkt.pts != AV_NOPTS_VALUE)
{
    audio_state->audio_clock = av_q2d(audio_state->stream->time_base) * pkt.pts;
}

由于一個(gè)packet中可以包含多個(gè)幀,packet中的PTS比真正的播放的PTS可能會(huì)早很多,可以根據(jù)Sample Rate 和 Sample Format來(lái)計(jì)算出該packet中的數(shù)據(jù)可以播放的時(shí)長(zhǎng),再次更新Audio clock 。

// 每秒鐘音頻播放的字節(jié)數(shù) sample_rate * channels * sample_format(一個(gè)sample占用的字節(jié)數(shù))
audio_state->audio_clock += static_cast<double>(data_size) / (2 * audio_state->stream->codec->channels *            
        audio_state->stream->codec->sample_rate);

上面乘以2是因?yàn)閟ample format是16位的無(wú)符號(hào)整型,占用2個(gè)字節(jié)。
有了Audio clock后,在外面獲取該值的時(shí)候卻不能直接返回該值,因?yàn)閍udio緩沖區(qū)的可能還有未播放的數(shù)據(jù),需要減去這部分的時(shí)間

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緩沖區(qū)中剩余的數(shù)據(jù)除以每秒播放的音頻數(shù)據(jù)得到剩余數(shù)據(jù)的播放時(shí)間,從Audio clock中減去這部分的值就是當(dāng)前的audio的播放時(shí)長(zhǎng)。

同步

現(xiàn)在有了video中Frame的顯示時(shí)間,并且得到了作為基準(zhǔn)時(shí)間的音頻播放時(shí)長(zhǎng)Audio clock ,可以將視頻同步到音頻了。

  • 用當(dāng)前幀的PTS - 上一播放幀的PTS得到一個(gè)延遲時(shí)間
  • 用當(dāng)前幀的PTS和Audio Clock進(jìn)行比較,來(lái)判斷視頻的播放速度是快了還是慢了
  • 根據(jù)上一步額判斷結(jié)果,設(shè)置播放下一幀的延遲時(shí)間。

使用要播放的當(dāng)前幀的PTS和上一幀的PTS差來(lái)估計(jì)播放下一幀的延遲時(shí)間,并根據(jù)video的播放速度來(lái)調(diào)整這個(gè)延遲時(shí)間,以實(shí)現(xiàn)視音頻的同步播放。
具體實(shí)現(xiàn):

// 將視頻同步到音頻上,計(jì)算下一幀的延遲時(shí)間
// 使用要播放的當(dāng)前幀的PTS和上一幀的PTS差來(lái)估計(jì)播放下一幀的延遲時(shí)間,并根據(jù)video的播放速度來(lái)調(diào)整這個(gè)延遲時(shí)間
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;

// 根據(jù)Audio clock來(lái)判斷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;

// 調(diào)整播放下一幀的延遲時(shí)間,以實(shí)現(xiàn)同步
if (fabs(diff) < NOSYNC_THRESHOLD) // 不同步
{
    if (diff <= -threshold) // 慢了,delay設(shè)為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; 

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

frame_last_ptsframe_last_delay是上一幀的PTS以及設(shè)置的播放上一幀時(shí)的延遲時(shí)間。

  • 首先根據(jù)當(dāng)前播放幀的PTS和上一播放幀的PTS估算出一個(gè)延遲時(shí)間。
  • 用當(dāng)前幀的PTS和Audio clock相比較判斷此時(shí)視頻播放的速度是快還是慢了
  • 視頻播放過(guò)快則加倍延遲,過(guò)慢則將延遲設(shè)置為0
  • frame_timer保存著視頻播放的延遲時(shí)間總和,這個(gè)值和當(dāng)前時(shí)間點(diǎn)的差值就是播放下一幀的真正的延遲時(shí)間
  • schedule_refresh 設(shè)置播放下一幀的延遲時(shí)間。

總結(jié)

本文主要描述如何利用audio的播放時(shí)長(zhǎng)作為基準(zhǔn),將視頻同步到音頻上以實(shí)現(xiàn)視音頻的同步播放。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,989評(píng)論 0 3
  • 最近學(xué)習(xí)播放器的一些東西,所以接觸了ffmpeg,看源碼的過(guò)程中,就想了解一下ffplay是怎么處理音視頻同步的,...
    smm987閱讀 4,602評(píng)論 0 5
  • 前幾篇文章,實(shí)現(xiàn)了音頻與視頻的單獨(dú)播放,但將音視頻結(jié)合到一塊之后會(huì)出現(xiàn)音頻與視頻不同步的問(wèn)題。經(jīng)研究之后,在此記錄...
    以帥服人的珂哥閱讀 20,064評(píng)論 2 33
  • 本文轉(zhuǎn)自:[FFmpeg 入門(mén)(5):視頻同步 | www.samirchen.com][2] 視頻如何同步 在之...
    SamirChen閱讀 3,036評(píng)論 0 5
  • 江西某處的無(wú)名野山上,吃罷晚飯,一老一少在院子里的大樹(shù)下整理農(nóng)具。 院子里的大樹(shù)參天,是因先有了大樹(shù),后蓋的院子。...
    一葉茶閱讀 407評(píng)論 0 4

友情鏈接更多精彩內(nèi)容