H.264視頻碼流解析

一些基本概念

YUV/RGB處理以及PCM處理處于對視音頻原始數據的處理,但是視音頻在傳輸的過程中是以視音頻碼流傳輸的。接下來介紹一下視頻碼流:

H.264的標準定義如下:同時也是MPEG-4第十部分,是由ITU-T視頻編碼專家組(VCEG)和ISO/IEC動態圖像專家組(MPEG)聯合組成的聯合視頻組(JVT,Joint Video Team)提出的高度壓縮數字視頻編解碼器標準。

什么是H.264

先通過PR編輯軟件的導出列表形象生動地感受一下H.264格式:

多種格式

H.264原始碼流(又稱為“裸流”)是由一個一個的NALU組成的,結構是這樣的:

NALU

每個NALU之間通過起始碼進行分隔,起始碼分成兩種:0x000001(3Byte)或者0x00000001(4Byte)。NALU對應的Slice為一幀的開始就用0x00000001,否則就用0x000001。H.264碼流解析的步驟就是首先從碼流中搜索0x000001和0x00000001,分離出NALU;然后再分析NALU的各個字段 。

什么是I幀、P幀、B幀

視頻壓縮中,每幀代表一幅靜止的圖像。而在實際壓縮時,會采取各種算法減少數據的容量,而IPB就是最常見的。I幀是關鍵幀(幀內壓縮),而P是向前搜索、B是雙向搜索,都是基于I幀來壓縮數據。

I幀表示關鍵幀,你可以理解為這一幀畫面的完整保留;解碼時只需要本幀數據就可以完成(因為包含完整畫面)

P幀表示的是這一幀跟之前的一個關鍵幀(或P幀)的差別,解碼時需要用之前緩存的畫面疊加上本幀定義的差別,生成最終畫面。(也就是差別幀,P幀沒有完整畫面數據,只有與前一幀的畫面差別的數據)

B幀是雙向差別幀,也就是B幀記錄的是本幀與前后幀的差別(具體比較復雜,有4種情況),換言之,要解碼B幀,不僅要取得之前的緩存畫面,還要解碼之后的畫面,通過前后畫面的與本幀數據的疊加取得最終的畫面。B幀壓縮率高,但是解碼時CPU會比較累~

網絡上的電影很多采用了B幀,因為B幀記錄的是前后幀的差別,比P幀能節約更多的空間。但是如果遇到不支持B幀的播放器就會很尷尬,因為雖然文件通過B幀壓縮確實縮小了非常多,但是在解碼時,不僅要用之前緩存的畫面,還要知道下一個I或者P的畫面。如果B幀丟失,就會用之前的畫面簡單重復,就會造成畫面卡,B幀越多,畫面越卡。

圖片.png
一般來說,I 幀的壓縮率是7(跟JPG差不多),P 幀是20,B 幀可以達到50。

在如上圖中,GOP (Group of Pictures)長度為13,S0~S7 表示 8個視點,T0~T12 為 GOP的 13個時刻。每個 GOP包含幀數為視點數 GOP 長度的乘積。在該圖中一個 GOP 中,包含94 個 B幀。B 幀占一個 GOP 總幀數的 90.38%。GOP 越長,B 幀所占比例更高,編碼的率失真性能越高

解釋一下GOP,是圖像組的意思,表示編碼的視頻序列分成了一組一組有序的幀的集合進行編碼。

H.264編碼原理

H.264采用的編碼算法就是幀內壓縮和幀間壓縮,幀內壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。

視頻中一般會存在一段變化不大的圖像畫面,我們可以先編碼出一個完整的圖像幀t1,之后的圖像幀t2就可以不編碼全部圖像,而只寫入與t1幀的差別,之后的圖像幀參考t3幀仍舊可以參照這種方式繼續進行編碼。這樣循環下去就可以得到一個序列。如果下一個圖像幀t4與之前的圖像變化很大,無法參考前面的圖像幀,就結束上一序列,并開始下一序列。

序列就可以定義為一段圖像編碼后的數據流,以I幀開始,到下一個I幀結束,H.264中圖像就是以序列為單位進行組織的。

