编解码标准-H.264

H.264是MPEG-4家族中的一员,即MPEG-4系列文档ISO-14496的第10部分,因此被称作MPEG-4 AVC,MPEG-4重点考虑灵活性和交互性,而H.264着重强调更高的编码压缩率和传输的可靠性。

1、H.264 编码流程

1.1、slice&block

第一步:切片、切宏块,宏块(16x16)是编码的基本单元

第二步:不合理的宏块之间会出现块效应,即色差明显,所以继续切割,分成很多个8*8或4*4的子块

第三步:对子块进行算法编码,期间使用到的算法包括:

  • 帧内预测(内部压缩)
  • 帧间预测(外部压缩)
  • 量化编码
  • 熵编码

其中,每一个切片都有片头 + 多个宏块组成。

以16x16的宏块为编码最小单元,一个宏块可以被分成多个4x4或8x8的块,同一个宏块内,像素的相似程度会比较高,若16x16的宏块中,像素相差较大,那么就需要继续细分。

当对一个宏块进行编码的时候,每个宏块都会被分割成多种不同大小的子块进行预测。

1.2、GOP内压缩

GOP内 I、B、P帧

帧大小:I > P > B

压缩率:B > P > I

编码B帧的时候,需要先把B后面的帧先编码,然后参考之后才能继续编码B帧,例如:

PTS:IDR1、B2、B3、P4、B5、B6、P7、B8、B9、I10、B11、B12、P13、B14、B15、P16

DTS:IDR1、P4、B2、B3、P7、B5、B6、I10、B8、B9、P13、B11、B12、P16、B14、B15

每次编码B的时候,就要把后面最近的 I 或者 P 拿到前面来,如果已经之前被编码过,就不需要了。

IDR帧

I 帧不需要参考任何帧,但B、P帧可能去参考I帧之前的帧,但如果遇到IDR帧,就不能参考IDR帧之前的帧

其核心的作用,就是为了让编码重新同步,立即将参考帧队列清空,已编码的数据全部清除掉。如果之前的参考序列出现了错误,这里就可以立刻矫正。IDR后面的数据又能重新开始编码,不会收到前面的错误影响。

1.3、编码配置

实时视频会议一直是继续向更高质量,更低带宽的方向发展。H.264 High profile 技术于2010年率先被polycom应用于视频会议系统。

比h.264 baseline进一步节约了近一半的带宽。在高清实时会议中,采用H.264 baseline,带宽要求还是比较高的,特别是要做1080P 30pfs甚至60pfs时。如果能减少一半带宽,意味着节省2-4M带宽,如果是在MCU侧,则带宽节省就更可观了。

AVC/H.264 规定了多种不同的配置:基线、主要、扩展、高

  • 基线(Baseline Profile),不支持B帧,只支持无交错模式,主要是用于可视电话,会议电视,无线通讯等实时通信;
  • 主要(Main Profile),提供I/P/B 帧,支持无交错和交错,用于数字广播电视和数字视频存储;
  • 扩展(Extend Profile),也叫扩展Profile,
  • 高(High Profile),在 Main Profile 的基础上增加了8x8 内部预测、提高了压缩效率;

2、H.264的功能分层

H.264的原始码流(裸流)是由⼀个接⼀个NALU组成,它的功能分为两层:“编码层”和“网络层”

VCL(视频编码层):包括核⼼压缩引擎和块、宏块和⽚的语法级别定义,设计⽬标是尽可能地独⽴于⽹络进⾏⾼效的编码;

NAL(⽹络提取层):负责将VCL产⽣的⽐特字符串适配到各种各样的⽹络,就是把已经编码后的数据封装到网络包中去;

H264在网络中的传输,是以一连串NALU的形式传输的,一张图像有可能存在2个NALU。

