H.264码流解析

全新系列文章已更新:


这一篇内容旨在对H.264码流中的一些概念做简单了解。

1、概念了解

VCL:Video Coding Layer视频编码层,它是H.264(AVC)编码中的核心,负责视频数据的编码工作。VCL层会应用一系列的图像压缩技术,如预测编码、变换编码、量化、熵编码等,将原始视频数据压缩成二进制比特流,压缩生成的比特流也被称为原始数据比特流SODB(String of Data Bits)

NAL:Network Abstraction Layer网络抽象层,将SODB打包封装便于网络传输,里面有如下内容需要做了解:

  • 网络传输一般是以字节为单位,但是SODB的长度不一定是整数字节,所以第一步是要把SODB扩充为整数字节,扩充后的数据被称为RBSP(Raw Byte Sequence Payload)。扩充方式是在SODB的末尾添加一个比特位1,如果数据仍不是整数字节,则在末尾添加比特位0,直到数据变成整数字节。

    • 假设我们原始的SODB是一个7位的二进制数据1001100,将它转化为RBSP时,我们会首先在尾部添加1得到10011001,长度变成8,所以不需要再添加0。

    • 如果原始的SODB是一个10位的二进制数据1001100110,将它转化为RBSP时,我们会首先在尾部添加1得到10011001101,长度变成11,需要再添加5个0,最后的RBSP为1001100110100000。

  • 要将SODB进行封装,形成的独立单元被称为NALU(Network Abstraction Layer Unit)。NAL层在每个NALU单元前面都加了Start Code,Start Code常见的形式有0x0000010x00000001,通过寻找Start Code,很容易就知道NALU在码流中起始和结束位置R了。什么时候用3字节的start code,什么时候用4字节的,我在网络上没有找到明确的说法,在实际解析过程中,只要找3字节的start code就可以了。

  • Start Code的加入会引发一些问题,如果SODB中原本就包含0x0000010x00000001这两个序列的数据,那么解析NALU起始和结束位置解析就会出现错误了。为了解决这个问题,需要对SODB中的部分字节序列进行处理,添加模拟防止字节(Emulation Prevention Byte)0x03。具体的做法是,如果检查到连续两个字节是0x0000,如果后面一个字节的值小于等于3(00、01、10、11),那么就在最后一个字节前面添加一个0x03:

    • 数据中原本是0x000000 -> 0x00000300
    • 数据中原本是0x000001 -> 0x00000301
    • 数据中原本是0x000002 -> 0x00000302
    • 数据中原本是0x000003 -> 0x00000303
    • SODB加入模拟防止字节后被称为封装字节序列负载EBSP(Encapsulate Byte Sequence Payload),NALU中封装的负载数据就是它了。

2、NALU

NALU结构如下:

在这里插入图片描述

一个NALU由Start Code、NALU Header以及NALU Payload三部分组成,Start Code在前面已经了解过了,所以这里先来看NALU Header。

NALU Header总共一个字节,它包含如下三个字段:
bit 0: forbidden_zero_bit:这一位始终为0;
bit 1-2: nal_ref_idc:参考指数,取值范围为0-3,表示这个NALU的重要性,如果等于11,表示是序列参数集,图像参数集,I帧等关键信息帧;如果非11,表示为其他可丢弃帧。
bit 3-7: nal_unit_type:NALU类型,取值范围为0-31,这里用表格列出值以及对应类型:

nal_unit_typeNALU 类型
0未使用
1编码切片的非IDR图像 Coded slice of a non-IDR picture
2编码片分区A Coded slice data partition A
3编码片分区B
4编码片分区C
5IDR图像 Coded slice of an IDR picture
6SEI补充增强信息 Supplemental enhancement information (SEI)
7序列参数集 Sequence parameter set
8图像参数集 Picture parameter set
9分界符 Access unit delimiter(AUD)
10序列结束 End of sequence
11流结束 End of stream
12填充 Filler data
13序列参数集扩展 Sequence parameter set extension
14-18保留 Reserved
19未分割的辅助图像单元包 Auxiliary slice of a coded picture
20-23保留 Reserved
24-31未规定 Unspecified

