H264相關知識學習


H264在網絡傳輸的是NALU,NALU的結構是:NAL頭+RBSP, 
其中NAL頭佔一個字節,其低5個bit位表示NAL type。
RBSP 爲原始字節序列載荷
在實際的H264數據幀中,往往幀NAL type前面帶有00 00 00 01 或 00 00 01分隔符,
一般來說編碼器編出的首幀數據爲PPS與SPS,接着爲I幀,然後是P幀

 


視頻傳輸原理   

     視頻是利用人眼視覺暫留的原理,通過播放一系列的圖片,使人眼產生運動的感覺。單純傳輸視頻畫面,視頻量非常大,
     對現有的網絡和存儲來說是不可接受的。爲了能夠使視頻便於傳輸和存儲,人們發現視頻有大量重複的信息,如果將重
     覆信息在發送端去掉,在接收端恢復出來,這樣就大大減少了視頻數據的文件,因此有了H.264視頻壓縮標準。
      在H.264壓縮標準中I幀、P幀、B幀用於表示傳輸的視頻畫面。


 


1、I幀

I幀又稱幀內編碼幀,是一種自帶全部信息的獨立幀,無需參考其他圖像便可獨立進行解碼,可以簡單理解爲一張靜態畫面。視頻
序列中的第一個幀始終都是I幀,因爲它是關鍵幀。

2、P幀

 P幀又稱幀間預測編碼幀,需要參考前面的I幀才能進行編碼。表示的是當前幀畫面與前一幀(前一幀可能是I幀也可能是P幀)的差別。
 解碼時需要用之前緩存的畫面疊加上本幀定義的差別,生成最終畫面。與I幀相比,P幀通常佔用更少的數據位,但不足是,由於P幀對
 前面的P和I參考幀有着複雜的依耐性,因此對傳輸錯誤非常敏感。


3、B幀

B幀又稱雙向預測編碼幀,也就是B幀記錄的是本幀與前後幀的差別。也就是說要解碼B幀,不僅要取得之前的緩存畫面,還要解碼之後的畫面,
通過前後畫面的與本幀數據的疊加取得最終的畫面。B幀壓縮率高,但是對解碼性能要求較高。

總結:

I幀只需考慮本幀;P幀記錄的是與前一幀的差別;B幀記錄的是前一幀及後一幀的差別,能節約更多的空間,視頻文件小了,但相對來說解碼的時候
就比較麻煩。因爲在解碼時,不僅要用之前緩存的畫面,而且要知道下一個I或者P的畫面,對於不支持B幀解碼的播放器容易卡頓。


視頻監控系統中預覽的視頻畫面是實時的,對畫面的流暢性要求較高。採用I幀、P幀進行視頻傳輸可以提高網絡的適應能力,且能降低解碼成本。所以現階段的視頻解碼都只採用I幀和P幀進行傳輸。海康攝像機編碼,I幀間隔是50,含49個P幀。


 


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

#define TAB44 "    "
#define PRINTF_DEBUG

#define PRTNTF_STR_LEN 10