其中,NALU装了不同的东西:

  • SPS:序列参数集,SRS中保存了一组编码视频序列的全局参数(发 I 帧之前至少要发一次)
  • PPS:图像参数集(发 I 帧之前至少要发一次)
  • I 帧:I 帧或 I 帧的一部分;
  • P帧:P帧或P帧的一部分;
  • B帧:B帧或B帧的一部分;

如果H.264码流解不出来,就要去看看是不是SPS或PPS不存在?

3、H.264流结构

2.1、AnnexB/AVCC

H.264流有两种格式:

  • 一种是annexb,也是传统模式,裸流一般都是annexb格式
  • annexb格式会在数据包前面加上startcode,然后在后面加上UALU包(NALU Header + RBSP)
  • 这个startcode用来做字节流对齐,以及分割流数据。
  • 将SPS和PPS都作为一个NALU进行封装,每一次遇到 I 帧之前,都是重复加上SPS和PPS,这个SPS的作用就是提供了序列信息,比如解码信息,而这个PPS提供的是图像信息,比如如何压缩等。
  • NALU封装了SPS、PPS、SEI、
  • annexb格式的好处,就是解码器可以从任意一个包开始解码。

  • 另外种就是AVCC模式,例如mp4、mkv都属于AVCC格式
  • 没有startcode,直接是一个个UALU包
  • 解码器配置参数在一开始就配置好了,使用NALU长度作为NALU的边界,不需要额外的起始码
  • SPS和PPS都封装在文件头部的extradata中;
  • 好处就是播放器直接能识别,去除了大量的startcode、sps、pps,缩小了文件大小;

比方说在ffmpeg中,我们解封装mp4后,需要对H264进行解码,而解码之前必须要对H264裸流进行一个转封装过滤,将h264的 “mp4版本” 转换为 “annexb版本” 的过程。

RTP包中接收的264包是不含有0x00,0x00,0x00,0x01头的,这部分是RTP接收以后,另外再加上去的,解码的时候再做判断的。

/** 
 * 解码PS流的Extradata
 * h264_parse()
 *        |
 *    ff_h264_decode_extradata() |--> decode_extradata_ps
 *                               |--> decode_extradata_ps_mp4()
 * 
 * decode_extradata_ps():
 *
 *
 * decode_extradata_ps_mp4(): 
 *    MP4中SPS和PPS存放在 moov->trak->mdia->minf->stbl->stsd: 
 *       Extensions = Size + Type(avcC) + Extradata
 *     
 */
static int decode_extradata_ps(const uint8_t *data, int size, H264ParamSets *ps,
int is_avc, void *logctx)
{
    // H264包
    H2645Packet pkt = { 0 };
    int i, ret = 0;
    
    ret = ff_h2645_packet_split(&pkt, data, size, logctx, is_avc, 2, AV_CODEC_ID_H264, 1, 0);
    if (ret < 0) {
        ret = 0;
        goto fail;
    }
    // 包里面有多少个NAL?
    for (i = 0; i < pkt.nb_nals; i++) {
        // 解析NAL类型
        H2645NAL *nal = &pkt.nals[i];
        switch (nal->type) {
            // SPS(7): 25字节左右
            case H264_NAL_SPS: {
                GetBitContext tmp_gb = nal->gb;
                ret = ff_h264_decode_seq_parameter_set(&tmp_gb, logctx, ps, 0);
                if (ret >= 0)
                    break;
                av_log(logctx, AV_LOG_DEBUG,
                    "SPS decoding failure, trying again with the complete NAL\n");
                init_get_bits8(&tmp_gb, nal->raw_data + 1, nal->raw_size - 1);
                ret = ff_h264_decode_seq_parameter_set(&tmp_gb, logctx, ps, 0);
                if (ret >= 0)
                    break;
                ret = ff_h264_decode_seq_parameter_set(&nal->gb, logctx, ps, 1);
                if (ret < 0)
                    goto fail;
                break;
            }
            // PPS(8): 5字节左右
            case H264_NAL_PPS:
                ret = ff_h264_decode_picture_parameter_set(&nal->gb, logctx, ps,
                    nal->size_bits);
                if (ret < 0)
                    goto fail;
                break;
            default:
                av_log(logctx, AV_LOG_VERBOSE, "Ignoring NAL type %d in extradata\n",
                    nal->type);
                break;
        }
    }
    fail:
    ff_h2645_packet_uninit(&pkt);
    return ret;
}
typedef struct H264ParamSets {
	// SPS列表
    AVBufferRef *sps_list[MAX_SPS_COUNT];
	// PPS列表
    AVBufferRef *pps_list[MAX_PPS_COUNT];

    AVBufferRef *pps_ref;

    /* currently active parameters sets */
    const PPS *pps;
    const SPS *sps;

    int overread_warning_printed[2];
} H264ParamSets;

