【从零实现一个H.264码流解析器】(一):从码流中找到NALU

1、本系列的目的

这个系列是为了配合【H.264/AVC 句法和语义】来写的,通俗点来讲,【句法和语义】和【解析器】这两个系列,前者相当于理论研究,后者相当于工程实践。而且【解析器】这个系列,是一步一步跟着【句法和语义】的脚步来走的。这样我们就可以一边学习理论,一边实际操作,二者结合,既能达到复习巩固的效果,又能增强项目能力。

2、为何名称叫“解析器”而不叫“解码器”

系列名称之所以取【解析器】,而不叫“解码器”,是因为在这个系列中,我们只会写到解码器的一部分,并不会去实现一个解码器。

实现的部分如下图所示:
在这里插入图片描述
H.264解码器

看过我之前文章的同学一定不会陌生,我曾在“描述子”(链接)一文中将H.264的解码器,分成3个学习阶段。而在本系列中,我们会实现图中的1部分,也即码流解析器。至于后面的重建图像部分,以后很有可能会在这个系列的基础上继续写下去。

之所以不一气呵成,是因为重建图像部分是整个解码器的核心,难度也相对较大。而且如果要实现重建图像部分,我们就得考虑H.264的等级划分,比如一开始我们肯定是实现基本等级(baseline profile),然后再考虑实现主要等级(main profile)和扩展等级(Extended profile),当然他们的实现难度也是逐渐增大的。

所以索性我们先不考虑这些问题,把重点先放在码流的解析上,这样相当于给自己减轻包袱。

3、本系列的实现宗旨

对于这个系列,我们的要求是:尽量贴合H.264的官方文档来实现!也就是说,我们在写代码的时候,代码结构和文档中的语法结构,是尽量一致的。这必然会导致算法的低效,但是我们不去优化它。因为我们的目的,不是要去实现一个牛逼的解码器,而是要通过这个项目,熟悉并掌握H.264的句法和语义,如何查看官方文档,以及如何进行熵解码、DCT变换、量化。内容还是比较多的,所以不要让这些优化的细节,挡住我们前行的脚步。

当我们做完这个项目之后,我们就会知道,这个项目有哪些点,是可以进行优化的。而且这个时候,我们就会拥有阅读像JM、x264等编解码器源码的能力,然后就可以通过阅读它们的源码,看看它们是怎么实现的,进而对整个H.264的编解码,有一个更深的理解。

4、本系列参照资料

这个系列的多数代码,参照自h264bitstream,它的GitHub地址为:
https://github.com/aizvorski/h264bitstream

部分代码,参照x264解码部分,较早版本的x264是有解码器的。而且在我查看x264解码器的过程中,发现h264bitstream的很多代码,也参照了x264解码器。

同时在写这个项目的时候,我也会参考JM的Decode部分的代码。

5、开发环境

本系列的开发环境为Mac,IDE为Xcode,在上传代码的时候,我会把整个项目都上传。如果有同学使用的是其他的IDE如VS、QT等,可以自行新建项目,然后把源代码拷贝过去应该就可以。因为项目中除了C语言的标准库,并没有用到其他的三方库。

另外这也是本人用C实现的第一个完整项目,诸多不足之处,请多多指教~

6、实现过程第一步

6.1 打开需要解析的h264文件

这是一切开始的第一步,过程很简单,就是打开要分析的h264文件,然后将它读入缓冲区。我们遵循最简单方便的原则,缓冲区开辟50M,这样就能将h264码流一次读入缓冲区,而不用考虑其他操作。

FILE *fp_h264 = fopen("test.h264", "rb");
   if (fp_h264 == NULL) {
       printf("打开h264文件失败");
       return -1;
   }
   // 0.使用指针指向的堆空间作为缓冲区
   uint8_t *buff = (uint8_t *)malloc(MAX_BUFFER_SIZE);
   // 1.一次读进缓冲区
   int buff_size = (int)fread(buff, sizeof(uint8_t), MAX_BUFFER_SIZE, fp_h264);
   printf("totalSize: %d\n", buff_size);

