C/C++實(shí)現(xiàn)H264文件解析
H264視頻編碼格式簡介
H264是視頻壓縮編碼標(biāo)準(zhǔn)。視頻?件的傳輸是?個(gè)極?的問題:?段分辨率為19201080的視頻,每個(gè)像素點(diǎn)為RGB占?3個(gè)字節(jié),幀率是25的視頻,對(duì)于傳輸帶寬的要求是:192010803*25/1024/1024=148.315MB/s,換成bps則意味著視頻每秒帶寬為1186.523Mbps,這樣的速率對(duì)于?絡(luò)存儲(chǔ)是不可接受的。因此視頻壓縮和編碼技術(shù)應(yīng)運(yùn)??。
H264碼流分析
通常我們使用H264碼流分析工具時(shí),我們會(huì)發(fā)現(xiàn)H264碼流是由連續(xù)的幀組成的,一組連續(xù)的幀成為1個(gè)GOP即1組,對(duì)于1個(gè)GOP來說,通常包括SPS(序列參數(shù)集,編碼視頻序列的全局參數(shù))PPS(圖像參數(shù)集,?個(gè)序列中某?幅圖像或者某?幅圖像的參數(shù)),IDR幀(第一個(gè)出現(xiàn)的I幀成為IDR幀,1個(gè)GOP中通常只包含1個(gè)IDR幀),P幀(GOP中剩余幀基本都是P幀);GOP描述如下:

H264碼流結(jié)構(gòu)如圖:

