10、詳解FFplay音視頻同步

[TOC]緩存

開始前的BB

有些沒有接觸過的童鞋可能還不知道音視頻同步是什麼意思,你們印象中應該看到過這樣的視頻,畫面中的人物說話和聲音出來的不在一塊兒,小時候看有些電視臺轉播的港片的時候(別想歪 TVB)有時候就會遇到 明明聲音已經播出來了,可是播的圖像比聲音慢了不少,看的極爲不舒服,這個時候就發生了音視頻不一樣步的狀況,而音視頻同步,就是讓聲音與畫面對應上bash

這裏有個知識點須要記一下框架

人對於圖像和聲音的接受靈敏程度不同,人對音頻比對視頻敏感;視頻放快一點,可能察覺的不是特別明顯,但音頻加快或減慢,人耳聽的很敏感ide

PTS的由來

音視頻同步依賴的一個東西就是pts(persentation time stamp )顯示時間戳 告訴咱們該什麼時間顯示這一幀 ,那麼,這個東西是從哪裏來的呢?ui

刨根問底欄目組將帶你深度挖掘this

PTS是在拍攝的時候打進去的時間戳,假如咱們如今拍攝一段小視頻(別想歪啊),什麼特效都不加,那麼走的就是如下的步驟spa

咱們根據這個圖能夠知道,PTS是在錄製的時候就打進Frame裏的debug

音視頻同步的方式

在ffplay中 音視頻同步有三種方式code

  1. 以視頻爲基準,同步音頻到視頻
    • 音頻慢了就加快音頻的播放速度,或者直接丟掉一部分音頻幀
    • 音頻快了就放慢音頻的播放速度
  2. 以音頻爲基準,同步視頻到音頻
    • 視頻慢了則加快播放或丟掉部分視頻幀
    • 視頻快了則延遲播放
  3. 之外部時鐘爲準,同步音頻和視頻到外部時鐘
    • 根據外部時鐘改版音頻和視頻的播放速度

視頻基準

若是以視頻爲基準進行同步,那麼咱們就要考慮可能出現的狀況,好比:cdn

掉幀

此時的音頻應該怎麼作呢?一般的方法有

  1. 音頻也丟掉相應的音頻幀(會有斷音,好比你說了一句,個人天啊,好漂亮的草原啊 很不湊巧丟了幾幀視頻,就成,,,臥槽!)
  2. 音頻加快速度播放(此處能夠直接用Audition加快個幾倍速的播放一首音樂)

音頻基準

若是以音頻爲基準進行同步,很不幸的碰到了掉幀的狀況,那麼視頻應該怎麼作呢?一般也有兩種作法

1.視頻丟幀 (畫面跳幀,丟的多的話,俗稱卡成PPT) 2.加快播放速度(畫面加快播放)

外部時鐘爲基準

假如之外部時鐘爲基準,若是音視頻出現了丟幀,怎麼辦呢?

若是丟幀較多,直接從新初始化外部時鐘 (pts和時鐘進行對比,超過必定閾值重設外部時鐘,好比1s)

音視頻時間換算

PTS 時間換算

以前咱們稍微講過pts的時間換算,pts換算成真正的秒是用如下操做

realTime = pts * av_q2d(stream.time_base)

stream是當前的視頻/音頻流

咱們這裏主要講一下在音頻解碼pts可能會遇到的狀況,有時候音頻幀的pts會以1/採樣率爲單位,像

pts1 = 0 pts2 = 1024 pts3 = 2048

像咱們例子中的這個視頻,咱們在解碼一幀音頻以後打印出來他的pts std::cout<<"audio pts : "<<frame->pts<<std::endl;

咱們知道當前視頻的音頻採樣率爲44100,那麼這個音頻幀pts的單位就是1/44100,那麼

pts1 = 0 * 1 / 44100 = 0 pts2 = 1024 * 1 / 44100 = 0.232 pts3 = 2048 * 1 / 44100 = 0.464

音頻流的time_base裏面正是記錄了這個值,咱們能夠經過debug來看一下

