第一章 前言
第三章 有关 SPS 和 PPS 的一切
第四章 有关 Slice 的一切
第七章 帧间编码
第八章 残差的熵编码: CAVLC 和 CABAC
之前的章节里,我们详细介绍了 AnnexB 的数据格式详情 (H264/AVC AnnexB)。这一章我们就来读取一个 AnnexB 的文件。
这里可以使用 FFmpeg 提供的命令行工具
ffmpeg -i demo.mp4 demo.h264
我们用一个二进制文件查看工具打开生成的文件,就可以看到文件详情。我们可以找一下文件里存在的起始码 0 0 0 1 或者 0 0 1,我们的程序,就是要按照起始码来分割这个文件里存在的 NALU。
首先,我们设计一个类,用来存放读取出来的 NALU。在这个类中,我们设计了三个成员变量,分别是 uint8_t * buf,int len 和 int startCodeLen。
其中,buf 和 len 用来存放提取出来的 NALU 的数据,startCodeLen 用来存放这个 NALU 中 Start Code 的长度。
除此之外,我们还设计了一个成员函数 int SetBuf(uint8_t * _buf, int _len);,这个函数中,通过这个函数把 NALU 的数据拷贝到这个对象中。
class Nalu {
public:
Nalu();
~Nalu();
int SetBuf(uint8_t * _buf, int _len);
public:
uint8_t * buf = nullptr;
int len = 0;
int startCodeLen = 0;
};
接下来,我们设计一个读取工具类。在这个类中,我们可以通过构造函数设置 AnnexB 的文件路径,然后循环调用 ReadNalu 函数,不断地在读出 NALU,直到 ReadNalu 函数返回读取结束。
class AnnexBReader
{
public:
AnnexBReader(std::string & filePath);
~AnnexBReader();
// 用来打开文件
int Open();
// 用来关闭文件
int Close();
// 用来读取一个 Nalu
int ReadNalu(Nalu & nalu);
};
我们先来看一下 Open 和 Close 函数。这两个函数用于打开和关闭构造函数传入的路径,实现很简单,我们直接看代码。
// 用来打开文件
int AnnexBReader::Open()
{
f = fopen(filePath.c_str(), "rb");
if(f == nullptr){
return -1;
}
return 0;
}
// 用来关闭文件
int AnnexBReader::Close()
{
if(f != nullptr){
fclose(f);
f = nullptr;
}
return 0;
}
在完成了打开和关闭之后,我们就要开始读取 NALU 了。整个读取 NALU 的过程是这样的:
要完成以上的操作,首先我们要写一个私有函数,这个函数用来检查一个 buffer 的开头是不是 start code。在这个函数中,你可以传入一个指针,和这个指针指向数据的长度,这个函数会帮你检查这个 buffer 的开头是不是 start code。如果是 start code,那么返回 true,并且会将 startCodeLen 赋值成 start code 的长度 (3 或者 4)
bool EyerAnnexB::CheckStartCode(int & startCodeLen, uint8_t * bufPtr, int bufLen)
{
if(bufLen <= 2){
startCodeLen = 0;
return false;
}
if(bufLen >= 4){
if(bufPtr[0] == 0) {
if (bufPtr[1] == 0) {
if (bufPtr[2] == 0) {
if (bufPtr[3] == 1) {
startCodeLen = 4;
return true;
}
}
if(bufPtr[2] == 1){
startCodeLen = 3;
return true;
}
}
}
}
if(bufLen <= 3){
if(bufPtr[0] == 0) {
if (bufPtr[1] == 0) {
if(bufPtr[2] == 1){
startCodeLen = 3;
return true;
}
}
}
}
startCodeLen = 0;
return false;
}
接下来,我们实现 ReadNalu 函数
int AnnexBReader::ReadNalu(Nalu & nalu)
{
while(1){
if(bufferLen <= 0){
int readedLen = ReadFromFile();
if(readedLen <= 0){
isEnd = true;
}
}
uint8_t * buf = buffer;
int startCodeLen = 0;
// Find Start Code
bool isStartCode = CheckStartCode(startCodeLen, buf, bufferLen);
if(!isStartCode){
break;
}
nalu.startCodeLen = startCodeLen;
// Find End Code
int endPos = -1;
for(int i=2;i<bufferLen;i++){
int startCodeLen = 0;
bool isStartCode = CheckStartCode(startCodeLen, buf + i, bufferLen - i);
if(isStartCode){
endPos = i;
break;
}
}
if(endPos > 0){
nalu.SetBuf(buffer, endPos);
uint8_t * _buffer = (uint8_t*)malloc(bufferLen - endPos);
memcpy(_buffer, buffer + endPos, bufferLen - endPos);
if(buffer != nullptr){
free(buffer);
buffer = nullptr;
}
buffer = _buffer;
bufferLen = bufferLen - endPos;
return 0;
}
else{
if(isEnd == true){
// 到达文件末尾,取所有 buffer 出来
nalu.SetBuf(buffer, bufferLen);
if(buffer != nullptr){
free(buffer);
buffer = nullptr;
}
buffer = nullptr;
bufferLen = 0;
return 0;
}
int readedLen = ReadFromFile();
if(readedLen <= 0){
isEnd = true;
}
}
}
return -1;
}
调用代码
#include "AnnexBReader.hpp"
int main(int argc, char const *argv[])
{
std::string filePath = "./demo_video_176x144_baseline.h264";
AnnexBReader reader(filePath);
int ret = reader.Open();
if(ret){
printf("Read Fail");
return -1;
}
while(1){
uint8_t buffer[1024 * 1024];
int bufferLen = 0;
int startcodeLen = 0;
ret = reader.ReadNalu(buffer, &bufferLen, &startcodeLen);
if(ret){
break;
}
printf("=====================\n");
printf("Buffer Len: %d\n", bufferLen);
printf("Start Code Len: %d\n", startcodeLen);
printf("%d %d %d %d %d\n", buffer[0], buffer[1], buffer[2], buffer[3], buffer[4]);
}
reader.Close();
return 0;
}
https://github.com/redknotmiaoyuqiao/EyerH264Decoder/tree/main/Lesson_2_3_ReadAnnexB