FFmpeg音視頻同步

SDL2文章列表

SDL2入門

SDL2事件處理

SDL2紋理渲染

SDL2音頻播放

FFmpeg+SDL2實現(xiàn)視頻流播放

FFmpeg+SDL2實現(xiàn)音頻流播放

前兩篇文章分別做了音頻和視頻的播放,要實現(xiàn)一個完整的簡易播放器就必須要做到音視頻同步播放了,而音視頻同步在音視頻開發(fā)中又是非常重要的知識點吵护,所以在這里記錄下音視頻同步相關(guān)知識的理解。

音視頻同步簡介

從前面的學習可以知道埋合,在一個視頻文件中卧须,音頻和視頻都是單獨以一條流的形式存在创淡,互不干擾痴晦。那么在播放時根據(jù)視頻的幀率(Frame Rate)和音頻的采樣率(Sample Rate)通過簡單的計算得到其在某一Frame(Sample)的播放時間分別播放,<u>理論</u>上應該是同步的琳彩。但是由于機器運行速度誊酌,解碼效率等等因素影響部凑,很有可能出現(xiàn)音頻和視頻不同步,例如出現(xiàn)視頻中人在說話碧浊,卻只能看到人物嘴動卻沒有聲音涂邀,非常影響用戶觀看體驗。

如何做到音視頻同步箱锐?要知道音視頻同步是一個動態(tài)的過程比勉,同步是暫時的,不同步才是常態(tài)驹止,需要一種隨著時間會線性增長的量浩聋,視頻和音頻的播放速度都以該量為標準,播放快了就減慢播放速度臊恋;播放慢了就加快播放的速度衣洁,在你追我趕中達到同步的狀態(tài)。目前主要有三種方式實現(xiàn)同步:

  • 將視頻和音頻同步外部的時鐘上抖仅,選擇一個外部時鐘為基準坊夫,視頻和音頻的播放速度都以該時鐘為標準。
  • 將音頻同步到視頻上岸售,就是以視頻的播放速度為基準來同步音頻践樱。
  • 將視頻同步到音頻上厂画,就是以音頻的播放速度為基準來同步視頻凸丸。

比較主流的是第三種,將視頻同步到音頻上袱院。至于為什么不使用前兩種屎慢,因為一般來說,人對于聲音的敏感度更高忽洛,如果頻繁地去調(diào)整音頻會產(chǎn)生雜音讓人感覺到刺耳不舒服腻惠,而人對圖像的敏感度就低很多了,所以一般都會采用第三種方式欲虚。

復習DTS集灌、PTS和時間基

  • PTS: Presentation Time Stamp,顯示渲染用的時間戳复哆,告訴我們什么時候需要顯示
  • DTS: Decode Time Stamp欣喧,視頻解碼時的時間戳,告訴我們什么時候需要解碼

在音頻中PTS和DTS一般相同梯找。但是在視頻中唆阿,由于B幀的存在,PTS和DTS可能會不同锈锤。

實際幀順序:I B B P

存放幀順序:I P B B

解碼時間戳:1 4 2 3

展示時間戳:1 2 3 4

  • 時間基
/**
 * This is the fundamental unit of time (in seconds) in terms
 * of which frame timestamps are represented.
 * 這是表示幀時間戳的基本時間單位(以秒為單位)驯鳖。
**/
typedef struct AVRational{
    int num; ///< Numerator 分子
    int den; ///< Denominator 分母
} AVRational;

時間基是一個分數(shù)闲询,以秒為單位,比如1/50秒浅辙,那它到底表示的是什么意思呢扭弧?以幀率為例,如果它的時間基是1/50秒记舆,那么就表示每隔1/50秒顯示一幀數(shù)據(jù)寄狼,也就是每1秒顯示50幀,幀率為50FPS氨淌。

每一幀數(shù)據(jù)都有對應的PTS泊愧,在播放視頻或音頻的時候我們需要將PTS時間戳轉(zhuǎn)化為以秒為單位的時間,用來最后的展示盛正。那如何計算一楨在整個視頻中的時間位置删咱?

static inline double av_q2d(AVRational a){
    return a.num / (double) a.den;
}

//計算一楨在整個視頻中的時間位置
timestamp(秒) = pts * av_q2d(st->time_base);

Audio_Clock

Audio_Clock,也就是Audio的播放時長豪筝,從開始到當前的時間痰滋。獲取Audio_Clock:

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

還沒有結(jié)束,由于一個packet中可以包含多個Frame幀续崖,packet中的PTS比真正的播放的PTS可能會早很多敲街,可以根據(jù)Sample Rate 和 Sample Format來計算出該packet中的數(shù)據(jù)可以播放的時長,再次更新Audio_Clock严望。

// 每秒鐘音頻播放的字節(jié)數(shù) 采樣率 * 通道數(shù) * 采樣位數(shù) (一個sample占用的字節(jié)數(shù))
n = 2 * state->audio_ctx->channels;
state->audio_clock += (double) data_size /
                   (double) (n * state->audio_ctx->sample_rate);

最后還有一步多艇,在我們獲取這個Audio_Clock時,很有可能音頻緩沖區(qū)還有沒有播放結(jié)束的數(shù)據(jù)像吻,也就是有一部分數(shù)據(jù)實際還沒有播放峻黍,所以就要在Audio_Clock上減去這部分數(shù)據(jù)的播放時間,才是真正的Audio_Clock拨匆。