H.264壓縮方法

知道視頻是如何編碼的了,那么怎樣具體進行編碼呢?有以下步驟:

(1)分組:把幾幀圖像分為一組(GOP,也就是一個序列),為防止運動變化,幀數不宜取多。
(2)定義幀:將每組內各幀圖像定義為三種類型,即 I 幀、B 幀和 P 幀;
(3)預測幀:以I幀做為基礎幀,以 I 幀預測 P 幀,再由 I 幀和 P 幀預測 B 幀;
(4)數據傳輸:最后將 I 幀數據與預測的差值信息進行存儲和傳輸。

幀內(Intraframe)壓縮也稱為空間壓縮(Spatial compression)。當壓縮一幀圖像時,僅考慮本幀的數據而不考慮相鄰幀之間的冗余信息,這實際上與靜態圖像壓縮類似。幀內一般采用有損壓縮算法,由于幀內壓縮是編碼一個完整的圖像,所以可以獨立的解碼、顯示。幀內壓縮一般達不到很高的壓縮,跟編碼 jpeg 差不多。

幀間(Interframe)壓縮的原理是:相鄰幾幀的數據有很大的相關性,或者說前后兩幀信息變化很小的特點。也即連續的視頻其相鄰幀之間具有冗余信息,根據這一特性,壓縮相鄰幀之間的冗余量就可以進一步提高壓縮量,減小壓縮比。幀間壓縮也稱為時間壓縮(Temporal compression),它通過比較時間軸上不同幀之間的數據進行壓縮。幀間壓縮一般是無損的。幀差值(Frame differencing)算法是一種典型的時間壓縮法,它通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少數據量。

IDR幀

重點看一下P幀的定義:P幀表示的是這一幀跟之前的一個關鍵幀(或P幀)的差別,解碼.......,P幀是可以參照前面的P幀的,并且GOP雖然一定是I幀開始的,但在一個GOP中不一定只有2個I幀,可能包含幾個I幀,只有第一個I幀才是關鍵幀。但是在這種情況下,舉個例子:I P B P B P B B B P1 I1 P2 B,為了方便表示,將需要用到的圖像幀定義為P1 I1 P2;P2幀除了會參照I1幀之外,還會參考P1幀,如果P1與I幀畫面相差較大的話,那么不但沒有意義還會造成畫面問題,所以我們需要一種幀能夠禁止后面的幀向前面的幀參照,這就是IDR幀。

  I 和 IDR 幀都是使用幀內預測的。它們都是同一個東西而已,在編碼和解碼中為了方便,要首個 I 幀和其他 I 幀區別開,所以才把第一個首個 I 幀叫 IDR,這樣就方便控制編碼和解碼流程。IDR 幀的作用是立刻刷新,使錯誤不致傳播,從IDR幀開始,重新算一個新的序列開始編碼。而 I 幀不具有隨機訪問的能力,這個功能是由 IDR 承擔。IDR 會導致DPB(參考幀列表——這是關鍵所在)清空,而 I 不會。IDR 圖像一定是 I 圖像,但I圖像不一定是 IDR 圖像。一個序列中可以有很多的I圖像,I 圖像之后的圖像可以引用 I 圖像之間的圖像做運動參考。一個序列中可以有很多的 I 圖像,I 圖像之后的圖象可以引用I圖像之間的圖像做運動參考。

詳解H.264 NALU語法結構

首先NALU是H.264編碼數據存儲或傳輸的基本單元,一般H.264碼流最開始的兩個NALU是SPS和PPS,第三個NALU是IDR。而SPS、PPS、SEI這三種NALU不屬于幀的范疇,它們的定義如下:

SPS(Sequence Parameter Sets):序列參數集,作用于一系列連續的編碼圖像。
PPS(Picture Parameter Set):圖像參數集,作用于編碼視頻序列中一個或多個獨立的圖像。
SEI(Supplemental enhancement information):附加增強信息,包含了視頻畫面定時等信息,一般放在主編碼圖像數據之前,在某些應用中,它可以被省略掉。
IDR(Instantaneous Decoding Refresh):即時解碼刷新。
HRD(Hypothetical Reference Decoder):假想碼流調度器。

