音視頻學習:H.264

H.264簡介

H.264格式

H.264編碼流程

ffmpeg命令

解析NAL Header

H.264

本文只涉及包結構,有個大概的認識,不涉及到編碼算法。後續有機會再瞭解。

H.264英文文檔

H.264中文文檔05版

H.264簡介

H.264,又稱爲MPEG-4第10部分,高級視頻編碼(英語:MPEG-4 Part 10, Advanced Video Coding,縮寫爲MPEG-4 AVC)是一種面向塊,基於運動補償視頻編碼標準 。到2014年,它已經成爲高精度視頻錄製、壓縮和發佈的最常用格式之一。

H.264/AVC項目的目的是爲了創建一個比以前的視頻壓縮標準,在更低的比特率的情況下依然能夠提供良好視頻質量的標準(如,一半或者更少於MPEG-2,H.263,或者MPEG-4 Part2 )。同時,還要不會太大的增加設計的複雜性。

H.264相比於更早的H.263等其他的編碼格式,有以下優勢:

  • 網絡親和性,適用於各種傳輸網絡
  • 高視頻壓縮比,最初提出的指標比H.263、MPEG-4壓縮率更高,約爲它們的2倍

2012年提出了H.265,H.265即HEVC(High Efficiency Video Coding),目的是爲了取代H.264,H.265能夠達到H.264兩倍的壓縮率。

H.264格式

H.264原始碼流(裸流),由一個接一個NALU組成,而它的功能分爲兩層:視頻編碼層(VCL, Video Coding Layer)和網絡提取層(NAL, Network Abstraction Layer)。

VCL數據即編碼處理的輸出,它表示被壓縮編碼後的視頻數據序列。在 VCL 數據傳輸或存儲之前,這些編碼的VCL數據,先被映射或封裝進NAL單元(NALU)中。

每個NAL單元包括:一組對應於視頻編碼數據的NAL頭信息和一個原始字節序列負荷(RBSP)。

頭信息中包含着一個可否丟棄的指示標記,標識着該NAL單元的丟棄能否引起錯誤擴散,一般如果NAL單元中的信息不用於構建參考圖像,則認爲可以將其丟棄;最後包含的是NAL單元的類型 信息,暗示着其內含有效載荷的內容。 **送到解碼器端的NAL單元必須遵守嚴格的順序,如果應用程序接收到的NAL單元處於亂序,則必須提供一種恢復其正確順序的方法。**對於RTP/UDP/IP系統,則可以直接將編碼器輸出的NAL單元作爲RTP的有效載荷。

NALU

NALU以0x00 00 010x00 00 00 01開頭,標準規定可以以0x00 00 00作爲一個NALU的結束標誌。
NALU=NALHeader+RBSP NALU = NAL Header + RBSP
下圖中NAL頭+RBSP就是一個NALU,傳輸時每個NALU都是獨立傳輸的。

NALU結構

NAL Header

NAL頭長度爲1字節,內部包括三個標誌:

  • forbidden_zero

    1bit,正常爲0,當網絡傳輸NALU可能存在錯誤時,爲1,解碼器可以考慮不對該NALU解碼。

  • nal_ref_idc

    2bit,取值從0~3,值越大,表示當前NALU越重要。

    當前NALU爲圖像參數集、序列參數集、IDR圖像或參考圖像條帶(片),或者爲參考圖像條帶數據分割時,nal_ref_idc必定不爲0。

    當nal_unit_type爲6、9、10、11、11、12時,nal_ref_type都爲0。

  • nal_unit_type

    5bit,表示NALU中RBSP的數據結構類型,類型見下表:

    nal_unit_type NAL單元和RBSP語法結構的內容
    0 未指定
    1 一個非IDR圖像的編碼條帶
    2 編碼條帶數據分割塊A
    3 編碼條帶數據分割塊B
    4 編碼條帶數據分割塊C
    5 IDR圖像的編碼條帶
    6 輔助增強信息(SEI)
    7 序列參數集
    8 圖像參數集
    9 訪問單元分隔符
    10 序列結尾
    11 流結尾
    12 填充數據
    13 序列參數集擴展
    14-18 保留
    19 未分割的輔助編碼圖像的編碼條帶
    21-23 保留
    24-31 未指定

RBSP

RBSP(原始字節序列載荷,Raw Byte Sequence Payload)中包含兩部分,SODB和RBSP尾部。

SODB

SODB(數據比特串,String Of Data Bits),這個內部存儲的是真正的原始編碼數據。

RBSP尾

RBSP尾部分爲兩種:

  • nal_unit_type不爲1~5時,則在尾部補一個比特位,值爲1,然後補若干個比特位,值爲0,若干個視當前SODB長度,讓字節對齊。
  • nal_unit_type爲1~5時,默認情況爲第一種補全方式,但是當entropy_coding_mode_flag值爲1,即當前採用的熵編碼爲CABAC,而且more_rbsp_trailing_data()返回值爲true,RBSP有更多數據的時候,在後面再添加一個或多個0x0000。

關於SODB轉換爲RBSP定義在nal.c

/*!
************************************************************************
* \brief
*    Converts String Of Data Bits (SODB) to Raw Byte Sequence
*    Packet (RBSP)
* \param currStream
*        Bitstream which contains data bits.
* \return None
* \note currStream is byte-aligned at the end of this function
*
************************************************************************
*/

