最简单的 H.264 视频码流解析程序

最简单的 H.264 视频码流解析程序

参考雷霄骅博士的文章:视音频数据处理入门:H.264视频码流解析

本文中的程序是一个H.264码流解析程序。该程序可以从H.264码流中分析得到它的基本单元NALU,并且可以简单解析NALU首部的字段。通过修改该程序可以实现不同的H.264码流处理功能。

原理

在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)和网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。

H.264原始码流(又称为“裸流”)是由一个一个的NALU组成的。他们的结构如下图所示。

在这里插入图片描述

其中每个NALU之间通过startcode(起始码)进行分隔,起始码分成两种:0x000001(3Byte)或者0x00000001(4Byte)。如果NALU对应的Slice为一帧的开始就用0x00000001,否则就用0x000001。

NALU = NALU header(一组对应于视频编码的NALU头部信息,占1 bit) + 一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)。

NAL Header 的组成为:forbidden_zero_bit(1 bit) + nal_ref_idc(2 bit) + nal_unit_type(5 bit)。

  • forbidden_zero_bit:禁止位,初始为0,当网络发现NAL单元有比特错误时可设置该比特为1,以便接收方纠错或丢掉该单元。
  • nal_ref_idc:重要性指示,标志该NAL单元的重要性,值越大,越重要,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU。
  • nal_unit_type:NALU类型取值。
    句法表中的 C 字段表示该句法元素的分类,这是为片区服务。

在这里插入图片描述

RBSP = 原始数据比特流(String Of Data Bits,SODB)+ RBSP trailing bits(填充位)

SODB 的长度不一定是8的倍数,故需要补齐。

拓展概念:扩展字节序列载荷。
在RBSP基础上填加了仿校验字节(0X03)。
它的原因是:在NALU加到Annexb上时,需要填加每组NALU之前的开始码StartCodePrefix,如果该NALU对应的slice为一帧的开始则用4位字节表示,ox00000001,否则用3位字节表示ox000001.为了使NALU主体中不包括与开始码相冲突的,在编码时,每遇到两个字节连续为0,就插入一个字节的0x03。解码时将0x03去掉。也称为脱壳操作。

H.264码流解析的步骤就是首先从码流中搜索0x000001和0x00000001,分离出NALU;然后再分析NALU的各个字段。本文的程序即实现了上述的两个步骤。

源程序

整个程序位于simplest_h264_parser()函数中,如下所示。

