@ltlovezh
        
        2021-11-23T03:24:01.000000Z
        字数 8776
        阅读 1737
    编码格式 音视频
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,表示SPS0000 0001 67 XX XX ......// nal_ref_idc为3,nal_unit_type为8,表示PPS0000 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,表示SEI0000 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 Code0x000000 -> 0x00000300// Start Code0x000001 -> 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 rectangleint cropLeft = MediaFormat.getInteger("crop-left");// The right-coordinate (x) MINUS 1 of the crop rectangleint 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: 816codedHeight: 544cropLeft: 0cropRight: 810cropTop: 0cropBottom: 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, ///< UndefinedAV_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-4AV_PICTURE_TYPE_SI, ///< Switching IntraAV_PICTURE_TYPE_SP, ///< Switching PredictedAV_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帧。