利用 realTime = pts * av_q2d(stream.time_base) 咱們能夠直接算出來當前音頻幀的pts

另外須要注意

在ffplay中作音視頻同步,都是以秒爲單位

音視頻幀播放時間換算

音頻幀播放時間計算

音頻幀的播放和音頻的屬性有關係,是採用

採樣點數 * 1 / 採樣率

來計算,像AAC當個通道採樣是1024個採樣點,那麼

  • 若是是44.1khz,那麼一幀的播放時長就是 1024 * 1 / 44100 = 23.3毫秒
  • 若是是48khz,那麼一幀的播放時長就是 1024 * 1 / 48000 = 21.33毫秒

視頻幀的播放時間計算

視頻幀的播放時間也有兩個計算方式

  1. 利用1/幀率獲取每一個幀平均播放時間,這個方式有一個很大的缺點就是,不能動態響應視頻幀的變化,好比說咱們作一些快速慢速的特效,有的廠商或者SDK(咱們的SDK不是)是直接改變視頻幀的增長/減小視頻幀之間的pts間距來實現的,這就致使在一些拿幀率計算顯示時間的播放器上發現是總體(快/慢)了,達不到想要的效果;還有一種狀況就是丟幀以後,時間顯示仍然是固定的
  2. 相鄰幀相減 這大程度上避免利用幀率去算的各類弊端,可是缺點是使用起來比較複雜,尤爲是暫停/Seek之類的操做的時候須要進行一些時間差值的計算

時間校訂

視頻時間校訂

在看ffplay的時候咱們會發現,他在裏面默認狀況下是用了 frame->pts = frame->best_effort_timestamp; 其實大多數狀況下ptsbest_effort_timestamp的值是同樣的,這個值是利用各類探索方法去計算當前幀的視頻戳

音頻時間校訂

音頻的pts獲取比視頻的要複雜一點,在ffplay中對音頻的pts作了三次修改

  1. frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb); 將其由stream->time_base轉爲(1/採樣率)(decoder_decode_frame()中)

  2. af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb); 將其由(1/採樣率)轉換爲秒 (audio_thread()中)

  3. is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec 根據實際輸入進SDL2播放的數據長度作調整 (sdl_audio_callback中)

ffplay 時鐘框架

ffplay中的時鐘框架主要依靠Clock結構體和相應的方法組成

/** 時鐘結構體 **/
typedef struct Clock {
    double pts;           /* clock base 時間基準*/
    double pts_drift;     /* clock base minus time at which we updated the clock 時間基減去更新時鐘的時間 */
    double last_updated;
    double speed;
    int serial;           /* clock is based on a packet with this serial */
    int paused;
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

/** 初始化時鐘 **/
static void init_clock(Clock *c, int *queue_serial);

/** 獲取當前時鐘 **/
static double get_clock(Clock *c);

/** 設置時鐘  內部調用set_clock_at()**/
static void set_clock(Clock *c, double pts, int serial);

/** 設置時鐘 **/
static void set_clock_at(Clock *c, double pts, int serial, double time);

/** 設置時鐘速度 **/
static void set_clock_speed(Clock *c, double speed);

/** 音/視頻設置時鐘的時候都回去跟外部時鐘進行對比,防止丟幀或者丟包狀況下時間差距比較大而進行的糾偏 **/
static void sync_clock_to_slave(Clock *c, Clock *slave);

/** 獲取作爲基準的類型  音頻 外部時鐘 視頻 **/
static int get_master_sync_type(VideoState *is);

/** 獲取主時間軸的時間 **/
static double get_master_clock(VideoState *is);

/** 檢查外部時鐘的速度 **/
static void check_external_clock_speed(VideoState *is);
複製代碼

這個時鐘框架也是比較簡單,能夠直接去看FFplay的源碼,這裏就不過多的敘述

音視頻同步時間軸

ffplay中,咱們不論是以哪一個方式作爲基準,都是有一個時間軸

就像這樣子,有一個時鐘一直在跑,所謂基於音頻、視頻、外部時間 作爲基準,也就是將那個軸的的時間作爲時間軸的基準,另外一個在軸參照主時間軸進行同步

假如是以音頻爲基準,視頻同步音頻的方式,那麼就是音頻在每播放一幀的時候,就去將當前的時間同步到時間軸,視頻參考時間軸作調整

音頻時鐘設置

音頻時鐘的設置的話須要考慮注意 硬件緩存數據 設置音頻時鐘的時候須要將 pts - 硬件緩衝數據的播放時間 詳情參考 ffplay 中 sdl_audio_callback(void *opaque, Uint8 *stream, int len)

set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);
複製代碼