2.2、SPS

序列参数集,保存了一组编码视频序列的全局参数,保存了:profile、level、视频宽和高、颜色空间等。在H.264的各种语法元素中,SPS中的信息至关重要。如果其中的数据丢失或出现错误,那么解码过程很可能会失败。

SPS 中的信息至关重要,如果其中的数据丢失,解码过程就可能失败。SPS 和 PPS 通常作为解码器的初始化参数。一般情况,SPS 和 PPS 所在的 NAL 单元位于整个码流的起始位置,但是在某些场景下,在码率中间也可能出现这两种结构:

  • 解码器要在码流中间开始解码。比如,直播流。
  • 编码器在编码过程中改变了码率的参数。比如,图像的分辨率。

2.3、PPS

每一帧编码后数据所依赖的参数,都保存在PPS中,主要体现的就是图像编码信息。

2.4、NALU

2.4.1、nal_unit_header

NALU头就一个字节,包含了对NALU的描述,1)重要程度;2)NALU类型

F

1B

禁止位,0表示正常,1表示错误,一般都是0

NRI

2B

重要级别,00不重要,01,10,11非常重要

TYPE

5B

表示该NALU的类型是什么?

例如:

每个NAL分割的时候,00 00 00 01为startcode,头部的2个startcode分别代表了SPS和PPS,从第3个startcode开始,就是NALU(I、B、P帧)。

  • 0x00 0x00 0x00 0x01 + 0x67

十六进制转为二进制:0x0 11 00111,NALU类型=7,表示PSP

  • 0x00 0x00 0x00 0x01 + 0x68

十六进制转为二进制:0x0 11 01000,NALU类型=8,表示PPS

  • 0x00 0x00 0x01 + 0x65

十六进制转为二进制:0x0 11 00101,NALU类型=5,表示 I 帧

  • 0x00 0x00 0x00 0x01 + 0x41

十六进制转为二进制:0x0 10 00001,NALU类型=1,表示 P 帧

  • 0x00 0x00 0x00 0x01 + 0x01

十六进制转为二进制:0x0 00 00001,NALU类型=1,表示 B 帧

2.4.2、nal_unit_rbsp

NALU的主体涉及到三个重要的名词,分别为EBSP、RBSP和SODB。

其中EBSP完全等价于NALU主体,而且它们三个的结构关系为:

EBSP包含RBSP,RBSP包含SODB。

NALU = EBSP + 0x03(防竞争字节)+ ...... + EBSP + 0x03

NALU = RBSP + 补齐字节

1、SODB

String Of Data Bits 原始数据比特流,就是最原始的编码/压缩得到的数据

2、RBSP

Raw Byte Sequence Payload,又称原始字节序列载荷。和SODB关系如下:

RBSP = SODB + RBSP Trailing Bits(RBSP尾部补齐字节)引入RBSP Trailing Bits做8位字节补齐。

3、EBSP

Encapsulated Byte Sequence Payload:扩展字节序列载荷。

如果RBSP中也包括了StartCode(0x000001或0x00000001)怎么办呢?所以,就有了防止竞争字节(0x03),编码时,扫描RBSP,如果遇到连续两个0x00字节,就在后面添加防止竞争字节(0x03);解码时,同样扫描EBSP,进行逆向操作即可。

