FFmpeg音視頻同步

SDL2文章列表git

SDL2入門github

SDL2事件處理ide

SDL2紋理渲染函數

SDL2音頻播放學習

FFmpeg+SDL2實現視頻流播放ui

FFmpeg+SDL2實現音頻流播放spa

前兩篇文章分別作了音頻和視頻的播放,要實現一個完整的簡易播放器就必需要作到音視頻同步播放了,而音視頻同步在音視頻開發中又是很是重要的知識點,因此在這裏記錄下音視頻同步相關知識的理解。code

音視頻同步簡介

從前面的學習能夠知道,在一個視頻文件中,音頻和視頻都是單獨以一條流的形式存在,互不干擾。那麼在播放時根據視頻的幀率(Frame Rate)和音頻的採樣率(Sample Rate)經過簡單的計算獲得其在某一Frame(Sample)的播放時間分別播放,**理論**上應該是同步的。可是因爲機器運行速度,解碼效率等等因素影響,頗有可能出現音頻和視頻不一樣步,例如出現視頻中人在說話,卻只能看到人物嘴動卻沒有聲音,很是影響用戶觀看體驗。orm

如何作到音視頻同步?要知道音視頻同步是一個動態的過程,同步是暫時的,不一樣步纔是常態,須要一種隨着時間會線性增加的量,視頻和音頻的播放速度都以該量爲標準,播放快了就減慢播放速度;播放慢了就加快播放的速度,在你追我趕中達到同步的狀態。目前主要有三種方式實現同步:視頻

  • 將視頻和音頻同步外部的時鐘上,選擇一個外部時鐘爲基準,視頻和音頻的播放速度都以該時鐘爲標準。
  • 將音頻同步到視頻上,就是以視頻的播放速度爲基準來同步音頻。
  • 將視頻同步到音頻上,就是以音頻的播放速度爲基準來同步視頻。

比較主流的是第三種,將視頻同步到音頻上。至於爲何不使用前兩種,由於通常來講,人對於聲音的敏感度更高,若是頻繁地去調整音頻會產生雜音讓人感受到刺耳不舒服,而人對圖像的敏感度就低不少了,因此通常都會採用第三種方式。

複習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;
複製代碼

時間基是一個分數,以秒爲單位,好比1/50秒,那它到底表示的是什麼意思呢?以幀率爲例,若是它的時間基是1/50秒,那麼就表示每隔1/50秒顯示一幀數據,也就是每1秒顯示50幀,幀率爲50FPS。

每一幀數據都有對應的PTS,在播放視頻或音頻的時候咱們須要將PTS時間戳轉化爲以秒爲單位的時間,用來最後的展現。那如何計算一楨在整個視頻中的時間位置?

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;
}
複製代碼

尚未結束,因爲一個packet中能夠包含多個Frame幀,packet中的PTS比真正的播放的PTS可能會早不少,能夠根據Sample Rate 和 Sample Format來計算出該packet中的數據能夠播放的時長,再次更新Audio_Clock。

// 每秒鐘音頻播放的字節數 採樣率 * 通道數 * 採樣位數 (一個sample佔用的字節數)
n = 2 * state->audio_ctx->channels;
state->audio_clock += (double) data_size /
                   (double) (n * state->audio_ctx->sample_rate);
複製代碼

最後還有一步,在咱們獲取這個Audio_Clock時,頗有可能音頻緩衝區還有沒有播放結束的數據,也就是有一部分數據實際尚未播放,因此就要在Audio_Clock上減去這部分數據的播放時間,纔是真正的Audio_Clock。

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

    //上一步獲取的PTS
    pts = state->audio_clock;
    // 音頻緩衝區尚未播放的數據
    buf_size = state->audio_buf_size - state->audio_buf_index; 
    // 每秒鐘音頻播放的字節數
    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函數獲取視頻的最合適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值
    }
    //根據時間基,計算每一幀的間隔時間
    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. 根據2的結果,設置播放下一幀的延遲時間
#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;

// 調整播放下一幀的延遲時間,以實現同步
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;
}
// 根據延時時間刷新視頻
schedule_refresh(is, (int) (actual_delay * 1000 + 0.5));
複製代碼

最後

將視頻同步到音頻上實現音視頻同步基本完成,整體就是動態的過程快了就等待,慢了就加速,在一個你追我趕的狀態下實現同步播放。

後面的博客會真正實現一個音視頻同步的播放器。

相關文章
相關標籤/搜索