上一篇中我们已经找到nalu,这一篇开始,我们就逐步搭建解析nalu的框架,在本篇中,核心任务有以下几个:
- 1、实现nal_to_rbsp,也即从nalu中提取出rbsp
- 2、实现rbsp_to_sodb,也即从rbsp中找到trailing_bits
- 3、导入在【h264/avc句法和语义详解】系列中,已经实现的指数哥伦布解码部分
内容很简单,为了显得更专业,我们先将上次的项目稍作改变:
(1)文件名:
没错,我们将main.c改为decode.c,h264_nal.h改为nalu.h,h264_nal.c改为nalu.c。
(2)封装打开文件操作
在上篇中我们将打开h264文件,和读取操作放到了main.c中,为此我们先新建文件stream.h和stream.c,来封装打开和读取这种流操作。封装实现如下:
分为读取和释放两步,其中读取包含了打开操作。并且我们将读取的h264文件缓冲,定义为全局变量file_buff
。因此在decode.c中就可以一步完成读取操作:
// 读取h264文件
int buff_size = readAnnexbBitStreamFile("silent_cif_baseline_5_frames.h264");
printf("totalSize: %d\n", buff_size);
(3)引入nalu_t结构体
在上篇中,我们在每次find_nal_unit()
的时候,都是将nalu的buf数据存入全局变量uint8_t nalu_buf[1024*1024]
中,这样不利于后续操作。因此我们废弃掉这个变量,并引入nalu_t
结构体。将每次读取到的nalu的buf数据,存入到nalu->buf
中。
而且待会我们要解析nalu_header的三个句法元素,因此一并实现如下:
/**
Network Abstraction Layer (NAL) unit
@see 7.3.1 NAL unit syntax
*/
typedef struct
{
// nal header
int forbidden_zero_bit; // f(1)
int nal_ref_idc; // u(2)
int nal_unit_type; // u(5)
int len; // 最开始保存nalu_size, 然后保存rbsp_size,最终保存SODB的长度
uint8_t *buf;
} nalu_t;
其中nalu->len
的值并不是一开始就固定的值,下面解析的时候我们将会看到这一变化。
综合(2)、(3)两步获取的file_buff全局变量和nalu_t结构体,对之前实现的find_nal_unit()函数稍作修改,将每次读取到的nalu的buf数据,存入nalu->buf,便于后续解析。这样一来,之前的main.c,也即现在的decode.c,就变成了这样:
int main(int argc, const char * argv[]) {
// 0. 读取h264文件
int buff_size = readAnnexbBitStreamFile("silent_cif_baseline_5_frames.h264");
printf("totalSize: %d\n", buff_size);
// 1. 开辟nalu_t保存nalu_header和SODB
nalu_t *nalu = allocNalu(MAX_NALU_SIZE);
int curr_nal_start = 0; // 当前找到的nalu起始位置
int curr_find_index = 0; // 当前查找的位置索引
// 2.找到h264码流中的各个nalu
while ((nalu->len = find_nal_unit(nalu, buff_size, &curr_nal_start, &curr_find_index)) > 0) {
}
freeNalu(nalu);
freeFilebuffer();
return 0;
}
其中的allocNalu()
为刚才实现nalu_t结构体时,顺手实现,就不贴代码了。
下面继续本篇的内容。
1、实现nal_to_rbsp()
代码实现如下:
/**
去除rbsp中的0x03
@see 7.3.1 NAL unit syntax
@see 7.4.1.1 Encapsulation of an SODB within an RBSP
@return 返回去除0x03后nalu的大小
*/
int nal_to_rbsp(nalu_t *nalu)
{
int nalu_size = nalu->len;
int j = 0;
int count = 0;
// 遇到0x000003则把03去掉,包含以cabac_zero_word结尾时,尾部为0x000003的情况
for (int i = 0; i < nalu_size; i++)
{
if (count == 2 && nalu->buf[i] == 0x03)
{
if (i == nalu_size - 1) // 结尾为0x000003
{
break; // 跳出循环
}
else
{
i++; // 继续下一个
count = 0;
}
}
nalu->buf[j] = nalu->buf[i];
if (nalu->buf[i] == 0x00)
{
count++;
}
else
{
count = 0;
}
j++;
}
return j;
}
注意此时nalu->buf中保存了当前nalu的全部数据,因此将nalu转换为rbsp的操作,也即从nalu->buf中去除0x000003中的03,然后把得到的rbsp数据重新赋值给nalu->buf,并得到新的nalu->len,也即去除了0x03之后的rbsp加1字节nalu_header的长度。
所以开始我们需要遍历nalu->len次,逐字节查找nalu->buf中是否有0x000003出现。其中的自增变量i有两个作用:
- (1)逐字节查找
- (2)控制重新赋值nalu->buf,如果遇到0x000003,就跳过一字节不赋值,相当于去除了0x03
而里面的count显而易见,是为了查找是否有0x000003出现的。如果遇到0字节,count就自增,否则就清零,直至有连续的两个字节的0出现,再去检测第三个字节是否为0x03。
2、实现rbsp_to_sodb()
代码实现如下:
/**
计算SODB的长度
【注】RBSP = SODB + trailing_bits
*/
int rbsp_to_sodb(nalu_t *nalu)
{
int ctr_bit, bitoffset, last_byte_pos;
bitoffset = 0;
last_byte_pos = nalu->len - 1;
// 0.从nalu->buf的最末尾的比特开始寻找
ctr_bit = (nalu->buf[last_byte_pos] & (0x01 << bitoffset));
// 1.循环找到trailing_bits中的rbsp_stop_one_bit
while (ctr_bit == 0)
{
bitoffset++;
if(bitoffset == 8)
{
// 因nalu->buf中保存的是nalu_header+RBSP,因此找到最后1字节的nalu_header就宣告RBSP查找结束
if(last_byte_pos == 1)
printf(" Panic: All zero data sequence in RBSP \n");
assert(last_byte_pos != 1);
last_byte_pos -= 1;
bitoffset = 0;
}
ctr_bit= nalu->buf[last_byte_pos-1] & (0x01 << bitoffset);
}
// 【注】函数开始已对last_byte_pos做减1处理,此时last_byte_pos表示相对于SODB的位置,然后赋值给nalu->len得到最终SODB的大小
return last_byte_pos;
}
注意这个过程中,我们并没有重新给nalu->buf赋值的情况出现。因为在nalu提取rbsp时,0x000003有可能出现在数据序列的中间,去除0x03会打乱原数据。而提取sodb则不一样,因为提取sodb,只需去掉rbsp尾部即可。说到尾部,它肯定位于rbsp的最后面,因此我们只需找到它,并重新计算nalu->len即可。
所以我们的重点就是找到rbsp的尾部,那么尾部如何找呢?其重点就是找到rbsp尾部中的rbsp_stop_one_bit
,这是值为1的一个比特位。位于它之前的数据,就是sodb,包含它之后的,就是rbsp的尾部。
因此我们只需从nalu->buf的最后一个字节,逐比特往前查找即可,遇到比特值为1,即查找结束。此时rbsp_stop_one_bit所在的那个字节,即是sodb的末字节。而末字节last_byte_pos的值,也即sodb的长度。
3、导入指数哥伦布解码实现bs.h
在【h264/avc句法和语义详解】系列中,我们是将指数哥伦布编码分为.h和.c文件来实现的,而在这里稍作修改,全部在.h中实现。因为它们在整个解码过程中频繁使用,因此需要当做内联函数使用。
此时我们就可以使用bs_t结构体,来从nalu->buf中,解析出nalu_header的三个句法元素出来:
// 初始化逐比特读取工具句柄
bs_t *bs = bs_new(nalu->buf, nalu->len);
// 读取nal header 7.3.1
nalu->forbidden_zero_bit = bs_read_u(bs, 1);
nalu->nal_ref_idc = bs_read_u(bs, 2);
nalu->nal_unit_type = bs_read_u(bs, 5);
4、综合
综合1、2、3步,我们就可以在find_nal_unit()
后,根据nalu->buf
来实现后续的解析操作:
/**
读取一个nalu
@see 7.3.1 NAL unit syntax
@see 7.4.1 NAL unit semantics
*/
void read_nal_unit(nalu_t *nalu)
{
// 1.去除nalu中的emulation_prevention_three_byte:0x03
nalu->len = nal_to_rbsp(nalu);
// 2.初始化逐比特读取工具句柄
bs_t *bs = bs_new(nalu->buf, nalu->len);
// 3. 读取nal header 7.3.1
nalu->forbidden_zero_bit = bs_read_u(bs, 1);
nalu->nal_ref_idc = bs_read_u(bs, 2);
nalu->nal_unit_type = bs_read_u(bs, 5);
switch (nalu->nal_unit_type)
{
case H264_NAL_SPS:
nalu->len = rbsp_to_sodb(nalu);
break;
case H264_NAL_PPS:
nalu->len = rbsp_to_sodb(nalu);
break;
case H264_NAL_SLICE:
case H264_NAL_IDR_SLICE:
nalu->len = rbsp_to_sodb(nalu);
break;
case H264_NAL_DPA:
nalu->len = rbsp_to_sodb(nalu);
break;
case H264_NAL_DPB:
nalu->len = rbsp_to_sodb(nalu);
break;
case H264_NAL_DPC:
nalu->len = rbsp_to_sodb(nalu);
break;
default:
break;
}
bs_free(bs);
}
其中的nalu->nal_unit_type的枚举值,是参照了h264文档和JM、FFmpeg的实现,选择的几个常用的做的定义:
/* 7.4.1 Table 7-1 NAL unit types */
enum nal_unit_type {
H264_NAL_UNKNOWN = 0,
H264_NAL_SLICE = 1,
H264_NAL_DPA = 2,
H264_NAL_DPB = 3,
H264_NAL_DPC = 4,
H264_NAL_IDR_SLICE = 5,
H264_NAL_SEI = 6,
H264_NAL_SPS = 7,
H264_NAL_PPS = 8,
H264_NAL_AUD = 9,
H264_NAL_END_SEQUENCE = 10,
H264_NAL_END_STREAM = 11,
H264_NAL_FILLER_DATA = 12,
H264_NAL_SPS_EXT = 13,
H264_NAL_AUXILIARY_SLICE = 19,
};
最后提一句,为了便于后续解析操作的进行,我更换了一个baseline编码级别的h264文件素材,并且全部使用I帧,帧数剪到只剩5帧。这样在后续进行解码时,我们只需先考虑baseline级别的I帧即可,而等到需要时,我们再更换其他的素材。
本文源码地址如下(H264Analysis_02中):