這是就是將音頻的pts - 硬件緩衝區裏剩下的時間設置到了音頻的時鐘裏

視頻時鐘設置

視頻時鐘設置的話就比較簡單了,直接設置pts,在ffplay中 queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)內,咱們能夠直接看到 vp->pts = pts; ,而後在video_refresh裏面update_video_pts(is, vp->pts, vp->pos, vp->serial);去調用了set_clock

static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
    /* update current video pts */
    set_clock(&is->vidclk, pts, serial);
    sync_clock_to_slave(&is->extclk, &is->vidclk);
}
複製代碼

音視頻同步操做

音視頻在同步上出的處理咱們上面有簡單講到過,咱們這裏來詳細看一下他具體是真麼作的

音頻同步操做

音頻的同步操做是在audio_decode_frame()中的wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);,注意synchronize_audio 方法,咱們來看他註釋

/* return the wanted number of samples to get better sync if sync_type is video
 * or external master clock
 *
 * 若是同步類型爲視頻或外部主時鐘,則返回所需的採樣數來更好的同步。
 *
 * */
static int synchronize_audio(VideoState *is, int nb_samples)
複製代碼

這個方法裏面的操做有點多,我這邊簡單說一下這個方法,主要是利用音頻時鐘與主時鐘相減獲得差值(須要先判斷音頻是否是主時間軸),而後返回若是要同步須要的採樣數,在audio_decode_frame()中用len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples); 進行重採樣,而後纔在sdl_audio_callback()中進行播放

視頻同步操做

視頻同步操做的主要步驟是在video_refresh()方法中,咱們來看一下關鍵的地方

/* compute nominal last_duration 根據當前幀和上一幀的pts計算出來上一幀顯示的持續時間 */
            last_duration = vp_duration(is, lastvp, vp);
            /** 計算當前幀須要顯示的時間 **/
            delay = compute_target_delay(last_duration, is);

            /** 獲取當前的時間 **/
            time= av_gettime_relative()/1000000.0;
            /** 若是當前時間小於顯示時間 則直接進行顯示**/
            if (time < is->frame_timer + delay) {
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

            /** 更新視頻的基準時間 **/
            is->frame_timer += delay;
            /** 若是當前時間與基準時間誤差大於 AV_SYNC_THRESHOLD_MAX 則把視頻基準時間設置爲當前時間 **/
            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
                is->frame_timer = time;

            /** 更新視頻時間軸 **/
            SDL_LockMutex(is->pictq.mutex);
            if (!isnan(vp->pts))
                update_video_pts(is, vp->pts, vp->pos, vp->serial);
            SDL_UnlockMutex(is->pictq.mutex);

            /** 若是隊列中有未顯示的幀,若是開啓了丟幀處理或者不是以視頻爲主時間軸,則進行丟幀處理 **/
            if (frame_queue_nb_remaining(&is->pictq) > 1) {
                Frame *nextvp = frame_queue_peek_next(&is->pictq);
                duration = vp_duration(is, vp, nextvp);
                if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
                    is->frame_drops_late++;
                    frame_queue_next(&is->pictq);
                    goto retry;
                }
            }

複製代碼

到這裏,ffplay中主要的音視頻同步就講完了,建議去看一下ffplay的源碼,多體會體會 印象纔會比較深入,說實話ffplay中同步的操做是比較複雜的,咱們在日常開發中要根據本身的實際業務進行一些簡化和改進的,下一章咱們就來寫一個以音頻爲基準的視頻播放器

未完持續...

相關文章
相關標籤/搜索