/**
* 最简单的 H.264 视频码流解析程序
* Simplest H.264 Parser
*
* 原程序:
* 雷霄骅 Lei Xiaohua
* leixiaohua1020@126.com
* 中国传媒大学/数字电视技术
* Communication University of China / Digital TV Technology
* http://blog.youkuaiyun.com/leixiaohua1020
*
* 修改:
* 刘文晨 Liu Wenchen
* 812288728@qq.com
* 电子科技大学/电子信息
* University of Electronic Science and Technology of China / Electronic and Information Science
* https://blog.youkuaiyun.com/ProgramNovice
*
* 本项目是一个 H.264 码流分析程序,可以分离并解析 NALU。
*
* This project is a H.264 stream analysis program.
* It can parse H.264 bitstream and analysis NALU of stream.
*
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 解决报错:fopen() 函数不安全
#pragma warning(disable:4996)

// NALU 类型取值,对应 NAL Header 的 nal_unit_type,占 5 bit
typedef enum {
	NALU_TYPE_SLICE = 1, // 非 IDR 帧中不采用数据划分的片
	NALU_TYPE_DPA = 2, // 非 IDR 帧中 A 类数据划分片
	NALU_TYPE_DPB = 3, // 非 IDR 帧中 B 类数据划分片
	NALU_TYPE_DPC = 4, // 非 IDR 帧中 C 类数据划分片
	NALU_TYPE_IDR = 5, // IDR(Instantaneous Decoding Refresh,即时解码刷新) 帧,它是一个 GOP 的开头,作用是立刻刷新,使错误不致传播
	NALU_TYPE_SEI = 6, // Supplemental enhancement information:附加增强信息,包含了视频画面定时等信息,一般放在主编码图像数据之前,在某些应用中,它可以被省略
	NALU_TYPE_SPS = 7, // Sequence Parameter Sets,序列参数集,保存了⼀组编码视频序列的全局参数
	NALU_TYPE_PPS = 8, // Picture Parameter Sets,图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数
	NALU_TYPE_AUD = 9, // 分界符
	NALU_TYPE_EOSEQ = 10, // 序列结束
	NALU_TYPE_EOSTREAM = 11, // 码流结束
	NALU_TYPE_FILL = 12, // 填充
} NaluType;

// NALU 优先级,对应 NAL Header 的 nal_ref_idc,占 2 bit。取值越⼤,表示当前 NAL 越重要,需要优先受到保护
typedef enum {
	NALU_PRIORITY_DISPOSABLE = 0,
	NALU_PRIORITY_LOW = 1,
	NALU_PRIORITY_HIGH = 2,
	NALU_PRIORITY_HIGHEST = 3
} NaluPriority;

// NALU 单元
typedef struct
{
	// NAL Header,1 Byte
	int forbidden_bit; // 禁止位,1 bit
	int nal_reference_idc; // 优先级,2 bit
	int nal_unit_type; // 类型,5 bit
	// 原始字节序列负荷(RBSP,Raw Byte Sequence Payload)
	int startcodeprefix_len; // StartCode 的长度,4 for parameter sets and first slice in picture, 3 for everything else (suggested)
	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;

FILE *h264bitstream = NULL; // the bit stream file

int info2 = 0, info3 = 0;

// 找到 3 字节的 StartCode
static int FindStartCode2(unsigned char *Buf)
{
	if (Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 1)
		return 0; // 0x000001
	else
		return 1;
}

// 找到 4 字节的 StartCode
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;
}

int GetAnnexbNALU(NALU_t *nalu)
{
	int pos = 0;
	int StartCodeFound, rewind;
	unsigned char *Buf;

	// 给 Buf 分配 nalu->max_size 的空间
	if ((Buf = (unsigned char*)calloc(nalu->max_size, sizeof(char))) == NULL)
		printf("GetAnnexbNALU: Could not allocate Buf memory.\n");

	nalu->startcodeprefix_len = 3;

	// 若输入的 H.264 流文件没有 3 bits,返回
	if (3 != fread(Buf, 1, 3, h264bitstream))
	{
		free(Buf);
		return 0;
	}
	info2 = FindStartCode2(Buf);
	if (info2 != 1) // StartCode 的长度不是 3 字节
	{
		if (1 != fread(Buf + 3, 1, 1, h264bitstream))
		{
			free(Buf);
			return 0;
		}
		info3 = FindStartCode3(Buf);
		if (info3 != 1) // StartCode 的长度不是 3 或者 4 字节,错误
		{
			free(Buf);
			return -1;
		}
		else // StartCode 的长度为 4 字节
		{
			pos = 4;
			nalu->startcodeprefix_len = 4;
		}
	}
	else // StartCode 的长度为 3 字节
	{
		nalu->startcodeprefix_len = 3;
		pos = 3;
	}

	StartCodeFound = 0;
	info2 = 0;
	info3 = 0;

	while (!StartCodeFound)
	{
		if (feof(h264bitstream))
		{
			nalu->len = (pos - 1) - nalu->startcodeprefix_len;
			memcpy(nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);
			nalu->forbidden_bit = nalu->buf[0] & 0x80; // 1 bit
			nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
			nalu->nal_unit_type = (nalu->buf[0]) & 0x1f; // 5 bit
			free(Buf);
			return pos - 1;
		}
		Buf[pos++] = fgetc(h264bitstream);
		// 读 Buf 的最后 4 字节,看看有没有 StartCode
		info3 = FindStartCode3(&Buf[pos - 4]);
		if (info3 != 1)
		{
			// 再读 Buf 的最后 3 字节,看看有没有 StartCode
			info2 = FindStartCode2(&Buf[pos - 3]);
		}
		StartCodeFound = (info2 == 1 || info3 == 1);
	}

	// Here, we have found another start code, and read length of startcode bytes more than we should have.
	// Hence, go back in the file.
	rewind = (info3 == 1) ? -4 : -3; // 回退的字节数

	if (0 != fseek(h264bitstream, rewind, SEEK_CUR))
	{
		free(Buf);
		printf("GetAnnexbNALU: Can not fseek in the bit stream file.\n");
	}

	// Here the Start code, the complete NALU, and the next start code is in the Buf.  
	// The size of Buf is pos, pos+rewind are the number of bytes excluding the next start code,
	// and (pos+rewind)-startcodeprefix_len is the size of the NALU excluding the start code.

	nalu->len = (pos + rewind) - nalu->startcodeprefix_len;
	// nalu->buf 只存储 NALU 单元,不含 StartCode
	memcpy(nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);
	// NAL Header
	nalu->forbidden_bit = nalu->buf[0] & 0x80; // 1 bit
	nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
	nalu->nal_unit_type = (nalu->buf[0]) & 0x1f; // 5 bit

	free(Buf);

	return (pos + rewind); // 返回 StartCode + 一个 NALU 的长度
}

/**
* Analysis H.264 Bitstream
* @param url Location of input H.264 bitstream file.
*/
int simplest_h264_parser(const char *url)
{

	NALU_t *n;
	int buffersize = 100000;

	FILE *myout = stdout;
	// FILE *myout = fopen("output_log.txt", "wb+");

	h264bitstream = fopen(url, "rb+");
	if (h264bitstream == NULL)
	{
		printf("Open file error.\n");
		return 0;
	}

	n = (NALU_t*)calloc(1, sizeof(NALU_t));
	if (n == NULL)
	{
		printf("Alloc NALU error.\n");
		return 0;
	}

	n->max_size = buffersize;
	n->buf = (char*)calloc(buffersize, sizeof(char));
	if (n->buf == NULL)
	{
		free(n);
		printf("Alloc NALU: n->buf error.\n");
		return 0;
	}

	int data_offset = 0;
	int nal_num = 0;
	printf("-----+---------+------ NALU Table ---+--------+---------+\n");
	printf(" NUM |   POS   | FORBIDDEN |   IDC   |  TYPE  |   LEN   |\n");
	printf("-----+---------+-----------+---------+--------+---------+\n");

	while (!feof(h264bitstream))
	{
		int nalu_length = GetAnnexbNALU(n);

		char type_str[20] = { 0 };
		switch (n->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;
		default: sprintf(type_str, "unknown"); break;
		}
		char idc_str[20] = { 0 };
		switch (n->nal_reference_idc >> 5)
		{
		case NALU_PRIORITY_DISPOSABLE: sprintf(idc_str, "DISPOS"); break;
		case NALU_PRIORITY_LOW: sprintf(idc_str, "LOW"); break;
		case NALU_PRIORITY_HIGH: sprintf(idc_str, "HIGH"); break;
		case NALU_PRIORITY_HIGHEST: sprintf(idc_str, "HIGHEST"); break;
		default: sprintf(type_str, "unknown"); break;
		}

		fprintf(myout, "%5d| %8d|%11d|%9s|%8s|%9d|\n", nal_num, data_offset, n->forbidden_bit, idc_str, type_str, n->len);

		data_offset = data_offset + nalu_length;

		nal_num++;
	}

	// Free
	if (n)
	{
		if (n->buf)
		{
			free(n->buf);
			n->buf = NULL;
		}
		free(n);
	}
	return 0;
}

int main()
{
	simplest_h264_parser("sintel.h264");

	system("pause");
	return 0;
}

运行结果

本程序的输入为一个H.264原始码流(裸流)的文件路径,输出为该码流的NALU统计数据,如下图所示。

在这里插入图片描述

下载链接

GitHub:UestcXiye / Simplest-H.264-Parser

优快云:Simplest H.264 Parser.zip

参考

  1. https://blog.youkuaiyun.com/leixiaohua1020/article/details/50534369
  2. https://blog.youkuaiyun.com/u010512264/article/details/82083467
  3. https://blog.youkuaiyun.com/stpeace/article/details/8221945
  4. https://blog.youkuaiyun.com/threewells_14/article/details/1508657
  5. https://blog.youkuaiyun.com/qq_29350001/article/details/78226286
  6. https://blog.youkuaiyun.com/jammg/article/details/52357245
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值