/************************************************************************************************************
**                                        nalu header: 負責將VCL產生的比特字符串適配到各種各樣的網絡和多元環境中, 
                                                       覆蓋了所有片級以上的語法級別(NALU的作用, 方便網絡傳輸)
**
-------------------------------------------------------------------------------------------------------------
**        字段名稱               |    長度(bits)    |        有關描述
-------------------------------------------------------------------------------------------------------------
**        forbidden_bit          |    1             |        編碼中默認值爲0, 當網絡識別此單元中存在比特錯誤時, 可將其設爲1, 以便接收方丟掉該單元
**        nal_reference_idc      |    2             |        0~3標識這個NALU的重要級別
**        nal_unit_type          |    5             |          NALU的類型(類型1~12是H.264定義的, 類型24~31是用於H.264以外的, 
                                                             RTP負荷規範使用這其中的一些值來定義包聚合和分裂, 其他值爲H.264保留)

** nal_unit_type:
    0                未使用
    1                未使用Data Partitioning, 非IDR圖像的Slice
    2                使用Data Partitioning且爲Slice A
    3                使用Data Partitioning且爲Slice B
    4                使用Data Partitioning且爲Slice C
    5                IDR圖像的Slice(立即刷新)
    6                補充增強信息(SEI)
    7                序列參數集(sequence parameter set, SPS)
    8                圖像參數集(picture parameter set, PPS)
    9                分界符
    10               序列結束
    11               碼流結束
    12               填充
    13...23          保留
    24...31          未使用
    
** SPS, PPS. SLICE等信息就不解析了. 爲了減少bits, 用了哥倫布編碼(自己解析比較麻煩, 但是網上有很多).

** SPS信息說明:
        1. 視頻寬高, 幀率等信息;
        2. seq_parameter_set_id, 指明本序列參數集的id號, 這個id號將被picture參數集引用;
        3. pic_width_in_mbs_minus1, 加1指定以宏塊(16*16)爲單位的每個解碼圖像的寬度, 即width = (pic_width_in_mbs_minus1 + 1) * 16
        4. pic_height_in_map_units_minus1;
        5. pic_order_cnt_type, 視頻的播放順序序號叫做POC(picture order count), 取值0,1,2;
        6. time_scale, fixed_frame_rate_flag, 計算幀率(fps).
           視頻幀率信息在SPS的VUI parameters syntax中, 需要根據time_scale, fixed_frame_rate_flag計算得到: fps = time_scale / num_units_in_tick.
           但是需要判斷參數timing_info_present_flag是否存在, 若不存在表示FPS在信息流中無法獲取.
           同時還存在另外一種情況: fixed_frame_rate_flag爲1時, 兩個連續圖像的HDR輸出時間頻率爲單位, 獲取的fps是實際的2倍.

** PPS信息說明:    
        1. pic_parameter_set_id, 用以指定本參數集的序號, 該序號在各片的片頭被引用;
        2. seq_parameter_set_id, 指明本圖像參數集所引用的序列參數集的序號;
        3. 其他高深的暫時還不理解, 指明參考幀隊列等.
        
** SLICE信息說明:
        1. slice_type, 片的類型;
        2. pic_parameter_set_id, 引用的圖像索引;
        3. frame_num, 每個參考幀都有一個連續的frame_num作爲它們的標識, 它指明瞭各圖像的解碼順序. 非參考幀也有,但沒有意義;
        4. least significant bits;
        5. 綜合三種poc(pic_order_cnt_type), 類型2應該是最省bit的, 因爲直接從frame_num獲得, 但是序列方式限制最大;
           類型1, 只需要一定的bit量在sps標誌出一些信息還在slice header中表示poc的變化, 但是比類型0要節省bit, 但是其序列並不是隨意的, 要週期變化;
           對於類型0因爲要對poc的lsb(pic_order_cnt_lsb, last bit)進行編碼所以用到的bit最多, 優點是序列可以隨意.
           ** 自我理解, 不一定準確(這邊算顯示順序, 要根據SPS中的pic_order_cnt_type, 爲2, 意味着碼流中沒有B幀, frame_num即爲顯示順序;
              爲1, 依賴frame_num求解POC; 爲0, 把POC的低位編進碼流內, 但這只是低位, 而POC的高位PicOrderCntMsb則要求解碼器自行計數,
              計數方式依賴於前一編碼幀(PrevPicOrderCntMsb與PrevPicOrderCntLsb.
              
           ** 一般的碼流分析所見(未仔細證實): pic_order_cnt_type=2, 只有frame_num(無B幀);
              pic_order_cnt_type=1, 暫未分析到;
              pic_order_cnt_type=0, pic_order_cnt_lsb指示顯示順序, 一般爲偶數增長(0, 2, 4, 6, 據說是什麼場方式和幀方式, 場時其實是0 0 2 2 4 4).
              
           ** 編碼與顯示的原因: 視頻編碼順序與視頻的播放順序, 並不完全相同, 視頻編碼時, 如果採用了B幀編碼, 由於B幀很多時候都是雙向預測得來的,
              這時會先編碼B幀的後向預測圖像(P幀), 然後再進行B幀編碼, 因此會把視頻原來的播放順序打亂, 以新的編碼順序輸出碼流,
              而在解碼斷接收到碼流後, 需要把順序還原成原本的播放順序, 以輸出正確的視頻. 在編解碼中, 視頻的播放順序序號叫做POC(picture order count).
              
** 總結: 1. 碼流中有很多SPS(序列), 一個序列中有多個圖像, 一個圖像中有多個片, 一個片中有多個塊;
         2. SPS中有seq_parameter_set_id. PPS中有pic_parameter_set_id, 並通過seq_parameter_set_id指明關聯的序列.
            SLICE中有pic_parameter_set_id, 指明關聯的圖像;
         3. SPS中可計算寬高以及幀率, pic_order_cnt_type(顯示順序的類型);
            SLICE HEADER中可算出解碼的順序, 以及根據pic_order_cnt_type算出顯示順序.            
************************************************************************************************************/
typedef enum e_h264_nalu_priority
{
    NALU_PRIORITY_DISPOSABLE     = 0,
    NALU_PRIORITY_LOW             = 1,
    NALU_PRIORITY_HIGH           = 2,
    NALU_PRIORITY_HIGHEST        = 3,
} E_H264_NALU_PRIORITY;

