[关闭]
@ltlovezh 2021-11-23T03:24:01.000000Z 字数 8776 阅读 1131

H264

编码格式 音视频


H264码流结构并不复杂,主要由一系列GOP构成,一个GOP是一个视频编码序列,每个GOP的第一帧是IDR帧,此外还包含了SPS和PPS信息,每个GOP都可以独立解码。
一个GOP由一系列I、P、B帧构成,一个视频帧又可以划分为Slice(切片),一个Slice则由宏块构成,整体结构如下所示:
H264

NALU

当从封装容器,Demux出H264码流时,实际是NALU序列,这些NALU序列不仅包含了编码数据,也包含了元数据,如下所示:
NAL

H264规范中,明确规定了VCL(视频编码层)和NAL两层结构(网络提取层),如下所示:
VCL-NAl

虽然都是NALU序列,但是不同封装容器Demux出的NALU序列也存在差异,主要分为AVCC和Annex-B。

AVCC是由NALU Size + NALU构成,主要适用于Mp4、FLV和MKV等封装容器,整体结构如下所示AVCC

SPS和PPS元数据包含在独立存储模块,例如:Mp4的avcC Box包含了AVCDecoderConfigurationRecord,hvcC Box包含了HEVCDecoderConfigurationRecord, Flv第一个视频Tag也包含了AVCDecoderConfigurationRecord或者HEVCDecoderConfigurationRecord。

Annex-B是由Start Code + NALU构成,主要适用于TS封装容器,整体结构如下所示:
Annex-B

SPS和PPS一般位于紧邻IDR NALU前面的两个NALU。

Start Code必须是0x00000001或0x000001。3字节的0x000001只有一种场合下使用,就是一个完整的帧被划分为多个Slice的时候,从第二个Slice开始,包含这些Slice的NALU使用3字节起始码。即若NALU包含的Slice为一帧的开始就用0x00000001,否则就用0x000001。

由上面介绍可知,每个NALU由NALU Header和NALU Payload组成,H264的NALU Header固定占用1字节,标识了负载数据是什么内容?,主要包含三部分:

  1. | forbidden_zero_bit | nal_ref_idc | nal_unit_type |
  2. `--------------------+-------------+---------------`
  3. | 1 bit | 2 bit | 5 bit |

1:I/P/B帧,当nal_unit_type为1时,需要根据Slice Header判断是I/P/B帧,此时一定不是IDR帧。
5:IDR帧,即时解码刷新帧,属于I帧的一种(IDR帧一定是I帧,反之不然),IDR之后的帧都不会参考IDR之前的帧,所以解码器可以清空参考帧队列,准备解码新的GOP。
6:SEI,补充增强信息,提供了向H264码流中加入额外信息的方法,特定场景很有用,后续单独介绍。
7:SPS,Sequence Paramater Set(序列参数集),保存了一组编码视频序列的全局参数。
8:PPS,Picture Paramater Set(图像参数集),保存了编码视频序列中一个或多个独立图像的参数。
9:AU分隔符(Access Unit),它是一个或者多个NALU的集合,代表了一个完整的帧。

nal_ref_idcnal_unit_type具有如下相关性:
ref_idc-nalu_type

随便查看一个H264的TS文件,可以看到类似数据:

  1. // nal_ref_idc为3,nal_unit_type为7,表示SPS
  2. 0000 0001 67 XX XX ......
  3. // nal_ref_idc为3,nal_unit_type为8,表示PPS
  4. 0000 0001 68 XX XX ......
  5. // nal_ref_idc为3,nal_unit_type为5,表示IDR帧
  6. 0000 0001 65 XX XX ......
  7. // nal_ref_idc为0,nal_unit_type为6,表示SEI
  8. 0000 0001 06 XX XX ......

SPS、PPS和IDR帧都是不可或缺的NALU,重要性是最高的,但是SEI可以不参与解码,重要性为0,当解码不过来时,可以直接丢弃。

NALU Payload数据是EBSP,如下所示:
EBSP

若SODB不是8bit对齐,那么有两种方式进行字节对齐:
nal_unit_type不等于1~5时,就按照下面的格式补齐字节对齐,即先补一个1,其余全是0。
RBSP