2.4.3、SliceHeader

  • first_mb_in_slice:片中的第一个宏块的地址, 片通过这个句法元素来标定它自己的地址。要注意的是在帧场自适应模式下,宏块都是成对出现,这时本句法元素表示的是第几个宏块对,对应的第一个宏块的真实地址应该是:2 * first_mb_in_slice;
  • slice_type:指明片的类型,IDR 图像时, slice_type 等于 2, 4, 7, 9;

slice_type 的值在 5 到 9 范围内表示,除了当前条带的编码类型,所有当前编码图像的其他条带的 slice_type 值应与当前条带的 slice_type 值一样,或者等于当前条带的 slice_type 值减 5。

当 nal_unit_type 等于 5(IDR 图像)时,slice_type 应等于 2、 4、 7 或 9。当 num_ref_frames 等于 0 时, slice_type 应等于 2、 4、 7 或 9。

  • pic_parameter_set_id:当前slice所依赖的pps的id;
  • colour_plane_id:当标识位separate_colour_plane_flag为true时,colour_plane_id表示当前的颜色分量,0、1、2分别表示Y、U、V分量;
  • frame_num:每个参考帧都有一个依次连续的 frame_num 作为它们的标识,这指明了各图像的解码顺序。但事实上我们在表 中可以看到, frame_num 的出现没有 if 语句限定条件,这表明非参考帧的片头也会出现 frame_num。只是当该个图像是参考帧时,它所携带的这个句法元素在解码时才有意义;

  • field_pic_flag:场编码标识位。当该标识位为1时表示当前slice按照场进行编码;该标识位为0时表示当前slice按照帧进行编码;
  • bottom_field_flag:底场标识位。该标志位为1表示当前slice是某一帧的底场;为0表示当前slice为某一帧的顶场;
  • idr_pic_id:表示IDR帧的序号。某一个IDR帧所属的所有slice,其idr_pic_id应保持一致。IDR 图像的标识。不同的 IDR 图像有不同的 idr_pic_id 值。值得注意的是,IDR 图像有不等价于 I 图像,只有在作为 IDR 图像的 I 帧才有这个句法元素,在场模式下, IDR 帧的两个场有相同的 idr_pic_id 值。 idr_pic_id 的取值范围是 [0,65535] 和 frame_num 类似,当它的值超出这个范围时,它会以循环的方式重新开始计数;
  • pic_order_cnt_lsb:表示当前帧序号的另一种计量方式;
  • delta_pic_order_cnt_bottom:表示顶场与底场POC差值的计算方法,不存在则默认为0;
  • slice_qp_delta:指出在用于当前片的所有宏块的量化参数的初始值;

2.4.4、rbsp_trailing_bits

但是只在 NALU 前面加上起始码是会产生问题了,因为原始码流中,是有可能出现 0 0 0 1 或者 0 0 1 的,这样就会导致读取程序将一个 NALU 误分割成多个 NALU。为了防止这种情况发生,AnnexB 引入了防竞争字节(Emulation Prevention Bytes)的概念。

所谓防竞争字节(Emulation Prevention Bytes),就是在给 NALU 添加起始码之前,先对码流进行一次遍历,查找码流里面的存在的 000、001、002、003 的字节,然后对其进行如下修改。

// EBSP->RBSP 反向处理
std::vector<uint8_t> EBSP2RBSP(uint8_t* buffer, int len) {
	// 00 00 03 去掉03
	std::vector<uint8_t> ebsp;
	int i = 0;
	for (i = 0; i < len-2; ++i) {
		if (buffer[i] == 0x00 && buffer[i+1] == 0x00 && buffer[i+2] == 0x03) {
			ebsp.push_back(buffer[i++]);
			ebsp.push_back(buffer[i++]);
		}
		else {
			ebsp.push_back(buffer[i]);
		}
	}
	for (; i < len; ++i) {
		ebsp.push_back(buffer[i]);
	}
	return ebsp;
}

