1. 程式人生 > >【視頻編解碼·學習筆記】6. H.264碼流分析工程創建

【視頻編解碼·學習筆記】6. H.264碼流分析工程創建

clear href mark 一個 html filename down 創建 fail

一、準備工作:

新建一個VS工程SimpleH264Analyzer, 修改工程屬性參數-> 輸出目錄:$(SolutionDir)bin\$(Configuration)\,工作目錄:$(SolutionDir)bin\$(Configuration)\

編譯一下工程,工程目錄下會生成bin文件夾,其中的debug文件夾中有剛才編譯生成的exe文件。將一個.264視頻文件拷貝到這個文件夾中(本次使用的仍是學習筆記3中生成的.264文件)。

將這個文件作為輸入參數傳到工程中:屬性 -> 調試 -> 命令參數:test.264 (最後那個文件名根據自己的改)

更改目錄結構,並新建兩個文件Stream.h

Stream.cpp,更改後目錄結構如下:
技術分享圖片

Stream.h頭文件中,新建一個類CStreamFile,用來表示.264文件,其中包括構造函數、私有成員變量,及自定義函數。代碼如下:

#ifndef _STREAM_H_
#define _STREAM_H_
#include <vector>

class CStreamFile
{
public:
    CStreamFile(TCHAR *fileName);
    ~CStreamFile();
    // Open API
    int Parse_h264_bitstream();

private:
    FILE *m_InputFile;
    TCHAR *m_fileName;
    std::vector<uint8> m_nalVec;
    