double get_audio_clock(VideoState *state) {
    double pts;
    int buf_size, bytes_per_sec;

    //上一步獲取的PTS
    pts = state->audio_clock;
    // 音頻緩沖區(qū)還沒有播放的數(shù)據(jù)
    buf_size = state->audio_buf_size - state->audio_buf_index; 
    // 每秒鐘音頻播放的字節(jié)數(shù)
    bytes_per_sec = state->audio_ctx->sample_rate * state->audio_ctx->channels * 2;
    pts -= (double) buf_size / bytes_per_sec;
    return pts;
}

get_audio_clock中返回的才是我們最終需要的Audio_Clock姆涩,當前的音頻的播放時長。

Video_Clock

Video_Clock惭每,視頻播放到當前幀時的已播放的時間長度骨饿。

avcodec_send_packet(state->video_ctx, packet);
while (avcodec_receive_frame(state->video_ctx, pFrame) == 0) {
    if ((pts = pFrame->best_effort_timestamp) != AV_NOPTS_VALUE) {
    } else {
        pts = 0;
    }
    pts *= av_q2d(state->video_st->time_base); // 時間基換算,單位為秒

    pts = synchronize_video(state, pFrame, pts);
    
    av_packet_unref(packet);
}

舊版的FFmpeg使用av_frame_get_best_effort_timestamp函數(shù)獲取視頻的最合適PTS台腥,新版本的則在解碼時生成了best_effort_timestamp宏赘。但是依然可能會獲取不到正確的PTS,所以在synchronize_video中進行處理览爵。

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

    double frame_delay;

    if (pts != 0) {
        state->video_clock = pts;
    } else {
        pts = state->video_clock;// PTS錯誤置鼻,使用上一次的PTS值
    }
    //根據(jù)時間基,計算每一幀的間隔時間
    frame_delay = av_q2d(state->video_ctx->time_base);
    //解碼后的幀要延時的時間
    frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
    state->video_clock += frame_delay;//得到video_clock,實際上也是預測的下一幀視頻的時間
    return pts;
}

同步

上面兩步獲得了Audio_Clock和Video_Clock蜓竹,這樣我們就有了視頻流中Frame的顯示時間箕母,并且得到了作為基準時間的音頻播放時長Audio clock 储藐,可以將視頻同步到音頻了。

  1. 用當前幀的PTS - 上一播放幀的PTS得到一個延遲時間
  2. 用當前幀的PTS和Audio_Clock進行比較嘶是,來判斷視頻的播放速度是快了還是慢了
  3. 根據(jù)2的結(jié)果钙勃,設置播放下一幀的延遲時間
#define AV_SYNC_THRESHOLD 0.01 // 同步最小閾值
#define AV_NOSYNC_THRESHOLD 10.0 //  不同步閾值
double actual_delay, delay, sync_threshold, ref_clock, diff;

// 當前Frame時間減去上一幀的時間,獲取兩幀間的延時
delay = vp->pts - is->frame_last_pts;
if (delay <= 0 || delay >= 1.0) { 
    // 延時小于0或大于1秒(太長)都是錯誤的聂喇,將延時時間設置為上一次的延時時間
    delay = is->frame_last_delay;
}

// 獲取音頻Audio_Clock
ref_clock = get_audio_clock(is);
// 得到當前PTS和Audio_Clock的差值
diff = vp->pts - ref_clock;

sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;

// 調(diào)整播放下一幀的延遲時間辖源,以實現(xiàn)同步
if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
    if (diff <= -sync_threshold) { // 慢了,delay設為0
        delay = 0;
    } else if (diff >= sync_threshold) { // 快了希太,加倍delay
        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;
}
// 根據(jù)延時時間刷新視頻
schedule_refresh(is, (int) (actual_delay * 1000 + 0.5));

最后

將視頻同步到音頻上實現(xiàn)音視頻同步基本完成克饶,總體就是動態(tài)的過程快了就等待,慢了就加速誊辉,在一個你追我趕的狀態(tài)下實現(xiàn)同步播放矾湃。

后面的博客會真正實現(xiàn)一個音視頻同步的播放器。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末堕澄,一起剝皮案震驚了整個濱河市邀跃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛙紫,老刑警劉巖拍屑,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異坑傅,居然都是意外死亡僵驰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門裁蚁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來矢渊,“玉大人,你說我怎么就攤上這事枉证。” “怎么了移必?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵室谚,是天一觀的道長。 經(jīng)常有香客問我崔泵,道長秒赤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任憎瘸,我火速辦了婚禮入篮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘幌甘。我一直安慰自己潮售,他們只是感情好痊项,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酥诽,像睡著了一般鞍泉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肮帐,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天咖驮,我揣著相機與錄音,去河邊找鬼训枢。 笑死托修,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的恒界。 我是一名探鬼主播诀黍,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起形导,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤旦袋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后楔敌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年郁轻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片文留。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡好唯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出燥翅,到底是詐尸還是另有隱情骑篙,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布森书,位于F島的核電站靶端,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏凛膏。R本人自食惡果不足惜杨名,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猖毫。 院中可真熱鬧台谍,春花似錦、人聲如沸吁断。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至介衔,卻和暖如春恨胚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背炎咖。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工赃泡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人乘盼。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓升熊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親级野。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容