编解码标准-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 关键帧
不需要参考其他画面,靠自己就能被解码成完整图像,属于“帧内编码”。
- 采用帧内编码
- 占数据信息量比较大
- 是一个GOP的基础帧
- 不需要考虑运动矢量
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,然后开始从这一点播放视频流。