在上篇中,我们已经解析出SPS的全部句法元素,但是有一个至关重要的问题是,我们不知道自己解析出的句法元素值是否正确!
为了严格保证我们每一步都是正确的,和防止我们在错误的道路上越走越远,我们这里仿造H264官方参考软件JM,也建立一套trace机制。
并且因为我们解析SPS句法元素时,是参照最新版H264协议文档,也即2017-04版来解析的。因此我们需要保证,我们自己写的这个码流解析器,要和JM的最新版也即JM_19.0_Decoder生成的trace文件保持一致。
保持一致的意思是:
- (1)记录在trace文件的句法元素,数量和名称应和JM_19.0_Decoder一致,这样才能说明我们的语法结构没有问题,没有多解析或者少解析部分句法元素
- (2)句法元素的值和JM解析出来的值必须相同,这点很好理解,不相同说明我们肯定哪里写错了
同时为了简化流程,我们只记录句法元素的名称,和对应的十进制的值,而不会像JM那样额外记录每个句法元素使用的比特数,和对应的二进制比特位。
下面我们正式开始。
建立这一trace机制,有三件事要做:
- (1)定义全局宏定义,控制整个项目是否需要生成trace文件
- (2)打开和关闭trace文件
- (3)每解析出一个句法元素,就记录至trace文件
当然是否进行2、3步,需要根据第1步宏定义的值来判断,下面我们一步步开始。
1、TRACE宏定义
和打开H264文件一样,对trace文件的操作同样属于流操作,因此我们将宏定义放入stream.h文件中。
#define TRACE 1
2、打开和关闭trace文件
2.1打开
需要注意的是,我们不需要新写一个函数,来打开trace文件,只需要在打开h264文件时,顺便判断TRACE宏定义并打开trace文件即可。因此我们对原读取h264文件函数稍作修改:
// 读取h264文件,读取失败返回-1,否则返回文件大小
int readAnnexbBitStreamFile(char *fp)
{
FILE *fp_h264 = fopen(fp, "rb");
if (fp_h264 == NULL) {
printf("打开h264文件失败\n");
return -1;
}
file_buff = (uint8_t *)malloc(MAX_BUFFER_SIZE);
int file_size = (int)fread(file_buff, sizeof(uint8_t), MAX_BUFFER_SIZE, fp_h264);
fclose(fp_h264);
#if TRACE
trace_fp = fopen("trace_dec.txt", "w");
if (trace_fp == NULL) {
printf("打开trace_dec.txt文件失败\n");
return -1;
}
#endif
return file_size;
}
其中#if TRACE
和#endif
之间为新增代码,trace_fp就是我们需要的trace文件全局变量的文件句柄。
2.2关闭
同样,关闭的操作和释放h264文件缓冲的位置一样。
void freeFilebuffer(void)
{
free(file_buff);
#if TRACE
fclose(trace_fp);
#endif
}
3、记录句法元素值
3.1 nal_header
nal_header是每个nalu的起始句法元素,因此我们需要先记录它。另外因为nal_header的三个句法元素名称固定,并且表示了当前nalu内包含的数据类型,所以我们将它单独记录为一行,便于查阅。
因此解析完nal_header时,也即在read_nal_unit()
中,我们作如下修改:
/**
读取一个nalu
@see 7.3.1 NAL unit syntax
@see 7.4.1 NAL unit semantics
*/
void read_nal_unit(nalu_t *nalu)
{
int nalu_size = nalu->len;
// 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, NULL);
nalu->nal_ref_idc = bs_read_u(bs, 2, NULL);
nalu->nal_unit_type = bs_read_u(bs, 5, NULL);
#if TRACE
fprintf (trace_fp, "\n\nAnnex B NALU len %d, forbidden_bit %d, nal_reference_idc %d, nal_unit_type %d\n\n", nalu_size, nalu->forbidden_zero_bit, nalu->nal_ref_idc, nalu->nal_unit_type);
fflush (trace_fp);
#endif
switch (nalu->nal_unit_type)
//......
注意记录的NALU len为nalu_size,因此我们也新定义了一个局部变量nalu_size,来临时保存它的值。
3.2 记录句法元素的值
这里同样仿造JM,我们需要对解析句法元素的工具bs.h进行修改,在此我们需要解决两个主要问题:
- (1)功能问题:有的句法元素已经不需要记录,比如nal_header的三个句法元素
- (2)代码结构问题:bs.h中解析函数相互调用的问题,比如bs_read_se()和bs_read_te()同时调用了bs_read_ue(),而bs_read_ue()又调用了bs_read_u()
我们的大致思路就是,对于需要记录的句法元素,传入一个字符串traceString,不需要记录的则传NULL。这样我们将上诉两个问题的解决方案,由简单到复杂列为两步:
-
(1)在解析句法元素时,目前我们用到的描述子为u(n)、ue(v)、se(v)、te(v),我们对这四个函数进行修改,新增参数traceString。待每解析完一个句法元素的值之后,将traceString和解析的值记录到trace文件中
-
(2)对于不需要记录的句法元素,和u(n)、ue(v)、se(v)、te(v)之间相互调用的情况,我们通通传NULL,因此traceString为NULL即代表不记录
下面我们对u(n)、ue(v)、se(v)、te(v)函数进行修改。
3.3 bs_read_u()
/**
读取n个比特
@param b 比特流操作句柄
@param n 读取多少个比特
@return 返回读取到的值
*/
static inline uint32_t bs_read_u(bs_t* b, int n, char *traceString)
{
uint32_t r = 0; // 读取比特返回值
int i; // 当前读取到的比特位索引
for (i = 0; i < n; i++)
{
// 1.每次读取1比特,并依次从高位到低位放在r中
r |= ( bs_read_u1(b) << ( n - i - 1 ) );
}
if (traceString != NULL) {
#if TRACE
traceInput(traceString, r);
#endif
}
return r;
}
3.4 bs_read_ue()
/**
ue(v) 解码
*/
static inline uint32_t bs_read_ue(bs_t* b, char *traceString)
{
int32_t r = 0; // 解码得到的返回值
int i = 0; // leadingZeroBits
// 1.计算leadingZeroBits
while( (bs_read_u1(b) == 0) && (i < 32) && (!bs_eof(b)) )
{
i++;
}
// 2.计算read_bits( leadingZeroBits )
r = bs_read_u(b, i, NULL);
// 3.计算codeNum,1 << i即为2的i次幂
r += (1 << i) - 1;
if (traceString != NULL) {
#if TRACE
traceInput(traceString, r);
#endif
}
return r;
}
3.5 bs_read_se()
/**
se(v) 解码
*/
static inline int32_t bs_read_se(bs_t* b, char *traceString)
{
// 1.解码出codeNum,记为r
int32_t r = bs_read_ue(b, NULL);
// 2.判断r的奇偶性
if (r & 0x01) // 如果为奇数,说明编码前>0
{
r = (r+1)/2;
}
else // 如果为偶数,说明编码前<=0
{
r = -(r/2);
}
if (traceString != NULL) {
#if TRACE
traceInput(traceString, r);
#endif
}
return r;
}
3.6 bs_read_te()
/**
te(v) 解码
*/
static inline uint32_t bs_read_te( bs_t *b, int x, char *traceString )
{
uint32_t r = 0;
// 1.判断取值上限
if( x == 1 ) // 如果为1则将读取到的比特值取反
{
r = 1 - bs_read_u1( b );
}
else if( x > 1 ) // 否则按照ue(v)进行解码
{
r = bs_read_ue( b , NULL);
}
if (traceString != NULL) {
#if TRACE
traceInput(traceString, r);
#endif
}
return r;
}
其中的函数traceInput(),功能为将traceString和解析出的句法元素值,存入trace文件
3.7 traceInput()
void traceInput(char *traceString, uint32_t eleValue)
{
int inputCharsCount = 0;
putc('@', trace_fp);
// 1.录入traceString
inputCharsCount += fprintf(trace_fp, " %s", traceString);
while(inputCharsCount++ < 55) {
putc(' ',trace_fp);
}
// 2.录入eleValue
fprintf(trace_fp, " (%3d)\n", eleValue);
// 3.将缓冲区的内容输出到文件中
fflush(trace_fp);
}
做完以上各步,会发现之前parset.c中,解析SPS的操作会报很多错误,因为我们没传traceString。因此修改之后的解析调用如下:
/**
解析sps句法元素
[h264协议文档位置]:7.3.2.1.1 Sequence parameter set data syntax
*/
void parse_sps_syntax_element(sps_t *sps, bs_t *b)
{
sps->profile_idc = bs_read_u(b, 8, "SPS: profile_idc");
sps->constraint_set0_flag = bs_read_u(b, 1, "SPS: constraint_set0_flag");
sps->constraint_set1_flag = bs_read_u(b, 1, "SPS: constraint_set1_flag");
sps->constraint_set2_flag = bs_read_u(b, 1, "SPS: constraint_set2_flag");
sps->constraint_set3_flag = bs_read_u(b, 1, "SPS: constraint_set3_flag");
sps->constraint_set4_flag = bs_read_u(b, 1, "SPS: constraint_set4_flag");
sps->constraint_set5_flag = bs_read_u(b, 1, "SPS: constraint_set5_flag");
sps->reserved_zero_2bits = bs_read_u(b, 2, "SPS: reserved_zero_2bits");
sps->level_idc = bs_read_u(b, 8, "SPS: level_idc");
//......
至此,trace机制已经建立,只需要将stream.h中的TRACE
宏定义置为1即可。生成的trace文件如下:
Annex B NALU len 9, forbidden_bit 0, nal_reference_idc 3, nal_unit_type 7
@ SPS: profile_idc ( 66)
@ SPS: constraint_set0_flag ( 0)
@ SPS: constraint_set1_flag ( 0)
@ SPS: constraint_set2_flag ( 0)
@ SPS: constraint_set3_flag ( 0)
@ SPS: constraint_set4_flag ( 0)
@ SPS: constraint_set5_flag ( 0)
@ SPS: reserved_zero_2bits ( 0)
@ SPS: level_idc ( 30)
@ SPS: seq_parameter_set_id ( 0)
@ SPS: log2_max_frame_num_minus4 ( 0)
@ SPS: pic_order_cnt_type ( 0)
@ SPS: log2_max_pic_order_cnt_lsb_minus4 ( 0)
@ SPS: max_num_ref_frames ( 10)
@ SPS: gaps_in_frame_num_value_allowed_flag ( 0)
@ SPS: pic_width_in_mbs_minus1 ( 21)
@ SPS: pic_height_in_map_units_minus1 ( 17)
@ SPS: frame_mbs_only_flag ( 1)
@ SPS: direct_8x8_inference_flag ( 0)
@ SPS: frame_cropping_flag ( 0)
@ SPS: vui_parameters_present_flag ( 0)
Annex B NALU len 5, forbidden_bit 0, nal_reference_idc 3, nal_unit_type 8
Annex B NALU len 11074, forbidden_bit 0, nal_reference_idc 3, nal_unit_type 5
Annex B NALU len 11057, forbidden_bit 0, nal_reference_idc 2, nal_unit_type 1
Annex B NALU len 11112, forbidden_bit 0, nal_reference_idc 2, nal_unit_type 1
Annex B NALU len 11074, forbidden_bit 0, nal_reference_idc 2, nal_unit_type 1
Annex B NALU len 11048, forbidden_bit 0, nal_reference_idc 2, nal_unit_type 1
需要额外注意的是,如果使用的是集成工具IDE,需要将工作目录设为工程项目的本地路径。下面以Xcode为例:
修改Edit Scheme -> Run -> Options -> 勾选Use custom working directory -> 选择项目路径
本文源码地址如下(H264Analysis_04中):