音视频开发基础(五)H264基础

一、H.264是什么(是一种标准)

首先H.264是一种高度压缩数字视频压缩编码器的标准。H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。这里说一下X264它是一个编码器,参照的标准是H.264标准。编码出来的数据就是H264数据。

二、H264相关名词解释

NALU:H264编码数据存储或传输的基本单元,一般H264码流最开始的两个NALU是SPS和PPS,第三个NALU是IDR。SPS、PPS、SEI这三种NALU不属于帧的范畴。

SPS(Sequence Parameter Sets):序列参数集,作用于一系列连续的编码图像。

PPS(Picture Parameter Set):图像参数集,作用于编码视频序列中一个或多个独立的图像。

SEI(Supplemental enhancement information):附加增强信息,包含了视频画面定时等信息,一般放在主编码图像数据之前,在某些应用中,它可以被省略掉。

IDR(Instantaneous Decoding Refresh):即时解码刷新

HRD(Hypothetical Reference Decoder):假想码流调度器

序列 sequence:一段h264码流其实是由多个sequence组成的。一个sequence就是一秒如果FPS为20,则包含20帧图像(I/PB)帧。每个sequence均有固定结构单元:1sps+1pps+1sei+1I帧+若干p帧(加上B帧一共有6种单元情况)

分隔符:H.264在编码的时候,生成一个序列时,序列中每个单元前面就会加上00 00 00 01或者00 00 01作为分隔符。

三、H.264压缩方法

  • 1.分组:把几帧图像分为一组(GOP)为防止运动变化,不宜取多

  • 2.定义帧:将每组内各帧图像定义为三种类型,即I\P\B帧

  • 3.预测帧:以I帧为基础帧(关键帧),以I帧预测P帧,再以I帧P帧预测B帧

  • 4.数据传输:最后将I帧数据与预测的差值信息进行存储和传输

  • 帧内压缩,也称为空间压缩。当压缩一帧图像时候,仅考虑本帧的数据,而考虑相邻帧直接的冗余信息。这实际与静态帧压缩相似。帧内压缩一般使用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立解码、显示。帧内压缩达不到很大的压缩程度,和JPEG相似

  • 帧间压缩,它的原理是:相邻几帧关联性很大,或者说前后两帧信息变化很小的特点。即连续的视频相邻的帧具有冗余信息,利用这个特点,压缩相邻帧直接的冗余信息就可以进一步提高压缩量。帧间压缩也叫时间压缩,通过比较时间轴之间的数据进行压缩。帧间压缩一般是无损的。(帧差值算法是一种典型的时间压缩算法)

  • 有损压缩与无损压缩:无损压缩即压缩前和解压后数据完全一致。多数无损压缩算法都用RLE行程编码算法。有损压缩意味着压缩后的数据与压缩前的不一致。在压缩过程中要丢失一些人眼和人耳所不敏感的图像或音频信息,而且丢失的信息不可恢复。几乎所有的高压缩算法都是有损压缩算法。

四、H.264码流 VCL NAL

H264码流可以分为两层 VCL与NAL

VCL:视频编码中采用的如预测编码、变化量化、熵编码等编码工具主要工作在slice层或以下,这一层通常被称为“视频编码层”(Video Coding Layer, VCL)。

VCL层存在三种封装格式:

  • SODB:数据比特串,即编码后的最原始的数据;
  • RBSP:原始字节序列载荷,即在SODB的后面添加了trailing bits,即一个bit 1和若干个bit 0,以便字节对齐;
  • EBSP:扩展字节序列载荷,即在RBSP的基础上添加了仿校验字节0x03.—-annexb
三者的联系

SODB是编码收的原始数据,经过封装后为RBSP,RBSP是NAL单元的数据部分的封装格式。在NAL内部为了防止与起始码竞争,从而引入填充字节0x03,这样便形成了EBSP。

