编解码标准-H.264
H.264是MPEG-4家族中的一員,即MPEG-4系列文檔ISO-14496的第10部分,因此被稱作MPEG-4 AVC,MPEG-4重點(diǎn)考慮靈活性和交互性,而H.264著重強(qiáng)調(diào)更高的編碼壓縮率和傳輸?shù)目煽啃浴?/p>
1、H.264 編碼流程
1.1、slice&block
第一步:切片、切宏塊,宏塊(16x16)是編碼的基本單元
第二步:不合理的宏塊之間會出現(xiàn)塊效應(yīng),即色差明顯,所以繼續(xù)切割,分成很多個(gè)8*8或4*4的子塊
第三步:對子塊進(jìn)行算法編碼,期間使用到的算法包括:
- 幀內(nèi)預(yù)測(內(nèi)部壓縮)
- 幀間預(yù)測(外部壓縮)
- 量化編碼
- 熵編碼
其中,每一個(gè)切片都有片頭 + 多個(gè)宏塊組成。
以16x16的宏塊為編碼最小單元,一個(gè)宏塊可以被分成多個(gè)4x4或8x8的塊,同一個(gè)宏塊內(nèi),像素的相似程度會比較高,若16x16的宏塊中,像素相差較大,那么就需要繼續(xù)細(xì)分。
當(dāng)對一個(gè)宏塊進(jìn)行編碼的時(shí)候,每個(gè)宏塊都會被分割成多種不同大小的子塊進(jìn)行預(yù)測。
1.2、GOP內(nèi)壓縮
GOP內(nèi) I、B、P幀
幀大小:I > P > B
壓縮率:B > P > I
編碼B幀的時(shí)候,需要先把B后面的幀先編碼,然后參考之后才能繼續(xù)編碼B幀,例如:
PTS:IDR1、B2、B3、P4、B5、B6、P7、B8、B9、I10、B11、B12、P13、B14、B15、P16
DTS:IDR1、P4、B2、B3、P7、B5、B6、I10、B8、B9、P13、B11、B12、P16、B14、B15
每次編碼B的時(shí)候,就要把后面最近的 I 或者 P 拿到前面來,如果已經(jīng)之前被編碼過,就不需要了。
IDR幀
I 幀不需要參考任何幀,但B、P幀可能去參考I幀之前的幀,但如果遇到IDR幀,就不能參考IDR幀之前的幀。
其核心的作用,就是為了讓編碼重新同步,立即將參考幀隊(duì)列清空,已編碼的數(shù)據(jù)全部清除掉。如果之前的參考序列出現(xiàn)了錯(cuò)誤,這里就可以立刻矯正。IDR后面的數(shù)據(jù)又能重新開始編碼,不會收到前面的錯(cuò)誤影響。
1.3、編碼配置
實(shí)時(shí)視頻會議一直是繼續(xù)向更高質(zhì)量,更低帶寬的方向發(fā)展。H.264 High profile 技術(shù)于2010年率先被polycom應(yīng)用于視頻會議系統(tǒng)。
比h.264 baseline進(jìn)一步節(jié)約了近一半的帶寬。在高清實(shí)時(shí)會議中,采用H.264 baseline,帶寬要求還是比較高的,特別是要做1080P 30pfs甚至60pfs時(shí)。如果能減少一半帶寬,意味著節(jié)省2-4M帶寬,如果是在MCU側(cè),則帶寬節(jié)省就更可觀了。
AVC/H.264 規(guī)定了多種不同的配置:基線、主要、擴(kuò)展、高
- 基線(Baseline Profile),不支持B幀,只支持無交錯(cuò)模式,主要是用于可視電話,會議電視,無線通訊等實(shí)時(shí)通信;
- 主要(Main Profile),提供I/P/B 幀,支持無交錯(cuò)和交錯(cuò),用于數(shù)字廣播電視和數(shù)字視頻存儲;
- 擴(kuò)展(Extend Profile),也叫擴(kuò)展Profile,
- 高(High Profile),在 Main Profile 的基礎(chǔ)上增加了8x8 內(nèi)部預(yù)測、提高了壓縮效率;
2、H.264的功能分層
H.264的原始碼流(裸流)是由?個(gè)接?個(gè)NALU組成,它的功能分為兩層:“編碼層”和“網(wǎng)絡(luò)層”
VCL(視頻編碼層):包括核?壓縮引擎和塊、宏塊和?的語法級別定義,設(shè)計(jì)?標(biāo)是盡可能地獨(dú)?于?絡(luò)進(jìn)??效的編碼;
NAL(?絡(luò)提取層):負(fù)責(zé)將VCL產(chǎn)?的?特字符串適配到各種各樣的?絡(luò),就是把已經(jīng)編碼后的數(shù)據(jù)封裝到網(wǎng)絡(luò)包中去;
H264在網(wǎng)絡(luò)中的傳輸,是以一連串NALU的形式傳輸?shù)?#xff0c;一張圖像有可能存在2個(gè)NALU。
其中,NALU裝了不同的東西:
- SPS:序列參數(shù)集,SRS中保存了一組編碼視頻序列的全局參數(shù)(發(fā) I 幀之前至少要發(fā)一次)
- PPS:圖像參數(shù)集(發(fā) I 幀之前至少要發(fā)一次)
- I 幀:I 幀或 I 幀的一部分;
- P幀:P幀或P幀的一部分;
- B幀:B幀或B幀的一部分;
如果H.264碼流解不出來,就要去看看是不是SPS或PPS不存在?
3、H.264流結(jié)構(gòu)
2.1、AnnexB/AVCC
H.264流有兩種格式:
- 一種是annexb,也是傳統(tǒng)模式,裸流一般都是annexb格式
- annexb格式會在數(shù)據(jù)包前面加上startcode,然后在后面加上UALU包(NALU Header + RBSP)
- 這個(gè)startcode用來做字節(jié)流對齊,以及分割流數(shù)據(jù)。
- 將SPS和PPS都作為一個(gè)NALU進(jìn)行封裝,每一次遇到 I 幀之前,都是重復(fù)加上SPS和PPS,這個(gè)SPS的作用就是提供了序列信息,比如解碼信息,而這個(gè)PPS提供的是圖像信息,比如如何壓縮等。
- NALU封裝了SPS、PPS、SEI、
- annexb格式的好處,就是解碼器可以從任意一個(gè)包開始解碼。
- 另外種就是AVCC模式,例如mp4、mkv都屬于AVCC格式
- 沒有startcode,直接是一個(gè)個(gè)UALU包
- 解碼器配置參數(shù)在一開始就配置好了,使用NALU長度作為NALU的邊界,不需要額外的起始碼
- SPS和PPS都封裝在文件頭部的extradata中;
- 好處就是播放器直接能識別,去除了大量的startcode、sps、pps,縮小了文件大小;
比方說在ffmpeg中,我們解封裝mp4后,需要對H264進(jìn)行解碼,而解碼之前必須要對H264裸流進(jìn)行一個(gè)轉(zhuǎn)封裝過濾,將h264的 “mp4版本” 轉(zhuǎn)換為 “annexb版本” 的過程。
RTP包中接收的264包是不含有0x00,0x00,0x00,0x01頭的,這部分是RTP接收以后,另外再加上去的,解碼的時(shí)候再做判斷的。
/** * 解碼PS流的Extradata* h264_parse()* |* ff_h264_decode_extradata() |--> decode_extradata_ps* |--> decode_extradata_ps_mp4()* * decode_extradata_ps():*** decode_extradata_ps_mp4(): * MP4中SPS和PPS存放在 moov->trak->mdia->minf->stbl->stsd: * Extensions = Size + Type(avcC) + Extradata* */ static int decode_extradata_ps(const uint8_t *data, int size, H264ParamSets *ps, int is_avc, void *logctx) {// H264包H2645Packet pkt = { 0 };int i, ret = 0;ret = ff_h2645_packet_split(&pkt, data, size, logctx, is_avc, 2, AV_CODEC_ID_H264, 1, 0);if (ret < 0) {ret = 0;goto fail;}// 包里面有多少個(gè)NAL?for (i = 0; i < pkt.nb_nals; i++) {// 解析NAL類型H2645NAL *nal = &pkt.nals[i];switch (nal->type) {// SPS(7): 25字節(jié)左右case H264_NAL_SPS: {GetBitContext tmp_gb = nal->gb;ret = ff_h264_decode_seq_parameter_set(&tmp_gb, logctx, ps, 0);if (ret >= 0)break;av_log(logctx, AV_LOG_DEBUG,"SPS decoding failure, trying again with the complete NAL\n");init_get_bits8(&tmp_gb, nal->raw_data + 1, nal->raw_size - 1);ret = ff_h264_decode_seq_parameter_set(&tmp_gb, logctx, ps, 0);if (ret >= 0)break;ret = ff_h264_decode_seq_parameter_set(&nal->gb, logctx, ps, 1);if (ret < 0)goto fail;break;}// PPS(8): 5字節(jié)左右case H264_NAL_PPS:ret = ff_h264_decode_picture_parameter_set(&nal->gb, logctx, ps,nal->size_bits);if (ret < 0)goto fail;break;default:av_log(logctx, AV_LOG_VERBOSE, "Ignoring NAL type %d in extradata\n",nal->type);break;}}fail:ff_h2645_packet_uninit(&pkt);return ret; } typedef struct H264ParamSets {// SPS列表AVBufferRef *sps_list[MAX_SPS_COUNT];// PPS列表AVBufferRef *pps_list[MAX_PPS_COUNT];AVBufferRef *pps_ref;/* currently active parameters sets */const PPS *pps;const SPS *sps;int overread_warning_printed[2]; } H264ParamSets;2.2、SPS
序列參數(shù)集,保存了一組編碼視頻序列的全局參數(shù),保存了:profile、level、視頻寬和高、顏色空間等。在H.264的各種語法元素中,SPS中的信息至關(guān)重要。如果其中的數(shù)據(jù)丟失或出現(xiàn)錯(cuò)誤,那么解碼過程很可能會失敗。
SPS 中的信息至關(guān)重要,如果其中的數(shù)據(jù)丟失,解碼過程就可能失敗。SPS 和 PPS 通常作為解碼器的初始化參數(shù)。一般情況,SPS 和 PPS 所在的 NAL 單元位于整個(gè)碼流的起始位置,但是在某些場景下,在碼率中間也可能出現(xiàn)這兩種結(jié)構(gòu):
- 解碼器要在碼流中間開始解碼。比如,直播流。
- 編碼器在編碼過程中改變了碼率的參數(shù)。比如,圖像的分辨率。
2.3、PPS
每一幀編碼后數(shù)據(jù)所依賴的參數(shù),都保存在PPS中,主要體現(xiàn)的就是圖像編碼信息。
2.4、NALU
2.4.1、nal_unit_header
NALU頭就一個(gè)字節(jié),包含了對NALU的描述,1)重要程度;2)NALU類型
| F | 1B | 禁止位,0表示正常,1表示錯(cuò)誤,一般都是0 | 
| NRI | 2B | 重要級別,00不重要,01,10,11非常重要 | 
| TYPE | 5B | 表示該NALU的類型是什么? | 
例如:
每個(gè)NAL分割的時(shí)候,00 00 00 01為startcode,頭部的2個(gè)startcode分別代表了SPS和PPS,從第3個(gè)startcode開始,就是NALU(I、B、P幀)。
- 0x00 0x00 0x00 0x01 + 0x67
十六進(jìn)制轉(zhuǎn)為二進(jìn)制:0x0 11 00111,NALU類型=7,表示PSP
- 0x00 0x00 0x00 0x01 + 0x68
十六進(jìn)制轉(zhuǎn)為二進(jìn)制:0x0 11 01000,NALU類型=8,表示PPS
- 0x00 0x00 0x01 + 0x65
十六進(jìn)制轉(zhuǎn)為二進(jìn)制:0x0 11 00101,NALU類型=5,表示 I 幀
- 0x00 0x00 0x00 0x01 + 0x41
十六進(jìn)制轉(zhuǎn)為二進(jìn)制:0x0 10 00001,NALU類型=1,表示 P 幀
- 0x00 0x00 0x00 0x01 + 0x01
十六進(jìn)制轉(zhuǎn)為二進(jìn)制:0x0 00 00001,NALU類型=1,表示 B 幀
2.4.2、nal_unit_rbsp
NALU的主體涉及到三個(gè)重要的名詞,分別為EBSP、RBSP和SODB。
其中EBSP完全等價(jià)于NALU主體,而且它們?nèi)齻€(gè)的結(jié)構(gòu)關(guān)系為:
EBSP包含RBSP,RBSP包含SODB。
NALU = EBSP + 0x03(防競爭字節(jié))+ ...... + EBSP + 0x03
NALU = RBSP + 補(bǔ)齊字節(jié)
1、SODB
String Of Data Bits 原始數(shù)據(jù)比特流,就是最原始的編碼/壓縮得到的數(shù)據(jù)
2、RBSP
Raw Byte Sequence Payload,又稱原始字節(jié)序列載荷。和SODB關(guān)系如下:
RBSP = SODB + RBSP Trailing Bits(RBSP尾部補(bǔ)齊字節(jié))引入RBSP Trailing Bits做8位字節(jié)補(bǔ)齊。
3、EBSP
Encapsulated Byte Sequence Payload:擴(kuò)展字節(jié)序列載荷。
如果RBSP中也包括了StartCode(0x000001或0x00000001)怎么辦呢?所以,就有了防止競爭字節(jié)(0x03),編碼時(shí),掃描RBSP,如果遇到連續(xù)兩個(gè)0x00字節(jié),就在后面添加防止競爭字節(jié)(0x03);解碼時(shí),同樣掃描EBSP,進(jìn)行逆向操作即可。
2.4.3、SliceHeader
- first_mb_in_slice:片中的第一個(gè)宏塊的地址, 片通過這個(gè)句法元素來標(biāo)定它自己的地址。要注意的是在幀場自適應(yīng)模式下,宏塊都是成對出現(xiàn),這時(shí)本句法元素表示的是第幾個(gè)宏塊對,對應(yīng)的第一個(gè)宏塊的真實(shí)地址應(yīng)該是:2 * first_mb_in_slice;
- slice_type:指明片的類型,IDR 圖像時(shí), slice_type 等于 2, 4, 7, 9;
slice_type 的值在 5 到 9 范圍內(nèi)表示,除了當(dāng)前條帶的編碼類型,所有當(dāng)前編碼圖像的其他條帶的 slice_type 值應(yīng)與當(dāng)前條帶的 slice_type 值一樣,或者等于當(dāng)前條帶的 slice_type 值減 5。
當(dāng) nal_unit_type 等于 5(IDR 圖像)時(shí),slice_type 應(yīng)等于 2、 4、 7 或 9。當(dāng) num_ref_frames 等于 0 時(shí), slice_type 應(yīng)等于 2、 4、 7 或 9。
- pic_parameter_set_id:當(dāng)前slice所依賴的pps的id;
- colour_plane_id:當(dāng)標(biāo)識位separate_colour_plane_flag為true時(shí),colour_plane_id表示當(dāng)前的顏色分量,0、1、2分別表示Y、U、V分量;
- frame_num:每個(gè)參考幀都有一個(gè)依次連續(xù)的 frame_num 作為它們的標(biāo)識,這指明了各圖像的解碼順序。但事實(shí)上我們在表 中可以看到, frame_num 的出現(xiàn)沒有 if 語句限定條件,這表明非參考幀的片頭也會出現(xiàn) frame_num。只是當(dāng)該個(gè)圖像是參考幀時(shí),它所攜帶的這個(gè)句法元素在解碼時(shí)才有意義;
- field_pic_flag:場編碼標(biāo)識位。當(dāng)該標(biāo)識位為1時(shí)表示當(dāng)前slice按照場進(jìn)行編碼;該標(biāo)識位為0時(shí)表示當(dāng)前slice按照幀進(jìn)行編碼;
- bottom_field_flag:底場標(biāo)識位。該標(biāo)志位為1表示當(dāng)前slice是某一幀的底場;為0表示當(dāng)前slice為某一幀的頂場;
- idr_pic_id:表示IDR幀的序號。某一個(gè)IDR幀所屬的所有slice,其idr_pic_id應(yīng)保持一致。IDR 圖像的標(biāo)識。不同的 IDR 圖像有不同的 idr_pic_id 值。值得注意的是,IDR 圖像有不等價(jià)于 I 圖像,只有在作為 IDR 圖像的 I 幀才有這個(gè)句法元素,在場模式下, IDR 幀的兩個(gè)場有相同的 idr_pic_id 值。 idr_pic_id 的取值范圍是 [0,65535] 和 frame_num 類似,當(dāng)它的值超出這個(gè)范圍時(shí),它會以循環(huán)的方式重新開始計(jì)數(shù);
- pic_order_cnt_lsb:表示當(dāng)前幀序號的另一種計(jì)量方式;
- delta_pic_order_cnt_bottom:表示頂場與底場POC差值的計(jì)算方法,不存在則默認(rèn)為0;
- slice_qp_delta:指出在用于當(dāng)前片的所有宏塊的量化參數(shù)的初始值;
2.4.4、rbsp_trailing_bits
但是只在 NALU 前面加上起始碼是會產(chǎn)生問題了,因?yàn)樵即a流中,是有可能出現(xiàn) 0 0 0 1 或者 0 0 1 的,這樣就會導(dǎo)致讀取程序?qū)⒁粋€(gè) NALU 誤分割成多個(gè) NALU。為了防止這種情況發(fā)生,AnnexB 引入了防競爭字節(jié)(Emulation Prevention Bytes)的概念。
所謂防競爭字節(jié)(Emulation Prevention Bytes),就是在給 NALU 添加起始碼之前,先對碼流進(jìn)行一次遍歷,查找碼流里面的存在的 000、001、002、003 的字節(jié),然后對其進(jìn)行如下修改。
// EBSP->RBSP 反向處理 std::vector<uint8_t> EBSP2RBSP(uint8_t* buffer, int len) {// 00 00 03 去掉03std::vector<uint8_t> ebsp;int i = 0;for (i = 0; i < len-2; ++i) {if (buffer[i] == 0x00 && buffer[i+1] == 0x00 && buffer[i+2] == 0x03) {ebsp.push_back(buffer[i++]);ebsp.push_back(buffer[i++]);}else {ebsp.push_back(buffer[i]);}}for (; i < len; ++i) {ebsp.push_back(buffer[i]);}return ebsp; }4、H.264內(nèi)I、B、P幀的關(guān)系?
GOP編碼后的順序是解碼順序,解碼后看到的是顯示順序。
控制GOP也可以控制延遲問題,減少I幀時(shí)間的間距,一般控制在2秒比較合適。
4.1、I 關(guān)鍵幀
不需要參考其他畫面,靠自己就能被解碼成完整圖像,屬于“幀內(nèi)編碼”。
4.2、P幀
P幀代表預(yù)測幀,除了空域預(yù)測以外,它還可以通過時(shí)域預(yù)測來進(jìn)行壓縮。
通過與其相鄰的前一幀(I 或 P)不同像素點(diǎn)進(jìn)行壓縮本幀數(shù)據(jù),屬于“幀間編碼”。
以I幀和P幀為例。如果你只使用這兩種類型的幀,那么每一幀要么參考自身(I 幀),要么參考前一幀(P 幀)。因此,幀可以以相同的順序進(jìn)出編碼器。這里,呈現(xiàn)順序(或顯示順序)與編碼、解碼順序相同。
4.3、B幀
B幀可以參考在其前后出現(xiàn)的幀,采用雙向預(yù)測(前后 I 或 P ),大大提高壓縮倍數(shù),想要理解B幀的作用,我們需要先理解顯示順序和解碼順序的概念。
按照解碼順序,解碼器先解碼幀1(I幀),然后是幀2(P幀)。但它卻無法顯示幀2,因?yàn)樵诮獯a順序中的實(shí)際上是幀4!所以,解碼器需要將幀2(按解碼順序)放入緩沖區(qū),然后等待顯示它的時(shí)機(jī)。
所以,編碼器和解碼器需要在內(nèi)存中維護(hù)兩個(gè)“順序”或“序列”:一個(gè)將幀放置在正確的顯示順序中,另一個(gè)用于將幀按照編碼和解碼所需順序放置。
- 顯示隊(duì)列:1、2、3、4、5、6、7
- 解碼隊(duì)列:1、2、3、2、6、7、5
所以在GOP內(nèi)部,I與I之間為一個(gè)組,I組內(nèi)部P與P之間一個(gè)組,P組內(nèi)部先解碼P,后解碼B
- B幀壓縮率最大,用來預(yù)測運(yùn)動軌跡;
- P次之,用來表示和前一幀的差異;
- I幀最小,其本身獨(dú)立完成編碼。
4.4、IDR幀
視頻第一個(gè)I幀,稱為IDR幀,IDR一定是I幀,但I(xiàn)幀不一定是IDR幀。
4.5、開放GOP和閉合GOP
所謂開放GOP,就是 I 幀可以被跨越,而閉合GOP,就是 I 幀是一個(gè)IDR,該 IDR 幀不能被之前的幀參考。
這也要就要求了,在 IDR 幀之前,必須有一個(gè)P幀,否則,IDR 幀相鄰的B幀就無法向后預(yù)測。
IDR和閉合GOP到底有什么用處?
- ABR視頻流:
在ABR視頻流中,播放器可以根據(jù)帶寬和解碼器緩沖器的填充程度在不同配置文件(組合不同碼率和分辨率的視頻)之間切換。如果播放器要從1080p切換到360p,那么它就需要這種利落的切換。此時(shí)IDR發(fā)揮作用,這樣播放器就能刷新緩沖,讓360p的視頻流進(jìn)入。
- 錯(cuò)誤恢復(fù):
如果你在流化視頻時(shí)使用HLS,并且每個(gè)視頻片段都以IDR開始,這意味著片段中的所有幀都不能參考前、后片段中的幀。所以如果因?yàn)槟硞€(gè)錯(cuò)誤而失去其中一個(gè)片段,播放器仍然能繼續(xù)接收下一個(gè)視頻片段。有趣的是,Apple 的 HLS 規(guī)范提到應(yīng)該每兩秒使用一次 IDR。(注意:規(guī)范沒有說視頻片段持續(xù)時(shí)間應(yīng)該是兩秒,而是指 GOP 的大小是兩秒)
所以說,IDR不能太頻繁,因?yàn)闀绊憠嚎s效率。但又要保證視頻播放過程中丟包不受到影響,所以最好的辦法,就是間隔一段時(shí)間使用固定的IDR。
- 快進(jìn)快退:
我們之前提到過,IDR非常有助于實(shí)現(xiàn)快進(jìn)快退。播放器需找到距離最近的IDR,然后開始從這一點(diǎn)播放視頻流。
總結(jié)
以上是生活随笔為你收集整理的编解码标准-H.264的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: java jacob下载_jacob-1
- 下一篇: U盘误删除文件?迅龙数据恢复软件轻松找回