    // 用來打印日誌
void file_info(); void file_error(int dex); // 提取NAL有效數據 int find_nal_prefix(); }; #endif

在Stream.cpp文件中,實現其構造方法及成員函數:

#include "stdafx.h"
#include "Stream.h"
#include <iostream>
using namespace std;

// 構造函數完成打開文件操作
CStreamFile::CStreamFile(TCHAR * fileName)
{
    m_fileName = fileName;
    file_info();
    // 打開視頻文件(只讀二進制)
_tfopen_s(&m_InputFile, m_fileName, _T("rb")); if (NULL == m_InputFile) { file_error(0); } } // 析構函數完成關閉文件操作 CStreamFile::~CStreamFile() { if (NULL != m_InputFile) { fclose(m_InputFile); m_InputFile = NULL; } } int CStreamFile::Parse_h264_bitstream() { return 0; } int CStreamFile::find_nal_prefix() { return 0; } // 打印文件信息 void CStreamFile::file_info() { if (m_fileName) { wcout << L"File name: " << m_fileName << endl; } } // 打印錯誤信息 void CStreamFile::file_error(int idx) { switch (idx) { case 0: wcout << L"Error: opening input file failed." << endl; break; default: break; } }

之後在主函數中,編寫打開文件代碼,測試以上代碼能否正常執行:

#include "stdafx.h"
#include "Stream.h"

int _tmain(int argc, _TCHAR* argv[])
{
    CStreamFile h264stream(argv[1]);

    // 此函數作為最上層函數,執行所有功能(暫時還未寫任何功能實現)
    h264stream.Parse_h264_bitstream();
    return 0;
}

編譯執行後,在cmd窗口中,能夠打印出文件名稱,即為正確執行。

接下來,設置一個全局的頭文件,用來定義所有文件中都會用到的數據類型。
Application目錄下,新建Global.h頭文件,輸入以下代碼:

#ifndef _GLOBAL_H_
#define _GLOBAL_H_

typedef unsigned char  uint8;
typedef unsigned int   uint32;

#endif // !_GLOBAL_H_

stdafx.h文件中,引入剛才新建的頭文件:

#include "Global.h"

二、提取NAL Unit:

1. 提取NAL有效數據:

實現find_nal_prefix()函數。實現方法與學習筆記4中代碼基本相同,僅修改一些變量名稱。(學習筆記4中有詳細講解,這裏不再說明)。Stream.cpp文件中,函數實現如下:

int CStreamFile::find_nal_prefix()
{
    uint8 prefix[3] = { 0 };
    uint8 fileByte;


    m_nalVec.clear();

    // 標記當前文件指針位置
    int pos = 0;
    // 標記查找的狀態
    int getPrefix = 0;
    // 讀取三個字節
    for (int idx = 0; idx < 3; idx++)
    {
        prefix[idx] = getc(m_InputFile);
        // 每次讀進來的字節 都放入vector中
        m_nalVec.push_back(prefix[idx]);
    }

    while (!feof(m_InputFile))
    {
        if ((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 1))
        {
            // 0x 00 00 01 found
            getPrefix = 1;
            m_nalVec.pop_back();
            m_nalVec.pop_back();
            m_nalVec.pop_back();
            break;
        }
        else if ((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 0))
        {
            if (1 == getc(m_InputFile))
            {
                // 0x 00 00 00 01 found
                getPrefix = 2;
                m_nalVec.pop_back();
                m_nalVec.pop_back();
                m_nalVec.pop_back();
                break;
            }
        }
        else
        {
            fileByte = getc(m_InputFile);
            prefix[(pos++) % 3] = fileByte;
            m_nalVec.push_back(fileByte);
        }
    }

    return getPrefix;
}

修改Stream.cpp中Parse_h264_bitstream()函數,循環調用find_nal_prefix()函數,不斷獲取起始碼之間數據。

int CStreamFile::Parse_h264_bitstream()
{
    int ret = 0;
    do
    {
        ret = find_nal_prefix();
    } while (ret);

    return 0;
}

對此文件編譯、調試,查看以上所寫代碼是否有問題:
第一次循環時,文件指針移動到第一個起始碼後;第二次循環時,讀取到兩個起始碼間的有效數據,通過調試可看到如下數據,與test.264中第一組有效數據相同:
技術分享圖片

2. 提取NAL Unit 類別:

① 首先提取每一個NAL Unit的類別,修改Parse_h264_bitstream()函數如下:

int CStreamFile::Parse_h264_bitstream()
{
    int ret = 0;
    do
    {
        ret = find_nal_prefix();
        // 解析NAL UNIT
        // 第一次執行循環的時候,m_nalVec為空,因此加個判斷
        if (m_nalVec.size())
        {
            // 識別NAL Unit類別
            // NAL Unit第一個字節為NAL Header,後面5位表示NAL Type(使用按位與運算,截取後面五位數據)
            uint8 nalType = m_nalVec[0] & 0x1F;
            wcout << L"NAL Unit Type: " << nalType << endl;
        }
    } while (ret);
    return 0;
}

編譯運行後,結果如下:
技術分享圖片
其所對應的類型為(可從H.264官方文檔,表7-1中查到):
技術分享圖片

三、NAL Unit 解封裝:

1. EBSP -> RBSP:

去除競爭校驗位(詳細概念看學習筆記5)
簡而言之,就是去除兩個連零後面的03。00 00 03 xx xx xx (其中的03即為競爭校驗位,在拆包的時候需要去除)

CStreamFile 類中添加私有函數 void ebsp_to_rbsp();
函數實現如下:

void CStreamFile::ebsp_to_rbsp()
{
    // 00 00 03 連續兩個00後面的03是防止競爭校驗字節,需要去掉
    // 在序列中找03,在查看前面兩個是不是00,如果是,就去掉03
    if (m_nalVec.size() < 3)
    {
        return;
    }

    for (vector<uint8>::iterator itor = m_nalVec.begin() + 2; itor != m_nalVec.end(); )
    {
        // 叠代器增長幅度為空,寫在循環內部,方便刪除元素
        if ((3 == *itor) && (0 == *(itor - 1)) && (0 == *(itor - 2)))
        {
            // 此處使用erase()時需要註意:
            // 1、當調用erase()後Itor叠代器就失效了,變成了一野指針
            // 2、而erase()這個函數會返回一個指針,仍指向清除元素的位置,只不過後面所有的數據都向前移動
            itor = m_nalVec.erase(itor);
        }
        else
        {
            itor++;
        }
    }

}

2. RBSP -> SODB:

這裏本應還有RBSP -> SODB的部分,也就是去除 rbsp_trailing_bits ,但對於分析 NAL Body 內部語法元素不會造成實際影響,這部分暫時空缺,有興趣的可以自己實現一下。







【對於NAL Body 編碼方式的解析,會涉及熵編碼知識,將在後續筆記中進行介紹。】

【視頻編解碼·學習筆記】6. H.264碼流分析工程創建