最终的RBSP如下所示,红色的1bit就是rbsp_stop_one_bit,灰色的bit就是rbsp_alignment_zero_bit
RBSP Show

nal_unit_type等于1~5时,按照Slice尾部进行字节对齐,如下所示:
rbsp_slice_trailing_bits
默认情况下,rbsp_slice_trailing_bits就是上面的rbsp_trailing_bits尾部。只是当entropy_coding_mode_flag为1,即当前采用的熵编码为CABAC,而且more_rbsp_trailing_data()返回为true,即RBSP中有更多数据时,添加一个或多个0x0000。

众所周知,NALU的Start Code为0x000001或0x00000001,同时H264规定,当检测到0x000000时,也可以表示当前NALU的结束。那这样就会产生一个问题:若NALU Payload出现了0x000001或0x000000该怎么办?

所以H264就提出了防止竞争的机制,当构建NALU Payload时,应该先检测RBSP是否包含下面左侧字节序列,当检测到它们存在时,编码器就在0x0000后面插入防竞争码:0x03,所以EBSP比RBSP多了防竞争码0x03。

  1. // NALU End Code
  2. 0x000000 -> 0x00000300
  3. // Start Code
  4. 0x000001 -> 0x00000301
  5. // 保留使用
  6. 0x000002 -> 0x00000302
  7. // 防竞争序列
  8. 0x000003 -> 0x00000303

NALU Payload = SODB + rbsp trailing bits + 防竞争码0x03,当解码时,需要反序去除防竞争码0x03和rbsp trailing bits。

当从NALU获取RBSP时,首先要做的就是去除防竞争码0x03,如下所示:遇到0x000003时,就跳过0x03。
emulation_prevention_three_byte

SPS

SPS结构如下所示:
SPS

libavcodec/h264_ps.c提供了ff_h264_decode_seq_parameter_set函数解析SPS,提供了ff_h264_decode_picture_parameter_set解析PPS。

pic_width_in_mbs_minus1

图像编码宽度,单位是宏块个数,因此图像编码像素宽度为:

  1. (pic_width_in_mbs_minus1 + 1) * 16

pic_height_in_map_units_minus1

