简单分析一下 H.264 AnnexB 格式的裸流格式
H.264简介
国际上制定视频编解码技术的组织有两个
- 国际电联(ITU-T),它制定的标准有
H.261
、H.263
、H.263+
等 - 国际标准化组织(ISO),它制定的标准有
MPEG-1
、MPEG-2
、MPEG-4
等。
而 H.264
则是由这两个组织联合组建的 联合视频组(JVT) 共同制定的 新数字视频编码标准,所以它既是 ITU-T
的 H.264
,又是 ISO/IEC
的 MPEG-4
高级视频编码(Advanced Video Coding,AVC),是新一代数字视频压缩格式。
H.264
的算法在概念上可以分为两层:
- 视频编码层(VCL:Video Coding Layer)负责高效的视频内容表示
- 网络提取层(NAL:Network Abstraction Layer)负责以网络所要求的恰当的方式对数据进行打包和传送
整个 H.264
可以简化成以下这张图
这里面涉及到很多要素,VCL
、SODB
、 RBSP
、 EBSP
等等,接下来逐个介绍,尽量说清楚
VCL
视频编码层主要就是通过各种编码压缩去除视频中重复部分,也就是冗余,常见的冗余分为以下几种:
- 帧内压缩,空间上存在大量冗余
- 帧间压缩,时间上存在大量冗余
- 感知冗余,高频信号敏感度不强,数值量化
- 编码冗余,使用可变编码,可压缩编码冗余
而 感知(视觉)冗余
又可以分为以下几种
- 对亮度的变化敏感,对色度的变化相对不敏感
- 对静止图像敏感,对运动图像相对不敏感
- 对图像的水平线条和竖直线条敏感,对斜线相对不敏感
- 对整体结构敏感,对内部细节相对不敏感
- 对低频信号敏感,对高频信号相对不敏感
一般情况下对视频压缩会经过以下几个过程
- 预测:去除空间和时间冗余:帧内预测(划分的宏块预测),帧间预测
- 变化:去除感知冗余:DCT, 小波变换
- 量化:去除视觉冗余,通过降低图像质量提高压缩比
- 熵编码:去除编码冗余:变长编码,算术编码
H.264
和以前的标准一样,也是 变换编码
加 预测编码
的 混合编码模式。
在此基础上还增加了如多模式运动估计、帧内预测、多帧预测、基于内容的变长编码、4x4二维整数变换等新的编码方式,提高了编码效率。
接下来对编码的一些基本单位稍微了解一下:
宏块
是 H.264
编码的基本单位,一个编码图像首先要划分成多个块(4x4 像素)才能进行处理,通常宏块大小为 16x16 个像素。
宏块也分为 I、P、B 宏块:
I宏块
只能利用当前片中已解码的像素作为参考进行帧内预测;P宏块
可以利用前面已解码的图像作为参考图像进行帧内预测;B宏块
则是利用前后向的参考图形进行帧内预测
片
一帧视频图像可编码成一个或者多个 片
(Slice),每 片
包含整数个 宏块
,即每 片
至少一个 宏块
,最多时包含整个图像的 宏块
。
片
的目的:为了限制误码的扩散和传输,使编码片相互间保持独立。
片共有5种类型:
I
片 :只包含I
宏块P
片 :P
和I
宏块B
片 :B
和I
宏块SP
片 :用于不同编码流之间的切换SI
片 :特殊类型的编码宏块
片组
片组,其实就是使用某一规则,将一帧画面中的某些 宏块
划分成一个组
而 片
就是是在 片组
内,对 宏块
做进一步划分。
所以这三者的关系是 一帧画面,是由一个或多个
片组
组成,片组
是由一个或多个片
组成,片
则由宏块
组成。
SODB
String of Data Bits
, 数据 bit 流,是最原始的编码后的数据
NAL
NAL
负责以网络所要求的恰当的方式对数据进行打包和传送
在一个 H.264 AnnexB 格式的比特流中,NALU
是其中一个基本单元,通过引入起始码(0x000001 or 0x00000001) 来将 NALU
隔开
在
SPS
、PPS
和Access Unit
的第一个NALU
使用 4 字节起始码,其余情况均使用 3 字节起始码。
对起始码的格式有个印象 (0x000001或者0x00000001) ,之后还有因为它引入其它的设计
我们继续从原始数据 SODB
开始,看最后如何变成一个比特流
RBSP
Raw Byte Sequence Payload
,原始字节序列载荷
在 SODB
的后面添加了 trailing bits
结尾比特
也就是一个bit 1 和若干个bit 0,以便字节对齐
EBSP
Encapsulate Byte Sequence Payload
扩展字节序列载荷
在 RBSP
基础上填加了防竞争字节 0x03
,这样便形成了 EBSP
。
为什么需要增加防竞争字节,主要是因为前面提到的起始码格式是 (0x000001或者0x00000001), 同时 H.264
规定,当检测到 0x000000
时,也可以表示当前 NALU
的结束。那就会产生一个问题,就是如果在 NALU
的内部,出现了 0x000001
或 0x000000
时该怎么办?
为了使 NALU
主体中不出现与 0x000001
或 0x000000
冲突的情况,在给 NALU
添加起始码之前,先对码流进行一次遍历,查找码流里面的存在的 0x000000
,0x000001
,0x000002
,0x000003
的字节,然后对其进行如下修改
0x000000 => 0x00000300
0x000001 => 0x00000301
0x000002 => 0x00000302
0x000003 => 0x00000303
0x000000
和 0x000001
是为了区分起始码和规定的结束,0x000002
是作为保留使用,而 0x000003
,则是为了防止 NALU
内部,原本就有序列为 0x000003
这样的数据。
那么解码时先遍历将 0x03
去掉即可。也称为脱壳操作。
NALU Header
在 EBSP
的基础上加一个 NALU Header
就变成一个 NALU
NALU Header
在整个 NALU
中,只占据一个字节。分别为:
- forbidden_zero_bit
- nal_ref_idc
- nal_unit_type
forbidden_zero_bit
值对应 1 个 bit, 这个值应该为 0,当它不为 0 时,表示网络传输过程中,当前 NALU
中可能存在错误,解码器可以考虑不对这个 NALU
进行解码。
nal_ref_idc
值对应 2 个 bit, 取值 0~3,代表当前这个 NALU
的重要性,取值越大,代表当前 NALU
越重要,就需要优先被保护。
nal_unit_type
值对应 5 个 bit, 代表 NALU Header
后面的 EBSP
的数据结构的类型。
裸流解析
本部分是对一个 H.264
的裸流进行分离并解析 NALU
,代码直接使用的是 雷霄骅
大佬博客中提供的例子,并添加了一些简单的注释
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
typedef enum {
NALU_PRIORITY_DISPOSABLE = 0,
NALU_PRIRITY_LOW = 1,
NALU_PRIORITY_HIGH = 2,
NALU_PRIORITY_HIGHEST = 3
} NaluPriority;
typedef struct
{
int startcodeprefix_len; // NALU Header 的长度,3个或者4个字节长度
unsigned len; // NALU 中 EBSP 的长度
unsigned max_size; // 缓存数据时的最大长度
int forbidden_bit; // 默认必须为 0
int nal_reference_idc; // NALU 重要性
int nal_unit_type; // NALU 的类型
char *buf; // 指向 EBSP 第一个字节的指针
} NALU_t;
// the bit stream file
FILE *h264bitstream = NULL;
int info2=0, info3=0;
static int FindStartCode2 (unsigned char *Buf){
if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=1) return 0; //0x000001?
else return 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;
}
int GetAnnexbNALU (NALU_t *nalu){
int pos = 0;
int StartCodeFound, rewind;
unsigned char *Buf;
if ((Buf = (unsigned char*)calloc (nalu->max_size , sizeof(char))) == NULL)
printf ("GetAnnexbNALU: Could not allocate Buf memory\n");
nalu->startcodeprefix_len=3;
// 先尝试读取 1*3 个字节
if (3 != fread (Buf, 1, 3, h264bitstream)){
free(Buf);
return 0;
}
// 判断裸流中 Header 时 0x000001 还是 0x00000001
info2 = FindStartCode2 (Buf);
if(info2 != 1) {
// 不是 0x000001, 则在多读一个字节
if(1 != fread(Buf+3, 1, 1, h264bitstream)){
free(Buf);
return 0;
}
// 判断是不是 0x00000001
info3 = FindStartCode3 (Buf);
if (info3 != 1){
// 既不是 0x000001 也不是 0x00000001,裸流有问题
free(Buf);
return -1;
}
else {
pos = 4;
nalu->startcodeprefix_len = 4;
}
}
else{
nalu->startcodeprefix_len = 3;
}
// 开始找下一个 NALU 的起始码,定位上一个 NALU 数据的长度
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;
}
// 尝试一个字节一个字节向后读取
// 判断是否含有 0x000001 还是 0x00000001
Buf[pos++] = fgetc (h264bitstream);
info3 = FindStartCode3(&Buf[pos-4]);
if(info3 != 1)
info2 = FindStartCode2(&Buf[pos-3]);
StartCodeFound = (info2 == 1 || info3 == 1);
}
// pos 相当于 记录了这一次读取的总字节数
// nalu->startcodeprefix_len 中记录了本次 NALU 中起始码的长度
// rewind 时记录了下一个 NALU 起始码的长度
rewind = (info3 == 1)? -4 : -3;
// 重置 fread 读取的位置到下一次 NALU 起始码前
if (0 != fseek (h264bitstream, rewind, SEEK_CUR)){
free(Buf);
printf("GetAnnexbNALU: Cannot fseek in the bit stream file");
}
nalu->len = (pos+rewind)-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+rewind);
}
int simplest_h264_parser(char *url){
NALU_t *n;
int buffersize=100000;
FILE *myout=stdout;
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 ("AllocNALU: n->buf");
return 0;
}
int data_offset=0;
int nal_num=0;
printf("-----+-------- NALU Table ------+---------+\n");
printf(" NUM | POS | IDC | TYPE | LEN |\n");
printf("-----+---------+--------+-------+---------+\n");
while(!feof(h264bitstream))
{
int data_lenth;
data_lenth=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;
}
char idc_str[20]={0};
switch(n->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;
}
fprintf(myout,"%5d| %8d| %7s| %6s| %8d|\n",nal_num,data_offset,idc_str,type_str,n->len);
data_offset=data_offset+data_lenth;
nal_num++;
}
if (n){
if (n->buf){
free(n->buf);
n->buf=NULL;
}
free (n);
}
return 0;
}