【从零实现一个H.264码流解析器】(二):导入指数哥伦布解码实现并初步解析NALU

上一篇中我们已经找到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中):

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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
H.264是一种视频压缩标准,它的解码原理是将压缩后的视频帧进行解码,恢复成原始的视频帧。具体的解码过程如下: 1. 读取H.264视频码流数据。 2. 解析码流数据,提取出视频帧的数据。H.264码流数据由一系列NALU(网络抽象层单元)组成,其中包含SPS(序列参数集)、PPS(图像参数集)和视频帧的数据。 3. 解码SPS和PPS,并初始化解码器。SPS和PPS包含了视频帧的一些参数,如分辨率、帧率、色彩空间等。 4. 解码视频帧数据。H.264视频帧数据由多个宏块(Macroblock)组成,每个宏块包含多个亚宏块(Sub-macroblock),亚宏块包含多个像素。解码器会对每个宏块进行解码,重建出原始的视频帧。 5. 输出解码后的视频帧。 下面是一个用C语言实现H.264视频解码的简单示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stdbool.h> #define MAX_FRAME_SIZE 65536 // H.264 NALU类型 typedef enum { NALU_TYPE_UNDEFINED = 0, NALU_TYPE_NON_IDR = 1, NALU_TYPE_IDR = 5, NALU_TYPE_SEI = 6, NALU_TYPE_SPS = 7, NALU_TYPE_PPS = 8 } NaluType; // H.264 NALU结构体 typedef struct { uint8_t *data; // NALU数据指针 int length; // NALU数据长度 NaluType type; // NALU类型 } Nalu; // H.264解码器结构体 typedef struct { void *codec; // 解码器句柄 uint8_t *frameBuffer; // 解码后的视频帧数据 int frameSize; // 解码后的视频帧数据长度 } H264Decoder; // 初始化H.264解码器 bool H264Decoder_Init(H264Decoder *decoder) { // 初始化解码器句柄 decoder->codec = NULL; decoder->frameBuffer = NULL; decoder->frameSize = 0; // TODO: 实现解码器初始化 return true; } // 释放H.264解码器 void H264Decoder_Free(H264Decoder *decoder) { // 释放解码器句柄 if (decoder->codec) { // TODO: 实现解码器释放 } // 释放视频帧数据 if (decoder->frameBuffer) { free(decoder->frameBuffer); decoder->frameBuffer = NULL; decoder->frameSize = 0; } } // H.264视频帧解码 bool H264Decoder_Decode(H264Decoder *decoder, const uint8_t *data, int length) { // 读取NALU头部 uint8_t naluType = (data[0] & 0x1f); uint8_t naluRefIdc = (data[0] >> 5); // 如果NALU类型为SPS或PPS,直接忽略 if (naluType == NALU_TYPE_SPS || naluType == NALU_TYPE_PPS) { return true; } // 如果NALU类型为非IDR帧或IDR帧,解码视频帧数据 if (naluType == NALU_TYPE_NON_IDR || naluType == NALU_TYPE_IDR) { // TODO: 实现视频帧解码 // 将解码后的视频帧数据保存到解码器结构体中 if (decoder->frameBuffer) { free(decoder->frameBuffer); } decoder->frameBuffer = (uint8_t*)malloc(MAX_FRAME_SIZE); memcpy(decoder->frameBuffer, decodedFrameData, decodedFrameSize); decoder->frameSize = decodedFrameSize; return true; } return false; } // 主函数 int main() { // TODO: 实现H.264视频解码器的测试代码 return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值