@ltlovezh
2021-11-23T11:24:01.000000Z
字数 8776
阅读 1303
编码格式
音视频
H264码流结构并不复杂,主要由一系列GOP构成,一个GOP是一个视频编码序列,每个GOP的第一帧是IDR帧,此外还包含了SPS和PPS信息,每个GOP都可以独立解码。
一个GOP由一系列I、P、B帧构成,一个视频帧又可以划分为Slice(切片),一个Slice则由宏块构成,整体结构如下所示:
当从封装容器,Demux出H264码流时,实际是NALU
序列,这些NALU序列不仅包含了编码数据,也包含了元数据,如下所示:
H264规范中,明确规定了VCL(视频编码层)和NAL两层结构(网络提取层),如下所示:
虽然都是NALU序列,但是不同封装容器Demux出的NALU序列也存在差异,主要分为AVCC和Annex-B。
AVCC是由NALU Size + NALU构成,主要适用于Mp4、FLV和MKV等封装容器,整体结构如下所示
SPS和PPS元数据包含在独立存储模块,例如:Mp4的avcC Box包含了AVCDecoderConfigurationRecord,hvcC Box包含了HEVCDecoderConfigurationRecord, Flv第一个视频Tag也包含了AVCDecoderConfigurationRecord或者HEVCDecoderConfigurationRecord。
Annex-B是由Start Code + NALU构成,主要适用于TS封装容器,整体结构如下所示:
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字节,标识了负载数据是什么内容?,主要包含三部分:
| forbidden_zero_bit | nal_ref_idc | nal_unit_type |
`--------------------+-------------+---------------`
| 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_idc
和nal_unit_type
具有如下相关性:
随便查看一个H264的TS文件,可以看到类似数据:
// nal_ref_idc为3,nal_unit_type为7,表示SPS
0000 0001 67 XX XX ......
// nal_ref_idc为3,nal_unit_type为8,表示PPS
0000 0001 68 XX XX ......
// nal_ref_idc为3,nal_unit_type为5,表示IDR帧
0000 0001 65 XX XX ......
// nal_ref_idc为0,nal_unit_type为6,表示SEI
0000 0001 06 XX XX ......
SPS、PPS和IDR帧都是不可或缺的NALU,重要性是最高的,但是SEI可以不参与解码,重要性为0,当解码不过来时,可以直接丢弃。
NALU Payload数据是EBSP,如下所示:
若SODB不是8bit对齐,那么有两种方式进行字节对齐:
nal_unit_type不等于1~5时,就按照下面的格式补齐字节对齐,即先补一个1,其余全是0。
最终的RBSP如下所示,红色的1bit就是rbsp_stop_one_bit
,灰色的bit就是rbsp_alignment_zero_bit
。
nal_unit_type等于1~5时,按照Slice尾部进行字节对齐,如下所示:
默认情况下,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。
// NALU End Code
0x000000 -> 0x00000300
// Start Code
0x000001 -> 0x00000301
// 保留使用
0x000002 -> 0x00000302
// 防竞争序列
0x000003 -> 0x00000303
NALU Payload = SODB + rbsp trailing bits + 防竞争码0x03,当解码时,需要反序去除防竞争码0x03和rbsp trailing bits。
当从NALU获取RBSP时,首先要做的就是去除防竞争码0x03,如下所示:遇到0x000003时,就跳过0x03。
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) * 16
图像编码高度,单位是宏块个数,因此图像编码像素高度为:
(2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16
与pic_height_in_map_units_minus1配合,计算出图像编码像素高度
标识是否需要对输出的图像帧进行裁剪,以得到真实有效分辨率。H264编码是以宏块为单位,所以编码分辨率一定是16的倍数,但是真实有效分辨率则不一定,所以需要裁剪出有效分辨率。
若frame_cropping_flag为1,则存在裁剪区域。
libavcodec/h264_ps.c
中ff_h264_decode_seq_parameter_set函数处理Crop的核心代码如下所示:
// 这里是横向和纵向的宏块个数,就是上面👆计算公式计算出来的
sps->mb_width = get_ue_golomb(gb) + 1;
sps->mb_height = get_ue_golomb(gb) + 1;
sps->frame_mbs_only_flag = get_bits1(gb);
sps->mb_height *= 2 - sps->frame_mbs_only_flag;
// 原始的frame_crop_left_offset、frame_crop_right_offset、frame_crop_right_top和frame_crop_right_bottom信息
unsigned int crop_left = get_ue_golomb(gb);
unsigned int crop_right = get_ue_golomb(gb);
unsigned int crop_top = get_ue_golomb(gb);
unsigned int crop_bottom = get_ue_golomb(gb);
// 图像编码像素尺寸,就是上面👆计算公式计算出来的
int width = 16 * sps->mb_width;
int height = 16 * sps->mb_height;
int vsub = (sps->chroma_format_idc == 1) ? 1 : 0;
int hsub = (sps->chroma_format_idc == 1 || sps->chroma_format_idc == 2) ? 1 : 0;
int step_x = 1 << hsub;
int step_y = (2 - sps->frame_mbs_only_flag) << vsub;
// 计算出Crop区域
sps->crop_left = crop_left * step_x;
sps->crop_right = crop_right * step_x;
sps->crop_top = crop_top * step_y;
sps->crop_bottom = crop_bottom * step_y;
上面计算好SPS的Crop信息,libavcodec/h264_slice.c
中init_dimensions函数会赋值给AVCodecContext,就是我们外部拿到的信息了,总结下:
AVCodecContext->coded_width = 16 * sps->mb_width;
AVCodecContext->coded_height = 16 * sps->mb_height;
AVCodecContext->width = AVCodecContext->coded_width - (sps->crop_right + sps->crop_left);
AVCodecContext->height = AVCodecContext->coded_height - (sps->crop_top + sps->crop_bottom);
解码时,需要兼容裁剪区域。
假设有一个810x540的H264视频,它的SPS如下所示:
按照上面介绍的计算方式,可以计算出编码和显示分辨率,如下所示:
编码尺寸是16像素对齐的,但是真实显示分辨率非16像素对齐。
MediaCodec硬解码时,MediaFormat指定了编码尺寸和Crop信息,
// 视频编码分辨率
int codedWidth = MediaFormat.getInteger(MediaFormat.KEY_WIDTH);
int codedHeight = MediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
// The left-coordinate (x) of the crop rectangle
int cropLeft = MediaFormat.getInteger("crop-left");
// The right-coordinate (x) MINUS 1 of the crop rectangle
int cropRight = MediaFormat.getInteger("crop-right") + 1;
int cropTop = MediaFormat.getInteger("crop-top");
int cropBottom = MediaFormat.getInteger("crop-bottom") + 1;
// 视频显示分辨率
int width = cropRight - cropLeft;
int height = cropBottom - cropTop;
// 实测值
codedWidth: 816
codedHeight: 544
cropLeft: 0
cropRight: 810
cropTop: 0
cropBottom: 540
FFmpeg软解码时,内部会自动 cpu crop:apply_cropping、av_frame_apply_cropping>
这个确认下在哪处理的呀。是不是width是810,然后stride是816?
若是图中指定的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。
libavcodec/h264_ps.c
提供了ff_h264_decode_seq_parameter_set
函数解析SPS,提供了ff_h264_decode_picture_parameter_set
解析PPS。
当前PPS的id。某个PPS在码流中会被相应的slice引用,slice引用PPS的方式就是在Slice header中保存PPS的id值,该值的取值范围为[0,255]。
当前PPS引用的SPS id,通过这种方式,PPS也可以取到对应SPS的参数,该值的取值范围为[0,31]。
熵编码模式标识,标识了码流中熵编码/解码选择的算法。
表示某一帧中slice group个数。当该值为0时,一帧中所有slice都属于一个slice group。slice group是一帧中宏块的组合方式。
当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 Header和Slice Body构成,Slice Header包含了slice_type等信息,Slice Body则包含了一组连续的宏块。
宏块是视频信息的主要承载者,它包含每个像素的亮度和色度信息。解码器的工作就是提供高效的方式从码流中获得宏块中的像素阵列。
H264中,宏块由一个16*16亮度像素和附加的一个8 * 8 Cb和一个8 * 8 Cr彩色像素块组成,即固定的16像素。宏块结构如下所示:
Slice Header中的slice_type非常重要,是判断帧类型(AVFrame->pict_type)的依据。
AVFrame->pict_type
的取值是AVPictureType结构体,如下所示:
enum AVPictureType {
AV_PICTURE_TYPE_NONE = 0, ///< Undefined
AV_PICTURE_TYPE_I, ///< Intra I帧
AV_PICTURE_TYPE_P, ///< Predicted P帧
AV_PICTURE_TYPE_B, ///< Bi-dir predicted B帧
AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG-4
AV_PICTURE_TYPE_SI, ///< Switching Intra
AV_PICTURE_TYPE_SP, ///< Switching Predicted
AV_PICTURE_TYPE_BI, ///< BI type
};
那怎么根据slice_type,确定pict_type那?
slice_type的取值范围是[0,9],具体取值如下所示:
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
帧类型.
const uint8_t ff_h264_golomb_to_pict_type[5] = {
AV_PICTURE_TYPE_P, AV_PICTURE_TYPE_B, AV_PICTURE_TYPE_I,
AV_PICTURE_TYPE_SP, AV_PICTURE_TYPE_SI
};
若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帧。
视频文件中,DTS是递增的,由于B帧的存在,PTS不一定是递增的。同一个视频帧,PTS >= DTS,因为必须先解码,才能渲染。
MediaCodec硬编视频时,若Profile大于等于High,即包含B帧,那怎么确定每一帧的PTS和DTS?
从工程角度来看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帧。