在上一篇文章中,視頻和音頻是各自獨立播放的,並不一樣步。本文主要描述瞭如何以音頻的播放時長爲基準,將視頻同步到音頻上以實現視音頻的同步播放的。主要有如下幾個方面的內容git
- 視音頻同步的簡單介紹
- DTS 和 PTS
- 計算視頻中Frame的顯示時間
- 獲取Audio clock(audio的播放時長)
- 將視頻同步到音頻上,實現視音頻同步播放
視音頻同步簡單介紹
通常來講,視頻同步指的是視頻和音頻同步,也就是說播放的聲音要和當前顯示的畫面保持一致。想象如下,看一部電影的時候只看到人物嘴動沒有聲音傳出;或者畫面是激烈的戰鬥場景,而聲音不是槍炮聲倒是人物說話的聲音,這是很是差的一種體驗。
在視頻流和音頻流中已包含了其以怎樣的速度播放的相關數據,視頻的幀率(Frame Rate)指示視頻一秒顯示的幀數(圖像數);音頻的採樣率(Sample Rate)表示音頻一秒播放的樣本(Sample)的個數。可使用以上數據經過簡單的計算獲得其在某一Frame(Sample)的播放時間,以這樣的速度音頻和視頻各自播放互不影響,在理想條件下,其應該是同步的,不會出現誤差。但,理想條件是什麼你們都懂得。若是用上面那種簡單的計算方式,慢慢的就會出現音視頻不一樣步的狀況。要不是視頻播放快了,要麼是音頻播放快了,很難準確的同步。這就須要一種隨着時間會線性增加的量,視頻和音頻的播放速度都以該量爲標準,播放快了就減慢播放速度;播放快了就加快播放的速度。因此呢,視頻和音頻的同步其實是一個動態的過程,同步是暫時的,不一樣步則是常態。以選擇的播放速度量爲標準,快的等待慢的,慢的則加快速度,是一個你等我趕的過程。github
播放速度標準量的的選擇通常來講有如下三種:緩存
- 將視頻同步到音頻上,就是以音頻的播放速度爲基準來同步視頻。視頻比音頻播放慢了,加快其播放速度;快了,則延遲播放。
- 將音頻同步到視頻上,就是以視頻的播放速度爲基準來同步音頻。
- 將視頻和音頻同步外部的時鐘上,選擇一個外部時鐘爲基準,視頻和音頻的播放速度都以該時鐘爲標準。
DTS和PTS
上面提到,視頻和音頻的同步過程是一個你等我趕的過程,快了則等待,慢了就加快速度。這就須要一個量來判斷(和選擇基準比較),究竟是播放的快了仍是慢了,或者正以同步的速度播放。在視音頻流中的包中都含有DTS和PTS,就是這樣的量(準確來講是PTS)。DTS,Decoding Time Stamp,解碼時間戳,告訴解碼器packet的解碼順序;PTS,Presentation Time Stamp,顯示時間戳,指示從packet中解碼出來的數據的顯示順序。
視音頻都是順序播放的,其解碼的順序不該該就是其播放的順序麼,爲啥還要有DTS和PTS之分呢。對於音頻來講,DTS和PTS是相同的,也就是其解碼的順序和解碼的順序是相同的,但對於視頻來講狀況就有些不一樣了。
視頻的編碼要比音頻複雜一些,特別的是預測編碼是視頻編碼的基本工具,這就會形成視頻的DTS和PTS的不一樣。這樣視頻編碼後會有三種不一樣類型的幀:markdown
- I幀 關鍵幀,包含了一幀的完整數據,解碼時只須要本幀的數據,不須要參考其餘幀。
- P幀 P是向前搜索,該幀的數據不徹底的,解碼時須要參考其前一幀的數據。
- B幀 B是雙向搜索,解碼這種類型的幀是最複雜,不但須要參考其一幀的數據,還須要其後一幀的數據。
I幀的解碼是最簡單的,只須要本幀的數據;P幀也不是很複雜,值須要緩存上一幀的數據便可,整體來講都是線性,其解碼順序和顯示順序是一致的。B幀就比較複雜了,須要先後兩幀的順序,而且不是線性的,也是形成了DTS和PTS的不一樣的「元兇」,也是在解碼後有可能得不到完整Frame的緣由。(更多I,B,P幀的信息可參考)
假如一個視頻序列,要這樣顯示I B B P,可是須要在B幀以前獲得P幀的信息,所以幀可能以這樣的順序來存儲I P B B,這樣其解碼順序和顯示的順序就不一樣了,這也是DTS和PTS同時存在的緣由。DTS指示解碼順序,PTS指示顯示順序。因此流中能夠是這樣的:ide
Stream : I P B B DTS 1 2 3 4 PTS 1 4 2 3
一般來講只有在流中含有B幀的時候,PTS和DTS纔會不一樣。函數
計算視頻Frame的顯示時間
在計算某一幀的顯示時間以前,現來弄清楚FFmpeg中的時間單位:時間基(TIME BASE)。在FFmpeg中存在這多個不一樣的時間基,對應着視頻處理的不一樣的階段(分佈於不一樣的結構體中)。在本文中使用的是AVStream
的時間基,來指示Frame顯示時的時間戳(timestamp)。工具
/** * This is the fundamental unit of time (in seconds) in terms * of which frame timestamps are represented. * */ AVRational time_base;
能夠看出,AVStream
中的time_base是以秒爲單位,表示frame顯示的時間,其類型爲AVRational
。 AVRational
是一個分數,其聲明以下:post
/** * rational number numerator/denominator */ typedef struct AVRational{ int num; ///< numerator int den; ///< denominator } AVRational;
num爲分子,den爲分母。
PTS爲一個uint64_t
的整型,其單位就是time_base
。表示視頻長度的duration
也是一個uint64_t
,那麼使用以下方法就能夠計算出一個視頻流的時間長度:ui
time(second) = st->duration * av_q2d(st->time_base)
st爲一個AVStream的指針,av_q2d
將一個AVRational
轉換爲雙精度浮點數。一樣的方法也能夠獲得視頻中某幀的顯示時間編碼
timestamp(second) = pts * av_q2d(st->time_base)
也就是說,獲得了Frame的PTS後,就能夠獲得該frame顯示的時間戳。
獲得Frame的PTS
經過上面的描述知道,若是有了Frame的PTS就計算出幀的顯示的時間。下面的代碼展現了在從packet中解碼出frame後,如何獲得frame的PTS
ret = avcodec_receive_frame(video->video_ctx, frame); if (ret < 0 && ret != AVERROR_EOF) continue; if ((pts = av_frame_get_best_effort_timestamp(frame)) == AV_NOPTS_VALUE) pts = 0; pts *= av_q2d(video->stream->time_base); pts = video->synchronize(frame, pts); frame->opaque = &pts;
注意,這裏的pts是double型,由於將其乘以了time_base,表明了該幀在視頻中的時間位置(秒爲單位)。有可能存在調用av_frame_get_best_effort_timestamp
得不到一個正確的PTS,這樣的狀況放在函數synchronize
中處理。
double VideoState::synchronize(AVFrame *srcFrame, double pts) { double frame_delay; if (pts != 0) video_clock = pts; // Get pts,then set video clock to it else pts = video_clock; // Don't get pts,set it to video clock frame_delay = av_q2d(stream->codec->time_base); frame_delay += srcFrame->repeat_pict * (frame_delay * 0.5); video_clock += frame_delay; return pts; }
video_clock
是視頻播放到當前幀時的已播放的時間長度。在synchronize
函數中,若是沒有獲得該幀的PTS就用當前的video_clock
來近似,而後更新video_clock的值。
到這裏已經知道了video中frame的顯示時間了(秒爲單位),下面就描述若是獲得Audio的播放時間,並以此時間爲基準來安排video中顯示時間。
獲取Audio Clock
Audio Clock,也就是Audio的播放時長,能夠在Audio時更新Audio Clock。在函數audio_decode_frame
中解碼新的packet,這是能夠設置Auddio clock爲該packet的PTS
if (pkt.pts != AV_NOPTS_VALUE) { audio_state->audio_clock = av_q2d(audio_state->stream->time_base) * pkt.pts; }
因爲一個packet中能夠包含多個幀,packet中的PTS比真正的播放的PTS可能會早不少,能夠根據Sample Rate 和 Sample Format來計算出該packet中的數據能夠播放的時長,再次更新Audio clock 。
// 每秒鐘音頻播放的字節數 sample_rate * channels * sample_format(一個sample佔用的字節數) audio_state->audio_clock += static_cast<double>(data_size) / (2 * audio_state->stream->codec->channels * audio_state->stream->codec->sample_rate);
上面乘以2是由於sample format是16位的無符號整型,佔用2個字節。
有了Audio clock後,在外面獲取該值的時候卻不能直接返回該值,由於audio緩衝區的可能還有未播放的數據,須要減去這部分的時間
double AudioState::get_audio_clock() { int hw_buf_size = audio_buff_size - audio_buff_index; int bytes_per_sec = stream->codec->sample_rate * audio_ctx->channels * 2; double pts = audio_clock - static_cast<double>(hw_buf_size) / bytes_per_sec; return pts; }
用audio緩衝區中剩餘的數據除以每秒播放的音頻數據獲得剩餘數據的播放時間,從Audio clock中減去這部分的值就是當前的audio的播放時長。
同步
如今有了video中Frame的顯示時間,而且獲得了做爲基準時間的音頻播放時長Audio clock ,能夠將視頻同步到音頻了。
- 用當前幀的PTS - 上一播放幀的PTS獲得一個延遲時間
- 用當前幀的PTS和Audio Clock進行比較,來判斷視頻的播放速度是快了仍是慢了
- 根據上一步額判斷結果,設置播放下一幀的延遲時間。
使用要播放的當前幀的PTS和上一幀的PTS差來估計播放下一幀的延遲時間,並根據video的播放速度來調整這個延遲時間,以實現視音頻的同步播放。
具體實現:
// 將視頻同步到音頻上,計算下一幀的延遲時間 // 使用要播放的當前幀的PTS和上一幀的PTS差來估計播放下一幀的延遲時間,並根據video的播放速度來調整這個延遲時間 double current_pts = *(double*)video->frame->opaque; double delay = current_pts - video->frame_last_pts; if (delay <= 0 || delay >= 1.0) delay = video->frame_last_delay; video->frame_last_delay = delay; video->frame_last_pts = current_pts; // 根據Audio clock來判斷Video播放的快慢 double ref_clock = media->audio->get_audio_clock(); double diff = current_pts - ref_clock;// diff < 0 => video slow,diff > 0 => video quick double threshold = (delay > SYNC_THRESHOLD) ? delay : SYNC_THRESHOLD; // 調整播放下一幀的延遲時間,以實現同步 if (fabs(diff) < NOSYNC_THRESHOLD) // 不一樣步 { if (diff <= -threshold) // 慢了,delay設爲0 delay = 0; else if (diff >= threshold) // 快了,加倍delay delay *= 2; } video->frame_timer += delay; double actual_delay = video->frame_timer - static_cast<double>(av_gettime()) / 1000000.0; if (actual_delay <= 0.010) actual_delay = 0.010; // 設置一下幀播放的延遲 schedule_refresh(media, static_cast<int>(actual_delay * 1000 + 0.5));
frame_last_pts
和frame_last_delay
是上一幀的PTS以及設置的播放上一幀時的延遲時間。
- 首先根據當前播放幀的PTS和上一播放幀的PTS估算出一個延遲時間。
- 用當前幀的PTS和Audio clock相比較判斷此時視頻播放的速度是快仍是慢了
- 視頻播放過快則加倍延遲,過慢則將延遲設置爲0
- frame_timer保存着視頻播放的延遲時間總和,這個值和當前時間點的差值就是播放下一幀的真正的延遲時間
- schedule_refresh 設置播放下一幀的延遲時間。
Summary
本文主要描述如何利用audio的播放時長做爲基準,將視頻同步到音頻上以實現視音頻的同步播放。視音頻的同步過程是一個動態過程,快者等待,慢則加快播放,在這樣的你等我趕的過程過程當中實現同步播放。
本文代碼:https://github.com/brookicv/FSplayer