H.264码流结构是怎么样的

        视频有很多种编码标准,H.264,H.265,AV1等等,其中我们可能最常见的便是H.264,因此,本文我们就主要来分下下H.264编码的码流结果具体是怎么样的。

一、前置知识

        了解H264码流结构之前,需要先了解一些前置的知识。

1、I/P/B帧和GOP

        需要先了解图像序列的层次结构,也就是一帧帧视频帧的一些概念。这些可以参考我前面的一篇文章 视频相关的一些基本概念 ,会比较系统和具体地讲解。

2、Slice

        上面主要是图像序列的层次结构,而图像的内部层次结构是怎么样的,这主要涉及Slice这个概念。

        Slice其实就是“片”的概念,图像内的层次结构就是一帧图像可以划分成一个或多个 Slice,而一个 Slice 包含多个宏块,且一个宏块又可以划分成多个不同尺寸的子块。大概就是类似下面这样的结构图:

二、H264码流结构

1、码流格式

        H264 码流有两种格式:一种是 Annexb 格式;一种是 MP4 格式。

(1)、Annexb 格式使用起始码来表示一个编码数据的开始。起始码本身不是图像编码的内容,只是用来分隔用的。起始码有两种,一种是 4 字节的“00 00 00 01”,一种是 3 字节的“00 00 01”。

        由于图像编码出来的数据中也有可能出现“00 00 00 01”和“00 00 01”的数据。为了防止出现这种情况,H264 会将图像编码数据中的下面的几种字节串做如下处理:

        “00 00 00”修改为“00 00 03 00”;

        “00 00 01”修改为“00 00 03 01”;

        “00 00 02”修改为“00 00 03 02”;

        “00 00 03”修改为“00 00 03 03”。

        其实也就是转义,同样地在解码端,我们在去掉起始码之后,也需要将对应的字节串转换回来。

 (2)、MP4 格式没有起始码,而是在图像编码数据的开始使用了 4 个字节作为长度标识,用来表示编码数据的长度,这样我们每次读取 4 个字节,计算出编码数据长度,然后取出编码数据,再继续读取 4 个字节得到长度,一直继续下去就可以取出所有的编码数据了。

         这两种格式差别不大,接下来我们主要使用 Annexb 格式来讲解 H264 码流结构。

2、SPS和PPS

        视频编码的时候还有一些编码参数数据的,为了能够将一些通用的编码参数提取出来,不在图像编码数据中重复,H264 设计了两个重要的参数集:一个是 SPS(序列参数集);一个是 PPS(图像参数集)。

        SPS 主要包含的是图像的宽、高、YUV 格式和位深等基本信息;PPS 则主要包含熵编码类型、基础 QP 和最大参考帧数量等基本编码信息。

        如果没有 SPS、PPS 里面的基础信息,之后的 I 帧、P 帧、B 帧就都没办法进行解码。因此 SPS 和 PPS 是至关重要的。

        这样的话,H264码流主要包含了 SPS、PPS、I 帧、P 帧和 B 帧。由于帧又可以划分成一个或多个 Slice。因此,帧在码流中实际上是以 Slice 的形式呈现的。所以,H264 的码流主要是由 SPS、PPS、I Slice、P Slice和B Slice 组成的。如下图所示:

 3、NALU

        上面说到H264码流的组成部分,但是每个部分是如何区分开?为了解决这个问题,H264 设计了 NALU(网络抽象层单元)。SPS 是一个 NALU、PPS 是一个 NALU、每一个 Slice 也是一个 NALU。每一个 NALU 又都是由一个 1 字节的 NALU Header 和若干字节的 NALU Data 组成的。而对于每一个 Slice NALU,其 NALU Data 又是由 Slice Header 和 Slice Data 组成,并且 Slice Data 又是由一个个 MB Data 组成。其结构如下:

        其中,NALU Header总共占用 1 个字节,具体如下图所示。

        其中,

        --> F:forbidden_zero_bit,占 1bit,禁止位,H264 码流必须为 0;

        --> NRI: nal_ref_idc,占 2bits,可以取 00~11,表示当前 NALU 的重要性。参考帧、SPS 和 PPS 对应的 NALU 必须要大于 0;

        --> Type: nal_unit_type,占 5bits,表示 NALU 类型。其取值如下表所示。

         有了这个,我们解析出 NALU Header 的 Type 字段,查询表格就可以得到哪个 NALU 是 SPS,哪个是 PPS,以及哪个是 IDR 帧了。

        不过NALU 类型只区分了 IDR Slice 和非 IDR Slice,至于非 IDR Slice 是普通 I Slice、P Slice 还是 B Slice,则需要继续解析 Slice Header 中的 Slice Type 字段得到。

        下面我们通过两个例子来看看常见的 NALU 里的 NALU Header 是怎样的:

         下面我们再来看一个实际码流的例子,看看在实际编码出来的二进制数据中,各种 NALU 是怎么“放置”在数据中的。下图是用二进制查看工具打开实际编码后的码流数据。我们可以看到在码流的开始部分是一个起始码,之后紧接着是一个 SPS 的 NALU。在 SPS 后面是一个 PPS 的 NALU。然后就是一个 IDR Slice 的 NALU 和一个非 IDR Slice NALU。

 三、如何判断哪几个 Slice 是同一帧的

         根据上面的分析,在H264 码流中,帧是以 Slice 的方式呈现的,或者可以说在 H264 码流里是没有“帧“这种数据的,只有 Slice。

        那么有个问题,一帧有几个 Slice 是不知道的。也就是说码流中没有字段表示一帧包含几个 Slice。既然没有办法知道一帧有几个 Slice,那我们如何知道多 Slice 编码时一帧的开始和结束分别对应哪个 Slice 呢?

        其实,Slice NALU 由 NALU Header 和 NALU Data 组成,其中 NALU Data 里面就是 Slice 数据,而 Slice 数据又是由 Slice Header 和 Slice Data 组成。在 Slice Header 开始的地方有一个 first_mb_in_slice 的字段,表示当前 Slice 的第一个宏块 MB 在当前编码图像中的序号。我们只要解析出这个宏块的序号出来。

        --> 如果 first_mb_in_slice 的值等于 0,就代表了当前 Slice 的第一个宏块是一帧的第一个宏块,也就是说当前 Slice 就是一帧的第一个 Slice。

        --> 如果 first_mb_in_slice 的值不等于 0,就代表了当前 Slice 不是一帧的第一个 Slice。并且,使用同样的方式一直往下找,直到找到下一个 first_mb_in_slice 为 0 的 Slice,就代表新的一帧的开始,那么其前一个 Slice 就是前一帧的最后一个 Slice 了。

         其中,first_mb_in_slice 是以无符号指数哥伦布编码的,需要使用对应的解码方式才能解码出来。但是有一个小技巧,如果只是需要判断 first_mb_in_slice 是不是等于 0,不需要计算出实际值的话,只需要通过下面的方式计算就可以了。

         以上便是对H264码流结构的讲解。

猜你喜欢

转载自blog.csdn.net/weekend_y45/article/details/125283998
今日推荐