H.264裸流结构分析

简单分析一下 H.264 AnnexB 格式的裸流格式

H.264简介

国际上制定视频编解码技术的组织有两个

  • 国际电联(ITU-T),它制定的标准有 H.261、H.263、H.263+ 等
  • 国际标准化组织(ISO),它制定的标准有 MPEG-1、MPEG-2、MPEG-4 等。

而 H.264 则是由这两个组织联合组建的 联合视频组(JVT) 共同制定的 新数字视频编码标准,所以它既是 ITU-T 的 H.264,又是 ISO/IEC 的 MPEG-4 高级视频编码(Advanced Video Coding,AVC),是新一代数字视频压缩格式。

H.264 的算法在概念上可以分为两层:

  • 视频编码层(VCL:Video Coding Layer)负责高效的视频内容表示
  • 网络提取层(NAL:Network Abstraction Layer)负责以网络所要求的恰当的方式对数据进行打包和传送

整个 H.264 可以简化成以下这张图

总览

这里面涉及到很多要素,VCL、SODB、 RBSP、 EBSP 等等,接下来逐个介绍,尽量说清楚

VCL

视频编码层主要就是通过各种编码压缩去除视频中重复部分,也就是冗余,常见的冗余分为以下几种:

  • 帧内压缩,空间上存在大量冗余
  • 帧间压缩,时间上存在大量冗余
  • 感知冗余,高频信号敏感度不强,数值量化
  • 编码冗余,使用可变编码,可压缩编码冗余

而 感知(视觉)冗余 又可以分为以下几种

  • 对亮度的变化敏感,对色度的变化相对不敏感
  • 对静止图像敏感,对运动图像相对不敏感
  • 对图像的水平线条和竖直线条敏感,对斜线相对不敏感
  • 对整体结构敏感,对内部细节相对不敏感
  • 对低频信号敏感,对高频信号相对不敏感

一般情况下对视频压缩会经过以下几个过程

  • 预测:去除空间和时间冗余:帧内预测(划分的宏块预测),帧间预测
  • 变化:去除感知冗余:DCT, 小波变换
  • 量化:去除视觉冗余,通过降低图像质量提高压缩比
  • 熵编码:去除编码冗余:变长编码,算术编码

H.264 和以前的标准一样,也是 变换编码 加 预测编码 的 混合编码模式。
在此基础上还增加了如多模式运动估计、帧内预测、多帧预测、基于内容的变长编码、4x4二维整数变换等新的编码方式,提高了编码效率。

接下来对编码的一些基本单位稍微了解一下:

宏块

宏块

是 H.264 编码的基本单位,一个编码图像首先要划分成多个块(4x4 像素)才能进行处理,通常宏块大小为 16x16 个像素。

宏块也分为 I、P、B 宏块:

  • I宏块 只能利用当前片中已解码的像素作为参考进行帧内预测;
  • P宏块 可以利用前面已解码的图像作为参考图像进行帧内预测;
  • B宏块 则是利用前后向的参考图形进行帧内预测

片

一帧视频图像可编码成一个或者多个 片(Slice),每 片 包含整数个 宏块,即每 片 至少一个 宏块,最多时包含整个图像的 宏块。

片 的目的:为了限制误码的扩散和传输,使编码片相互间保持独立。

片共有5种类型:

  • I 片 :只包含 I 宏块
  • P 片 :P 和 I 宏块
  • B 片 :B 和 I 宏块
  • SP 片 :用于不同编码流之间的切换
  • SI 片 :特殊类型的编码宏块

片组

片组,其实就是使用某一规则,将一帧画面中的某些 宏块 划分成一个组
而 片 就是是在 片组 内,对 宏块 做进一步划分。

所以这三者的关系是 一帧画面,是由一个或多个 片组 组成,片组 是由一个或多个 片 组成,片 则由 宏块 组成。

SODB

String of Data Bits, 数据 bit 流,是最原始的编码后的数据

NAL

NAL 负责以网络所要求的恰当的方式对数据进行打包和传送
在一个 H.264 AnnexB 格式的比特流中,NALU 是其中一个基本单元,通过引入起始码(0x000001 or 0x00000001) 来将 NALU 隔开

在 SPS、PPS 和 Access Unit 的第一个 NALU 使用 4 字节起始码,其余情况均使用 3 字节起始码。

对起始码的格式有个印象 (0x000001或者0x00000001) ,之后还有因为它引入其它的设计

我们继续从原始数据 SODB 开始,看最后如何变成一个比特流

RBSP

Raw Byte Sequence Payload,原始字节序列载荷
在 SODB 的后面添加了 trailing bits 结尾比特
也就是一个bit 1 和若干个bit 0,以便字节对齐

EBSP

Encapsulate Byte Sequence Payload 扩展字节序列载荷
在 RBSP 基础上填加了防竞争字节 0x03 ,这样便形成了 EBSP。