NAL:相对的,在slice以上所进行的数据和算法通常称之为“网络抽象层”(Network Abstraction Layer, NAL)。设计定义NAL层的主要意义在于提升H.264格式的视频对网络传输和数据存储的亲和性。

NAL保存了H264相关参数信息和图像信息。NAL层由多个NALU组成。
一个完整的NALU单元结构图如下。
一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成,其中 Start Code 用于标示这是一个NALU 单元的开始,必须是"00 00 00 01" 或"00 00 01"(如果NALU对应的Slice为一帧的开始,则用4字节表示,即0x00000001;否则用3字节表示,0x000001)

H264在网络传输的是NALU,NALU的结构是:NAL头(一个字节)+RBSP。
音视频开发基础(五)H264基础_第1张图片

五、H.264Nal单元NAL头(注意0x00000001/0x000001是分隔符不是NAL头)

在上面我们已经讲了在NAL层传输的是NALU单元,而每个NALU单元都包含一个字节的NAL头。
NAL Header:

  • forbidden_bit 1bit
  • nal_reference_bit(优先级)2bit
  • nal_unit_type(类型)5bit。

由上述八位来表示一个NAL头,NAL头标识NALU单元中RBSP的数据类型。
其中,nal_unit_type为1, 2, 3, 4, 5的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元。如NAL头为ox27二进制为
100111,取得后5位为 00111 十进制为7对于sps

Nal type类型
  • 0:未规定
  • 1:非IDR图像中不采用数据划分的片段
  • 2:非IDR图像中A类数据划分片段
  • 3:非IDR图像中B类数据划分片段
  • 4:非IDR图像中C类数据划分片段
  • 5:IDR图像的片段
  • 6:补充增强信息(SEI)
  • 7:序列参数集(SPS)
  • 8:图像参数集(PPS)
  • 9:分割符
  • 10:序列结束符
  • 11:流结束符
  • 12:填充数据
  • 13:序列参数集扩展
  • 14:带前缀的NAL单元
  • 15:子序列参数集
  • 16 – 18:保留
  • 19:不采用数据划分的辅助编码图像片段
  • 20:编码片段扩展
  • 21 – 23:保留
  • 24 – 31:未规定

六、H.264的I/P/B帧/sps/pps

H.264的SPS和PPS串包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile、level、图像的宽高、deblock滤波器等。

  • I帧:帧内编码帧、I帧表示关键帧,可以理解为这一帧画面的完整保留。解码时只需要本帧数据既可以完成。

  • I帧特点

    1. 它是一个全帧压缩编码帧,它将全帧图像进行JEPG压缩编码及传输。
    2. 解码时仅用I帧即可重构完整图像
    3. I帧描述了图像背景和运动主体的详情
    4. I帧不需要参考其它画面而生成
    5. I帧是P帧B帧的参考帧
    6. I帧是帧组GOP基础帧的第一帧
    7. I帧不需要考虑运动矢量
    8. I帧所占的数据量较大
  • P帧前向预测编码帧。P帧表示的是这一帧和之前一个关键帧或P帧的差别。解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面(也就是说P帧没有完整画面数据,只有与前一帧的画面的差别数据)

  • P帧的预测与重构:P帧是以I帧为参考帧,在I帧中找到P帧“某点“的预测值和运动矢量。取预测差值和预测矢量一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值。从而可以得到完整的P帧

  • P帧特点

    1. P帧是I帧后面1-2个编码帧
    2. P帧采用运动补偿的方法来传送与它之前的I帧或P帧的差值和运动矢量
    3. 解码时必须将I帧的预测值与预测误差求和之后才能重构完整的P帧图像
    4. P帧属于向前预测帧间编码帧,它只参考前面最靠近它的I帧或P帧
    5. P帧可以是其后面P帧的参考帧,也可以是后面B帧的参考帧
    6. 由于P帧是参考帧,可能造成解码错误的扩散
    7. 由于是差值传送,P帧的压缩比较高
  • B帧双向预测内插编码帧。B帧是双差别帧。即B帧记录的是本帧与前后帧的差别。要解码B帧。不仅要取得之前的缓存画面,还要取得之后的画面。通过前后画面与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累。

  • B帧的预测和重构:B帧以前面的I帧或P帧和后面的P帧作为参考帧,找出B帧“某点”的预测值和两个运动矢量,并取得差值和运动矢量传送。接收端根据差值和运动矢量在两个参考帧中“找出(算出)” 预测值并与差值求和。得到B帧“某点”的样值,从而得到完整的图像。

  • B帧的特点

    1. B帧是由前面的I帧或P帧和后面的P帧来进行预测的
    2. B帧传送的是与它前面的I帧或P帧和后面P帧之间的预测误差及运动矢量
    3. B帧是双向运动编码帧
    4. B真的压缩最高,它只反映参考帧之间运动主体的变化情况
    5. B帧不是参考帧,不会造成解码错误的扩散。