語法結構

在H.264/AVC視頻編碼標準中,整個系統框架被分為了兩個層面:視頻編碼層面(VCL)和網絡抽象層面(NAL)。其中,前者負責有效表示視頻數據的內容,而后者則負責格式化數據并提供頭信息,以保證數據適合各種信道和存儲介質上的傳輸。因此我們平時的每幀數據就是一個NAL單元(SPS與PPS除外)。在實際的H264數據幀中,往往幀前面帶有00 00 00 01 或 00 00 01分隔符,一般來說編碼器編出的首幀數據為PPS與SPS,接著為I幀……

一個H.264文件如下:

圖片.png

在H264碼流中,都是以"0x00 0x00 0x01"或者"0x00 0x00 0x00 0x01"為開始碼的,找到開始碼之后,使用開始碼之后的第一個字節的低 5 位判斷是否為 7(sps)或者 8(pps), 及 data[4] & 0x1f == 7 || data[4] & 0x1f == 8。然后對獲取的 nal 去掉開始碼之后進行 base64 編碼,得到的信息就可以用于 sdp。 sps和pps需要用逗號分隔開來。

上圖中,00 00 00 01是一個NALU的起始標志。后面的第一個字節,0x67,是NALU的類型,type &0x1f==0x7表示這個NALU是SPS,type &0x1f==0x8表示是PPS。

H.264基本碼流結構分兩層,視頻編碼層VCL和網絡適配層NAL,這樣使信號處理和網絡傳輸分離,而H.264碼流在網絡中傳輸時實際上是以一系列的NALU組成,原始的NALU單元組成:

[start code] + [NALU header] + [NALU payload] 

[start code]占1字節,為0x000001或0x00000001。

其中[NALU header]如下:

forbidden_zero_bit(1bit) + nal_ref_idc(2bit) + nal_unit_type(5bit)
圖片.png

00 00 00 01 為起始符,67 即 nal_unit_type。

0x67的二進制是 0110 0111

則 forbidden_zero_bit(1bit) = 0;

nal_ref_idc(2bit) = 3;

nal_unit_type(5bit) = 7;即 SPS 類型。

更多關于H.264NALU的語法可以看這幾篇:

https://blog.csdn.net/wutong_login/article/details/5818763

https://blog.csdn.net/wutong_login/article/details/5824509

應用:分離NALU并分析字段

來自雷神:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef enum {
    NALU_TYPE_SLICE    = 1,
    NALU_TYPE_DPA      = 2,
    NALU_TYPE_DPB      = 3,
    NALU_TYPE_DPC      = 4,
    NALU_TYPE_IDR      = 5,
    NALU_TYPE_SEI      = 6,
    NALU_TYPE_SPS      = 7,
    NALU_TYPE_PPS      = 8,
    NALU_TYPE_AUD      = 9,
    NALU_TYPE_EOSEQ    = 10,
    NALU_TYPE_EOSTREAM = 11,
    NALU_TYPE_FILL     = 12,
} NaluType;
 
typedef enum {
    NALU_PRIORITY_DISPOSABLE = 0,
    NALU_PRIRITY_LOW         = 1,
    NALU_PRIORITY_HIGH       = 2,
    NALU_PRIORITY_HIGHEST    = 3
} NaluPriority;
 
 
typedef struct
{
    int startcodeprefix_len;      //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)
    unsigned len;                 //! Length of the NAL unit (Excluding the start code, which does not belong to the NALU)
    unsigned max_size;            //! Nal Unit Buffer size
    int forbidden_bit;            //! should be always FALSE
    int nal_reference_idc;        //! NALU_PRIORITY_xxxx
    int nal_unit_type;            //! NALU_TYPE_xxxx    
    char *buf;                    //! contains the first byte followed by the EBSP
} NALU_t;
 
FILE *h264bitstream = NULL;                //!< the bit stream file
 
int info2=0, info3=0;
 