图像编码高度,单位是宏块个数,因此图像编码像素高度为:

  1. (2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16

frame_mbs_only_flag

与pic_height_in_map_units_minus1配合,计算出图像编码像素高度

frame_cropping_flag

标识是否需要对输出的图像帧进行裁剪,以得到真实有效分辨率。H264编码是以宏块为单位,所以编码分辨率一定是16的倍数,但是真实有效分辨率则不一定,所以需要裁剪出有效分辨率。

frame_crop_left_offset、frame_crop_right_offset、frame_crop_right_top和frame_crop_right_bottom

若frame_cropping_flag为1,则存在裁剪区域。
libavcodec/h264_ps.c中ff_h264_decode_seq_parameter_set函数处理Crop的核心代码如下所示:

  1. // 这里是横向和纵向的宏块个数,就是上面👆计算公式计算出来的
  2. sps->mb_width = get_ue_golomb(gb) + 1;
  3. sps->mb_height = get_ue_golomb(gb) + 1;
  4. sps->frame_mbs_only_flag = get_bits1(gb);
  5. sps->mb_height *= 2 - sps->frame_mbs_only_flag;
  6. // 原始的frame_crop_left_offset、frame_crop_right_offset、frame_crop_right_top和frame_crop_right_bottom信息
  7. unsigned int crop_left = get_ue_golomb(gb);
  8. unsigned int crop_right = get_ue_golomb(gb);
  9. unsigned int crop_top = get_ue_golomb(gb);
  10. unsigned int crop_bottom = get_ue_golomb(gb);
  11. // 图像编码像素尺寸,就是上面👆计算公式计算出来的
  12. int width = 16 * sps->mb_width;
  13. int height = 16 * sps->mb_height;
  14. int vsub = (sps->chroma_format_idc == 1) ? 1 : 0;
  15. int hsub = (sps->chroma_format_idc == 1 || sps->chroma_format_idc == 2) ? 1 : 0;
  16. int step_x = 1 << hsub;
  17. int step_y = (2 - sps->frame_mbs_only_flag) << vsub;
  18. // 计算出Crop区域
  19. sps->crop_left = crop_left * step_x;
  20. sps->crop_right = crop_right * step_x;
  21. sps->crop_top = crop_top * step_y;
  22. sps->crop_bottom = crop_bottom * step_y;

上面计算好SPS的Crop信息,libavcodec/h264_slice.c中init_dimensions函数会赋值给AVCodecContext,就是我们外部拿到的信息了,总结下:

  1. AVCodecContext->coded_width = 16 * sps->mb_width
  2. AVCodecContext->coded_height = 16 * sps->mb_height
  3. AVCodecContext->width = AVCodecContext->coded_width - (sps->crop_right + sps->crop_left);
  4. AVCodecContext->height = AVCodecContext->coded_height - (sps->crop_top + sps->crop_bottom);

解码时,需要兼容裁剪区域。
假设有一个810x540的H264视频,它的SPS如下所示:
crop

按照上面介绍的计算方式,可以计算出编码和显示分辨率,如下所示:
crop-demo
编码尺寸是16像素对齐的,但是真实显示分辨率非16像素对齐。

MediaCodec硬解码时,MediaFormat指定了编码尺寸和Crop信息,

  1. // 视频编码分辨率
  2. int codedWidth = MediaFormat.getInteger(MediaFormat.KEY_WIDTH);
  3. int codedHeight = MediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
  4. // The left-coordinate (x) of the crop rectangle
  5. int cropLeft = MediaFormat.getInteger("crop-left");
  6. // The right-coordinate (x) MINUS 1 of the crop rectangle
  7. int cropRight = MediaFormat.getInteger("crop-right") + 1;
  8. int cropTop = MediaFormat.getInteger("crop-top");
  9. int cropBottom = MediaFormat.getInteger("crop-bottom") + 1;
  10. // 视频显示分辨率
  11. int width = cropRight - cropLeft
  12. int height = cropBottom - cropTop
  13. // 实测值
  14. codedWidth: 816
  15. codedHeight: 544
  16. cropLeft: 0
  17. cropRight: 810
  18. cropTop: 0
  19. cropBottom: 540

FFmpeg软解码时,内部会自动 cpu crop:apply_cropping、av_frame_apply_cropping>
这个确认下在哪处理的呀。是不是width是810,然后stride是816?

bit_depth_luma_minus8和bit_depth_chroma_minus8

若是图中指定的Profile,则会解析位深,即一个颜色通道用几个bit表示,默认情况下是8bit,
位深等于bit_depth_luma_minus8 + 8,bit_depth_luma_minus8和bit_depth_chroma_minus8一般是一致的。

例如:H264 10bit视频,那么bit_depth_luma_minus8和bit_depth_chroma_minus8都是2。

PPS

libavcodec/h264_ps.c提供了ff_h264_decode_seq_parameter_set函数解析SPS,提供了ff_h264_decode_picture_parameter_set解析PPS。

PPS

pic_parameter_set_id

当前PPS的id。某个PPS在码流中会被相应的slice引用,slice引用PPS的方式就是在Slice header中保存PPS的id值,该值的取值范围为[0,255]。

seq_parameter_set_id

当前PPS引用的SPS id,通过这种方式,PPS也可以取到对应SPS的参数,该值的取值范围为[0,31]。

entropy_coding_mode_flag

熵编码模式标识,标识了码流中熵编码/解码选择的算法。

num_slice_groups_minus1

表示某一帧中slice group个数。当该值为0时,一帧中所有slice都属于一个slice group。slice group是一帧中宏块的组合方式。

num_ref_idx_l0_default_active_minus1、num_ref_idx_l0_default_active_minus1

当Slice Header中的num_ref_idx_active_override_flag标识位为0时,P/SP/B
slice语法元素num_ref_idx_l0_active_minus1和num_ref_idx_l1_active_minus1的默认值。

Slice

一个Slice包含一帧图像的部分或全部数据,即:一帧视频图像可以编码为一个或若干个Slice。一个Slice最少包含一个宏块,最多包含整帧图像数据。在不同的编码实现中,同一帧图像被分割成的Slice数量可能是不同的。
Slice的主要目的是防止误码扩散,因为不同的slice编码时,是不能互相参考的,解码时也是相互独立解码,所以同一个视频帧的多个Slice可以多线程并行解码。

关于Slice,可以用一张图来表示:
Slice

由上图可知,每个Slice由Slice Header和Slice Body构成,Slice Header包含了slice_type等信息,Slice Body则包含了一组连续的宏块。

宏块是视频信息的主要承载者,它包含每个像素的亮度和色度信息。解码器的工作就是提供高效的方式从码流中获得宏块中的像素阵列。
H264中,宏块由一个16*16亮度像素和附加的一个8 * 8 Cb和一个8 * 8 Cr彩色像素块组成,即固定的16像素。宏块结构如下所示:
macroblock

Slice Header中的slice_type非常重要,是判断帧类型(AVFrame->pict_type)的依据。
AVFrame->pict_type的取值是AVPictureType结构体,如下所示:

  1. enum AVPictureType {
  2. AV_PICTURE_TYPE_NONE = 0, ///< Undefined
  3. AV_PICTURE_TYPE_I, ///< Intra I帧
  4. AV_PICTURE_TYPE_P, ///< Predicted P帧
  5. AV_PICTURE_TYPE_B, ///< Bi-dir predicted B帧
  6. AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG-4
  7. AV_PICTURE_TYPE_SI, ///< Switching Intra
  8. AV_PICTURE_TYPE_SP, ///< Switching Predicted
  9. AV_PICTURE_TYPE_BI, ///< BI type
  10. };

那怎么根据slice_type,确定pict_type那?
slice_type的取值范围是[0,9],具体取值如下所示:
slice_type

slice_type与nal_unit_type存在对应关系:

根据slice_type计算pict_type的具体规则是:若slice_type大于4,则取slice_type - 5作为索引(否则就是slice_type直接作为索引)从ff_h264_golomb_to_pict_type数组取出的值就是AVFrame->pict_type帧类型.

  1. const uint8_t ff_h264_golomb_to_pict_type[5] = {
  2. AV_PICTURE_TYPE_P, AV_PICTURE_TYPE_B, AV_PICTURE_TYPE_I,
  3. AV_PICTURE_TYPE_SP, AV_PICTURE_TYPE_SI
  4. };

若nal_unit_type是5,即IDR帧,那么从AVFrame->pict_type & 3必须是AV_PICTURE_TYPE_I,即:所有的IDR帧都是I帧。

具体代码逻辑可以参考libavcodec/h264_slice.c中的h264_slice_header_parse函数。

根据Slice Header中slice_type判断是否是I、P、B帧,根据NALU Type判断是否是IDR帧。
若是IDR帧,那么slice_type必然是I帧,但是slice_type是I帧,NALU Type不一定是IDR帧,即所有的IDR帧都是I帧,但是I帧不一定是IDR帧。

PTS和DTS

视频文件中,DTS是递增的,由于B帧的存在,PTS不一定是递增的。同一个视频帧,PTS >= DTS,因为必须先解码,才能渲染。

MediaCodec硬编视频时,若Profile大于等于High,即包含B帧,那怎么确定每一帧的PTS和DTS?

分析工具

  1. H264BSAnalyzer

参考文章

  1. H264编码格式整理
  2. H264视频码流解析
  3. H.264/AVC Video Coding Standard
  4. FFmpeg从入门到精通——进阶篇,SEI那些事儿
  5. [VCB-Studio 科普教程 3] 视频格式基础知识
  6. 10-Bit H.264
  7. H.264编码格式以及I、B、P帧判断

从工程角度来看H264的编码格式。

从算法角度来看,H264的编码算法:帧内压缩、帧间压缩、DCT变换、CABAC字节流无损编码。

libavcodec/h264_parse.c提供了ff_h264_decode_extradata函数解析H264的AVCodecContext->extradata,兼容AVCDecoderConfigurationRecord和00 00 00 01 SPS 00 00 00 01 PPS两种形式。

libavcodec/h264_slice.c提供了ff_h264_queue_decode_slice -> h264_slice_header_parse函数解析Slice Header,确定是I、P、还是B帧。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注