FFmpeg音視訊同步
SDL2文章列表
前兩篇文章分別做了音訊和視訊的播放,要實現一個完整的簡易播放器就必須要做到音視訊同步播放了,而音視訊同步在音視訊開發中又是非常重要的知識點,所以在這裡記錄下音視訊同步相關知識的理解。
音視訊同步簡介
從前面的學習可以知道,在一個視訊檔案中,音訊和視訊都是單獨以一條流的形式存在,互不干擾。那麼在播放時根據視訊的幀率(Frame Rate)和音訊的取樣率(Sample Rate)通過簡單的計算得到其在某一Frame(Sample)的播放時間分別播放,**理論 **上應該是同步的。但是由於機器執行速度,解碼效率等等因素影響,很有可能出現音訊和視訊不同步,例如出現視訊中人在說話,卻只能看到人物嘴動卻沒有聲音,非常影響使用者觀看體驗。
如何做到音視訊同步?要知道音視訊同步是一個動態的過程,同步是暫時的,不同步才是常態,需要一種隨著時間會線性增長的量,視訊和音訊的播放速度都以該量為標準,播放快了就減慢播放速度;播放慢了就加快播放的速度,在你追我趕中達到同步的狀態。目前主要有三種方式實現同步:
- 將視訊和音訊同步外部的時鐘上 ,選擇一個外部時鐘為基準,視訊和音訊的播放速度都以該時鐘為標準。
- 將音訊同步到視訊上 ,就是以視訊的播放速度為基準來同步音訊。
- 將視訊同步到音訊上 ,就是以音訊的播放速度為基準來同步視訊。
比較主流的是第三種,將視訊同步到音訊上。至於為什麼不使用前兩種,因為一般來說,人對於聲音的敏感度更高,如果頻繁地去調整音訊會產生雜音讓人感覺到刺耳不舒服,而人對影象的敏感度就低很多了,所以一般都會採用第三種方式。
複習DTS、PTS和時間基
- PTS: PresentationTime 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 ,可以將視訊同步到音訊了。
- 用當前幀的PTS - 上一播放幀的PTS得到一個延遲時間
- 用當前幀的PTS和Audio_Clock進行比較,來判斷視訊的播放速度是快了還是慢了
- 根據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)); 複製程式碼