static int FindStartCode2 (unsigned char *Buf){
    if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=1) return 0; //0x000001?
    else return 1;
}
 
static int FindStartCode3 (unsigned char *Buf){
    if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=0 || Buf[3] !=1) return 0;//0x00000001?
    else return 1;
}
 
 
int GetAnnexbNALU (NALU_t *nalu){
    int pos = 0;
    int StartCodeFound, rewind;
    unsigned char *Buf;
 
    if ((Buf = (unsigned char*)calloc (nalu->max_size , sizeof(char))) == NULL) 
        printf ("GetAnnexbNALU: Could not allocate Buf memory\n");
 
    nalu->startcodeprefix_len=3;
 
    if (3 != fread (Buf, 1, 3, h264bitstream)){
        free(Buf);
        return 0;
    }
    info2 = FindStartCode2 (Buf);
    if(info2 != 1) {
        if(1 != fread(Buf+3, 1, 1, h264bitstream)){
            free(Buf);
            return 0;
        }
        info3 = FindStartCode3 (Buf);
        if (info3 != 1){ 
            free(Buf);
            return -1;
        }
        else {
            pos = 4;
            nalu->startcodeprefix_len = 4;
        }
    }
    else{
        nalu->startcodeprefix_len = 3;
        pos = 3;
    }
    StartCodeFound = 0;
    info2 = 0;
    info3 = 0;
 
    while (!StartCodeFound){
        if (feof (h264bitstream)){
            nalu->len = (pos-1)-nalu->startcodeprefix_len;
            memcpy (nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);     
            nalu->forbidden_bit = nalu->buf[0] & 0x80; //1 bit
            nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
            nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;// 5 bit
            free(Buf);
            return pos-1;
        }
        Buf[pos++] = fgetc (h264bitstream);
        info3 = FindStartCode3(&Buf[pos-4]);
        if(info3 != 1)
            info2 = FindStartCode2(&Buf[pos-3]);
        StartCodeFound = (info2 == 1 || info3 == 1);
    }
 
    // Here, we have found another start code (and read length of startcode bytes more than we should
    // have.  Hence, go back in the file
    rewind = (info3 == 1)? -4 : -3;
 
    if (0 != fseek (h264bitstream, rewind, SEEK_CUR)){
        free(Buf);
        printf("GetAnnexbNALU: Cannot fseek in the bit stream file");
    }
 
    // Here the Start code, the complete NALU, and the next start code is in the Buf.  
    // The size of Buf is pos, pos+rewind are the number of bytes excluding the next
    // start code, and (pos+rewind)-startcodeprefix_len is the size of the NALU excluding the start code
 
    nalu->len = (pos+rewind)-nalu->startcodeprefix_len;
    memcpy (nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);//
    nalu->forbidden_bit = nalu->buf[0] & 0x80; //1 bit
    nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
    nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;// 5 bit
    free(Buf);
 
    return (pos+rewind);
}
 
/**
 * Analysis H.264 Bitstream
 * @param url    Location of input H.264 bitstream file.
 */