C/C++ H264文件解析
C++實(shí)現(xiàn)H264文件以及一段H264碼流解析,源碼如下:
h264Parse.h:
#ifndef _H264PARSE_H_
#define _H264PARSE_H_
#include <fstream>
class H264Parse
{
public:
int open_file(const std::string &filename);
/**
* @brief 從文件中讀取一個(gè)nalu,包含起始碼
* @param buf 存放nalu的緩沖區(qū)
* @param size 緩沖區(qū)大小
* @param len nalu的長度
* @param n 每次讀取多少個(gè)字節(jié)
* @return -1 失敗 0 已到文件末尾 1 成功獲取到一個(gè)nalu
*/
int read_nalu(uint8_t *buf, uint32_t size, uint32_t &len, uint32_t n);
void close_file();
// 獲取起始碼長度
static int get_startCode_len(const uint8_t *ptr);
static const uint8_t *find_startCode_pos(const uint8_t *buf, uint32_t len);
/**
* @brief 從一段h264碼流中分割nalu,包含起始碼
* @param stream h264碼流
* @param streamLen 碼流大小
* @param nalu Pointer to the extracted nalu
* @param naluLen nalu的長度
* @param record Pointer用于記錄狀態(tài),第一次分割時(shí)把 *record 賦值為NULL
* @return -1 失敗 0 已分割完 1 成功獲取到一個(gè)nalu
*/
static int nalu_tok(const uint8_t *stream, uint32_t streamLen, const uint8_t **nalu,
uint32_t &naluLen, const uint8_t **record);
private:
std::fstream h264File;
int read_start_code(uint8_t *buf);
int adjust_filePointer_pos(uint32_t totalRead, uint32_t naluLen);
};
#endif // _H264PARSE_H_h264Parse.cpp:
#include "h264Parse.h"
#include <iostream>
#include <cstring>
int H264Parse::open_file(const std::string &filename)
{
h264File.open(filename, std::ios::in | std::ios::binary);
if (!h264File.is_open())
{
std::cout << "Failed to open the H.264 file." << std::endl;
return -1;
}
return 0;
}
int H264Parse::get_startCode_len(const uint8_t *ptr)
{
if (ptr[0] == 0x00 && ptr[1] == 0x00)
{
if (ptr[2] == 0x01)
return 3;
else if (ptr[2] == 0x00 && ptr[3] == 0x01)
return 4;
}
return -1; // 無效的起始碼
}
// 讀取起始碼,并返回其長度
int H264Parse::read_start_code(uint8_t *buf)
{
// 讀取前4個(gè)字節(jié)來判斷起始碼長度
h264File.read(reinterpret_cast<char *>(buf), 4);
if (h264File.gcount() < 4)
{
return -1;
}
return get_startCode_len(buf);
}
// 尋找NALU的起始碼位置
const uint8_t *H264Parse::find_startCode_pos(const uint8_t *buf, uint32_t len)
{
const uint8_t *p = buf;
if (len < 3)
return NULL;
for (uint32_t i = 0; i < len - 3; ++i)
{
if ((p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01) ||
(p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x00 && p[3] == 0x01))
{
return p;
}
p++;
}
// 檢查最后3字節(jié)是不是起始碼
if (p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01)
return p;
return NULL;
}
// 調(diào)整文件指針位置
int H264Parse::adjust_filePointer_pos(uint32_t totalRead, uint32_t naluLen)
{
int offset = -(totalRead - naluLen);
if (!h264File.eof())
{
h264File.seekg(offset, std::ios::cur);
}
else
{
h264File.clear(); // 達(dá)到文件末尾了要先清除 eof 標(biāo)志
h264File.seekg(offset, std::ios::end);
}
if (h264File.fail())
{
std::cout << "seekg failed!" << std::endl;
return -1;
}
return 0;
}
int H264Parse::read_nalu(uint8_t *buf, uint32_t size, uint32_t &len, uint32_t n)
{
uint32_t totalRead = 0;
int startCodeLength = read_start_code(buf);
if (startCodeLength == -1)
{
printf("read_start_code failed.\n");
return -1;
}
totalRead += 4; // 已經(jīng)讀取了4字節(jié)的長度
while (true)
{
if (size < totalRead + n)
{
std::cout << "Buffer size is too small: size=" << size
<< ", needed=" << totalRead + n << std::endl;
return -1;
}
h264File.read(reinterpret_cast<char *>(buf + totalRead), n);
std::streamsize bytesRead = h264File.gcount();
if (bytesRead <= 0)
{
std::cout << "Failed to read from file!" << std::endl;
return -1;
}
uint32_t searchStart = (totalRead > 4) ? totalRead - 3 : startCodeLength;
const uint8_t *naluEnd = find_startCode_pos(buf + searchStart,
bytesRead + (totalRead > 4 ? 3 : 0));
totalRead += bytesRead;
if (naluEnd != nullptr)
{
len = naluEnd - buf;
if (adjust_filePointer_pos(totalRead, len) < 0)
return -1;
break;
}
// 是否讀取到文件末尾
if (h264File.peek() == std::char_traits<char>::eof())
{
len = totalRead;
return 0; // NALU完整讀取
}
}
memset(buf + len, 0, size - len); // 清空剩余部分
return 1; // 成功讀取
}
void H264Parse::close_file()
{
h264File.close();
}
int H264Parse::nalu_tok(const uint8_t *stream, uint32_t streamLen, const uint8_t **nalu,
uint32_t &naluLen, const uint8_t **record)
{
const uint8_t *current = (record && *record) ? *record : stream;
uint32_t offset = static_cast<uint32_t>(current - stream);
if (offset >= streamLen)
{
return -1; // 當(dāng)前記錄位置超出緩沖區(qū)
}
int scLen = get_startCode_len(current);
if (scLen == -1 || (current + scLen) > (stream + streamLen))
{
return -1; // 無效的起始碼或起始碼長度超出緩沖區(qū)
}
// 查找下一個(gè)起始碼的位置
const uint8_t *next_start = find_startCode_pos(current + scLen, streamLen - offset - scLen);
if (next_start)
{
*nalu = current;
naluLen = static_cast<uint32_t>(next_start - current);
*record = next_start;
return 1; // 成功獲取到一個(gè) NALU
}
else
{
// 最后一個(gè) NALU
*nalu = current;
naluLen = streamLen - offset;
*record = NULL; // 重置記錄指針
return 0; // 分割完畢
}
}測(cè)試:
#include <iostream>
#include <vector>
#include "h264Parse.h"
void test1()
{
int ret;
int number = 0;
H264Parse h264;
uint8_t buf[1024 * 1024];
uint32_t len = 0;
h264.open_file("/home/tl/work/app/res/output.h264");
while ((ret = h264.read_nalu(buf, sizeof(buf), len, 1024 * 2)) != -1)
{
printf("number: %d nalu len: %u\n", number, len - h264.get_startCode_len(buf));
number++;
if (ret == 0)
break;
}
if (ret == -1)
{
std::cout << "read_nalu failed." << std::endl;
}
h264.close_file();
}
// 輔助函數(shù):打印 NALU 信息
void print_nalu(const uint8_t *nalu, uint32_t len, int index)
{
std::cout << "NALU " << index << ": Length = " << len << " bytes, Data = ";
for (uint32_t i = 0; i < len; ++i)
{
printf("%02X ", nalu[i]);
}
std::cout << std::endl;
}
void test2()
{
// 構(gòu)造一個(gè)模擬的 H.264 碼流緩沖區(qū),包含多個(gè) NALU
// 起始碼格式:0x000001 (3 字節(jié)) 和 0x00000001 (4 字節(jié))
// NALU 內(nèi)容:隨機(jī)填充的字節(jié)數(shù)據(jù)
std::vector<uint8_t> buffer;
// NALU 1: 3 字節(jié)起始碼 + 5 字節(jié)數(shù)據(jù)
std::vector<uint8_t> nalu1 = {0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x21, 0xA0};
buffer.insert(buffer.end(), nalu1.begin(), nalu1.end());
// NALU 2: 4 字節(jié)起始碼 + 6 字節(jié)數(shù)據(jù)
std::vector<uint8_t> nalu2 = {0x00, 0x00, 0x00, 0x01, 0x41, 0x9A, 0x5C, 0xD4, 0x00, 0x11};
buffer.insert(buffer.end(), nalu2.begin(), nalu2.end());
// NALU 3: 3 字節(jié)起始碼 + 4 字節(jié)數(shù)據(jù)
std::vector<uint8_t> nalu3 = {0x00, 0x00, 0x01, 0x06, 0x05, 0xFF, 0xEE};
buffer.insert(buffer.end(), nalu3.begin(), nalu3.end());
// NALU 4: 3 字節(jié)起始碼 + 3 字節(jié)數(shù)據(jù) (測(cè)試末尾)
std::vector<uint8_t> nalu4 = {0x00, 0x00, 0x01, 0x07, 0xAD, 0xBE};
buffer.insert(buffer.end(), nalu4.begin(), nalu4.end());
// 輸出構(gòu)建的緩沖區(qū)(可選)
std::cout << "Constructed H.264 Buffer: ";
for (size_t i = 0; i < buffer.size(); ++i)
{
printf("%02X ", buffer[i]);
}
std::cout << "\n\n";
const uint8_t *pnalu = nullptr;
uint32_t nale_len = 0;
const uint8_t *pRecord = NULL; // 初始時(shí)為 NULL
int ret;
int nalu_index = 1;
// 循環(huán)分割并打印每個(gè) NALU
while ((ret = H264Parse::nalu_tok(buffer.data(), buffer.size(), &pnalu, nale_len, &pRecord)) != -1)
{
print_nalu(pnalu, nale_len, nalu_index);
nalu_index++;
if (ret == 0)
break;
}
if (ret == -1)
{
std::cout << "Error occurred during NALU tokenization." << std::endl;
}
}
// 主函數(shù)
int main()
{
test1();
// test2();
return 0;
}以上就是C/C++實(shí)現(xiàn)H264文件解析的詳細(xì)內(nèi)容,更多關(guān)于C++ H264文件解析的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C/C++表格組件Qt?TableWidget應(yīng)用詳解
本文詳細(xì)講解了C/C++中使用列表框組件Qt?TableWidget的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12
Qt實(shí)現(xiàn)FTP的上傳和下載的實(shí)例代碼
本篇文章主要介紹了Qt實(shí)現(xiàn)FTP的上傳和下載的實(shí)例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07
c++如何在主函數(shù)文件中調(diào)用其他函數(shù)文件
這篇文章主要介紹了c++如何在主函數(shù)文件中調(diào)用其他函數(shù)文件問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
C++定時(shí)器Timer在項(xiàng)目中的使用方法
這篇文章主要給大家介紹了關(guān)于C++定時(shí)器Timer在項(xiàng)目中的基本使用方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用C++具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
C語言編程數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)詳解小白篇
這篇文章主要介紹了數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ),非常適合初學(xué)數(shù)據(jù)結(jié)構(gòu)的小白,有需要的朋友可以借鑒參考下,希望可以有所幫助,祝大家多多進(jìn)步,早日升職加薪2021-09-09
C++基于reactor的服務(wù)器百萬并發(fā)實(shí)現(xiàn)與講解
這篇文章主要介紹了C++基于reactor的服務(wù)器百萬并發(fā)實(shí)現(xiàn)與講解,本文通過實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07
C++一個(gè)函數(shù)如何調(diào)用其他.cpp文件中的函數(shù)
這篇文章主要介紹了C++一個(gè)函數(shù)如何調(diào)用其他.cpp文件中的函數(shù)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02

