需求在移動(dòng)端做音視頻開發(fā)不同于基本的UI業(yè)務(wù)邏輯工作,音視頻開發(fā)需要你懂得音視頻中一些基本概念,針對(duì)編解碼而言,我們必須提前懂得編解碼器的一些特性,碼流的結(jié)構(gòu),碼流中一些重要信息如sps,pps,vps,start code以及基本的工作原理,而大多同學(xué)都只是一知半解,所以導(dǎo)致代碼中的部分內(nèi)容雖可以簡(jiǎn)單理解卻不知其意,所以,在這里總結(jié)出了當(dāng)前主流的H.264,H.265編碼相關(guān)的原理,以供學(xué)習(xí). 閱讀前提:
1. 概覽1.1. 為什么要編碼眾所周知,視頻數(shù)據(jù)原始體積是巨大的,以720P 30fps的視頻為例,一個(gè)像素大約3個(gè)字節(jié),如下所得,每秒鐘產(chǎn)生87MB,這樣計(jì)算可得一分鐘就將產(chǎn)生5.22GB. 因此,像這樣體積重大的視頻是無法在網(wǎng)絡(luò)中直接傳輸?shù)?而視頻編碼技術(shù)也就因運(yùn)而生.關(guān)于視頻編碼原理的技術(shù)可以參考本人其他文章,這里不做過多描述. 1.2. 編碼技術(shù)經(jīng)過很多年的開發(fā)迭代,已經(jīng)有很多大牛實(shí)現(xiàn)了視頻編碼技術(shù),其中最主流的有H.264編碼,以及新一代的H.265編碼,谷歌也開發(fā)了VP8,VP9編碼技術(shù).對(duì)移動(dòng)端而言,蘋果內(nèi)部已經(jīng)實(shí)現(xiàn)了如H.264,H.265編碼,我們需要使用蘋果提供的VideoToolbox框架來實(shí)現(xiàn)它. 1.3. 編碼分類
優(yōu)缺點(diǎn)
iOS系統(tǒng)中的硬編碼 蘋果在iOS 8.0系統(tǒng)之前,沒有開放系統(tǒng)的硬件編碼解碼功能,不過Mac OS系統(tǒng)一直有,被稱為Video ToolBox的框架來處理硬件的編碼和解碼,終于在iOS 8.0后,蘋果將該框架引入iOS系統(tǒng)。 1.4. 編碼原理對(duì)視頻執(zhí)行編碼操作后,原始視頻數(shù)據(jù)會(huì)被壓縮成三種不同類型的視頻幀: I幀,P幀,B幀.
兩種核心算法
當(dāng)壓縮一幀圖像時(shí),僅考慮本幀的數(shù)據(jù)而不考慮相鄰幀之間的冗余信息,這實(shí)際上與靜態(tài)圖像壓縮類似。幀內(nèi)一般采用有損壓縮算法,由于幀內(nèi)壓縮是編碼一個(gè)完整的圖像,所以可以獨(dú)立的解碼、顯示。幀內(nèi)壓縮一般達(dá)不到很高的壓縮,跟編碼jpeg差不多。 如下圖:我們可以通過第 1、2、3、4、5 塊的編碼來推測(cè)和計(jì)算第 6 塊的編碼,因此就不需要對(duì)第 6 塊進(jìn)行編碼了,從而壓縮了第 6 塊,節(jié)省了空間
相鄰幾幀的數(shù)據(jù)有很大的相關(guān)性,或者說前后兩幀信息變化很小的特點(diǎn)。也即連續(xù)的視頻其相鄰幀之間具有冗余信息,根據(jù)這一特性,壓縮相鄰幀之間的冗余量就可以進(jìn)一步提高壓縮量,減小壓縮比。幀間壓縮也稱為時(shí)間壓縮(Temporal compression),它通過比較時(shí)間軸上不同幀之間的數(shù)據(jù)進(jìn)行壓縮。幀間壓縮一般是無損的。幀差值(Frame differencing)算法是一種典型的時(shí)間壓縮法,它通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少數(shù)據(jù)量。 如下圖:可以看到前后兩幀的差異其實(shí)是很小的,這時(shí)候用幀間壓縮就很有意義。 有損壓縮與無損壓縮
DTS和PTS
如上圖:I幀的解碼不依賴于任何的其它的幀.而P幀的解碼則依賴于其前面的I幀或者P幀.B幀的解碼則依賴于其前的最近的一個(gè)I幀或者P幀 及其后的最近的一個(gè)P幀. 2. 編碼數(shù)據(jù)碼流結(jié)構(gòu)在我們的印象中,一張圖片就是一張圖像,視頻就是很多張圖片的集合.。但是因?yàn)槲覀円鲆粢曨l編程,就需要更加深入理解視頻的本質(zhì). 2.1 刷新圖像概念.在編碼的碼流中圖像是個(gè)集合的概念,幀、頂場(chǎng)、底場(chǎng)都可以稱為圖像,一幀通常就是一幅完整的圖像.
2.2. 重要參數(shù)
VPS主要用于傳輸視頻分級(jí)信息,有利于兼容標(biāo)準(zhǔn)在可分級(jí)視頻編碼或多視點(diǎn)視頻的擴(kuò)展。 (1)用于解釋編碼過的視頻序列的整體結(jié)構(gòu),包括時(shí)域子層依賴關(guān)系等。HEVC中加入該結(jié)構(gòu)的主要目的是兼容標(biāo)準(zhǔn)在系統(tǒng)的多子層方面的擴(kuò)展,處理比如未來的可分級(jí)或者多視點(diǎn)視頻使用原先的解碼器進(jìn)行解碼但是其所需的信息可能會(huì)被解碼器忽略的問題。 (2)對(duì)于給定視頻序列的某一個(gè)子層,無論其SPS相不相同,都共享一個(gè)VPS。其主要包含的信息有:多個(gè)子層或操作點(diǎn)共享的語法元素;檔次和級(jí)別等會(huì)話關(guān)鍵信息;其他不屬于SPS的操作點(diǎn)特定信息。 (3)編碼生成的碼流中,第一個(gè)NAL單元攜帶的就是VPS信息
包含一個(gè)CVS中所有編碼圖像的共享編碼參數(shù)。 (1)一段HEVC碼流可能包含一個(gè)或者多個(gè)編碼視頻序列,每個(gè)視頻序列由一個(gè)隨機(jī)接入點(diǎn)開始,即IDR/BLA/CRA。序列參數(shù)集SPS包含該視頻序列中所有slice需要的信息。 (2)SPS的內(nèi)容大致可以分為幾個(gè)部分:1、自引ID;2、解碼相關(guān)信息,如檔次級(jí)別、分辨率、子層數(shù)等;3、某檔次中的功能開關(guān)標(biāo)識(shí)及該功能的參數(shù);4、對(duì)結(jié)構(gòu)和變換系數(shù)編碼靈活性的限制信息;5、時(shí)域可分級(jí)信息;6、VUI。
包含一幅圖像所用的公共參數(shù),即一幅圖像中所有片段SS(Slice Segment)引用同一個(gè)PPS。 (1)PPS包含每一幀可能不同的設(shè)置信息,其內(nèi)容同H.264中的大致類似,主要包括:1、自引信息;2、初始圖像控制信息,如初始QP等;3、分塊信息。 (2)在解碼開始的時(shí)候,所有的PPS全部是非活動(dòng)狀態(tài),而且在解碼的任意時(shí)刻,最多只能有一個(gè)PPS處于激活狀態(tài)。當(dāng)某部分碼流引用了某個(gè)PPS的時(shí)候,這個(gè)PPS便被激活,稱為活動(dòng)PPS,一直到另一個(gè)PPS被激活。 參數(shù)集包含了相應(yīng)的編碼圖像的信息。SPS包含的是針對(duì)一連續(xù)編碼視頻序列的參數(shù)(標(biāo)識(shí)符seq_parameter_set_id、幀數(shù)及POC的約束、參考幀數(shù)目、解碼圖像尺寸和幀場(chǎng)編碼模式選擇標(biāo)識(shí)等等)。PPS對(duì)應(yīng)的是一個(gè)序列中某一幅圖像或者某幾幅圖像 ,其參數(shù)如標(biāo)識(shí)符pic_parameter_set_id、可選的seq_parameter_set_id、熵編碼模式選擇標(biāo)識(shí)、片組數(shù)目、初始量化參數(shù)和去方塊濾波系數(shù)調(diào)整標(biāo)識(shí)等等。 通常,SPS 和PPS 在片的頭信息和數(shù)據(jù)解碼前傳送至解碼器。每個(gè)片的頭信息對(duì)應(yīng)一個(gè) pic_parameter_set_id,PPS被其激活后一直有效到下一個(gè)PPS被激活;類似的,每個(gè)PPS對(duì)應(yīng)一個(gè) seq_parameter_set_id,SPS被其激活以后將一直有效到下一個(gè)SPS被激活。 參數(shù)集機(jī)制將一些重要的、改變少的序列參數(shù)和圖像參數(shù)與編碼片分離,并在編碼片之前傳送 至解碼端,或者通過其他機(jī)制傳輸。 擴(kuò)展知識(shí)點(diǎn):檔次(Profile)、層(Tier)和級(jí)別(Level)
考慮到應(yīng)用可根據(jù)最大的碼率和CPB大小來區(qū)分,因此有些級(jí)別定義了兩個(gè)層Tier:主層和高層,主層用于大多數(shù)應(yīng)用,而高層用于那些最嚴(yán)苛的應(yīng)用。 2.3. 原始碼流
一個(gè)序列的第一個(gè)圖像叫做 IDR 圖像(立即刷新圖像),IDR 圖像都是 I 幀圖像。引入 IDR 圖像是為了解碼的重同步,當(dāng)解碼器解碼到 IDR 圖像時(shí),立即將參考幀隊(duì)列清空,將已解碼的數(shù)據(jù)全部輸出或拋棄,重新查找參數(shù)集,開始一個(gè)新的序列。這樣,如果前一個(gè)序列出現(xiàn)重大錯(cuò)誤,在這里可以獲得重新同步的機(jī)會(huì)。IDR圖像之后的圖像永遠(yuǎn)不會(huì)使用IDR之前的圖像的數(shù)據(jù)來解碼。
由一個(gè)接一個(gè)的 NALU 組成的,而它的功能分為兩層,VCL(視頻編碼層)和 NAL(網(wǎng)絡(luò)提取層). 下圖以h264的碼流結(jié)構(gòu)為例,如果是h265則在sps前還有vps.
NALU (Nal Unit) = NALU頭 RBSP 在 VCL 數(shù)據(jù)傳輸或存儲(chǔ)之前,這些編碼的 VCL 數(shù)據(jù),先被映射或封裝進(jìn) NAL 單元(以下簡(jiǎn)稱 NALU,Nal Unit) 中。每個(gè) NALU 包括一個(gè)原始字節(jié)序列負(fù)荷(RBSP, Raw Byte Sequence Payload)、一組 對(duì)應(yīng)于視頻編碼的 NALU 頭部信息。RBSP 的基本結(jié)構(gòu)是:在原始編碼數(shù)據(jù)的后面填加了結(jié)尾 比特。一個(gè) bit“1”若干比特“0”,以便字節(jié)對(duì)齊。 2.3.1. H.264碼流一個(gè)原始的H.264 NALU 單元常由 [StartCode] [NALU Header] [NALU Payload] 三部分組成
例如,下面幅圖分別代表IDR與非IDR幀具體的碼流信息: 在一個(gè)NALU中,第一個(gè)字節(jié)(即NALU header)用以表示其包含數(shù)據(jù)的類型及其他信息。我們假定一個(gè)頭信息字節(jié)為0x67作為例子:
如表所示,頭字節(jié)可以被解析成3個(gè)部分,其中: 1>. forbidden_zero_bit = 0:占1個(gè)bit,禁止位,用以檢查傳輸過程中是否發(fā)生錯(cuò)誤,0表示正常,1表示違反語法; 2>. nal_ref_idc = 3:占2個(gè)bit,用來表示當(dāng)前NAL單元的優(yōu)先級(jí)。非0值表示參考字段/幀/圖片數(shù)據(jù),其他不那么重要的數(shù)據(jù)則為0。對(duì)于非0值,值越大表示NALU重要性越高 3>. nal_unit_type = 7:最后5位用以指定NALU類型,NALU類型定義如上表 從表中我們可以獲知,NALU類型1-5為視頻幀,其余則為非視頻幀。在解碼過程中,我們只需要取出NALU頭字節(jié)的后5位,即將NALU頭字節(jié)和0x1F進(jìn)行與計(jì)算即可得知NALU類型,即:
碼流格式H.264標(biāo)準(zhǔn)中指定了視頻如何編碼成獨(dú)立的包,但如何存儲(chǔ)和傳輸這些包卻未作規(guī)范,雖然標(biāo)準(zhǔn)中包含了一個(gè)Annex附件,里面描述了一種可能的格式Annex B,但這并不是一個(gè)必須要求的格式。 為了針對(duì)不同的存儲(chǔ)傳輸需求,出現(xiàn)了兩種打包方法。一種即Annex B格式,另一種稱為AVCC格式。
從上文可知,一個(gè)NALU中的數(shù)據(jù)并未包含他的大?。ㄩL度)信息,因此我們并不能簡(jiǎn)單的將一個(gè)個(gè)NALU連接起來生成一個(gè)流,因?yàn)閿?shù)據(jù)流的接收端并不知道一個(gè)NALU從哪里結(jié)束,另一個(gè)NALU從哪里開始。 Annex B格式用起始碼(Start Code)來解決這個(gè)問題,它在每個(gè)NALU的開始處添加三字節(jié)或四字節(jié)的起始碼0x000001或0x00000001。通過定位起始碼,解碼器就可以很容易的識(shí)別NALU的邊界。 當(dāng)然,用起始碼定位NALU邊界存在一個(gè)問題,即NALU中可能存在與起始碼相同的數(shù)據(jù)。為了防止這個(gè)問題,在構(gòu)建NALU時(shí),需要將數(shù)據(jù)中的0x000000,0x000001,0x000002,0x000003中插入防競(jìng)爭(zhēng)字節(jié)(Emulation Prevention Bytes)0x03,使其變?yōu)椋?/p> 0x000000 = 0x0000 03 00 0x000001 = 0x0000 03 01 0x000002 = 0x0000 03 02 0x000003 = 0x0000 03 03 解碼器在檢測(cè)到0x000003時(shí),將0x03拋棄,恢復(fù)原始數(shù)據(jù)。 由于Annex B格式每個(gè)NALU都包含起始碼,所以解碼器可以從視頻流隨機(jī)點(diǎn)開始進(jìn)行解碼,常用于實(shí)時(shí)的流格式。在這種格式中通常會(huì)周期性的重復(fù)SPS和PPS,并且經(jīng)常時(shí)在每一個(gè)關(guān)鍵幀之前。
AVCC格式不使用起始碼作為NALU的分界,這種格式在每個(gè)NALU前都加上一個(gè)指定NALU長度的大端格式表示的前綴。這個(gè)前綴可以是1、2或4個(gè)字節(jié),所以在解析AVCC格式的時(shí)候需要將指定的前綴字節(jié)數(shù)的值保存在一個(gè)頭部對(duì)象中,這個(gè)都通常稱為extradata或者sequence header。同時(shí),SPS和PPS數(shù)據(jù)也需要保存在extradata中。 H.264 extradata語法如下:
其中第5字節(jié)的后2位表示的就是NAL size的字節(jié)數(shù)。需要注意的是,這個(gè)NALULengthSizeMinusOne是NALU前綴長度減一,即,假設(shè)前綴長度為4,那么這個(gè)值應(yīng)該為3。 這里還需要注意的一點(diǎn)是,雖然AVCC格式不使用起始碼,但防競(jìng)爭(zhēng)字節(jié)還是有的。 AVCC格式的一個(gè)優(yōu)點(diǎn)在于解碼器配置參數(shù)在一開始就配置好了,系統(tǒng)可以很容易的識(shí)別NALU的邊界,不需要額外的起始碼,減少了資源的浪費(fèi),同時(shí)可以在播放時(shí)調(diào)到視頻的中間位置。這種格式通常被用于可以被隨機(jī)訪問的多媒體數(shù)據(jù),如存儲(chǔ)在硬盤的文件。 2.3.2. H.265碼流HEVC全稱High Efficiency Video Coding(高效率視頻編碼,又稱H.265),是比H.264更優(yōu)秀的一種視頻壓縮標(biāo)準(zhǔn)。HEVC在低碼率視頻壓縮上,提升視頻質(zhì)量、減少容量即節(jié)省帶寬方面都有突出表現(xiàn)。 H.265標(biāo)準(zhǔn)圍繞H.264編碼標(biāo)準(zhǔn),保留原有的某些技術(shù),同時(shí)對(duì)一些技術(shù)進(jìn)行改進(jìn),編碼結(jié)構(gòu)大致上和H.264的架構(gòu)類似。這里著重講一下兩者編碼格式的區(qū)別。 同H.264一樣,H.265也是以NALU的形式組織起來。而在NALU header上,H.264的HALU header是一個(gè)字節(jié),而H.265則是兩個(gè)字節(jié)。我們同樣假定一個(gè)頭信息為0x4001作為例子:
如表所示,頭信息可以被解析成4個(gè)部分,其中:
對(duì)比H.264的頭信息,H.265移除了nal_ref_idc,此信息被合并到了nal_unit_type中,H.265NALU類型規(guī)定如下:
具體type含義可以參考這篇文檔type類型 H.265的NALU類型是在信息頭的第一個(gè)字節(jié)的第2到7位,所以判斷H.265NALU類型的方法是將NALU第一個(gè)字節(jié)與0x7E進(jìn)行與操作并右移一位,即: 與H.264類似,H.265碼流也有兩種封裝格式,一種是用起始碼作為分界的Annex B格式,另一種則是在NALU頭添加NALU長度前綴的格式,稱為HVCC。在HVCC中,同樣需要一個(gè)extradata來保存視頻流的編解碼參數(shù),其格式定義如下:
Repeated of Array(VPS/SPS/PPS) 1| array_completeness 1| reserved| '0’b 6| NAL_unit_type 16| numNalus 16| nalUnitLength N| NALU data 從上表可以看到,在H.265的extradata后半段是一段格式重復(fù)的數(shù)組數(shù)據(jù),里面需要包含的除了與H.264相同的SPS、PPS外,還需多添加一個(gè)VPS。 VPS(Video Parament Set,視頻參數(shù)集),在H.265中類型為32。VPS用于解釋編碼過的視頻的整體結(jié)構(gòu),包括時(shí)域子層依賴關(guān)系等,主要目的在于兼容H.265標(biāo)準(zhǔn)在系統(tǒng)的多子層方面的擴(kuò)展。 3. iOS中的視頻編解碼iOS中視頻處理框架分層 3.1. 框架的選擇對(duì)于 AVKit、AVFoundation 和 VideoToolbox 來說,他們的功能和可定制性越來越強(qiáng),但相應(yīng)的使用難度也越大,因此你應(yīng)該根據(jù)實(shí)際需求合理的選擇使用哪個(gè)層級(jí)的接口。事實(shí)上,即使你使用 AVFoundation 或 AVKit 依然可以獲得硬件加速的效果,你失去的只是直接訪問硬編解碼器的權(quán)限。對(duì)于 VideoToolbox 來說,你可以通過直接訪問硬編解碼器,將 H.264 文件或傳輸流轉(zhuǎn)換為 iOS 上的 CMSampleBuffer 并解碼成 CVPixelBuffer,或?qū)閴嚎s的 CVPixelBuffer 編碼成 CMSampleBuffer: 3.2. 視頻數(shù)據(jù)的容器調(diào)用 AVCaptureSession 拍攝輸出的每一幀圖像都會(huì)被包裝成 CMSampleBuffer 對(duì)象,通過這個(gè) CMSampleBuffer 對(duì)象你就可以獲取到未壓縮的 CVPixelBuffer 對(duì)象;如果讀取 H.264 文件你也可以獲取數(shù)據(jù)生成壓縮的 CMBlockBuffer 對(duì)象并創(chuàng)建一個(gè) CMSampleBuffer 對(duì)象給 VideoToolbox 來解碼。 也就是說,CMSampleBuffer 既可以作為 CVPixelBuffer 對(duì)象的容器,也可以作為 CMBlockBuffer 對(duì)象的容器,CVPixelBuffer 可以說是未壓縮的圖像數(shù)據(jù)容器,而 CMBlockBuffer 則是壓縮圖像數(shù)據(jù)容器。 3.3. 編碼數(shù)據(jù)裸流NALU. 對(duì)于一個(gè) H.264 裸流或者文件來說,它是由一個(gè)一個(gè)的 NALU(Network Abstraction Layer Unit) 單元組成,每個(gè) NALU 既可以表示圖像數(shù)據(jù),也可以表示處理圖像所需要的參數(shù)數(shù)據(jù)。它主要有兩種格式:Annex B 和 AVCC。也被稱為 Elementary Stream 和 MPEG-4 格式,Annex B 格式以 0x000001 或 0x00000001 開頭,AVCC 格式以所在的 NALU 的長度開頭。 3.4. VideoToolBox中常用數(shù)據(jù)結(jié)構(gòu)
3.5. 為編碼的數(shù)據(jù)添加start code.在iOS中使用vtCompressionSession編碼后的數(shù)據(jù)不包含start code.需要我們自行添加,為什么要添加start code? 如上文所說,為了區(qū)分每個(gè)NALU及以后提取vps,sps,pps等關(guān)鍵信息. 如下圖,我們?cè)诰幋a回調(diào)中可以拿到編碼后的 接下來,我們就需要處理這一幀視頻的圖像數(shù)據(jù)了。通過 CMSampleBufferGetDataBuffer 和 CMBlockBufferGetDataPointer 我們可以獲取視頻數(shù)據(jù)的內(nèi)存地址。VTCompressionSession 編碼出來的視頻幀為 AVCC 格式,因此我們可以讀取頭部 4 個(gè)字節(jié)數(shù)據(jù)來獲取當(dāng)前 NALU 的長度。這里有一個(gè)需要注意的是,AVCC 格式使用大端字節(jié)序,它可能跟當(dāng)前使用的系統(tǒng)字節(jié)序不一樣,事實(shí)上,iOS 系統(tǒng)使用小端字節(jié)序,因此我們需要先將這個(gè)長度數(shù)據(jù)轉(zhuǎn)換為 iOS 系統(tǒng)使用的小端字節(jié)序: NALUnitLength = CFSwapInt32BigToHost(NALUnitLength)
3.6. 將H.264裸流解碼為CMSampleBuffer如上圖,我們知道,CMSampleBuffer = CMTime FormatDesc CMBlockBuffer . 需要從H264的碼流里面提取出以上的三個(gè)信息。最后組合成CMSampleBuffer,提供給硬解碼接口來進(jìn)行解碼工作。 在H.264, H.265的語法中,有一個(gè)最基礎(chǔ)的層,叫做Network Abstraction Layer, 簡(jiǎn)稱為NAL。編碼數(shù)據(jù)正是由一系列的NAL單元(NAL Unit, 簡(jiǎn)稱NAUL)組成的。 編碼碼流由NALU單元組成,一個(gè)NALU可能包含有:
流數(shù)據(jù)中,屬性集合可能是這樣的:(如果是H.265碼流,SPS前面還有VPS) 經(jīng)過處理之后,在Format Description中則是: 要從基礎(chǔ)的流數(shù)據(jù)將SPS和PPS轉(zhuǎn)化為Format Desc中的話,需要調(diào)用
對(duì)于流數(shù)據(jù)來說,一個(gè)NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作為開頭(兩者都有可能,下面以0x00 00 01作為例子)。0x00 00 01因此被稱為開始碼(Start code). 總結(jié)以上知識(shí),我們知道編碼碼流由NALU單元組成,NALU單元包含視頻圖像數(shù)據(jù)和編碼器的參數(shù)信息。其中視頻圖像數(shù)據(jù)就是CMBlockBuffer,而編碼器參數(shù)信息則可以組合成FormatDesc。具體來說參數(shù)信息包含VPS,SPS(Sequence Parameter Set)和PPS(Picture Parameter Set).如下圖顯示了一個(gè)H.264碼流結(jié)構(gòu):
根據(jù)上述得到CMVideoFormatDescriptionRef、CMBlockBufferRef和可選的時(shí)間信息,使用CMSampleBufferCreate接口得到CMSampleBuffer數(shù)據(jù)這個(gè)待解碼的原始的數(shù)據(jù)。如下圖所示的H264數(shù)據(jù)轉(zhuǎn)換示意圖。 3.7. 將 CMSampleBuffer渲染到界面顯示的方式有兩種:
參考文章 |
|
|