H.264裸流结构分析

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

H.264简介

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

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

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

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

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

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

总览

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

VCL

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

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

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

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

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

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

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

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

宏块

宏块

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

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

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

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

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

片共有5种类型:

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

片组

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

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

SODB

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

NAL

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

SPSPPSAccess 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 的内部,出现了 0x0000010x000000 时该怎么办?

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

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

0x0000000x000001 是为了区分起始码和规定的结束,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;
}