前言
音视频开发需要你懂得音视频中一些基本概念,针对编解码而言,我们必须提前懂得编解码器的一些特性,码流的结构,码流中一些重要信息如sps,pps,vps,start code以及基本的工作原理,而大多同学都只是一知半解,所以导致代码中的部分内容虽可以简单理解却不知其意,所以,在这里总结出了当前主流的H.264,H.265编码相关的原理,以供学习.
1. 概览
1.1. 为什么要编码
众所周知,视频数据原始体积是巨大的,以720P 30fps的视频为例,一个像素大约3个字节,如下所得,每秒钟产生87MB,这样计算可得一分钟就将产生5.22GB。
数据量/每秒=1280*720*33*3/1024/1024=87MB
因此,像这样体积重大的视频是无法在网络中直接传输的.而视频编码技术也就因运而生.关于视频编码原理的技术可以参考本人其他文章,这里不做过多描述.
1.2. 编码技术
经过很多年的开发迭代,已经有很多大牛实现了视频编码技术,其中最主流的有H.264编码,以及新一代的H.265编码,谷歌也开发了VP8,VP9编码技术.对移动端而言,苹果内部已经实现了如H.264,H.265编码,我们需要使用苹果提供的VideoToolbox框架来实现它.
1.3. 编码分类
- 软件编码(简称软编):使用CPU进行编码。
- 硬件编码(简称硬编):不使用CPU进行编码,使用显卡GPU,专用的DSP、FPGA、ASIC芯片等硬件进行编码。
优缺点
- 软编:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。
- 硬编:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
iOS系统中的硬编码
苹果在iOS 8.0系统之前,没有开放系统的硬件编码解码功能,不过Mac OS系统一直有,被称为Video ToolBox的框架来处理硬件的编码和解码,终于在iOS 8.0后,苹果将该框架引入iOS系统。
1.4. 编码原理
对视频执行编码操作后,原始视频数据会被压缩成三种不同类型的视频帧: I帧,P帧,B帧.
- I帧:关键帧.完整编码的帧.可以理解成是一张完整画面,不依赖其他帧
- P帧:参考前面的I帧或P帧,即通过前面的I帧与自己记录的不同的部分可以形成完整的画面.因此,单独的P帧无法形成画面.
- B帧:参考前面的I帧或P帧以及后面的P帧
补充: I帧的压缩率是7(跟JPG差不多),P帧是20,B帧可以达到50. 但是iOS中一般不开启B帧,因为B帧的存在会导致时间戳同步较为复杂.
两种核心算法
- 帧内压缩
当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立的解码、显示。帧内压缩一般达不到很高的压缩,跟编码jpeg差不多。
如下图:我们可以通过第 1、2、3、4、5 块的编码来推测和计算第 6 块的编码,因此就不需要对第 6 块进行编码了,从而压缩了第 6 块,节省了空间
帧内预测.png
- 帧间压缩: P帧与B帧的压缩算法
相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。帧间压缩也称为时间压缩(Temporal compression),它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的。帧差值(Frame differencing)算法是一种典型的时间压缩法,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。
如下图:可以看到前后两帧的差异其实是很小的,这时候用帧间压缩就很有意义。
帧间压缩11.jpg
有损压缩与无损压缩
- 有损压缩: 解压缩后的数据与压缩前的数据不一致.在压缩的过程中要丢失一些人眼和人耳所不敏感的图像或音频信息,而且丢失的信息不可恢复
- 无损压缩: 压缩前和解压缩后的数据完全一致.优化数据的排列等.
DTS和PTS
DTS和PTS的解释
FFmpeg里有两种时间戳:DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。顾名思义,前者是解码的时间,后者是显示的时间。要仔细理解这两个概念,需要先了解FFmpeg中的packet和frame的概念。
FFmpeg中用AVPacket结构体来描述解码前或编码后的压缩包,用AVFrame结构体来描述解码后或编码前的信号帧。对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。DTS是AVPacket里的一个成员,表示这个压缩包应该什么时候被解码。如果视频里各帧的编码是按输入顺序(也就是显示顺序)依次进行的,那么解码和显示时间应该是一致的。可事实上,在大多数编解码标准(如H.264或HEVC,当出现B帧的时候)中,编码顺序和输入顺序并不一致。于是才会需要PTS和DTS这两种不同的时间戳。
- DTS:主要用于视频的解码,在解码阶段使用.
- PTS:主要用于视频的同步和输出.在渲染的时候使用.在没有B frame的情况下.DTS和PTS的输出顺序是一样的。
1.dtspts
如上图:I帧的解码不依赖于任何的其它的帧.而P帧的解码则依赖于其前面的I帧或者P帧.B帧的解码则依赖于其前的最近的一个I帧或者P帧 及其后的最近的一个P帧.
2. 编码数据码流结构
在我们的印象中,一张图片就是一张图像,视频就是很多张图片的集合.。但是因为我们要做音视频编程,就需要更加深入理解视频的本质.
2.1 刷新图像概念.
在编码的码流中图像是个集合的概念,帧、顶场、底场都可以称为图像,一帧通常就是一幅完整的图像.
- 逐行扫描:每次扫描得到的信号就是一副图像,也就是一帧. 逐行扫描适合于运动图像
- 隔行扫描:扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」.适合于非运动图像
逐行扫描与隔行扫描.png
帧与场.png
2.2. 重要参数
- 视频参数集VPS(Video Parameter Set)
VPS主要用于传输视频分级信息,有利于兼容标准在可分级视频编码或多视点视频的扩展。
(1)用于解释编码过的视频序列的整体结构,包括时域子层依赖关系等。HEVC高效率视频编码(High Efficiency Video Coding)中加入该结构的主要目的是兼容标准在系统的多子层方面的扩展,处理比如未来的可分级或者多视点视频使用原先的解码器进行解码但是其所需的信息可能会被解码器忽略的问题。
(2)对于给定视频序列的某一个子层,无论其SPS相不相同,都共享一个VPS。其主要包含的信息有:多个子层或操作点共享的语法元素;档次和级别等会话关键信息;其他不属于SPS的操作点特定信息。
(3)编码生成的码流中,第一个NAL(Network Abstract Layer)单元,携带的就是VPS信息
- 序列参数集SPS(Sequence Parameter Set)
包含一个CVS 编码视频序列(Coded Video Sequence)中所有编码图像的共享编码参数。
(1)一段HEVC码流可能包含一个或者多个编码视频序列,每个视频序列由一个随机接入点开始,即IDR/BLA/CRA。序列参数集SPS包含该视频序列中所有slice需要的信息。
(2)SPS的内容大致可以分为几个部分:1、自引ID;2、解码相关信息,如档次级别、分辨率、子层数等;3、某档次中的功能开关标识及该功能的参数;4、对结构和变换系数编码灵活性的限制信息;5、时域可分级信息;6、VUI。
- 图像参数集PPS(Picture Parameter Set)
包含一幅图像所用的公共参数,即一幅图像中所有片段SS(Slice Segment)引用同一个PPS。
(1)PPS包含每一帧可能不同的设置信息,其内容同H.264中的大致类似,主要包括:1、自引信息;2、初始图像控制信息,如初始QP等;3、分块信息。
(2)在解码开始的时候,所有的PPS全部是非活动状态,而且在解码的任意时刻,最多只能有一个PPS处于激活状态。当某部分码流引用了某个PPS的时候,这个PPS便被激活,称为活动PPS,一直到另一个PPS被激活。
参数集包含了相应的编码图像的信息。SPS包含的是针对一连续编码视频序列的参数(标识符seq_parameter_set_id、帧数及POC的约束、参考帧数目、解码图像尺寸和帧场编码模式选择标识等等)。PPS对应的是一个序列中某一幅图像或者某几幅图像 ,其参数如标识符pic_parameter_set_id、可选的seq_parameter_set_id、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等等。
通常,SPS 和PPS 在片的头信息和数据解码前传送至解码器。每个片的头信息对应一个
pic_parameter_set_id,PPS被其激活后一直有效到下一个PPS被激活;类似的,每个PPS对应一个
seq_parameter_set_id,SPS被其激活以后将一直有效到下一个SPS被激活。
参数集机制将一些重要的、改变少的序列参数和图像参数与编码片分离,并在编码片之前传送
至解码端,或者通过其他机制传输。
扩展知识点:档次(Profile)、层(Tier)和级别(Level)
- 档次: 主要规定编码器可采用哪些编码工具或算法。
- 级别: 指根据解码端的负载和存储空间情况对关键参数(最大采样率、最大图像尺寸、分辨率、最小压缩比、最大比特率、解码缓冲区DPB大小等)加以限制。
考虑到应用可根据最大的码率和CPB大小来区分,因此有些级别定义了两个层Tier:主层和高层,主层用于大多数应用,而高层用于那些最严苛的应用。
2.3. 原始码流
- IDR
一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。
- 结构
由一个接一个的 NALU 组成的,而它的功能分为两层,VCL(视频编码层)和 NAL(网络提取层).
H.264从层次来看分为两层:视频编码层(VCL, Video Coding Layer)和网络提取层(NAL,Network Abstraction Layer)。VCL输出的是原始数据比特流(SODB,String of data bits),表示H.264的语法元素编码完成后的实际的原始二进制码流。SODB通常不能保证字节对齐,故需要补齐为原始字节序列负荷(RBSP,Raw Byte Sequence Payload)。NAL层实际上就是最终输出的H.264码流,它是由一个个NALU组成的,每个NALU包括一组对应于视频编码数据的NAL头信息和一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)。以上名词之间的关系如下:
RBSP = SODB + RBSP trailing bits
NALU = NAL header(1 byte) + RBSP
H.264 = Start Code Prefix(3 bytes) + NALU + Start Code Prefix(3 bytes) + NALU +…
所以H.264码流的结构如下:
- 组成
NALU (Nal Unit) = NALU头 + RBSP 在 VCL
数据传输或存储之前,这些编码的 VCL 数据,先被映射或封装进 NAL 单元(以下简称 NALU,Nal Unit) 中。每个 NALU 包括一个原始字节序列负荷(RBSP, Raw Byte Sequence Payload)、一组 对应于视频编码的 NALU 头部信息。RBSP 的基本结构是:在原始编码数据的后面填加了结尾 比特。一个 bit“1”若干比特“0”,以便字节对齐。
2.3.1. H.264码流
一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成
NALU组成.jpeg
- StartCode : Start Code 用于标示这是一个NALU 单元的开始,必须是”00 00 00 01” 或”00 00 01”(Annex B码流格式才必须是”00 00 00 01” 或”00 00 01”)
- NALU Header下表为 NAL Header Type
NAL Header Type.png
例如,下面幅图分别代表IDR与非IDR帧具体的码流信息:
2.IDR
在一个NALU中,第一个字节(即NALU header)用以表示其包含数据的类型及其他信息。我们假定一个头信息字节为0x67作为例子:
十六进制 | 二进制 |
0x67 | 0 11 00111 |
如表所示,头字节可以被解析成3个部分,其中:
1>. forbidden_zero_bit = 0:占1个bit,禁止位,用以检查传输过程中是否发生错误,0表示正常,1表示违反语法;
2>. nal_ref_idc = 3:占2个bit,用来表示当前NAL单元的优先级。非0值表示参考字段/帧/图片数据,其他不那么重要的数据则为0。对于非0值,值越大表示NALU重要性越高
3>. nal_unit_type = 7:最后5位用以指定NALU类型,NALU类型定义如上表
从表中我们可以获知,NALU类型1-5为视频帧,其余则为非视频帧。在解码过程中,我们只需要取出NALU头字节的后5位,即将NALU头字节和0x1F进行与计算即可得知NALU类型,即:
NALU类型 = NALU头字节 & 0x1F
注意: 可以将start code理解为不同nalu的分隔符,header是某种类型的key,payload是该key的value.
2.3.2.码流格式
H.264标准中指定了视频如何编码成独立的包,但如何存储和传输这些包却未作规范,虽然标准中包含了一个Annex附件,里面描述了一种可能的格式Annex B,但这并不是一个必须要求的格式。
为了针对不同的存储传输需求,出现了两种打包方法。一种即Annex B格式,另一种称为AVCC格式。
- Annex B
从上文可知,一个NALU中的数据并未包含他的大小(长度)信息,因此我们并不能简单的将一个个NALU连接起来生成一个流,因为数据流的接收端并不知道一个NALU从哪里结束,另一个NALU从哪里开始。
Annex B格式用起始码(Start Code)来解决这个问题,它在每个NALU的开始处添加三字节或四字节的起始码0x000001或0x00000001。通过定位起始码,解码器就可以很容易的识别NALU的边界。
当然,用起始码定位NALU边界存在一个问题,即NALU中可能存在与起始码相同的数据。为了防止这个问题,在构建NALU时,需要将数据中的0x000000,0x000001,0x000002,0x000003中插入防竞争字节(Emulation Prevention Bytes)0x03,使其变为:
0x000000 = 0x0000 03 00
0x000001 = 0x0000 03 01
0x000002 = 0x0000 03 02
0x000003 = 0x0000 03 03
解码器在检测到0x000003时,将0x03抛弃,恢复原始数据。
由于Annex B格式每个NALU都包含起始码,所以解码器可以从视频流随机点开始进行解码,常用于实时的流格式。在这种格式中通常会周期性的重复SPS和PPS,并且经常时在每一个关键帧之前。
- AVCC
另一个存储H.264流的方式是AVCC格式,在这种格式中,每一个NALU包都加上了一个指定其长度(NALU包大小)的前缀(in big endian format大端格式),这种格式的包非常容易解析,但是这种格式去掉了Annex B格式中的字节对齐特性,而且前缀可以是1、2或4字节,这让AVCC格式变得更复杂了,指定前缀字节数(1、2或4字节)的值保存在一个头部对象中(流开始的部分),这个头通常称为'extradata'或者'sequence header',SPS和PPS数据也需要保存在extradata中。
你会发现SPS和PPS被存储在了非NALU包中(out of band带外),即独立于基本流数据。这些数据的存储和传输是文件容器的任务,超出了本文的范畴。注意:虽然AVCC格式不使用起始码,防竞争字节还是有的。
H.264 extradata语法如下:
bits | line by byte | remark |
|
8 | version | always | 0x01 |
8 | avc profile | sps[0][1] |
|
8 | avc compatibility | sps[0][2] |
|
8 | avc level | sps[0][3] |
|
6 | reserved | all bits on |
|
2 | NALULengthSi*usOne |
|
|
3 | reserved | all bits on |
|
5 | number of SPS NALUs usually | 1 |
|
16 | SPS size |
|
|
N | variable SPS NALU data |
|
|
8 | number of PPS NALUs usually | 1 |
|
16 | PPS size |
|
|
N | variable PPS NALU data |
|
|
我们注意一下这个值 NALULengthSi*usOne,通过将这个值加 1 ,我们就得出了后续每个 NALU 前面前缀(也就是表示长度的整数)的字节数
例如,这个 NALULengthSi*usOne 是 3,那么每个 NALU 前面前缀的长度就是 4 个字节。我们在读取后续数据时,可以先读 4 个字节,然后把这四个字节转成整数,就是这个 NALU 的长度了,注意,这个长度并不包含起始的4个字节,是单纯 NALU 的长度。
这里还需要注意的一点是,虽然AVCC格式不使用起始码,但防竞争字节还是有的。
AVCC格式的一个优点在于解码器配置参数在一开始就配置好了,系统可以很容易的识别NALU的边界,不需要额外的起始码,减少了资源的浪费,同时可以在播放时调到视频的中间位置。这种格式通常被用于可以被随机访问的多媒体数据,如存储在硬盘的文件。
2.3.3. H.265码流
HEVC全称High Efficiency Video Coding(高效率视频编码,又称H.265),是比H.264更优秀的一种视频压缩标准。HEVC在低码率视频压缩上,提升视频质量、减少容量即节省带宽方面都有突出表现。
H.265标准围绕H.264编码标准,保留原有的某些技术,同时对一些技术进行改进,编码结构大致上和H.264的架构类似。这里着重讲一下两者编码格式的区别。
同H.264一样,H.265也是以NALU的形式组织起来。而在NALU header上,H.264的HALU header是一个字节,而H.265则是两个字节。我们同样假定一个头信息为0x4001作为例子:
十六进制 | 二进制 |
0x4001 | 0 100000 000000 001 |
如表所示,头信息可以被解析成4个部分,其中:
- forbidden_zero_bit = 0:占1个bit,与H.264相同,禁止位,用以检查传输过程中是否发生错误,0表示正常,1表示违反语法;
- nal_unit_type = 32:占6个bit,用来用以指定NALU类型
- nuh_reserved_zero_6bits = 0:占6位,预留位,要求为0,用于未来扩展或3D视频编码
- nuh_temporal_id_plus1 = 1:占3个bit,表示NAL所在的时间层ID
对比H.264的头信息,H.265移除了nal_ref_idc,此信息被合并到了nal_unit_type中,H.265NALU类型规定如下:
nal_unit_type | NALU类型 | 备注 |
0 | NAL_UNIT_CODE_SLICE_TRAIL_N | 非关键帧 |
1 | NAL_UNIT_CODED_SLICE_TRAIL_R |
|
2 | NAL_UNIT_CODED_SLICE_TSA_N |
|
3 | NAL_UINT_CODED_SLICE_TSA_R |
|
4 | NAL_UINT_CODED_SLICE_STSA_N |
|
5 | NAL_UINT_CODED_SLICE_STSA_R |
|
6 | NAL_UNIT_CODED_SLICE_RADL_N |
|
7 | NAL_UNIT_CODED_SLICE_RADL_R |
|
8 | NAL_UNIT_CODED_SLICE_RASL_N |
|
9 | NAL_UNIT_CODE_SLICE_RASL_R |
|
10 ~ 15 | NAL_UNIT_RESERVED_X | 保留 |
16 | NAL_UNIT_CODED_SLICE_BLA_W_LP | 关键帧 |
17 | NAL_UNIT_CODE_SLICE_BLA_W_RADL |
|
18 | NAL_UNIT_CODE_SLICE_BLA_N_LP |
|
19 | NAL_UNIT_CODE_SLICE_IDR_W_RADL |
|
20 | NAL_UNIT_CODE_SLICE_IDR_N_LP |
|
21 | NAL_UNIT_CODE_SLICE_CRA |
|
22 ~ 31 | NAL_UNIT_RESERVED_X | 保留 |
32 | NAL_UNIT_VPS | VPS(Video Paramater Set) |
33 | NAL_UNIT_SPS | SPS |
34 | NAL_UNIT_PPS | PPS |
35 | NAL_UNIT_ACCESS_UNIT_DELIMITER |
|
36 | NAL_UNIT_EOS |
|
37 | NAL_UNIT_EOB |
|
38 | NAL_UNIT_FILLER_DATA |
|
39 | NAL_UNIT_SEI | Prefix SEI |
40 | NAL_UNIT_SEI_SUFFIX | Suffix SEI |
41 ~ 47 | NAL_UNIT_RESERVED_X | 保留 |
48 ~ 63 | NAL_UNIT_UNSPECIFIED_X | 未规定 |
64 | NAL_UNIT_INVALID |
|
H.265的NALU类型是在信息头的第一个字节的第2到7位,所以判断H.265NALU类型的方法是将NALU第一个字节与0x7E进行与操作并右移一位,即:
NALU类型 = (NALU头第一字节 & 0x7E) >> 1
与H.264类似,H.265码流也有两种封装格式,一种是用起始码作为分界的Annex B格式,另一种则是在NALU头添加NALU长度前缀的格式,称为HVCC。在HVCC中,同样需要一个extradata来保存视频流的编解码参数,其格式定义如下:
bits | line by byte | remark |
8 | configurationVersion | always 0x01 |
2 | general_profile_space |
|
1 | general_tier_flag |
|
5 | general_profile_idc |
|
32 | general_profile_compatibility_flags |
|
48 | general_constraint_indicator_flags |
|
8 | general_level_idc |
|
4 | reserved | ‘1111’b |
12 | min_spatial_segmentation_idc |
|
6 | reserved | ‘111111’b |
2 | parallelismType |
|
6 | reserved | ‘111111’b |
2 | chromaFormat |
|
5 | reserved | ‘11111’b |
3 | bitDepthLumaMinus8 |
|
5 | reserved | ‘11111’b |
3 | bitDepthChromaMinus8 |
|
16 | avgFrameRate |
|
2 | constantFrameRate |
|
3 | numTemporalLayers |
|
1 | tmporalIdNested |
|
2 | lengthSi*usOne |
|
8 | numOfArrays |
|
Repeated of Array(VPS/SPS/PPS)
1| array_completeness
1| reserved| ‘0’b
6| NAL_unit_type
16| numNalus
16| nalUnitLength
N| NALU data
从上表可以看到,在H.265的extradata后半段是一段格式重复的数组数据,里面需要包含的除了与H.264相同的SPS、PPS外,还需多添加一个VPS。
VPS(Video Parament Set,视频参数集),在H.265中类型为32。VPS用于解释编码过的视频的整体结构,包括时域子层依赖关系等,主要目的在于兼容H.265标准在系统的多子层方面的扩展。