void SODBtoRBSP(Bitstream *currStream)
{
    currStream->byte_buf <<= 1;
    currStream->byte_buf |= 1;
    currStream->bits_to_go--;
    currStream->byte_buf <<= currStream->bits_to_go;
    currStream->streamBuffer[currStream->byte_pos++] = currStream->byte_buf;
    currStream->bits_to_go = 8;
    currStream->byte_buf = 0;
}

EBSP

EBSP(擴展字節序列載荷,Encapsulated Byte Sequence Payload)則是對RBSP的拓展,EBSP很容易理解,是爲了防止競爭而存在的。

爲了防止競爭,H.264規定了防止競爭機制:如果在EBSP內部出現0x00 00 00或者0x00 00 01,則在最後一個字節前加入0x03。

  • 0x00 00 00 —> 0x00 00 03 00(NALU結束標誌)
  • 0x00 00 01 —> 0x00 00 03 01(NALU起始標誌)
  • 0x00 00 02 —> 0x00 00 03 02(保留標誌)
  • 0x00 00 03 —> 0x00 00 03 03(防止視頻內本身存在的0x00 00 03被作爲防競爭標誌)

所以EBSP的本質就是,當RBSP內部存在競爭出現歧義時,對其加上0x03。

拿到一個EBSP,只需要將所有0x00 00 03修改成0x00 00即可獲得RBSP。

/*!
************************************************************************
*  \brief
*     This function add emulation_prevention_three_byte for all occurrences
*     of the following byte sequences in the stream
*       0x000000  -> 0x00000300
*       0x000001  -> 0x00000301
*       0x000002  -> 0x00000302
*       0x000003  -> 0x00000303
*
*  \param NaluBuffer
*            pointer to target buffer
*  \param rbsp
*            pointer to source buffer
*  \param rbsp_size
*           Size of source
*  \return
*           Size target buffer after emulation prevention.
*
************************************************************************
*/

int RBSPtoEBSP(byte *NaluBuffer, unsigned char *rbsp, int rbsp_size)
{
    int j     = 0;
    int count = 0;
    int i;

    for(i = 0; i < rbsp_size; i++)
    {
        if(count == ZEROBYTES_SHORTSTARTCODE && !(rbsp[i] & 0xFC))
        {
            NaluBuffer[j] = 0x03;
            j++;
            count = 0;
        }
        NaluBuffer[j] = rbsp[i];
        if(rbsp[i] == 0x00)
            count++;
        else
            count = 0;
        j++;
    }
    return j;
}

H.264編碼流程

幀間和幀內預測(Estimation)、變換(Transform)和反變換、量化(Quantization)和反量化、環路濾波(Loop Filter)、熵編碼(Entropy Coding)。

簡單記錄下大概的流程。詳細編碼每個步驟流程後期再去學習,目前主要是希望對音視頻整體有個基礎的認識~

ffmpeg命令

轉換mp4變成h264裸流

ffmpeg -i NewRules.mp4 -vcodec copy -f h264 NewRules.264

轉換mp4前10秒視頻變成h264裸流

ffmpeg -ss 0:0:0 -t 0:0:10 -i NewRules.mp4 -vcodec copy NewRules_cut.264

解析NAL Header

代碼參照雷神的博客編寫。

代碼邏輯很簡單,按照格式將每個NALU分割找到,然後再解析第一個字節,即NAL Header。解析結果如圖:

NAL Header Parser Result

這裏展示下部分代碼,詳細代碼訪問項目地址

bool H264Parser::GetNextNALU()
{
    int cur_pos = 0;
    bool start_code_found = false;
    bool info2 = false, info3 = false;
    bool eof_flag = false;
    char start_code[4];

    // 找NALU開頭
    if (3 != fread(start_code, 1, 3, h264_stream)) {
        return false;
    }
    info2 = FindStartCode2(start_code);
    if (info2) {
        nalu->start_code_len = 3;
    } else {
        if (1 != fread(start_code + 3, 1, 1, h264_stream)) {
            return false;
        }
        info3 = FindStartCode3(start_code);
        if (info3) {
            nalu->start_code_len = 4;
        } else {
            return false;
        }
    }

    // 找下一個NALU的開頭
    start_code_found = false;
    info2 = info3 = false;
    while (!start_code_found) {
        if (feof(h264_stream)) {
            eof_flag = true;
            break;
        }
        nalu->buffer[cur_pos++] = fgetc(h264_stream);
        info3 = FindStartCode3(nalu->buffer + cur_pos - 4);
        if (!info3) {
            info2 = FindStartCode2(nalu->buffer + cur_pos - 3);
        }
        start_code_found = (info2 || info3);
    }

    // 當前NALU開頭到下一個NALU開頭之間的數據就是我們要的
    // 然後把FILE指針恢復到下一個NALU開頭開始的位置
    if (eof_flag) {
        nalu->sodb_len = cur_pos - 1;
    } else {
        int rwind = info2 ? -3 : -4;
        if (0 != fseek(h264_stream, rwind, SEEK_CUR)) {
            std::cout << "Error: fseek error\n";
            return false;
        }
        nalu->sodb_len = cur_pos + rwind;
    }
    // 分析NAL Header
    nalu->forbidden_bit = nalu->buffer[0] & 0x80;      //1 bit
    nalu->nalu_reference_idc = nalu->buffer[0] & 0x60; // 2 bit
    nalu->nalu_unit_type = (nalu->buffer[0]) & 0x1f;   // 5 bit
    return true;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章