int simplest_h264_parser(char *url){
 
    NALU_t *n;
    int buffersize=100000;
 
    //FILE *myout=fopen("output_log.txt","wb+");
    FILE *myout=stdout;
 
    h264bitstream=fopen(url, "rb+");
    if (h264bitstream==NULL){
        printf("Open file error\n");
        return 0;
    }
 
    n = (NALU_t*)calloc (1, sizeof (NALU_t));
    if (n == NULL){
        printf("Alloc NALU Error\n");
        return 0;
    }
 
    n->max_size=buffersize;
    n->buf = (char*)calloc (buffersize, sizeof (char));
    if (n->buf == NULL){
        free (n);
        printf ("AllocNALU: n->buf");
        return 0;
    }
 
    int data_offset=0;
    int nal_num=0;
    printf("-----+-------- NALU Table ------+---------+\n");
    printf(" NUM |    POS  |    IDC |  TYPE |   LEN   |\n");
    printf("-----+---------+--------+-------+---------+\n");
 
    while(!feof(h264bitstream)) 
    {
        int data_lenth;
        data_lenth=GetAnnexbNALU(n);
 
        char type_str[20]={0};
        switch(n->nal_unit_type){
            case NALU_TYPE_SLICE:sprintf(type_str,"SLICE");break;
            case NALU_TYPE_DPA:sprintf(type_str,"DPA");break;
            case NALU_TYPE_DPB:sprintf(type_str,"DPB");break;
            case NALU_TYPE_DPC:sprintf(type_str,"DPC");break;
            case NALU_TYPE_IDR:sprintf(type_str,"IDR");break;
            case NALU_TYPE_SEI:sprintf(type_str,"SEI");break;
            case NALU_TYPE_SPS:sprintf(type_str,"SPS");break;
            case NALU_TYPE_PPS:sprintf(type_str,"PPS");break;
            case NALU_TYPE_AUD:sprintf(type_str,"AUD");break;
            case NALU_TYPE_EOSEQ:sprintf(type_str,"EOSEQ");break;
            case NALU_TYPE_EOSTREAM:sprintf(type_str,"EOSTREAM");break;
            case NALU_TYPE_FILL:sprintf(type_str,"FILL");break;
        }
        char idc_str[20]={0};
        switch(n->nal_reference_idc>>5){
            case NALU_PRIORITY_DISPOSABLE:sprintf(idc_str,"DISPOS");break;
            case NALU_PRIRITY_LOW:sprintf(idc_str,"LOW");break;
            case NALU_PRIORITY_HIGH:sprintf(idc_str,"HIGH");break;
            case NALU_PRIORITY_HIGHEST:sprintf(idc_str,"HIGHEST");break;
        }
 
        fprintf(myout,"%5d| %8d| %7s| %6s| %8d|\n",nal_num,data_offset,idc_str,type_str,n->len);
 
        data_offset=data_offset+data_lenth;
 
        nal_num++;
    }
 
    //Free
    if (n){
        if (n->buf){
            free(n->buf);
            n->buf=NULL;
        }
        free (n);
    }
    return 0;
}

結果如下:

圖片.png

應用:網頁視頻流m3u8/ts視頻下載

現在很多視頻網站播放流視頻,都不采用mp4/flv文件播放,而是采用m3u8/ts這種方式播放。這種方式既是為了緩解瀏覽器緩存的壓力,也是為了防止視頻泄漏。簡單來說就是,網站后臺把視頻切片成上千個xx.ts文件,一般10s一個,每個都幾百個kb很小,然后通過xx.m3u8播放列表把這些文件連接起來。

圖片.png

我們直接點擊這個playlist.m3u8播放列表文件,在旁邊的preview欄中查看內容,可以看到:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:11
#EXTINF:5.250000,
out000.ts
#EXTINF:9.500000,
out001.ts
#EXTINF:8.375000,
out002.ts
#EXTINF:5.375000,
out003.ts
#EXTINF:9.000000,
out004.ts
...........

這里本來應該有個實戰的,看了一下手下分析的不可描述的網站(逃~),作罷.....

為了下載視頻,得想辦法把所有的ts切片文件下載下來,然后合成一個完整的視頻。因為ts流命名規范有規律,非常容易下載,配合xx.m3u8播放列表文件,就可以直接用ffmpeg在線下載所有的視頻并合并。直接執行這一句命令:

$ ffmpeg -i <m3u8-path> -c copy OUTPUT.mp4
$ ffmpeg -i <m3u8-path> -vcodec copy -acodec copy OUTPUT.mp4

而m3u8實際上是HLS直播協議,在之后的文章中會介紹到。但是我們完全可以利用VLC播放軟件播放直播源,從而看到視頻。

為什么下載播放列表就能下載所有的切片文件?
因為播放列表里的都是相對路徑,既然我們有了播放列表的絕對路徑,那么其它所有文件的絕對路徑也就不難獲取了。
好在ffmpeg直接實現了這種播放列表一鍵下載的方式。

當然部分網站可能會對視頻流數據進行加密,從而導致ts文件不能合并。但是比如有key的話,同樣可以通過key進行合并視頻。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容