FFMPEG之H264获取NALU并且解析其RBSP类型03

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;
	}
}

//取出EDSP中的0x3,成为RBSP.即拓客操作
void Edsp_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的类型数据并进行解析为RTSP
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);
			Edsp_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的偏移地址
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的操作不是特别明白,有时间再去测测)
			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;		//返回文件最后一个字节的下标(并不是end,pos下标才对应eof)
		}
		//7 往Buf一字节一字节的读数据(注:pos之前有起始码,所以pos++开始存)
		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),直接减去起始码长度不就可以了吗?有空再测测。。。

猜你喜欢

转载自blog.csdn.net/weixin_44517656/article/details/108304159