随便说一些

在直播中,或者网络视频的时候,我们打开会发现有时候会出现LOGING…黑屏的情况。这个好像最近几年不会出现了。造成这个问题就是因为我们刚好点进去的不是I帧,因我们解码一个I帧立马可以得到一个完整的图片。所以我们再接收端可以对接收的数据做缓冲处理。

H264nal解析,此代码是参考雷神写的demo。用于解析h264数据头

#include 
#include 
#include 
//shuju 类型,头的后五5位
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,
} nalu_type;

//找到startcode
int find_start_code(unsigned char* buffer)
{
    //00000001
    if(buffer[0] != 0x00 || buffer[1] != 0x00 || buffer[2] != 0x00 || buffer[3] != 0x01) return -1;
    else return 1;
}
//还有一种H264头的开始是以000001开头的
int find_start_code2(unsigned char *buffer)
{
     //00000001
    if(buffer[0] != 0x00 || buffer[1] != 0x00 || buffer[2] != 0x01) return -1;
    else return 1;
}

//H264头占8位1位禁止为,2位优先级 5位类型
typedef struct
{
	int startcode_len;            // h264数据的分隔符的长度NALU对应的Slice为一帧的开始,则用4字节表示,即0x00000001;否则用3字节表示,0x000001)
	unsigned len;                 // 整个h264头的长度  不包括了startcode的长度
	unsigned max_size;            //! Nal Unit Buffer size
	int forbidden_bit;            //! 禁止位
	int nal_reference_idc;        //! 优先级
	int nal_unit_type;            //! 类型
	char *buf;                    //! contains the first byte followed by the EBSP
} nalu_t;