6.2 实现找到一个nalu

/**
找到h264码流中的nalu
[h264协议文档位置]:Annex B

@param buff h264码流
@param buff_size 码流大小
@param curr_nal_start 当前找到的nalu的起始位置
@param curr_find_index 当前读取的指针位置
@return nalu的大小
*/
int find_nal_unit(uint8_t *buff, int buff_size, int *curr_nal_start, int *curr_find_index) {
   int *i = curr_find_index;
   //( next_bits( 24 ) != 0x000001 && next_bits( 32 ) != 0x00000001 )
   // 寻找起始码,只要有一位不满足,则继续向下寻找
   while (
          (buff[*i] != 0x00 || buff[*i+1] != 0x00 || buff[*i+2] != 0x01) &&
          (buff[*i] != 0x00 || buff[*i+1] != 0x00 || buff[*i+2] != 0x00 || buff[*i+3] != 0x01)
          ) {
       *i = *i + 1;
       if (*i+3 > buff_size) {return 0;} // 没有找到,退出函数
   }
   
   // 找到起始码,判断如果不是0x000001,则是0x00000001,则将读取索引加1
   // if( next_bits( 24 ) != 0x000001 )
   if (buff[*i] != 0x00 || buff[*i+1] != 0x00 || buff[*i+2] != 0x01) {
       *i = *i + 1; // 读取索引加1
   }
   
   *i += 3; // 读取索引加3
   *curr_nal_start = *i;
   
   // 到达nalu部分
   int j = 0;
   // 寻找结尾
   //( next_bits( 24 ) != 0x000000 && next_bits( 24 ) != 0x000001 )
   while (
          (buff[*i] != 0x00 || buff[*i+1] != 0x00 || buff[*i+2] != 0x00) &&
          (buff[*i] != 0x00 || buff[*i+1] != 0x00 || buff[*i+2] != 0x01)
          ) {
       
       nalu_buf[j] = buff[*i]; // 将读取到的nalu存放在全局变量nalu当中
       j++;
       *i = *i +1;
       if (*i+3 >= buff_size) { // 寻找到文件结尾
      
           nalu_buf[j] = buff[*i];
           nalu_buf[j+1] = buff[*i+1];
           nalu_buf[j+2] = buff[*i+2];
           nalu_buf[j+3] = buff[*i+3];
           return buff_size - *curr_nal_start;
       }
   }
   return *curr_find_index - *curr_nal_start;
}

实现过程如上所示,输入参数定为4个:

  • 第一个肯定是h264码流的buff
  • 第二个是buff的大小
  • 第三个是当前读取到的nalu的起始位置,也即Start_Code_Prefix下一字节在buff中的位置
  • 第四个记录了当前读取指针在buff中的位置

其中第三和第四个参数,因为要循环使用,所以传入的是指针。这一过程会读取到一个nal单元,并将读取到的nalu的字节流,暂时存放在全局变量nalu_buf中。

这样写可以高度契合h264的协议文档,文档部分参照Annex B,也即附录B部分。

6.3 循环读取码流中的所有nalu

6.2节只是实现了读取一个nalu,我们需要读取所有的nalu,这就需要一个循环:

int nalu_i = 0;
int nalu_size = 0;
int curr_nal_start = 0;  // 当前找到的nalu起始位置
int curr_find_index = 0; // 当前查找的位置索引
   
// 3.找到h264码流中的各个nalu
while ((nalu_size = find_nal_unit(buff, buff_size, &curr_nal_start, &curr_find_index)) > 0) {
	printf("nalu: %d, start: %d, index: %d, size: %d\n", nalu_i, curr_nal_start, curr_find_index, curr_find_index - curr_nal_start);
	nalu_i++;
}

循环很简单,就是不停的find_nal_unit(),直到返回的nalu_size不为正结束。

本文源码地址如下:

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值