typedef enum e_h264_nalu_type
{
    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,
} E_H264_NALU_TYPE;

typedef struct t_h264_nalu_header
{
    unsigned char forbidden_bit:1, nal_reference_idc:2, nal_unit_type:5;
} T_H264_NALU_HEADER;

typedef struct t_h264_nalu
{
    int startCodeLen;
    
    T_H264_NALU_HEADER h264NaluHeader;
    
    unsigned int bodyLen;
    
    unsigned char *bodyData;
} T_H264_NALU;

/**********************************************************************************
 1. h264的起始碼: 0x000001(3 Bytes)或0x00000001(4 Bytes);
 2. 文件流中用起始碼來區分NALU.
***********************************************************************************/
static int FindStartCode3Bytes(unsigned char *scData)
{
    int isFind = 0;

    if ((0==scData[0]) && (0==scData[1]) && (1==scData[2]))
    {
        isFind = 1;
    }
    
    return isFind;
}

static int FindStartCode4Bytes(unsigned char *scData)
{
    int isFind = 0;

    if ((0==scData[0]) && (0==scData[1]) && (0==scData[2]) && (1 == scData[3]))
    {
        isFind = 1;
    }
    
    return isFind;
}

static int GetNaluDataLen(int startPos, int h264BitsSize, unsigned char *h264Bits)
{
    int parsePos = 0;
    
    parsePos = startPos;
    
    while (parsePos < h264BitsSize)
    {
        if (FindStartCode3Bytes(&h264Bits[parsePos]))
        {
            return parsePos - startPos;
        }
        else if (FindStartCode4Bytes(&h264Bits[parsePos]))
        {
            return parsePos - startPos;
        }
        else
        {
            parsePos++;
        }
    }
    
    return parsePos - startPos; // if file is end
}

