【視頻編解碼·學習筆記】6. H.264碼流分析工程創建
一、準備工作:
新建一個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碼流分析工程創建