【从零实现一个H.264码流解析器】(四):生成句法元素跟踪trace文件

在上篇中,我们已经解析出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中):

1、GitHub:https://github.com/Gosivn/H264Analysis

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值