十、詳解FFplay音視訊同步

Mirs發表於2019-04-10

[TOC]

開始前的BB

有些沒有接觸過的童鞋可能還不知道音視訊同步是什麼意思,大家印象中應該看到過這樣的視訊,畫面中的人物說話和聲音出來的不在一起,小時候看有些電視臺轉播的港片的時候(別想歪 TVB)有時候就會遇到 明明聲音已經播出來了,但是播的影象比聲音慢了很多,看的極為不舒服,這個時候就發生了音視訊不同步的情況,而音視訊同步,就是讓聲音與畫面對應上

這裡有個知識點需要記一下

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

PTS的由來

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

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

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

十、詳解FFplay音視訊同步

我們根據這個圖可以知道,PTS是在錄製的時候就打進Frame裡的

音視訊同步的方式

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

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

視訊基準

如果以視訊為基準進行同步,那麼我們就要考慮可能出現的情況,比如:

掉幀

此時的音訊應該怎麼做呢?通常的方法有

  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;

十、詳解FFplay音視訊同步

我們知道當前視訊的音訊取樣率為44100,那麼這個音訊幀pts的單位就是1/44100,那麼

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

音訊流的time_base裡面正是記錄了這個值,我們可以通過debug來看一下

十、詳解FFplay音視訊同步
利用 realTime = pts * av_q2d(stream.time_base) 我們可以直接算出來當前音訊幀的pts

十、詳解FFplay音視訊同步

另外需要注意

在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中,我們不管是以哪個方式做為基準,都是有一個時間軸

十、詳解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中同步的操作是比較複雜的,我們在平常開發中要根據自己的實際業務進行一些簡化和改進的,下一章我們就來寫一個以音訊為基準的視訊播放器

未完持續...

相關文章