FFMPEG之H264获取NALU并且解析其RBSP类型03
前言
FFMPEG之H264理论篇。
理论的就不多讲了,可以参考上面那篇文章,下面将给出两种版本,一种是我自己的,用C++方法实现,另一种是雷神版本的,基本是纯C语言。区别是我多了一个将EBSP转成RBSP的函数,而雷神只是简单的将码流数据转成NALU后,将头部信息的优先级、NALU的类型和统计每一个NALU的字节大小LEN。并且都是经过测试没有bug(针对于我),如果有的话大家可以指出共同研究。 我的版本注释很清楚了,而雷神版本的有空再把注释打一下换上来。
1 C++版本实现获取NALU及解析内部数据
本来两个都想封装成C++类的,但是为了方便大家理解,就直接写了。
#pragma warning(disable:4996)
#include <iostream>
#include <cassert>
#include <vector>
using namespace std;//以后多文件操作尽量少用,容易改变命名空间规则出错
typedef unsigned char uint_8;
/*//
下面实现了H264->NALU->RBSP(原始字节序列载荷)
最好自己有空封装成一个类
/*//
//从H264获取NALU单元
//0-读完 1,2-没读完
int Find_NALU(FILE *fp, vector<uint_8> &nalu) {
//1 每次读新的NALU都清除上一次的NALU数据
nalu.clear();
//用于检测起始码
uint_8 buf[3] = { 0 };
/*即:
四字节起始码时:
[0][1][2]={0,0,0}->[1][2][0]={0,0,0}->[2][0][1]={0,0,0}
fgetc()==1
三字节时:
[0][1][2]={0,0,0}->[1][2][0]={0,0,0}->[2][0][1]={0,0,0}
*/
//2 先读三个字节判断
for (int i = 0; i < 3; i++) {
buf[i] = fgetc(fp);
nalu.push_back(buf[i]);//必须放进去,防止buf不是0 0 0而丢失NALU数据
}
uint_8 byte;//用于存储每次读NALU数据的值
int getPrefix = 0;//用于记录返回值.0-NALU读完;1,2-有NALU数据,实际区别不大
int pos = 0;//当前文件指针位置,用于循环判断起始码
//3 循环判断并取数据
while (!feof(fp)) {
//上面看不懂可看这里
//0,1,2下标不为0->pos++后->pos=1,取余3后下标变成1,2,0->也不为0->pos=2->2,0,1...以此类推
if (buf[pos % 3] == 0 && buf[(pos + 1) % 3] == 0 && buf[(pos + 2) % 3] == 1) {
//found 00 00 01
nalu.pop_back();//起始码不属于NALU数据
nalu.pop_back();
nalu.pop_back();
getPrefix = 1;
break;
}
else if (buf[pos % 3] == 0 && buf[(pos + 1) % 3] == 0 && buf[(pos + 2) % 3] == 0) {
uint_8 tmp = fgetc(fp);
if (tmp == 1) {
//found 00 00 00 01
nalu.pop_back();//起始码不属于NALU数据
nalu.pop_back();
nalu.pop_back();
getPrefix = 2;
break;
}
else {
nalu.push_back(tmp); //这一字节也是NALU数据 别忘了!!!
}
}
//否则说明不是起始码,继续读数据放对应取余下标(即每次取余后都是放在一个下标,不懂看上面的),并且读NALU数据进vector
else {
byte = fgetc(fp);
//buf[(pos++) % 3] = byte; 一样
buf[pos % 3] = byte;
pos++;
nalu.push_back(byte); //因为这里一开始什么数据都读进,所以上面判断是起始码必须pop掉最后的
}
}
return getPrefix;
}
void SwicthType(uint_8 type) {
switch (type)
{
case 0:
printf("未使用 Type.\n");
break;
case 1:
printf("非IDR片 Type.\n");
break;
case 2:
printf("片分区A Type.\n");
break;
case 3:
printf("片分区B Type.\n");
break;
case 4:
printf("片分区C Type.\n");
break;
case 5:
printf("IDR Type.\n"); //I帧
break;
case 6:
printf("SEI Type.\n"); //补充增强信息单元
break;
case 7:
printf("SPS Type.\n");//序列参数集
break;
case 8:
printf("PPS Type.\n");//图像参数集
break;
case 9:
printf("分界符 Type.\n");
break;
case 10:
printf("序列结束 Type.\n");
break;
case 11:
printf("码流结束 Type.\n");
break;
case 12:
printf("填充 Type.\n");
break;
default:
printf("Unkonw.\n");
break;
}
}
//取出EBSP中的0x3,成为RBSP.即拓客操作
void Ebsp_To_Rbsp(vector<uint_8> &nalu) {
//因为0 0 3必须最少三个数
if (nalu.size() < 3) {
return;
}
for (vector<uint_8>::iterator it = nalu.begin() + 2; it != nalu.end();) {
if (*it == 3 && *(it - 1) == 0 && *(it - 2) == 0) {
it = nalu.erase(it);
}
else {
it++;
}
}
}
//利用上面的函数进行获取NALU的类型数据并进行解析为RBSP
int Parser_H264_Nalu(FILE *fp,vector<uint_8> &nalu) {
//返回0表示解析完h264文件的NALU
int ret = 0;
//用于测试与雷神版本是否一致无错,统计每次的nalu总个数
int cnt = 0;
uint_8 typeNalu;
do
{
ret = Find_NALU(fp, nalu);
//首个NALU不做处理
if (nalu.size() != 0) {
//typeNalu = nalu.front() & 0x1f;
typeNalu = nalu[0] & 0x1f;//同上
SwicthType(typeNalu);
Ebsp_To_Rbsp(nalu);
cnt++;
}
} while (ret != 0);
cout << cnt << endl;
return 0;
}
void test01() {
char fileName[100] = "sintel.h264";
FILE *fp = fopen(fileName, "rb+");
assert(fp);
//用于存储NALU.注:读首个NALU时,vector大小为0,因为起始码一开始就满足,被读进三个又pop掉三个,只有第二个开始采用数据
vector<uint_8> nalu;
Find_NALU(fp, nalu);
Find_NALU(fp, nalu);
Find_NALU(fp, nalu);
Find_NALU(fp, nalu);
cout << "NALU元素个数为:" << nalu.size() << endl;
for (vector<uint_8>::iterator it = nalu.begin(); it != nalu.end(); it++) {
printf("%x ", *it);
}
fclose(fp);
}
void test02() {
char fileName[100] = "sintel.h264";
FILE *fp = fopen(fileName, "rb+");
assert(fp);
//解析NALU类型
vector<uint_8> nalu;
Parser_H264_Nalu(fp, nalu);
fclose(fp);
}
int main() {
//test01()
test02();
return 0;
}
2 雷神版本实现获取NALU及打印头部相关信息
当然,你学习了之后你可以改成你想要的方式解析内部数据。前面的也一样。
#pragma warning(disable:4996)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*//
雷霄骅版本
下面实现了H264->NALU->RBSP(原始字节序列载荷)
最好自己有空封装成一个类
/*//
//NALU的类型,由NALU头部的后5位得出
typedef enum {
NALU_TYPE_SLICE = 1,
NALU_TYPE_DPA = 2,
NALU_TYPE_DPB = 3,
NALU_TYPE_DPC = 4,
NALU_TYPE_IDR = 5,
NALU_TYPE_SEI = 6,
NALU_TYPE_SPS = 7,
NALU_TYPE_PPS = 8,
NALU_TYPE_AUD = 9,
NALU_TYPE_EOSEQ = 10,
NALU_TYPE_EOSTREAM = 11,
NALU_TYPE_FILL = 12,
} NaluType;
//IDC,NALU的优先级,NALU头部的第2、3位得出
typedef enum {
NALU_PRIORITY_DISPOSABLE = 0,
NALU_PRIRITY_LOW = 1,
NALU_PRIORITY_HIGH = 2,
NALU_PRIORITY_HIGHEST = 3
} NaluPriority;
//雷神自定义的结构体,用于保存开始码长度、NALU数据的长度(包括首字节)、临时缓冲区及其大小、头部信息
typedef struct
{
int startcodeprefix_len; //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)
int forbidden_bit;
int nal_reference_idc;
int nal_unit_type;
unsigned len; //! Length of the NAL unit (Excluding the start code, which does not belong to the NALU)
unsigned max_size; //! Nal Unit Buffer size
char *buf; //! contains the first byte followed by the EBSP
} NALU_t;
//H264码流文件
FILE *h264bitstream = NULL;
//用于判断是否是起始码
int info2 = 0, info3 = 0;
//0不是起始码;1是起始码
static int FindStartCode2(unsigned char *Buf) {
if (Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 1) return 0; //0x000001?
else return 1;
}
//0不是起始码;1是起始码
static int FindStartCode3(unsigned char *Buf) {
if (Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 0 || Buf[3] != 1) return 0;//0x00000001?
else return 1;
}
//获取码流中的NALU数据
//返回-1代表没找到起始码;0代表读文件出错或者calloc出错;其它返回带起始码的nalu长度,但是参数nalu->len是记录着不带起始码的长度
int GetAnnexbNALU(NALU_t *nalu) {
//准备环节
int pos = 0;//NALU的下标
int StartCodeFound;//用于标记是否找到下一NALU的起始码
int rewind;//下一起始码的位数,pos+rewind为下一起始码的首字节
unsigned char *Buf;//临时缓存,用于每次存储一个NALU
if ((Buf = (unsigned char*)calloc(nalu->max_size, sizeof(char))) == NULL) {
printf("GetAnnexbNALU: Could not allocate Buf memory\n");
return 0;
}
//假设起始码为三个字节长度
nalu->startcodeprefix_len = 3;
//1 先从码流中读取三个字节
if (3 != fread(Buf, 1, 3, h264bitstream)) {
free(Buf);
return 0;
}
//2 判断是否满足00 00 01
info2 = FindStartCode2(Buf);
//3 如果不满足的话,再读一个进buf判断是否为00 00 00 01
if (info2 != 1) {
if (1 != fread(Buf + 3, 1, 1, h264bitstream)) {
free(Buf);
return 0;
}
info3 = FindStartCode3(Buf);
//4 也不是00 00 00 01的话,则退出本次查找;否则记录NALU开始的下标与对应起始码长度
if (info3 != 1) {
free(Buf);
return -1;
}
else {
pos = 4;
nalu->startcodeprefix_len = 4;
}
}
//5 满足三字节的起始码,记录NALU开始的下标与对应起始码长度
else {
nalu->startcodeprefix_len = 3;
pos = 3;
}
//来到这里说明找到了首个起始码,开始循环读NALU数据与找起始码(pos指向NALU的第一个字节数据)
//6 先重置这些是否找到起始码的标志位
StartCodeFound = 0;
info2 = 0;
info3 = 0;
while (!StartCodeFound) {
//15 由于最后一个NALU没有下一个起始码,所以当读到末尾时,直接将pos-1后减去起始码就是数据的长度
//非0表示文件尾,0不是
if (feof(h264bitstream) != 0) {
nalu->len = (pos - 1) - nalu->startcodeprefix_len;//最后一个NALU数据的长度(减1是因为最后一个nalu时,会一个个读到buf,pos会自增,直到读到eof)
memcpy(nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);//该NALU数据拷贝至该结构体成员buf中保存
nalu->forbidden_bit = nalu->buf[0] & 0x80; //用该NALU的头部数据给赋给雷神自定义的NALU结构体中
nalu->nal_reference_idc = nalu->buf[0] & 0x60;
nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;
free(Buf); //每次获取完一个NALU都清空该NALU的数据(下面的获取也一样)
return pos - 1; //返回文件最后一个字节的下标,在最后一帧pos代表eof(并不是end,pos下标才对应eof)
//return nalu->len; // 这样返回才是不带起始码长度,上面pos-1是带的
}
//7 往Buf一字节一字节的读数据(注:pos之前有起始码,所以pos开始存,然后pos自增1)
Buf[pos++] = fgetc(h264bitstream);
//8 判断是否为四位起始码.例如0 0 0 1 2(实际上pos=5),此时从0 0 1 2开始判断,所以取Buf[pos - 4]元素开始判断
info3 = FindStartCode3(&Buf[pos - 4]);
if (info3 != 1) {
//9 不是则判断是否是三位起始码.当前下标减去3即可(本来减2,但上面pos++了)
info2 = FindStartCode2(&Buf[pos - 3]);
}
//10 若找到下一个起始码则退出(证明知道了一个NALU数据的长度嘛)
StartCodeFound = (info2 == 1 || info3 == 1);
}
//11 判断下一个起始码是3还是4(他这里用info3,代表4位,所以一会需要将文件指针回调rewind个字节,为了下一次判断)
//当然你也可以用info2
rewind = (info3 == 1) ? -4 : -3;
//12 回调文件指针
if (0 != fseek(h264bitstream, rewind, SEEK_CUR)) {
free(Buf);
printf("GetAnnexbNALU: Cannot fseek in the bit stream file");
}
//13 开始获取NALU的数据
nalu->len = (pos + rewind) - nalu->startcodeprefix_len;//注:rewind为负数,加相当于减,然后再减去上一起始码就是NALU的数据长度
memcpy(nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);//从起始码开始拷贝len长度的NALU数据至自定义结构体的buf中
nalu->forbidden_bit = nalu->buf[0] & 0x80; //用该NALU的头部数据给赋给雷神自定义的NALU结构体中
nalu->nal_reference_idc = nalu->buf[0] & 0x60;
nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;
free(Buf);
//14 返回当前文件指针位置,即下一起始码的首字节(rewind为负数) 至此,一个NALU的数据获取完毕
return (pos + rewind);
}
/*
解析码流h264
成功返回0;失败返回-1
*/
int simplest_h264_parser(char *url) {
//FILE *myout=fopen("output_log.txt","wb+");
FILE *myout = stdout;//用于输出屏幕的文件指针,你可以认为该文件是屏幕
//1 打开文件
h264bitstream = fopen(url, "rb+");
if (h264bitstream == NULL) {
printf("Open file error\n");
return -1;
}
//2 开辟nalu结构体以及用于存储nalu数据的成员nalu->buf
NALU_t *nalu;//雷神自定义的NALU数据,额外包含头部与长度信息
nalu = (NALU_t*)calloc(1, sizeof(NALU_t));
if (nalu == NULL) {
printf("Alloc NALU Error\n");
return -1;
}
int buffersize = 100000;//临时缓存,足够大于一个NALU的字节数即可
nalu->max_size = buffersize;
nalu->buf = (char*)calloc(buffersize, sizeof(char));
if (nalu->buf == NULL) {
free(nalu);
printf("AllocNALU: n->buf");
return -1;
}
//累加每一次的偏移量,用于记录每个NALU的起始地址,雷神的这个偏移量是包括对应的起始码(3或者4字节),即显示的POS字段
int data_offset = 0;
int nal_num = 0;//NALU的数量,从0开始算
int data_lenth;//接收返回值,文件指针的位置,也就是下一起始码首字节,或者说下一NALU的偏移地址
printf("-----+-------- NALU Table ------+---------+\n");
printf(" NUM | POS | IDC | TYPE | LEN |\n");
printf("-----+---------+--------+-------+---------+\n");
//3 循环读取码流获取NALU
while (!feof(h264bitstream))
{
data_lenth = GetAnnexbNALU(nalu);
//4 获取NALU的类型
char type_str[20] = { 0 };
switch (nalu->nal_unit_type) {
case NALU_TYPE_SLICE:sprintf(type_str, "SLICE"); break;
case NALU_TYPE_DPA:sprintf(type_str, "DPA"); break;
case NALU_TYPE_DPB:sprintf(type_str, "DPB"); break;
case NALU_TYPE_DPC:sprintf(type_str, "DPC"); break;
case NALU_TYPE_IDR:sprintf(type_str, "IDR"); break;
case NALU_TYPE_SEI:sprintf(type_str, "SEI"); break;
case NALU_TYPE_SPS:sprintf(type_str, "SPS"); break;
case NALU_TYPE_PPS:sprintf(type_str, "PPS"); break;
case NALU_TYPE_AUD:sprintf(type_str, "AUD"); break;
case NALU_TYPE_EOSEQ:sprintf(type_str, "EOSEQ"); break;
case NALU_TYPE_EOSTREAM:sprintf(type_str, "EOSTREAM"); break;
case NALU_TYPE_FILL:sprintf(type_str, "FILL"); break;
}
//5 获取NALU的IDC即优先级
char idc_str[20] = { 0 };
switch (nalu->nal_reference_idc >> 5) {
case NALU_PRIORITY_DISPOSABLE:sprintf(idc_str, "DISPOS"); break;
case NALU_PRIRITY_LOW:sprintf(idc_str, "LOW"); break;
case NALU_PRIORITY_HIGH:sprintf(idc_str, "HIGH"); break;
case NALU_PRIORITY_HIGHEST:sprintf(idc_str, "HIGHEST"); break;
}
//6 输出nal个数 此时数据的偏移量,优先级,NALU类型,NALU数据的长度
fprintf(myout, "%5d| %8d| %7s| %6s| %8d|\n", nal_num, data_offset, idc_str, type_str, nalu->len);
//7 记录下一NALU的偏移地址,即计算后,该偏移地址就是下一NALU的偏移地址.例如显示0后,0+29就是下一NALU的偏移地址
data_offset = data_offset + data_lenth;
nal_num++;
}
//8 Free掉nalu与nalu->buf
if (nalu != NULL) {
if (nalu->buf != NULL) {
free(nalu->buf);
nalu->buf = NULL;
}
free(nalu);
nalu = NULL;
}
return 0;
}
int main() {
char fileName[100] = "sintel.h264";
simplest_h264_parser(fileName);
return 0;
}
结果分析:
我的:
雷神的:
结果可以看出,雷神的是由下标0开始算第一个SPS的NALU,而我是从1开始算第一个NALU的,代码可以从nalu.size()!=0中看出,即第一次的大小为0,导致第一个NALU数据对应的cnt=1。所以都是667个NALU数据,证明正确
上面雷神唯一我没看懂的一点就是:文件最后一个NALU没找到起始码时,pos为什么要减1操作,pos不是指向末尾了吗(end即eof),直接减去起始码长度不就可以了吗?有空再测测。。。