接下来介绍几个概念:

  • I帧:Intra-coded Picture,I帧被称为关键帧,包含了图像全景的所有信息,所以解码不需要依靠其他任何帧;

  • P帧:Predicted Picture,P帧被称为预测帧,P帧表示了前一帧与当前帧之间的差异,前一帧可以是I帧也可以是P帧,用该变量替代图像所有信息可以大大压缩数据;

  • B帧:Bidirectional Predicted Frame,B帧被称为双向预测帧,它的内容通过在其之前和之后的帧(可以是I帧或P帧)上进行插值或运动估计来预测,更进一步提高了压缩效率,但是增加了编码和解码的复杂度;

  • IDR帧:Instantaneous Decoding Refresh Frame,IDR帧被称为即时刷新帧,解码器会丢弃所有历史数据开始新的解码流程,这也是为什么叫即时刷新帧的原因;解码时P帧可能要参考之前的P帧或者I帧,B帧可能要依赖前后帧,IDR帧出现后,后面的帧不会再参考之前的帧;IDR帧解码也是不需要参考其他任何帧的,所以它也属于I帧;

  • GOP:Group of Pictures图像组,指的是由I帧(关键帧)、P帧(预测帧)和B帧(双向预测帧)按一定的顺序组成的视频帧序列,第一个帧通常是I帧,跟随后的帧可以是P帧或者B帧,I帧用于提供全新的画面,P帧和B帧则表示从前一个帧到当前帧的变化;在进行视频播放时,如果中途接收的数据中出现丢失,那么可以通过快速识别GOP结构,丢弃从丢失点开始的整个GOP,然后从下一个I帧(即下一个GOP的开始)重新拉取数据,避免了由于数据丢失造成的视频播放卡顿。

接下来了解以下,如何用代码查找一个完整的NALU:

status_t getNextNALUnit(
        const uint8_t **_data, size_t *_size,
        const uint8_t **nalStart, size_t *nalSize,
        bool startCodeFollows) {
    const uint8_t *data = *_data;
    size_t size = *_size;

    *nalStart = NULL;
    *nalSize = 0;
    // 1
    if (size < 3) {
        return -EAGAIN;
    }

    size_t offset = 0;

    // 2
    for (; offset + 2 < size; ++offset) {
        if (data[offset + 2] == 0x01 && data[offset] == 0x00
                && data[offset + 1] == 0x00) {
            break;
        }
    }
    // 3
    if (offset + 2 >= size) {
        *_data = &data[offset];
        *_size = 2;
        return -EAGAIN;
    }
    offset += 3;

    size_t startOffset = offset;

    for (;;) {
        while (offset < size && data[offset] != 0x01) {
            ++offset;
        }

        if (offset == size) {
            if (startCodeFollows) {
                offset = size + 2;
                break;
            }

            return -EAGAIN;
        }
        // 4
        if (data[offset - 1] == 0x00 && data[offset - 2] == 0x00) {
            break;
        }

        ++offset;
    }
    // 5
    size_t endOffset = offset - 2;
    // 6
    while (endOffset > startOffset + 1 && data[endOffset - 1] == 0x00) {
        --endOffset;
    }

    *nalStart = &data[startOffset];
    *nalSize = endOffset - startOffset;

    if (offset + 2 < size) {
        *_data = &data[offset - 2];
        *_size = size - offset + 2;
    } else {
        *_data = NULL;
        *_size = 0;
    }

    return OK;
}
  1. 检查数据长度是否大于3,一个NALU至少要包含3字节的start code,如果长度小于3那么肯定不包含完整的NALU;
  2. 查找第一个NALU的start code位置;
  3. 检查是否找到start code,如果到数据末尾三位都没找到,说明数据中不包含NALU;
  4. 如果找到start code,那么就要去找下一个NALU的start code;
  5. 如果找到了下一个NALU的start code,那么此时endOffset位于下一个start code 0x01位置上,此时需要把offset移动到0x00 0x00 0x01序列的第一个0x00上(start code第一位上);
  6. 第六步的动作有一点要说明,如果NALU的结尾是0x00,那么在下一个start code之前需要添加一个0x00,这个0x00不属于前面一个NALU,所以要跳过添加的0x00这个字节;
    • 按照我的理解,只要检查start code前面一个字节是否为0x00就行,如果是endOffset向前移动一位久可以了,不知道这里为什么哟用while来处理。

3、SPS

SPS:Sequence Parameter Set序列参数集,SPS包含了编解码一系列的参数,包含的信息涉及字段非常多,主要包括以下几类:

  • 基本编码参数:比如profile_idc、level_idc等用以指明编码级别和复杂度的参数;
  • 视频序列的相关信息:包括编码图像的宽和高、图像比例、帧率等信息;
  • 编码结构的参数:诸如用于帧内预测和帧间预测的模式选择、参考帧数、B帧数等参数;
  • 熵编码模式:CABAC或CAVLC;
  • 量化参数:包括量化矩阵和初始QP等。

SPS的解析需要参考相关spec,这里只贴出android的部分解析代码:

关注公众号《青山渺渺》阅读完整内容; 如有问题可在公众号后台私信,也可进入音视频开发技术分享群一起讨论!

在这里插入图片描述

  • 27
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青山渺渺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值