音視頻同步在音視頻開發(fā)中是非常重要的知識(shí)點(diǎn),所以在這里記錄下音視頻同步相關(guān)知識(shí)的理解。
音視頻同步簡(jiǎn)介
從前面的學(xué)習(xí)可以知道,在一個(gè)視頻文件中,音頻和視頻都是單獨(dú)以一條流的形式存在,互不干擾。那么在播放時(shí)根據(jù)視頻的幀率(Frame Rate)和音頻的采樣率(Sample Rate)通過(guò)簡(jiǎn)單的計(jì)算得到其在某一Frame(Sample)的播放時(shí)間分別播放,**理論**上應(yīng)該是同步的。但是由于機(jī)器運(yùn)行速度,解碼效率等等因素影響,很有可能出現(xiàn)音頻和視頻不同步,例如出現(xiàn)視頻中人在說(shuō)話,卻只能看到人物嘴動(dòng)卻沒(méi)有聲音,非常影響用戶觀看體驗(yàn)。
如何做到音視頻同步?要知道音視頻同步是一個(gè)動(dòng)態(tài)的過(guò)程,同步是暫時(shí)的,不同步才是常態(tài),需要一種隨著時(shí)間會(huì)線性增長(zhǎng)的量,視頻和音頻的播放速度都以該量為標(biāo)準(zhǔn),播放快了就減慢播放速度;播放慢了就加快播放的速度,在你追我趕中達(dá)到同步的狀態(tài)。目前主要有三種方式實(shí)現(xiàn)同步:
- 將視頻和音頻同步外部的時(shí)鐘上,選擇一個(gè)外部時(shí)鐘為基準(zhǔn),視頻和音頻的播放速度都以該時(shí)鐘為標(biāo)準(zhǔn)。
- 將音頻同步到視頻上,就是以視頻的播放速度為基準(zhǔn)來(lái)同步音頻。
- 將視頻同步到音頻上,就是以音頻的播放速度為基準(zhǔn)來(lái)同步視頻。
比較主流的是第三種,將視頻同步到音頻上。至于為什么不使用前兩種,因?yàn)橐话銇?lái)說(shuō),人對(duì)于聲音的敏感度更高,如果頻繁地去調(diào)整音頻會(huì)產(chǎn)生雜音讓人感覺(jué)到刺耳不舒服,而人對(duì)圖像的敏感度就低很多了,所以一般都會(huì)采用第三種方式。
復(fù)習(xí)DTS、PTS和時(shí)間基
- PTS: Presentation Time Stamp,顯示渲染用的時(shí)間戳,告訴我們什么時(shí)候需要顯示
- DTS: Decode Time Stamp,視頻解碼時(shí)的時(shí)間戳,告訴我們什么時(shí)候需要解碼
在音頻中PTS和DTS一般相同。但是在視頻中,由于B幀的存在,PTS和DTS可能會(huì)不同。
實(shí)際幀順序:I B B P
存放幀順序:I P B B
解碼時(shí)間戳:1 4 2 3
展示時(shí)間戳:1 2 3 4
- 時(shí)間基
/** * This is the fundamental unit of time (in seconds) in terms * of which frame timestamps are represented. * 這是表示幀時(shí)間戳的基本時(shí)間單位(以秒為單位)。**/typedef struct AVRational{ int num; ///< Numerator 分子 int den; ///< Denominator 分母} AVRational;
時(shí)間基是一個(gè)分?jǐn)?shù),以秒為單位,比如1/50秒,那它到底表示的是什么意思呢?以幀率為例,如果它的時(shí)間基是1/50秒,那么就表示每隔1/50秒顯示一幀數(shù)據(jù),也就是每1秒顯示50幀,幀率為50FPS。
每一幀數(shù)據(jù)都有對(duì)應(yīng)的PTS,在播放視頻或音頻的時(shí)候我們需要將PTS時(shí)間戳轉(zhuǎn)化為以秒為單位的時(shí)間,用來(lái)最后的展示。那如何計(jì)算一楨在整個(gè)視頻中的時(shí)間位置?
static inline double av_q2d(AVRational a){ return a.num / (double) a.den;}//計(jì)算一楨在整個(gè)視頻中的時(shí)間位置timestamp(秒) = pts * av_q2d(st->time_base);
Audio_Clock
Audio_Clock,也就是Audio的播放時(shí)長(zhǎng),從開始到當(dāng)前的時(shí)間。獲取Audio_Clock:
if (pkt->pts != AV_NOPTS_VALUE) { state->audio_clock = av_q2d(state->audio_st->time_base) * pkt->pts;}
【免費(fèi)分享】整理了一些學(xué)習(xí)資料、教學(xué)視頻和學(xué)習(xí)路線圖,資料包括《Andoird音視頻開發(fā)必備手冊(cè)+音視頻學(xué)習(xí)視頻+學(xué)習(xí)文檔資料包+大廠面試真題+2022最新學(xué)習(xí)路線圖》等
點(diǎn)擊下方鏈接加衛(wèi)星號(hào)獲取,領(lǐng)取資料一定要備注來(lái)源:“007”,會(huì)優(yōu)先通過(guò)
FFmpegWebRTCRTMPRTSPHLSRTP播放器-音視頻流媒體高級(jí)開發(fā)
還沒(méi)有結(jié)束,由于一個(gè)packet中可以包含多個(gè)Frame幀,packet中的PTS比真正的播放的PTS可能會(huì)早很多,可以根據(jù)Sample Rate 和 Sample Format來(lái)計(jì)算出該packet中的數(shù)據(jù)可以播放的時(shí)長(zhǎng),再次更新Audio_Clock。
// 每秒鐘音頻播放的字節(jié)數(shù) 采樣率 * 通道數(shù) * 采樣位數(shù) (一個(gè)sample占用的字節(jié)數(shù))n = 2 * state->audio_ctx->channels;state->audio_clock += (double) data_size / (double) (n * state->audio_ctx->sample_rate);
最后還有一步,在我們獲取這個(gè)Audio_Clock時(shí),很有可能音頻緩沖區(qū)還有沒(méi)有播放結(jié)束的數(shù)據(jù),也就是有一部分?jǐn)?shù)據(jù)實(shí)際還沒(méi)有播放,所以就要在Audio_Clock上減去這部分?jǐn)?shù)據(jù)的播放時(shí)間,才是真正的Audio_Clock。
double get_audio_clock(VideoState *state) { double pts; int buf_size, bytes_per_sec; //上一步獲取的PTS pts = state->audio_clock; // 音頻緩沖區(qū)還沒(méi)有播放的數(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,當(dāng)前的音頻的播放時(shí)長(zhǎng)。
Video_Clock
Video_Clock,視頻播放到當(dāng)前幀時(shí)的已播放的時(shí)間長(zhǎng)度。
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); // 時(shí)間基換算,單位為秒 pts = synchronize_video(state, pFrame, pts); av_packet_unref(packet);}
舊版的FFmpeg使用av_frame_get_best_effort_timestamp函數(shù)獲取視頻的最合適PTS,新版本的則在解碼時(shí)生成了best_effort_timestamp。但是依然可能會(huì)獲取不到正確的PTS,所以在synchronize_video中進(jìn)行處理。
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錯(cuò)誤,使用上一次的PTS值 } //根據(jù)時(shí)間基,計(jì)算每一幀的間隔時(shí)間 frame_delay = av_q2d(state->video_ctx->time_base); //解碼后的幀要延時(shí)的時(shí)間 frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); state->video_clock += frame_delay;//得到video_clock,實(shí)際上也是預(yù)測(cè)的下一幀視頻的時(shí)間 return pts;}
同步
上面兩步獲得了Audio_Clock和Video_Clock,這樣我們就有了視頻流中Frame的顯示時(shí)間,并且得到了作為基準(zhǔn)時(shí)間的音頻播放時(shí)長(zhǎng)Audio clock ,可以將視頻同步到音頻了。
#define AV_SYNC_THRESHOLD 0.01 // 同步最小閾值#define AV_NOSYNC_THRESHOLD 10.0 // 不同步閾值double actual_delay, delay, sync_threshold, ref_clock, diff;// 當(dāng)前Frame時(shí)間減去上一幀的時(shí)間,獲取兩幀間的延時(shí)delay = vp->pts – is->frame_last_pts;if (delay = 1.0) { // 延時(shí)小于0或大于1秒(太長(zhǎng))都是錯(cuò)誤的,將延時(shí)時(shí)間設(shè)置為上一次的延時(shí)時(shí)間 delay = is->frame_last_delay;}// 獲取音頻Audio_Clockref_clock = get_audio_clock(is);// 得到當(dāng)前PTS和Audio_Clock的差值diff = vp->pts – ref_clock;sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;// 調(diào)整播放下一幀的延遲時(shí)間,以實(shí)現(xiàn)同步if (fabs(diff) frame_timer += delay;// 最終真正要延時(shí)的時(shí)間actual_delay = is->frame_timer – (av_gettime() / 1000000.0);if (actual_delay < 0.010) { // 延時(shí)時(shí)間過(guò)小就設(shè)置個(gè)最小值 actual_delay = 0.010;}// 根據(jù)延時(shí)時(shí)間刷新視頻schedule_refresh(is, (int) (actual_delay * 1000 + 0.5));
最后
將視頻同步到音頻上實(shí)現(xiàn)音視頻同步基本完成,總體就是動(dòng)態(tài)的過(guò)程快了就等待,慢了就加速,在一個(gè)你追我趕的狀態(tài)下實(shí)現(xiàn)同步播放。
后面的博客會(huì)持續(xù)更新真正實(shí)現(xiàn)一個(gè)音視頻同步的播放器。
分享一個(gè)非常好的音視頻學(xué)習(xí)地址,可點(diǎn)擊免費(fèi)報(bào)名訂閱學(xué)習(xí),先關(guān)注不迷路。[給力]
【免費(fèi)】FFmpeg/WebRTC/RTMP/NDK/Android音視頻流媒體高級(jí)開發(fā)-學(xué)習(xí)視頻教程-騰訊課堂