上篇中我们实现了初步解析NALU的步骤,下面我们开始依次解析码流中出现的NALU。最开始出现的就是序列参数集SPS,通过上篇解析nalu_header我们也能看到,第一个NALU的nalu->nal_unit_type
等于7,因此我们先解析它。
解析SPS的步骤分为两大类:
- (1)数据存放:定义与h264文档相匹配的SPS的语法结构,在c语言中我们常用的就是结构体
- (2)解析实现:从nalu->buf中逐个解析句法元素,存放在结构体元素中
下面我们首先实现第一条,为此我们首先新建文件parset.h和parset.c文件,头文件用于存放定义的结构体,.c文件用于存放函数的实现。
1、数据存放
我们先将SPS的句法元素定义为结构体sps_t
,注意到SPS中还包含另一类句法元素,也即vui_parameters(),在sps_t的基础之上,我们将它定义为包含在结构体sps_t中的结构体vui_parameters_t
。
以此类推,包含在vui_parameters()里的两个hrd_parameters(),也定义为包含在结构体vui_parameters_t中的结构体hrd_parameters_t
。
下面我们根据h264文档,依次定义这几个结构体,为了便于学习,我们将文档中的if判断、for循环等语法结构也以注释的形式包含在内。而且对于每个句法元素,我们通通定义它们的类型为int,这样就省去了不必要的麻烦。
1.1、sps_t
/**
Sequence Parameter Set
@see 7.3.2.1 Sequence parameter set RBSP syntax
*/
typedef struct
{
int profile_idc; // u(8)
/* —————————— 编码级别的制约条件 Start —————————— */
int constraint_set0_flag; // u(1)
int constraint_set1_flag; // u(1)
int constraint_set2_flag; // u(1)
int constraint_set3_flag; // u(1)
int constraint_set4_flag; // u(1)
int constraint_set5_flag; // u(1)
int reserved_zero_2bits; // u(2)
/* —————————— 编码级别的制约条件 End —————————— */
int level_idc; // u(8)
int seq_parameter_set_id; // ue(v)
/* —————————— 几个罕见级别对应的句法元素 Start —————————— */
/*
if( profile_idc = = 100 | | profile_idc = = 110 | |
profile_idc = = 122 | | profile_idc = = 244 | |
profile_idc = = 44 | | profile_idc = = 83 | |
profile_idc = = 86 | | profile_idc = = 118 | |
profile_idc = = 128 | | profile_idc = = 138 | |
profile_idc = = 139 | | profile_idc = = 134 | |
profile_idc = = 135 ) {
*/
int chroma_format_idc; // ue(v)
// if( chroma_format_idc = = 3 )
int separate_colour_plane_flag; // u(1)
int bit_depth_luma_minus8; // ue(v)
int bit_depth_chroma_minus8; // ue(v)
int qpprime_y_zero_transform_bypass_flag; // u(1)
int seq_scaling_matrix_present_flag; // u(1)
// if( seq_scaling_matrix_present_flag )
// for( i = 0; i < ( ( chroma_format_idc != 3 ) ? 8 : 12 ); i++ ) {
int seq_scaling_list_present_flag[12]; // 最大值12数组 // u(1)
// if( seq_scaling_list_present_flag[ i ] )
// if( i < 6 )
int ScalingList4x4[6][16]; // 二维数组遍历
int UseDefaultScalingMatrix4x4Flag[6];
// else
int ScalingList8x8[6][64];
int UseDefaultScalingMatrix8x8Flag[6];
/* —————————— 几个罕见级别对应的句法元素 End —————————— */
/* —————————— 用来计算POC的句法元素 Start —————————— */
int log2_max_frame_num_minus4; // ue(v)
int pic_order_cnt_type; // ue(v)
// if( pic_order_cnt_type = = 0 )
int log2_max_pic_order_cnt_lsb_minus4; // ue(v)
// else if( pic_order_cnt_type = = 1 ) {
int delta_pic_order_always_zero_flag; // u(1)
int offset_for_non_ref_pic; // se(v)
int offset_for_top_to_bottom_field; // se(v)
int num_ref_frames_in_pic_order_cnt_cycle; // ue(v)
// for( i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++ )
int offset_for_ref_frame[256]; // 最大值256数组 // se(v)
/* —————————— 用来计算POC的句法元素 End —————————— */
int max_num_ref_frames; // ue(v)
int gaps_in_frame_num_value_allowed_flag; // u(1)
/* —————————— 图像宽高相关 Start —————————— */
int pic_width_in_mbs_minus1; // ue(v)
int pic_height_in_map_units_minus1; // ue(v)
int frame_mbs_only_flag; // u(1)
// if( !frame_mbs_only_flag )
int mb_adaptive_frame_field_flag; // u(1)
/* —————————— 图像宽高相关 End —————————— */
int direct_8x8_inference_flag; // u(1)
/* —————————— 解码后图像剪裁的几个句法元素 Start —————————— */
int frame_cropping_flag; // u(1)
// if( frame_cropping_flag ) {
int frame_crop_left_offset; // ue(v)
int frame_crop_right_offset; // ue(v)
int frame_crop_top_offset; // ue(v)
int frame_crop_bottom_offset; // ue(v)
/* —————————— 解码后图像剪裁的几个句法元素 End —————————— */
int vui_parameters_present_flag; // u(1)
// if( vui_parameters_present_flag )
vui_parameters_t vui_parameters; // Annex E E.1.1
} sps_t;
要注意里面有些句法元素,需要结合它前后的语法结构,和它的语义推敲得来。比如scaling_list相关几个数组,它们的数组大小是根据for循环的遍历次数得来。而且ScalingList4x4和ScalingList8x8被定义为了二维数组,因为在调用scaling_list()时,传进去的ScalingList4x4[i]本身是个一维数组,因此ScalingList4x4是二维数组。
而其中数组offset_for_ref_frame[]的大小,是由for循环的次数num_ref_frames_in_pic_order_cnt_cycle决定的,我们已经知道num_ref_frames_in_pic_order_cnt_cycle的取值为[0,255],因此数组offset_for_ref_frame[]的大小就是256。
1.2、vui_parameters_t
/**
vui_parameters()
@see Annex E E.1.1 VUI parameters syntax
*/
typedef struct {
int aspect_ratio_info_present_flag; // u(1)
// if( aspect_ratio_info_present_flag ) {
int aspect_ratio_idc; // u(8)
// if( aspect_ratio_idc = = Extended_SAR ) {
int sar_width; // u(16)
int sar_height; // u(16)
int overscan_info_present_flag; // u(1)
// if( overscan_info_present_flag )
int overscan_appropriate_flag; // u(1)
int video_signal_type_present_flag; // u(1)
// if( video_signal_type_present_flag ) {
int video_format; // u(3)
int video_full_range_flag; // u(1)
int colour_description_present_flag; // u(1)
// if( colour_description_present_flag ) {
int colour_primaries; // u(8)
int transfer_characteristics; // u(8)
int matrix_coefficients; // u(8)
int chroma_loc_info_present_flag; // u(1)
// if( chroma_loc_info_present_flag ) {
int chroma_sample_loc_type_top_field; // ue(v)
int chroma_sample_loc_type_bottom_field; // ue(v)
int timing_info_present_flag; // u(1)
// if( timing_info_present_flag ) {
int num_units_in_tick; // u(32)
int time_scale; // u(32)
int fixed_frame_rate_flag; // u(1)
int nal_hrd_parameters_present_flag; // u(1)
// if( nal_hrd_parameters_present_flag )
hrd_parameters_t nal_hrd_parameters;
int vcl_hrd_parameters_present_flag; // u(1)
// if( vcl_hrd_parameters_present_flag )
hrd_parameters_t vcl_hrd_parameters;
// if( nal_hrd_parameters_present_flag | | vcl_hrd_parameters_present_flag )
int low_delay_hrd_flag; // u(1)
int pic_struct_present_flag; // u(1)
int bitstream_restriction_flag; // u(1)
// if( bitstream_restriction_flag ) {
int motion_vectors_over_pic_boundaries_flag; // u(1)
int max_bytes_per_pic_denom; // ue(v)
int max_bits_per_mb_denom; // ue(v)
int log2_max_mv_length_horizontal; // ue(v)
int log2_max_mv_length_vertical; // ue(v)
int max_num_reorder_frames; // ue(v)
int max_dec_frame_buffering; // ue(v)
}vui_parameters_t;
1.3、hrd_parameters_t
/**
hrd_parameters()
@see Annex E E.1.2 HRD parameters syntax
*/
typedef struct {
int cpb_cnt_minus1; // ue(v)
int bit_rate_scale; // u(4)
int cpb_size_scale; // u(4)
// for( SchedSelIdx = 0; SchedSelIdx <= cpb_cnt_minus1; SchedSelIdx++ ) {
int bit_rate_value_minus1[MAX_CPB_CNT]; // ue(v)
int cpb_size_value_minus1[MAX_CPB_CNT]; // ue(v)
int cbr_flag[MAX_CPB_CNT]; // u(1)
int initial_cpb_removal_delay_length_minus1; // u(5)
int cpb_removal_delay_length_minus1; // u(5)
int dpb_output_delay_length_minus1; // u(5)
int time_offset_length; // u(5)
}hrd_parameters_t;
这样用到的结构体就已经定义完成,当然为了方便使用,我们还写了两个初始化结构体allocSPS()和释放的函数freeSPS(),就不贴代码了。下面我们开始实现解析部分。
2、解析实现
SPS的句法元素解析分为两步:
- (1)解析句法元素,存入结构体实例sps
- (2)保存解析后的结构体实例sps,以便后续使用
为此实现一个概括性函数:processSPS()
/**
处理SPS,包含两步:
先解析、后保存
*/
void processSPS(bs_t *b)
{
sps_t *sps = allocSPS();
// 0.解析
parse_sps_syntax_element(sps, b);
// 1.保存
save_sps_as_available(sps);
freeSPS(sps);
}
它接收码流读取工具句柄bs_t
,并负责控制解析和保存流程。
2.1 parse_sps_syntax_element()
解析的步骤很简单,我们只需对照h264文档,逐元素的解析即可。下面为parse_sps_syntax_element()的实现:
/**
解析sps句法元素
[h264协议文档位置]:7.3.2.1.1 Sequence parameter set data syntax
*/
void parse_sps_syntax_element(sps_t *sps, bs_t *b)
{
sps->profile_idc = bs_read_u(b, 8);
sps->constraint_set0_flag = bs_read_u(b, 1);
sps->constraint_set1_flag = bs_read_u(b, 1);
sps->constraint_set2_flag = bs_read_u(b, 1);
sps->constraint_set3_flag = bs_read_u(b, 1);
sps->constraint_set4_flag = bs_read_u(b, 1);
sps->constraint_set5_flag = bs_read_u(b, 1);
sps->reserved_zero_2bits = bs_read_u(b, 2);
sps->level_idc = bs_read_u(b, 8);
sps->seq_parameter_set_id = bs_read_ue(b);
if (sps->profile_idc == 100 || sps->profile_idc ==
110 || sps->profile_idc == 122 || sps->profile_idc ==
244 || sps->profile_idc == 44 || sps->profile_idc == 83
|| sps->profile_idc == 86 || sps->profile_idc == 118 ||
sps->profile_idc == 128 || sps->profile_idc == 138 ||
sps->profile_idc == 139 || sps->profile_idc == 134 ||
sps->profile_idc == 135) {
sps->chroma_format_idc = bs_read_ue(b);
if (sps->chroma_format_idc == YUV_4_4_4) {
sps->separate_colour_plane_flag = bs_read_u(b, 1);
}
sps->bit_depth_luma_minus8 = bs_read_ue(b);
sps->bit_depth_chroma_minus8 = bs_read_ue(b);
sps->qpprime_y_zero_transform_bypass_flag = bs_read_u(b, 1);
sps->seq_scaling_matrix_present_flag = bs_read_u(b, 1);
if (sps->seq_scaling_matrix_present_flag) {
int scalingListCycle = (sps->chroma_format_idc != YUV_4_4_4) ? 8 : 12;
for (int i = 0; i < scalingListCycle; i++) {
sps->seq_scaling_list_present_flag[i] = bs_read_u(b, 1);
if (sps->seq_scaling_list_present_flag[i]) {
if (i < 6) {
scaling_list(sps->ScalingList4x4[i], 16, &sps->UseDefaultScalingMatrix4x4Flag[i], b);
}else {
scaling_list(sps->ScalingList8x8[i-6], 64, &sps->UseDefaultScalingMatrix8x8Flag[i-6], b);
}
}
}
}
}
sps->log2_max_frame_num_minus4 = bs_read_ue(b);
sps->pic_order_cnt_type = bs_read_ue(b);
if (sps->pic_order_cnt_type == 0) {
sps->log2_max_pic_order_cnt_lsb_minus4 = bs_read_ue(b);
}else if (sps->pic_order_cnt_type == 1) {
sps->delta_pic_order_always_zero_flag = bs_read_u(b, 1);
sps->offset_for_non_ref_pic = bs_read_se(b);
sps->offset_for_top_to_bottom_field = bs_read_se(b);
sps->num_ref_frames_in_pic_order_cnt_cycle = bs_read_ue(b);
for (int i = 0; i < sps->num_ref_frames_in_pic_order_cnt_cycle; i++) {
sps->offset_for_ref_frame[i] = bs_read_se(b);
}
}
sps->max_num_ref_frames = bs_read_ue(b);
sps->gaps_in_frame_num_value_allowed_flag = bs_read_u(b, 1);
sps->pic_width_in_mbs_minus1 = bs_read_ue(b);
sps->pic_height_in_map_units_minus1 = bs_read_ue(b);
sps->frame_mbs_only_flag = bs_read_u(b, 1);
if (!sps->frame_mbs_only_flag) {
sps->mb_adaptive_frame_field_flag = bs_read_u(b, 1);
}
sps->direct_8x8_inference_flag = bs_read_u(b, 1);
sps->frame_cropping_flag = bs_read_u(b, 1);
if (sps->frame_cropping_flag) {
sps->frame_crop_left_offset = bs_read_ue(b);
sps->frame_crop_right_offset = bs_read_ue(b);
sps->frame_crop_top_offset = bs_read_ue(b);
sps->frame_crop_bottom_offset = bs_read_ue(b);
}
sps->vui_parameters_present_flag = bs_read_u(b, 1);
if (sps->vui_parameters_present_flag) {
parse_vui_parameters(sps, b);
}
}
注意到我们在讲语义时,曾说过句法元素chroma_format_idc代表了颜色空间的子采样格式,因此我们仿造JM,定义了几个枚举值,存放在frame.h文件中。
typedef enum {
COLOR_SPACE_FORMAT_UNKNOWN = -1, //!< Unknown color space format
YUV_4_0_0 = 0, //!< 没有色度chrome,只采样亮度luma
YUV_4_2_0 = 1, //!< 4:2:0
YUV_4_2_2 = 2, //!< 4:2:2
YUV_4_4_4 = 3 //!< 4:4:4
} Color_Space_Format;
另外解析sps时,与sps_t结构体一致,依次会调用scaling_list()、parse_vui_parameters()、parse_vui_hrd_parameters()三个函数:
void scaling_list(int *scalingList, int sizeOfScalingList, int *useDefaultScalingMatrixFlag, bs_t *b);
void parse_vui_parameters(sps_t *sps, bs_t *b);
void parse_vui_hrd_parameters(hrd_parameters_t *hrd, bs_t *b);
函数实现就不贴代码了。
2.2 save_sps_as_available()
此时sps已解析完毕,我们只需将它保存即可。为了便于后续pps根据它引用的sps_id,找出对应的sps,我们需要将它按照索引sps_id保存到全局变量的结构体数组中,也即:Sequence_Parameters_Set_Array[]
中。
而数组的大小,也即sps_id的取值范围,根据h264文档7.4.2.1节我们知道,seq_parameter_set_id的取值为[0, 31],因此数组最大容量即为32。
save_sps_as_available()实现如下:
void save_sps_as_available(sps_t *sps)
{
memcpy (&Sequence_Parameters_Set_Array[sps->seq_parameter_set_id], sps, sizeof (sps_t));
}
Sequence_Parameters_Set_Array[]定义如下:
// 因为sps_id的取值范围为[0,31],因此数组容量最大为32,详见7.4.2.1
sps_t Sequence_Parameters_Set_Array[32];
本文源码地址如下(H264Analysis_03中):