H264分层结构
H264的主要目标是为了有高的视频压缩比和良好的网络亲和性,为了达成这两个目标,H264的解决方案是将系统框架分为两个层面,分别是视频编码层面(VCL:Video Coding Layer)和网络抽象层面(NAL:Network Coding Layer)
VLC层
是对核心算法引擎、块、宏块及片的语法级别的定义,负责有效表示视频数据的内容,最终输出编码完的数据SODB;在VCL进⾏数据传输或存储之前,这些编码的VCL数据,被映射或封装进NAL单元。
NAL层
定义了片级以上的语法级别(如序列参数集和图像参数集 等),负责以网络所要求的恰当方式去格式化数据并提供头信息,以保证数据适合各种信道的传输和保存在存储介质上。NAL层将SODB打包成RBSP然后加上NAL头组成一个NALU单元,具体NAL单元的组成也会在后面详细描述。
H.264部分概念
帧
视频由一系列连续图像组成,一张图像也称作一帧。但是H.264对图像做了压缩,所以帧也分为几种:
- I帧:帧内编码帧 ,I 帧通常是每个 GOP(MPEG 所使⽤的⼀种视频压缩技术) 的第⼀个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是⼀个图像经过压缩后的产物。 ⾃身可以通过视频解压算法解压成⼀张单独的完整的图⽚。
- P帧:前向预测编码帧,通过充分将低于图像序列中前⾯已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧。需要参考其前⾯的⼀个I frame 或者P frame来⽣成⼀张完整 的图⽚。
- B帧:双向预测帧,既考虑与源图像序列前⾯已编码帧,也顾及源图像序列后⾯ 已编码帧之间的时间冗余信息来压缩传输数据量的编码图像, 也叫双向预测帧。要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完 整的图⽚。
一般是说,I帧的压缩率是7,p帧的压缩率是20,b帧的压缩率是50
IDR
IDR(Instantaneous Decoding Refresh,即时解码刷新) ⼀个序列的第⼀个图像叫做 IDR 图像(⽴即刷新图像),IDR 图像都是 I 帧图像,I和IDR帧都使⽤帧内预测。I帧不⽤参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之 前的帧的。IDR就不允许这样。
IDR帧核⼼作⽤是:是为了解码的重同步,当解码器解码到 IDR 图像时,⽴即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始⼀个新的序列。这样,如果前⼀ 个序列出现重⼤错误,在这⾥可以获得重新同步的机会。IDR图像之后的图像永远不会使⽤ IDR之前的图像的数据来解码。
GOP
是图像组的意思,表示编码的视频序列分成了一组一组有序的帧的集合进行编码。GOP第一帧永远是I帧。
H.264码流组织形式
H.264有两种封装形式:
- ⼀种是annex B模式,传统模式,有startcode。
- ⼀种是mp4模式,⼀般mp4 mkv都是mp4模式,没有startcode,SPS和PPS以及其它信息被封装在container中
这里我们只关注annex B模式。H.264的数据以Nal Unit的形式存在,裸码流保存的时候会在Nal Unit之前插入start code。
其中SPS、PPS、IDR和SLICE是NAL单元某一类型的数据。
起始码(start code)
H.264 的基本流由一系列NALU (Network Abstraction Layer Unit )组成,不同的NALU数据量各不相同。H.264 草案指出,当数据流是储存在介质上时,在每个NALU 前添加起始码:0x000001或0x00000001,用来指示一个NALU 的起始和终止位置。在这样的机制下,在码流中检测起始码,作为一个NALU得起始标识,当检测到下一个起始码时,当前NALU结束。
H.264 码流中每个帧的开头的3~4个字节是H.264 的start_code(起始码),0x00000001或0x000001。3字节的0x000001只有一种场合下使用,就是一个完整的帧被编为多个slice(片)的时候,从第二个slice开始,包含这些slice的NALU 使用3字节起始码。也就是说,如果NALU对应的slice为一帧的开始就用0x00000001,否则就用0x000001。
start code并不是NALU的一部分,只是用来分割NALU的一种手段。
NAL单元 - NALU
H.264 最基本的数据存储单元。
NAL Unit 包含 [ Header (1 Byte) + Payload ]
NALU Header
首先,NALU Header只占 1 个字节,即 8 位,其组成如下图所示:
- forbidden_zero_bit
禁止位,初始为0,当网络发现NAL单元有比特错误时可设置该比特为1,以便接收方纠错或丢掉该单元。 - nal_ref_idc
用于表示当前NALU的重要性,值越大,越重要。
解码器在解码处理不过来的时候,可以丢掉重要性为 0 的 NALU。 - nal_unit_type
表示 NALU 数据的类型,有以下几种:
其中比较注意的应该是以下几个:
- 1-4:I/P/B帧,如果 nal_ref_idc 为 0,则表示 I 帧,不为 0 则为 P/B 帧。
- 5:IDR帧,I 帧的一种,告诉解码器,之前依赖的解码参数集合(接下来要出现的 SPS\PPS 等)可以被刷新了。
- 6:SEI,英文全称 Supplemental Enhancement Information,翻译为“补充增强信息”,提供了向视频码流中加入额外信息的方法。
- 7:SPS,全称 Sequence Paramater Set,翻译为“序列参数集”。SPS 中保存了一组编码视频序列(Coded Video Sequence)的全局参数。因此该类型保存的是和编码序列相关的参数。
- 8: PPS,全称 Picture Paramater Set,翻译为“图像参数集”。该类型保存了整体图像相关的参数。
- 9:AU 分隔符,AU 全称 Access Unit,它是一个或者多个 NALU 的集合,代表了一个完整的帧,有时候用于解码中的帧边界识别。
SPS 和 PPS 存储了编解码需要一些图像参数,SPS,PPS 需要在 I 帧前出现,不然解码器没法解码。而 SPS,PPS 出现的频率也跟不同应用场景有关,对于一个本地 h264 流,可能只要在第一个 I 帧前面出现一次就可以,但对于直播流,每个 I 帧前面都应该插入 sps 或 pps,因为直播时客户端进入的时间是不确定的。
分割NALU
基于以上的基本信息,我们就可以对NALU根据start code来做切割了,NALU内部信息先不做分析。
nal_unit_type不同值对应不同NALU类型。
雷神的代码,做了些修改,动态可扩展buffer。
执行结果如下:
源代码,h264_nalu.cpp
#include <iostream>
#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,
} H264NaluType;
typedef enum {
NALU_PRIORITY_DISPOSABLE = 0,
NALU_PRIRITY_LOW = 1,
NALU_PRIORITY_HIGH = 2,
NALU_PRIORITY_HIGHEST = 3
} H264NaluPriority;
typedef struct {
int statcode_length; //! 4 for parameter sets and first slice in
//! picture, 3 for everything else (suggested)
int forbidden_bit; //! should be always FALSE
int nal_reference_idc; //! NALU_PRIORITY_xxxx
int nal_unit_type; //! NALU_TYPE_xxxx
size_t len; //! Length of the NAL unit (Excluding the start code, which
//! does not belong to the NALU)
size_t max_size; //! Nal Unit Buffer size
unsigned char *buf; //! contains the first byte followed by the EBSP
void AppendData(unsigned char *data_buf, size_t data_len) {
if (max_size - len >= data_len) { // space is not enough
memcpy(this->buf + this->len, data_buf, data_len);
this->len += data_len;
} else {
this->max_size = this->max_size << 1;
unsigned char *new_buf = new unsigned char[this->max_size];
memcpy(new_buf, this->buf, this->len);
delete[] this->buf;
this->buf = new_buf;
this->AppendData(data_buf, data_len);
}
}
} H264Nalu;
FILE *h264bitstream = NULL; //!< the bit stream file
static bool MatchStartCode3Bytes(unsigned char *Buf) {
if (Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 1)
return false; // 0x000001?
else
return true;
}
static bool MatchStartCode4Bytes(unsigned char *Buf) {
if (Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 0 || Buf[3] != 1)
return false; // 0x00000001?
else
return true;
}
int GetAnnexbNALU(H264Nalu *nalu) {
nalu->len = 0; // reset immediately
nalu->statcode_length = 0;
size_t data_buf_len = 10000;
unsigned char *data_buf = new unsigned char[data_buf_len];
size_t read_len = fread(data_buf, 1, data_buf_len, h264bitstream);
if (read_len < 3) {
delete[] data_buf;
return 0;
}
// data cursor
unsigned char *pdata = data_buf;
// pdata must be smaller than this
unsigned char *pdata_end = data_buf + read_len;
if (!MatchStartCode3Bytes(pdata)) {
if (read_len < 4) {
delete[] data_buf;
return 0;
}
if (!MatchStartCode4Bytes(pdata)) {
delete[] data_buf;
return -1;
} else {
nalu->statcode_length = 4;
pdata += 4;
}
} else {
nalu->statcode_length = 3;
pdata += 3;
}
int next_startcode_length = 0;
ssize_t rewind = 0;
while (pdata != pdata_end) {
if (!MatchStartCode4Bytes(pdata)) {
if (MatchStartCode3Bytes(pdata)) {
next_startcode_length = 3;
break;
}
} else {
next_startcode_length = 4;
break;
}
++pdata;
if (pdata == pdata_end) {
if (feof(h264bitstream)) {
break;
}
rewind = -3;
if (0 != fseek(h264bitstream, rewind, SEEK_CUR)) {
delete[] data_buf;
printf("GetAnnexbNALU: Cannot fseek in the bit stream file");
return -1;
}
pdata -= 3;
ssize_t cur_data_len = pdata - data_buf;
if (nalu->len == 0) { // first buf
nalu->AppendData(data_buf + nalu->statcode_length,
cur_data_len - nalu->statcode_length);
} else {
nalu->AppendData(data_buf, cur_data_len);
}
read_len = fread(data_buf, 1, data_buf_len, h264bitstream);
if (read_len <= 0) {
delete[] data_buf;
return -1;
}
pdata_end = data_buf + read_len; // update
pdata = data_buf;
}
}
// 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 = pdata - pdata_end;
if (0 != fseek(h264bitstream, rewind, SEEK_CUR)) {
delete[] data_buf;
printf("GetAnnexbNALU: Cannot fseek in the bit stream file");
return -1;
}
ssize_t cur_data_len = pdata - data_buf;
if (nalu->len) {
nalu->AppendData(data_buf, cur_data_len);
} else {
nalu->AppendData(data_buf + nalu->statcode_length,
cur_data_len - nalu->statcode_length);
}
delete[] data_buf;
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
return nalu->len + nalu->statcode_length;
}
/**
* Analysis H.264 Bitstream
* @param url Location of input H.264 bitstream file.
*/
int simplest_h264_parser(char *url) {
H264Nalu *n;
int buffersize = 100000;
// FILE *myout=fopen("output_log.txt","wb+");
FILE *myout = stdout;
h264bitstream = fopen(url, "rb+");
if (h264bitstream == NULL) {
printf("Open file error\n");
return 0;
}
n = new H264Nalu;
n->max_size = buffersize;
n->buf = new unsigned char[buffersize];
if (n->buf == NULL) {
delete n;
printf("AllocNALU: n->buf");
return 0;
}
int data_offset = 0;
int nal_num = 0;
printf("-----+---------------- NALU Table ------+---------+\n");
printf(" NUM | POS | SCL | IDC | TYPE | LEN |\n");
printf("-----+-----------+--------+--------+-------+---------+\n");
while (!feof(h264bitstream)) {
int data_lenth;
data_lenth = GetAnnexbNALU(n);
if (data_lenth == 0)
break;
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);
fprintf(myout, "%5d| 0x%08X| %7d| %7s| %6s| %8ld|\n", nal_num,
data_offset, n->statcode_length, idc_str, type_str, n->len);
data_offset = data_offset + data_lenth;
nal_num++;
}
// Free
if (n) {
if (n->buf) {
delete[] n->buf;
n->buf = NULL;
}
delete n;
}
return 0;
}
int main(int argc, char *argv[]) {
char *url = argv[1];
std::cout << "url: " << url << std::endl;
simplest_h264_parser(url);
return 0;
}