为什么需要增加防竞争字节,主要是因为前面提到的起始码格式是 (0x000001或者0x00000001), 同时 H.264规定,当检测到 0x000000 时,也可以表示当前 NALU 的结束。那就会产生一个问题,就是如果在 NALU 的内部,出现了 0x000001 或 0x000000 时该怎么办?

为了使 NALU 主体中不出现与 0x000001 或 0x000000 冲突的情况,在给 NALU 添加起始码之前,先对码流进行一次遍历,查找码流里面的存在的 0x000000,0x000001,0x000002,0x000003 的字节,然后对其进行如下修改

0x000000 => 0x00000300
0x000001 => 0x00000301
0x000002 => 0x00000302
0x000003 => 0x00000303

0x000000 和 0x000001 是为了区分起始码和规定的结束,0x000002 是作为保留使用,而 0x000003,则是为了防止 NALU 内部,原本就有序列为 0x000003 这样的数据。

那么解码时先遍历将 0x03 去掉即可。也称为脱壳操作。

NALU Header

在 EBSP 的基础上加一个 NALU Header 就变成一个 NALU

NALU Header 在整个 NALU 中,只占据一个字节。分别为:

  • forbidden_zero_bit
  • nal_ref_idc
  • nal_unit_type

forbidden_zero_bit 值对应 1 个 bit, 这个值应该为 0,当它不为 0 时,表示网络传输过程中,当前 NALU 中可能存在错误,解码器可以考虑不对这个 NALU 进行解码。

nal_ref_idc 值对应 2 个 bit, 取值 0~3,代表当前这个 NALU 的重要性,取值越大,代表当前 NALU 越重要,就需要优先被保护。

nal_unit_type 值对应 5 个 bit, 代表 NALU Header 后面的 EBSP 的数据结构的类型。

nal_unit_type

裸流解析

本部分是对一个 H.264 的裸流进行分离并解析 NALU,代码直接使用的是 雷霄骅 大佬博客中提供的例子,并添加了一些简单的注释

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef enum {
    NALU_TYPE_SLICE    = 1,
    NALU_TYPE_DPA      = 2,
    NALU_TYPE_DPB      = 3,
    NALU_TYPE_DPC      = 4,
    NALU_TYPE_IDR      = 5,
    NALU_TYPE_SEI      = 6,
    NALU_TYPE_SPS      = 7,
    NALU_TYPE_PPS      = 8,
    NALU_TYPE_AUD      = 9,
    NALU_TYPE_EOSEQ    = 10,
    NALU_TYPE_EOSTREAM = 11,
    NALU_TYPE_FILL     = 12,
} NaluType;

typedef enum {
    NALU_PRIORITY_DISPOSABLE = 0,
    NALU_PRIRITY_LOW         = 1,
    NALU_PRIORITY_HIGH       = 2,
    NALU_PRIORITY_HIGHEST    = 3
} NaluPriority;

typedef struct
{
    int startcodeprefix_len;      // NALU Header 的长度,3个或者4个字节长度
    unsigned len;                 // NALU 中 EBSP 的长度
    unsigned max_size;            // 缓存数据时的最大长度
    int forbidden_bit;            // 默认必须为 0
    int nal_reference_idc;        // NALU 重要性
    int nal_unit_type;            // NALU 的类型
    char *buf;                    // 指向 EBSP 第一个字节的指针
} NALU_t;

// the bit stream file
FILE *h264bitstream = NULL;               

int info2=0, info3=0;

static int FindStartCode2 (unsigned char *Buf){
    if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=1) return 0; //0x000001?
    else return 1;
}

static int FindStartCode3 (unsigned char *Buf){
    if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=0 || Buf[3] !=1) return 0;//0x00000001?
    else return 1;
}


