1.H264格式簡介(視訊解碼同步相關)
----------------------
前言
-----------------------
H264是新一代的編碼标準,以高壓縮高品質和支援多種網絡的流媒體傳輸著稱,在編碼方面,我了解的他的理論依據是:參照一段時間内圖像的統計結果表明,在相鄰幾幅圖像畫面中,一般有差别的像素隻有10%以内的點,亮度內插補點變化不超過2%,而色度內插補點的變化隻有1%以内。是以對于一段變化不大圖像畫面,我們可以先編碼出一個完整的圖像幀A,随後的B幀就不編碼全部圖像,隻寫入與A幀的差别,這樣B幀的大小就隻有完整幀的1/10或更小!B幀之後的C幀如果變化不大,我們可以繼續以參考B的方式編碼C幀,這樣循環下去。這段圖像我們稱為一個序列(序列就是有相同特點的一段資料),當某個圖像與之前的圖像變化很大,無法參考前面的幀來生成,那我們就結束上一個序列,開始下一段序列,也就是對這個圖像生成一個完整幀A1,随後的圖像就參考A1生成,隻寫入與A1的差别内容。
在H264協定裡定義了三種幀,完整編碼的幀叫I幀,參考之前的I幀生成的隻包含差異部分編碼的幀叫P幀,還有一種參考前後的幀編碼的幀叫B幀。
H264采用的核心算法是幀内壓縮和幀間壓縮,幀内壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。
序列的說明
在H264中圖像以序列為機關進行組織,一個序列是一段圖像編碼後的資料流,以I幀開始,到下一個I幀結束。
一個序列的第一個圖像叫做 IDR 圖像(立即重新整理圖像),IDR 圖像都是 I 幀圖像。 H.264 引入 IDR 圖像是為了解碼的重同步,當解碼器解碼到 IDR 圖像時,立即将參考幀隊列清空,将已解碼的資料全部輸出或抛棄,重新查找參數集,開始一個新的序列。這樣,如果前一個序列出現重大錯誤,在這裡可以獲得重新同步的機會。IDR圖像之後的圖像永遠不會使用IDR之前的圖像的資料來解碼。
一個序列就是一段内容差異不太大的圖像編碼後生成的一串資料流。當運動變化比較少時,一個序列可以很長,因為運動變化少就代表圖像畫面的内容變動很小,是以就可以編一個I幀,然後一直P幀、B幀了。當運動變化多時,可能一個序列就比較短了,比如就包含一個I幀和3、4個P幀。
幀間預測
I幀:幀内編碼幀 ,I幀表示關鍵幀,你可以了解為這一幀畫面的完整保留;解碼時隻需要本幀資料就可以完成(因為包含完整畫面)。
P幀:前向預測編碼幀。P幀表示的是這一幀跟之前的一個關鍵幀(或P幀)的差别,解碼時需要用之前緩存的畫面疊加上本幀定義的差别,生成最終畫面。(也就是差别幀,P幀沒有完整畫面資料,隻有與前一幀的畫面差别的資料);
P幀的預測與重構:
P幀是以I幀為參考幀,在I幀中找出P幀“某點”的預測值和運動矢量以及預測內插補點。在接收端根據運動矢量從I幀中找出P幀“某點”的預測值并與預測內插補點相加以得到P幀“某點”樣值,進而可得到完整的P幀。
B幀:雙向預測内插編碼幀。B幀是雙向差别幀,也就是B幀記錄的是本幀與前後幀的差别(具體比較複雜,有4種情況,但我這樣說簡單些),換言之,要解碼B幀,不僅要取得之前的緩存畫面,還要解碼之後的畫面,通過前後畫面的與本幀資料的疊加取得最終的畫面。B幀壓縮率高,但是解碼時CPU占用會比較高。
B幀的預測與重構:
B幀以前面的I或P幀和後面的P幀為參考幀,“找出”B幀“某點”的預測值和兩個運動矢量,接收端根據運動矢量在兩個參考幀中的預測值與預測內插補點求和,來得到B幀“某點”值,進而可得到完整的B幀。
幀内預測
場和幀:視訊的一場或一幀可用來産生一個編碼圖像。在電視中,為減少大面積閃爍現象,把一幀分成兩個隔行的場。
宏塊:一個編碼圖像通常劃分成若幹宏塊組成,一個宏塊由一個16×16亮度像素和附加的一個8×8 Cb和一個8×8 Cr彩色像素塊組成。
片:每個圖象中,若幹宏塊被排列成片的形式。片分為I片、B片、P片和其他一些片。
I片隻包含I宏塊,P片可包含P和I宏塊,而B片可包含B和I宏塊。
I宏塊利用從目前片中已解碼的像素作為參考進行幀内預測。
P宏塊利用前面已編碼圖象作為參考圖象進行幀内預測。
B宏塊則利用雙向的參考圖象(前一幀和後一幀)進行幀内預測。
片的目的是為了限制誤碼的擴散和傳輸,使編碼片互相間是獨立的。
某片的預測不能以其它片中的宏塊為參考圖像,這樣某一片中的預測誤差才不會傳播到其它片中去。
2.音頻相關基礎知識
采樣率與比特率的辨析
采樣率表示了每秒鐘的采樣次數。
比特率表示了每秒鐘傳輸的音頻資料量,該值也和采樣率相關。
采樣率類似于動态影像的幀數,比如電影的采樣率是24HZ,PAL制式的采樣率是25HZ,NTSC制式的采樣率是30HZ。當我們把采樣到的一個個靜止畫面再以采樣率同樣的速度回放時,看到的就是連續的畫面。同樣的道理,把以44100HZ采樣率記錄的CD以同樣的速率播放時,就能聽到連續的聲音。顯然,這個采樣率越高,聽到的聲音和看到的圖像就越連貫。可是人的聽覺和視覺器官能分辨的采樣率是有限的,基本上高于44100HZ采樣的聲音,絕大部分人已經覺察不到其中的分别了。
而聲音的位數就相當于畫面的顔色數(8位就是0-255),表示每個取樣的資料量,當然資料量越大,回放的聲音越準确,不至于把開水壺的叫聲和火車的鳴笛混淆。同樣的道理,對于畫面來說就是更清晰和準确,不至于把血和蕃茄醬混淆。不過受人的器官的機能限制,16位的聲音和24位的畫面基本已經是普通人類的極限了,更高位數就隻能靠儀器才能分辨出來了。比如電話就是3000HZ取樣的7位聲音,而CD是44100HZ取樣的16位聲音,是以CD就比電話更清楚。
有了以上這兩個概念,比特率就很容易了解了。以電話為例,每秒3000次取樣,每個取樣是7比特,那麼電話的比特率是21000。而CD是每秒44100次取樣,兩個聲道,每個取樣是16位PCM編碼,是以CD的比特率是44100*2*16=1411200,也就是說CD每秒的資料量大約是176KB。
注意事項:
① Android ffmpeg音頻編碼器對AAC是預設關閉的,需要通過設定m_pAudioCodecCtx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL,才能順利打開編碼器。
② 在将mp3轉碼為AAC音頻時,需要利用FIFO Buffer(AVAudioFifoBuffer)的結構,對轉換後的每幀采樣進行緩沖。
③ 音視訊編碼最後時刻可能讀取資料時傳回為-1,但仍有若幹包存放在編碼器緩存中,需要flush緩存才不會導緻最後的幾幀丢失,導緻視訊時間錯誤。
④ AV_SAMPLE_FMT_S16、AV_SAMPLE_FMT_S16P格式(兩者如果都是雙通道)差別在于前者解碼出的Frame的數組有效資料隻有一維,則是交錯存儲的;而後者的有效資料有兩維(兩個Plane),一維存儲一個通道。在做音頻混合時,由于需要存入不同通道,是以需要注意以上問題。
3.ffmpeg音視訊同步
前言:
ffmpeg通過AVStream結構的time_base(有理數結構體——AVRational,由分子和分母兩部分組成)可以擷取一個參考時間機關,所有音視訊流的timestamp都是基于這個時間機關順序遞增,比如time_base.num=1,time_base.den=90000,表示把1秒分成90000等份,音視訊包的PTS(顯示時間戳)和DTS(解碼時間戳)就表示有多少個1/90000 (time_base)機關時間,更簡單一點假設time_base.num=1,time_base.den=1000,就表示1秒分成1000等份,相當于1毫秒,那時間戳就表示是以毫秒為機關的,在做音視訊處理時候,如果解碼的速度比按照時間戳顯示的速度快,那就簡單直接處理,不用丟幀(Drop Frame),當解碼速度很慢時(比如手機裝置),就需要丢幀處理,是每兩幀丟一幀資料,還是每三幀丟一幀資料,就需要根據延時顯示程度來計算丢幀的比率。
擷取視訊幀的PTS:
雖然視訊流能提供視訊流幀率值,但如果簡單地通過 幀數/幀率 來同步視訊,可能會使音視訊不同步。正确的方式是利用視訊流中每個包的DTS和PTS,即包的解碼時間戳(DTS--Decompressed Time Stamp)和顯示時間戳(PTS--Presentation Time Stamp)。為了搞清楚這兩個概念,需要知道視訊的編碼存儲方式。正如之前提到的H264編碼原理,将視訊幀分為I幀、P幀、B幀,這也就是為什麼調用avcodec_decode_video可能沒有得到完整一幀的原因。
假設有一段視訊序列,其幀排序為:I B B P。但是在播放B幀之前需要知道P幀的資訊,是以幀的實際存儲順序可能是I P B B。這就是為什麼我們會有一個DTS和PTS。DTS告訴我們什麼時候解碼的,PTS告訴我們什麼時候顯示。是以,在這個例子中,流可能是這樣的:
解碼器輸入
Stream: I P B B P B B // 存儲順序
PTS: 0312645
DTS: 0123456
解碼器輸出
Stream: I B B P B B P
PTS: 0123456
通常隻有當顯示B幀時,PTS和DTS才會不一緻。
值得注意的是,通過 av_read_frame() 得到的 AVPacket 中的 DTS 通常才有正确的值,而通過 avcodec_decode_video2() 得到的 AVFrame 的 PTS 并沒有包含有用資訊。第一種方法是利用在解碼包時 ffmpeg 會按照 PTS 重新對包進行排序, 是以被 avcodec_decode_video2() 處理過的包的 DTS 和傳回的幀的 PTS 是相同的, 這樣就可以得到幀的 PTS 了。然而我們并不是總能獲得該值(經測試影響不大),是以第二種方法是我們需要儲存第一幀的第一包的 PTS(之後的PTS可以根據幀率計算出來), 将其作為這一幀的 PTS。因為當一幀開始發送第一包時,avcodec_decode_video() 會調用相關函數為幀申請存儲空間,我們可以重寫這個函數,在函數中加入擷取包 DTS 的方法,并用全局變量進行儲存。通過以上兩種方法就計算出了幀的 PTS 。
計算視訊幀實際的PTS:
考慮重複幀的情況(甚至得不到 PTS 的情況),使用 VideoState 的 video_clock 字段時刻記錄視訊已經播放的時間,通過換算到流的時間基來計算實際正确的 PTS(儲存在 VideoPicture 的 PTS 字段中),并重新排序視訊幀(queue_picture),這樣便可以設定合适的重新整理速率(比如通過簡單計算前一幀和現在這一幀的時間戳來預測出下一個時間戳的時間)。接下來我們将同步視訊到音頻。
計算音頻實際的PTS:
雖然音視訊流都包含了播放速率的資訊,音頻使用采樣率來表示,而視訊則采用幀率來表示,是以我們不能簡單使用兩個資料來對音視訊進行同步,而是需要使用 DTS 和 PTS。
現在看一下怎樣得到音頻時鐘。我們可以在音頻解碼函數 audio_decode_frame() 中更新音頻時間。然而需要注意的是我們并不是每次調用這個函數的時候都在處理新的包,是以更新時鐘的時刻有兩個:
①當我們得到新的包的時候,我們簡單的設定音頻時鐘為這個包的時間戳 PTS。
②如果一個包裡有許多幀,我們通過樣本數和采樣率來計算:
n = 16/8 * channels; (當采樣精度為16位時)
audio_clock += data_size / (n * sample_rate)(當緩沖區滿時的播放時間);
然而我們不能把 audio_clock 直接作為音頻的PTS,因為在 audio_decode_frame()計算的 audio_clock 的是假定緩沖滿的情況,而實際上可能緩沖是不滿的,是以實際播放時根據緩沖大小和播放速率計算播放時間:需要減去空閑部分的時間:
PTS -= (double)hw_buf_size / bytes_per_sec (hw_buf_size表示空閑緩沖大小,bytes_per_sec表示每秒播放的位元組數)
同步視訊到音頻:
通過以上方式可以擷取到實際的音頻時間。有了這個值, 在音頻和視訊不同步的時候,我們會調整下次重新整理的值:如果視訊時間戳(類似序号)相比音頻時間戳大于一定門檻值(即視訊播放太快),我們加倍延遲。如果視訊時間戳相較于音頻時間戳小于一定門檻值(即視訊播放太慢),我們将盡可能快的重新整理。
同步流程(比喻):
有一把尺子 一隻螞蟻(視訊)跟着一個标杆(音頻)走, 标杆是勻速的 螞蟻或快或慢,慢了你就抽它 讓它跑起來,快了就拽它。這樣音視訊就能同步了。 這裡最大的問題就是音頻是勻速的,視訊是非線性的。
其他同步方法:
分别獲得音視訊的PTS後,我們有三個選擇:視訊同步音頻(計算音視訊PTS之差,來判定視訊是否有延遲)、音頻同步視訊(根據音視訊PTS內插補點調整音頻取的樣值,即改變音頻緩沖區的大小)和音頻視訊同步外部時鐘(同前一個),因為調整音頻範圍過大,會造成令使用者不适的尖銳聲,是以通常我們選擇第一種。
time_base(時間基)的基本概念:----------------------
AVRational的結構如下:
typedef struct AVRational{
int num; ///< numerator
int den; ///< denominator
} AVRational;
AVRational這個結構辨別一個分數,num為分數,den為分母。
1.時間基的轉換的概念
實際上time_base的意思就是時間的刻度:
比如,編解碼器上下文的時間基為(1,25),那麼時間刻度就是1/25
檔案容器中的流的時間基為(1,90000),那麼時間刻度就是1/90000。
那麼,在刻度為1/25的體系下的time=5,轉換成在刻度為1/90000體系下的時間time為(5*(1/25)) / (1/90000)= 3600*5=18000
【ffmpeg提供了av_rescale_q_rnd函數進行轉換。
av_rescale_q_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd)
此函數主要用于對于不同時間戳的轉換。具體來說是将原來以 "時間基b" 表示的 數值a 轉換成以 "時間基c" 來表示的新值。AVRounding表示取整的政策。】