4、H.264内I、B、P帧的关系?

GOP编码后的顺序是解码顺序,解码后看到的是显示顺序。

控制GOP也可以控制延迟问题,减少I帧时间的间距,一般控制在2秒比较合适。

4.1、I 关键帧

不需要参考其他画面,靠自己就能被解码成完整图像,属于“帧内编码”。

  1. 采用帧内编码
  2. 占数据信息量比较大
  3. 是一个GOP的基础帧
  4. 不需要考虑运动矢量

4.2、P帧

P帧代表预测帧,除了空域预测以外,它还可以通过时域预测来进行压缩。

通过与其相邻的前一帧(I 或 P)不同像素点进行压缩本帧数据,属于“帧间编码”。

以I帧和P帧为例。如果你只使用这两种类型的帧,那么每一帧要么参考自身(I 帧),要么参考前一帧(P 帧)。因此,帧可以以相同的顺序进出编码器。这里,呈现顺序(或显示顺序)与编码、解码顺序相同。

4.3、B帧

B帧可以参考在其前后出现的帧,采用双向预测(前后 I 或 P ),大大提高压缩倍数,想要理解B帧的作用,我们需要先理解显示顺序和解码顺序的概念。

按照解码顺序,解码器先解码帧1(I帧),然后是帧2(P帧)。但它却无法显示帧2,因为在解码顺序中的实际上是帧4!所以,解码器需要将帧2(按解码顺序)放入缓冲区,然后等待显示它的时机。

所以,编码器和解码器需要在内存中维护两个“顺序”或“序列”:一个将帧放置在正确的显示顺序中,另一个用于将帧按照编码和解码所需顺序放置。

  • 显示队列:1、2、3、4、5、6、7
  • 解码队列:1、2、3、2、6、7、5

所以在GOP内部,I与I之间为一个组,I组内部P与P之间一个组,P组内部先解码P,后解码B

  • B帧压缩率最大,用来预测运动轨迹;
  • P次之,用来表示和前一帧的差异;
  • I帧最小,其本身独立完成编码。

4.4、IDR帧

视频第一个I帧,称为IDR帧,IDR一定是I帧,但I帧不一定是IDR帧。

4.5、开放GOP和闭合GOP

所谓开放GOP,就是 I 帧可以被跨越,而闭合GOP,就是 I 帧是一个IDR,该 IDR 帧不能被之前的帧参考。

这也要就要求了,在 IDR 帧之前,必须有一个P帧,否则,IDR 帧相邻的B帧就无法向后预测。

IDR和闭合GOP到底有什么用处?

  • ABR视频流:

在ABR视频流中,播放器可以根据带宽和解码器缓冲器的填充程度在不同配置文件(组合不同码率和分辨率的视频)之间切换。如果播放器要从1080p切换到360p,那么它就需要这种利落的切换。此时IDR发挥作用,这样播放器就能刷新缓冲,让360p的视频流进入。

  • 错误恢复:

如果你在流化视频时使用HLS,并且每个视频片段都以IDR开始,这意味着片段中的所有帧都不能参考前、后片段中的帧。所以如果因为某个错误而失去其中一个片段,播放器仍然能继续接收下一个视频片段。有趣的是,Apple 的 HLS 规范提到应该每两秒使用一次 IDR。(注意:规范没有说视频片段持续时间应该是两秒,而是指 GOP 的大小是两秒)

所以说,IDR不能太频繁,因为会影响压缩效率。但又要保证视频播放过程中丢包不受到影响,所以最好的办法,就是间隔一段时间使用固定的IDR。

  • 快进快退:

我们之前提到过,IDR非常有助于实现快进快退。播放器需找到距离最近的IDR,然后开始从这一点播放视频流。

猜你喜欢

转载自blog.csdn.net/davidsguo008/article/details/128696144