/*
H264数据分析
*/
FILE *h264stream;
int info2 = 0;
int info3 = 0;
//得到头部信息
int get_annexb_nalu(nalu_t *nal)
{  
    int pos = 0;
    int start_code;
    int rewind;

    unsigned char *buffer = (unsigned char *)malloc(nal->max_size);

    //设开始的分隔符为ox000001
    nal->startcode_len = 3;
    if(3 != fread(buffer, 1, 3, h264stream))
    {
        printf("read 3 of startcoder err!\n");
        free(buffer);
        return -1;
    }
    //去找到分隔符 ox000001
    info2 = find_start_code2(buffer);
    if(info2 != 1)
    {
        //不是ox000001时再去读一个是不是ox00000001
        if(1 != fread(buffer + 3, 1, 1, h264stream))
        {
            printf("fread 4 of startcode err!\n");
            free(buffer);
            return -1;
        }
        info3 = find_start_code(buffer);
        if(info3 != 1)
        {
            printf("cant find start code!\n");
            free(buffer);
            return -1;
            
        }
        else
        {
            pos = 4;
            nal->startcode_len = 4;
        }
        
    }
    else
    {
        //startcode为0x000001
        pos = 3;
        nal->startcode_len = 3;
    }

    start_code = 0;
    info2 = 0;
    info3 = 0;
    //开始去读取头部,当到达下一个分隔符的时候就退出
    while(!start_code)
    {
        //当达到文件末尾
        if(feof(h264stream))
        {
            nal->len = (pos - 1) - nal->startcode_len;
            memcpy(nal->buf, &buffer[nal->startcode_len], nal->len);
            nal->forbidden_bit = nal->buf[0] & 0x80;
            nal->nal_reference_idc = nal->buf[0] & 0x60;
            nal->nal_unit_type = nal->buf[0] & 0x1f;
            free(buffer);
            return pos - 1;
        }

        //读取头部信息
        buffer[pos++] = fgetc(h264stream);
        info3 = find_start_code(&buffer[pos - 4]);
        if(info3 != 1)
        {
            info2 = find_start_code2(&buffer[pos - 3]);
        }
        start_code = (info2 == 1 || info3 == 1) ? 1 : 0;
    }

    //回到分隔符之前的距离
    rewind = (info3 == 1) ? -4 : -3;
    if(0 != fseek(h264stream, rewind, SEEK_CUR))
    {
        printf("feek startcode err!\n");
        free(buffer);
        return -1;
    }

    //nal长度以及数据
    nal->len = (pos + rewind) - nal->startcode_len;
    memcpy(nal->buf, &buffer[nal->startcode_len], nal->len);
    nal->forbidden_bit = nal->buf[0] & 0x80;
    nal->nal_reference_idc = nal->buf[0] & 0x60;
    nal->nal_unit_type = nal->buf[0] & 0x1f;
    
    free(buffer);

    //返回读到下一个分隔符之前的长度包括了这一帧的分隔符长度
    return (pos + rewind);

}


int main(int argc, char *argv[])
{
    if(argc < 2)
    {
        printf("please input h264 file to ana!\n");
        return -1;
    }

    int buffer_size = 10000;
    h264stream = fopen(argv[1], "rb");
    if(h264stream == NULL)
    {
        printf("oepn h264 file err!\n");
        return -1;
    }

    nalu_t *nal = (nalu_t*)malloc(sizeof(nalu_t));
    if(nal == NULL)
    {
        printf("malloc nal err!\n");
        return -1;
    }
    nal->max_size = buffer_size;
    nal->buf = (char *)malloc(buffer_size);
    if(nal->buf == NULL)
    {
        printf("malloc nal buf err!\n");
        return -1;
    }
    int offset = 0;
    //开始读取文件分析
    while(!feof(h264stream))
    {  
        int data_length;
        data_length = get_annexb_nalu(nal);
        switch (nal->nal_unit_type)
        {
            case NALU_TYPE_SLICE:printf("pos :%d SLICE p or b\n", offset);break;
			case NALU_TYPE_DPA:printf("pos :%d DPA\n", offset);break;
			case NALU_TYPE_DPB:printf("pos :%d DPB\n", offset);break;
			case NALU_TYPE_DPC:printf("pos :%d DPC\n", offset);break;
			case NALU_TYPE_IDR:printf("pos :%d IDR\n", offset);break;
			case NALU_TYPE_SEI:printf("pos :%d SEI\n", offset);break;
			case NALU_TYPE_SPS:printf("pos :%d SPS\n", offset);break;
			case NALU_TYPE_PPS:printf("pos :%d PPS\n", offset);break;
			case NALU_TYPE_AUD:printf("pos :%d AUD\n", offset);break;
			case NALU_TYPE_EOSEQ:printf("pos :%d EOSEQ\n", offset);break;
			case NALU_TYPE_EOSTREAM:printf("pos :%d EOSTREAM\n", offset);break;
			case NALU_TYPE_FILL:printf("pos :%d FILL\n", offset);break;
            default:break;
        }
        offset += data_length;
    }

    fclose(h264stream);
    return 0;
}

你可能感兴趣的:(音视频开发学习)