static void ParseNaluData(const unsigned int naluLen, unsigned char* const nuluData)
{
    static int naluNum = 0;
    
    unsigned char *data = NULL;
    unsigned char priorityStr[PRTNTF_STR_LEN+1] = {0};
    unsigned char typeStr[PRTNTF_STR_LEN+1] = {0};
    
    T_H264_NALU_HEADER h264NaluHeader = {0};
    
    data = nuluData;
    
    memset(&h264NaluHeader, 0x0, sizeof(T_H264_NALU_HEADER));
    
    h264NaluHeader.nal_reference_idc = data[0]>>5 & 0x3;
    h264NaluHeader.nal_unit_type = data[0] & 0x1f;
    
    naluNum++;
    
#ifdef PRINTF_DEBUG
    switch (h264NaluHeader.nal_reference_idc)
    {
        case NALU_PRIORITY_DISPOSABLE:
            sprintf(priorityStr, "DISPOS");
            break;
            
        case NALU_PRIORITY_LOW:
            sprintf(priorityStr, "LOW");
            break;

        case NALU_PRIORITY_HIGH:
            sprintf(priorityStr, "HIGH");
            break;

        case NALU_PRIORITY_HIGHEST:
            sprintf(priorityStr, "HIGHEST");
            break;

        default:
            break;
    }
    
    switch (h264NaluHeader.nal_unit_type)
    {
        case NALU_TYPE_SLICE:
            sprintf(typeStr, "SLICE");
            break;
            
        case NALU_TYPE_DPA:
            sprintf(typeStr, "DPA");
            break;
            
        case NALU_TYPE_DPB:
            sprintf(typeStr, "DPB");
            break;
            
        case NALU_TYPE_DPC:
            sprintf(typeStr, "DPC");
            break;
            
        case NALU_TYPE_IDR:
            sprintf(typeStr, "IDR");
            break;
            
        case NALU_TYPE_SEI:
            sprintf(typeStr, "SEI");
            break;
            
        case NALU_TYPE_SPS:
            sprintf(typeStr, "SPS");
            break;
            
        case NALU_TYPE_PPS:
            sprintf(typeStr, "PPS");
            break;
            
        case NALU_TYPE_AUD:
            sprintf(typeStr, "AUD");
            break;
            
        case NALU_TYPE_EOSEQ:
            sprintf(typeStr, "EOSEQ");
            break;
            
        case NALU_TYPE_EOSTREAM:
            sprintf(typeStr, "EOSTREAM");
            break;
            
        case NALU_TYPE_FILL:
            sprintf(typeStr, "FILL");
            break;
        
        default:
            break;
    }
    
    printf("%5d| %7s| %6s| %8d|\n", naluNum, priorityStr, typeStr, naluLen);
#endif
}

int main(int argc, char *argv[])
{
    int fileLen = 0;
    int naluLen = 0;
    int h264BitsPos = 0;

    unsigned char *h264Bits = NULL;
    unsigned char *naluData = NULL;
    
    FILE *fp = NULL;
    
    if (2 != argc)
    {
        printf("Usage: flvparse **.flv\n");

        return -1;
    }

    fp = fopen(argv[1], "rb");
    if (!fp)
    {
        printf("open file[%s] error!\n", argv[1]);

        return -1;
    }
    
    fseek(fp, 0, SEEK_END);
    
    fileLen = ftell(fp);
    
    fseek(fp, 0, SEEK_SET);
    
    h264Bits = (unsigned char*)malloc(fileLen);
    if (!h264Bits)
    {
        printf("maybe file is too long, or memery is not enough!\n");
        
        fclose(fp);
    
        return -1;
    }
    
    memset(h264Bits, 0x0, fileLen);
    
    if (fread(h264Bits, 1, fileLen, fp) < 0)
    {
        printf("read file data to h264Bits error!\n");
        
        fclose(fp);
        free(h264Bits);
        
        h264Bits = NULL;
        
        return -1;
    }
    
    fclose(fp);
    
    printf("-----+-------- NALU Table ------+\n");
    printf(" NUM |    IDC |  TYPE |   LEN   |\n");
    printf("-----+--------+-------+---------+\n");

    while (h264BitsPos < (fileLen-4))
    {
        if (FindStartCode3Bytes(&h264Bits[h264BitsPos]))
        {
            naluLen = GetNaluDataLen(h264BitsPos+3, fileLen, h264Bits);

            naluData = (unsigned char*)malloc(naluLen);
            if (naluData)
            {
                memset(naluData, 0x0, naluLen);
                
                memcpy(naluData, h264Bits+h264BitsPos+3, naluLen);
                
                ParseNaluData(naluLen, naluData);
                
                free(naluData);
                naluData = NULL;
            }
            
            h264BitsPos += (naluLen+3);
        }
        else if (FindStartCode4Bytes(&h264Bits[h264BitsPos]))
        {
            naluLen = GetNaluDataLen(h264BitsPos+4, fileLen, h264Bits);

            naluData = (unsigned char*)malloc(naluLen);
            if (naluData)
            {
                memset(naluData, 0x0, naluLen);

                memcpy(naluData, h264Bits+h264BitsPos+4, naluLen);

                ParseNaluData(naluLen, naluData);
                
                free(naluData);
                naluData = NULL;
            }
            
            h264BitsPos += (naluLen+4);
        }
        else
        {
            h264BitsPos++;
        }
    }

    return 0;
}


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章