最简单的 H.264 视频码流解析程序
- 最简单的 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.csdn.net/leixiaohua1020
*
* 修改:
* 刘文晨 Liu Wenchen
* 812288728@qq.com
* 电子科技大学/电子信息
* University of Electronic Science and Technology of China / Electronic and Information Science
* https://blog.csdn.net/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
CSDN:Simplest H.264 Parser.zip
参考
- https://blog.csdn.net/leixiaohua1020/article/details/50534369
- https://blog.csdn.net/u010512264/article/details/82083467
- https://blog.csdn.net/stpeace/article/details/8221945
- https://blog.csdn.net/threewells_14/article/details/1508657
- https://blog.csdn.net/qq_29350001/article/details/78226286
- https://blog.csdn.net/jammg/article/details/52357245