int GetAnnexbNALU (NALU_t *nalu){
    int pos = 0;
    int StartCodeFound, rewind;
    unsigned char *Buf;

    if ((Buf = (unsigned char*)calloc (nalu->max_size , sizeof(char))) == NULL) 
        printf ("GetAnnexbNALU: Could not allocate Buf memory\n");

    nalu->startcodeprefix_len=3;

    // 先尝试读取 1*3 个字节
    if (3 != fread (Buf, 1, 3, h264bitstream)){
        free(Buf);
        return 0;
    }

    // 判断裸流中 Header 时 0x000001 还是 0x00000001
    info2 = FindStartCode2 (Buf);
    if(info2 != 1) {
        // 不是 0x000001, 则在多读一个字节
        if(1 != fread(Buf+3, 1, 1, h264bitstream)){
            free(Buf);
            return 0;
        }
        // 判断是不是 0x00000001
        info3 = FindStartCode3 (Buf);
        if (info3 != 1){ 
            // 既不是 0x000001 也不是 0x00000001,裸流有问题
            free(Buf);
            return -1;
        }
        else {
            pos = 4;
            nalu->startcodeprefix_len = 4;
        }
    }
    else{
        nalu->startcodeprefix_len = 3;
    }

    // 开始找下一个 NALU 的起始码,定位上一个 NALU 数据的长度
    StartCodeFound = 0;
    info2 = 0;
    info3 = 0;

    while (!StartCodeFound){
        // 如果到文件尾了,直接结算
        if (feof (h264bitstream)){
            nalu->len = (pos-1)-nalu->startcodeprefix_len;
            memcpy (nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);     
            nalu->forbidden_bit = nalu->buf[0] & 0x80; //1 bit
            nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
            nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;// 5 bit
            free(Buf);
            return pos-1;
        }
        // 尝试一个字节一个字节向后读取
        // 判断是否含有 0x000001 还是 0x00000001
        Buf[pos++] = fgetc (h264bitstream);
        info3 = FindStartCode3(&Buf[pos-4]);
        if(info3 != 1)
            info2 = FindStartCode2(&Buf[pos-3]);
        StartCodeFound = (info2 == 1 || info3 == 1);
    }
    
    // pos 相当于 记录了这一次读取的总字节数
    // nalu->startcodeprefix_len 中记录了本次 NALU 中起始码的长度
    // rewind 时记录了下一个 NALU 起始码的长度
    rewind = (info3 == 1)? -4 : -3;

    // 重置 fread 读取的位置到下一次 NALU 起始码前
    if (0 != fseek (h264bitstream, rewind, SEEK_CUR)){
        free(Buf);
        printf("GetAnnexbNALU: Cannot fseek in the bit stream file");
    }

    nalu->len = (pos+rewind)-nalu->startcodeprefix_len;
    memcpy (nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);
    nalu->forbidden_bit = nalu->buf[0] & 0x80; //1 bit
    nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
    nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;// 5 bit
    free(Buf);

    return (pos+rewind);
}

int simplest_h264_parser(char *url){

    NALU_t *n;
    int buffersize=100000;

    FILE *myout=stdout;

    h264bitstream=fopen(url, "rb+");
    if (h264bitstream==NULL){
        printf("Open file error\n");
        return 0;
    }

    n = (NALU_t*)calloc (1, sizeof (NALU_t));
    if (n == NULL){
        printf("Alloc NALU Error\n");
        return 0;
    }

    n->max_size=buffersize;
    n->buf = (char*)calloc (buffersize, sizeof (char));
    if (n->buf == NULL){
        free (n);
        printf ("AllocNALU: n->buf");
        return 0;
    }

    int data_offset=0;
    int nal_num=0;
    printf("-----+-------- NALU Table ------+---------+\n");
    printf(" NUM |    POS  |    IDC |  TYPE |   LEN   |\n");
    printf("-----+---------+--------+-------+---------+\n");

    while(!feof(h264bitstream)) 
    {
        int data_lenth;
        data_lenth=GetAnnexbNALU(n);

        char type_str[20]={0};
        switch(n->nal_unit_type){
            case NALU_TYPE_SLICE:sprintf(type_str,"SLICE");break;
            case NALU_TYPE_DPA:sprintf(type_str,"DPA");break;
            case NALU_TYPE_DPB:sprintf(type_str,"DPB");break;
            case NALU_TYPE_DPC:sprintf(type_str,"DPC");break;
            case NALU_TYPE_IDR:sprintf(type_str,"IDR");break;
            case NALU_TYPE_SEI:sprintf(type_str,"SEI");break;
            case NALU_TYPE_SPS:sprintf(type_str,"SPS");break;
            case NALU_TYPE_PPS:sprintf(type_str,"PPS");break;
            case NALU_TYPE_AUD:sprintf(type_str,"AUD");break;
            case NALU_TYPE_EOSEQ:sprintf(type_str,"EOSEQ");break;
            case NALU_TYPE_EOSTREAM:sprintf(type_str,"EOSTREAM");break;
            case NALU_TYPE_FILL:sprintf(type_str,"FILL");break;
        }
        char idc_str[20]={0};
        switch(n->nal_reference_idc>>5){
            case NALU_PRIORITY_DISPOSABLE:sprintf(idc_str,"DISPOS");break;
            case NALU_PRIRITY_LOW:sprintf(idc_str,"LOW");break;
            case NALU_PRIORITY_HIGH:sprintf(idc_str,"HIGH");break;
            case NALU_PRIORITY_HIGHEST:sprintf(idc_str,"HIGHEST");break;
        }

        fprintf(myout,"%5d| %8d| %7s| %6s| %8d|\n",nal_num,data_offset,idc_str,type_str,n->len);

        data_offset=data_offset+data_lenth;

        nal_num++;
    }

    if (n){
        if (n->buf){
            free(n->buf);
            n->buf=NULL;
        }
        free (n);
    }
    return 0;
}