在做音视频开发的时候,存在不解码视频帧的前提下需要获取视频宽高、帧率等信息,而H.264中的SPS数据可为我们提供这些相关的信息。在此之前,我们需要对一些协议和算法有一定的初步了解,后文中有完整的代码展示。
我们在此不讲解H.264协议,但是我们需要了解NAL单元语法(NAL unit syntax)、序列参数集数据语法(Sequence parameter set data syntax)、视频可用参数语法(VUI parameters syntax)。具体可去ITU-T官网下载H.264文档,我查看的是T-REC-H.264-201704-I!!PDF-E.pdf。
第一列:相关参数信息名称;
第二列:规定条带数据可以至多划分为三种条带数据类别(这个可以不用理解);
第三列中的u(n)表示:使用n位无符号整数表示,由n位bit换算得到,即从左到右读取n个bit位所表示的值;ue(v)表示:无符号指数哥伦布编码值;ue(v)表示:有符号指数哥伦布编码值。
u(n)代码实现:
static UINT u(sps_bit_stream *bs, BYTE bitCount)
{
UINT val = 0;
for (BYTE i=0; idata[bs->index / 8] & (0x80 >> (bs->index % 8))) { //计算index所在的位是否为1
val |= 1;
}
bs->index++;
}
return val;
}
Golomb编码是一种无损的数据压缩方法,由数学家Solomon W.Golomb在1960年代发明。Golomb编码只能对非负整数进行编码,符号表中的符号出现的概率符合几何分布(Geometric Distribution)时,使用Golomb编码可以取得最优效果,也就是说Golomb编码比较适合小的数字比大的数字出现概率比较高的编码。它使用较短的码长编码较小的数字,较长的码长编码较大的数字。在此,我们只需要了解:无符号指数哥伦布编码ue(v)、有符号指数哥伦布编码se(v)。
3. 根据这个公式得到计算结果code_num = Info – 1 + 2M
代码实现:
static UINT ue(sps_bit_stream *bs)
{
UINT zeroNum = 0;
while (u(bs, 1) == 0 && !eof(bs) && zeroNum < 32) {
zeroNum ++;
}
return (UINT)((1 << zeroNum) - 1 + u(bs, zeroNum));
}
有符号的指数哥伦布编码值是通过无符号的指数哥伦布编码的值通过换算得到的,其换算关系为:n = (-1)^(k+1) * ceil(k/2)。
代码实现:
INT se(sps_bit_stream *bs)
{
INT ueVal = (INT)ue(bs);
double k = ueVal;
INT seVal = (INT)ceil(k / 2); //ceil:返回大于或者等于指定表达式的最小整数
if (ueVal % 2 == 0) { //偶数取反,即(-1)^(k+1)
seVal = -seVal;
}
return seVal;
}
从H.264协议文档中可以看出视频宽高涉及到的参数有:
pic_width_in_mbs_minus1:加1指定以宏块(16*16)为单位的每个解码图像的宽度,即PicWidthInSamplesL = (pic_width_in_mbs_minus1 + 1) * 16 ;
pic_height_in_map_units_minus1:加1指定解码帧或场中的一个切片组的高度,即PicSizeInMapUnits = PicWidthInMbs * (pic_height_in_map_units_minus1 + 1) ;
frame_mbs_only_flag:等于0指明了视频序列的编码图象可能是编码场或编码帧,等于1指明了每个编码视频序列的编码图像都是只含帧宏块的编码帧,即FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) * PicHeightInMapUnits ;
frame_cropping_flag:等于0表明不存在帧剪切偏移量,等于1表明在sps中下一个使用的帧剪切偏移量参数,即需要使用到frame_crop_left_offset、frame_crop_right_offset、frame_crop_top_offset、frame_crop_bottom_offset四个参数;
frame_crop_left_offset、frame_crop_right_offset、frame_crop_top_offset、frame_crop_bottom_offset:左右上下帧裁剪偏移量,需要根据chroma format计算实际的视频宽高;
chroma_format_idc:亮度取样对应的色度取样值,包括0~3,不存在时默认为1(即YUV420);
根据上述参数的定义说明,计算视频分辨率存在多种情况:1、需要判断是否包含宏片的编码帧或场;2、需要判断视频帧是否被裁剪;3、需要判断采样类型。因此代码大致如下:
UINT pic_width_in_mbs_minus1 = ue(&bs); //第36位开始
UINT pic_height_in_map_units_minus1 = ue(&bs); //47
UINT frame_mbs_only_flag = u(&bs, 1);
info->width = (INT)(pic_width_in_mbs_minus1 + 1) * 16;
info->height = (INT)(2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16;
if (!frame_mbs_only_flag) {
u(&bs, 1); //mb_adaptive_frame_field_flag
}
u(&bs, 1); //direct_8x8_inference_flag
UINT frame_cropping_flag = u(&bs, 1);
if (frame_cropping_flag) {
UINT frame_crop_left_offset = ue(&bs);
UINT frame_crop_right_offset = ue(&bs);
UINT frame_crop_top_offset = ue(&bs);
UINT frame_crop_bottom_offset= ue(&bs);
//See 6.2 Source, decoded, and output picture formats
INT crop_unit_x = 1;
INT crop_unit_y = 2 - frame_mbs_only_flag; //monochrome or 4:4:4
if (chroma_format_idc == 1) { //4:2:0
crop_unit_x = 2;
crop_unit_y = 2 * (2 - frame_mbs_only_flag);
} else if (chroma_format_idc == 2) { //4:2:2
crop_unit_x = 2;
crop_unit_y = 2 - frame_mbs_only_flag;
}
info->width -= crop_unit_x * (frame_crop_left_offset + frame_crop_right_offset);
info->height -= crop_unit_y * (frame_crop_top_offset + frame_crop_bottom_offset);
}
视频帧率信息在SPS的VUI parameters syntax中,需要根据time_scale、fixed_frame_rate_flag计算得到:fps = time_scale / num_units_in_tick。但是需要判断参数timing_info_present_flag是否存在,若不存在表示FPS在信息流中无法获取。同时还存在另外一种情况:fixed_frame_rate_flag为1时,两个连续图像的HDR输出时间频率为单位,获取的fps是实际的2倍。
UINT timing_info_present_flag = u(bs, 1);
if (timing_info_present_flag) {
UINT num_units_in_tick = u(bs, 32);
UINT time_scale = u(bs, 32);
UINT fixed_frame_rate_flag = u(bs, 1);
info->fps = (UINT)((float)time_scale / (float)num_units_in_tick);
if (fixed_frame_rate_flag) {
info->fps = info->fps/2;
}
}
H264ParseSPS.h
//
// H264ParseSPS.h
//
// Created by lzj on 2018/7/6.
// Copyright © 2018年 LZJ. All rights reserved.
//
#ifndef H264ParseSPS_h
#define H264ParseSPS_h
#ifdef __cplusplus
extern "C"
{
#endif
#include
typedef struct
{
unsigned int profile_idc;
unsigned int level_idc;
unsigned int width;
unsigned int height;
unsigned int fps; //SPS中可能不包含FPS信息
} sps_info_struct;
/**
解析SPS数据信息
@param data SPS数据内容,需要Nal类型为0x7数据的开始(比如:67 42 00 28 ab 40 22 01 e3 cb cd c0 80 80 a9 02)
@param dataSize SPS数据的长度
@param info SPS解析之后的信息数据结构体
@return success:1,fail:0
*/
int h264_parse_sps(const unsigned char *data, unsigned int dataSize, sps_info_struct *info);
#ifdef __cplusplus
}
#endif
#endif /* H264ParseSPS_h */
H264ParseSPS.c
//
// H264ParseSPS.c
//
// Created by lzj on 2018/7/6.
// Copyright © 2018年 LZJ. All rights reserved.
//
// See https://www.itu.int/rec/T-REC-H.264-201610-S
//
#include "H264ParseSPS.h"
#include
#include
#include
typedef unsigned char BYTE;
typedef int INT;
typedef unsigned int UINT;
typedef struct
{
const BYTE *data; //sps数据
UINT size; //sps数据大小
UINT index; //当前计算位所在的位置标记
} sps_bit_stream;
/**
移除H264的NAL防竞争码(0x03)
@param data sps数据
@param dataSize sps数据大小
*/
static void del_emulation_prevention(BYTE *data, UINT *dataSize)
{
UINT dataSizeTemp = *dataSize;
for (UINT i=0, j=0; i<(dataSizeTemp-2); i++) {
int val = (data[i]^0x0) + (data[i+1]^0x0) + (data[i+2]^0x3); //检测是否是竞争码
if (val == 0) {
for (j=i+2; jdata = data;
bs->size = size;
bs->index = 0;
}
}
/**
是否已经到数据流最后
@param bs sps_bit_stream数据
@return 1:yes,0:no
*/
static INT eof(sps_bit_stream *bs)
{
return (bs->index >= bs->size * 8); //位偏移已经超出数据
}
/**
读取从起始位开始的BitCount个位所表示的值
@param bs sps_bit_stream数据
@param bitCount bit位个数(从低到高)
@return value
*/
static UINT u(sps_bit_stream *bs, BYTE bitCount)
{
UINT val = 0;
for (BYTE i=0; idata[bs->index / 8] & (0x80 >> (bs->index % 8))) { //计算index所在的位是否为1
val |= 1;
}
bs->index++; //递增当前起始位(表示该位已经被计算,在后面的计算过程中不需要再次去计算所在的起始位索引,缺点:后面每个bit位都需要去位移)
}
return val;
}
/**
读取无符号哥伦布编码值(UE)
#2^LeadingZeroBits - 1 + (xxx)
@param bs sps_bit_stream数据
@return value
*/
static UINT ue(sps_bit_stream *bs)
{
UINT zeroNum = 0;
while (u(bs, 1) == 0 && !eof(bs) && zeroNum < 32) {
zeroNum ++;
}
return (UINT)((1 << zeroNum) - 1 + u(bs, zeroNum));
}
/**
读取有符号哥伦布编码值(SE)
#(-1)^(k+1) * Ceil(k/2)
@param bs sps_bit_stream数据
@return value
*/
INT se(sps_bit_stream *bs)
{
INT ueVal = (INT)ue(bs);
double k = ueVal;
INT seVal = (INT)ceil(k / 2); //ceil:返回大于或者等于指定表达式的最小整数
if (ueVal % 2 == 0) { //偶数取反,即(-1)^(k+1)
seVal = -seVal;
}
return seVal;
}
/**
视频可用性信息(Video usability information)解析
@param bs sps_bit_stream数据
@param info sps解析之后的信息数据及结构体
@see E.1.1 VUI parameters syntax
*/
void vui_para_parse(sps_bit_stream *bs, sps_info_struct *info)
{
UINT aspect_ratio_info_present_flag = u(bs, 1);
if (aspect_ratio_info_present_flag) {
UINT aspect_ratio_idc = u(bs, 8);
if (aspect_ratio_idc == 255) { //Extended_SAR
u(bs, 16); //sar_width
u(bs, 16); //sar_height
}
}
UINT overscan_info_present_flag = u(bs, 1);
if (overscan_info_present_flag) {
u(bs, 1); //overscan_appropriate_flag
}
UINT video_signal_type_present_flag = u(bs, 1);
if (video_signal_type_present_flag) {
u(bs, 3); //video_format
u(bs, 1); //video_full_range_flag
UINT colour_description_present_flag = u(bs, 1);
if (colour_description_present_flag) {
u(bs, 8); //colour_primaries
u(bs, 8); //transfer_characteristics
u(bs, 8); //matrix_coefficients
}
}
UINT chroma_loc_info_present_flag = u(bs, 1);
if (chroma_loc_info_present_flag) {
ue(bs); //chroma_sample_loc_type_top_field
ue(bs); //chroma_sample_loc_type_bottom_field
}
UINT timing_info_present_flag = u(bs, 1);
if (timing_info_present_flag) {
UINT num_units_in_tick = u(bs, 32);
UINT time_scale = u(bs, 32);
UINT fixed_frame_rate_flag = u(bs, 1);
info->fps = (UINT)((float)time_scale / (float)num_units_in_tick);
if (fixed_frame_rate_flag) {
info->fps = info->fps/2;
}
}
UINT nal_hrd_parameters_present_flag = u(bs, 1);
if (nal_hrd_parameters_present_flag) {
//hrd_parameters() //see E.1.2 HRD parameters syntax
}
//后面代码需要hrd_parameters()函数接口实现才有用
UINT vcl_hrd_parameters_present_flag = u(bs, 1);
if (vcl_hrd_parameters_present_flag) {
//hrd_parameters()
}
if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag) {
u(bs, 1); //low_delay_hrd_flag
}
u(bs, 1); //pic_struct_present_flag
UINT bitstream_restriction_flag = u(bs, 1);
if (bitstream_restriction_flag) {
u(bs, 1); //motion_vectors_over_pic_boundaries_flag
ue(bs); //max_bytes_per_pic_denom
ue(bs); //max_bits_per_mb_denom
ue(bs); //log2_max_mv_length_horizontal
ue(bs); //log2_max_mv_length_vertical
ue(bs); //max_num_reorder_frames
ue(bs); //max_dec_frame_buffering
}
}
//See 7.3.1 NAL unit syntax
//See 7.3.2.1.1 Sequence parameter set data syntax
INT h264_parse_sps(const BYTE *data, UINT dataSize, sps_info_struct *info)
{
if (!data || dataSize <= 0 || !info) return 0;
INT ret = 0;
BYTE *dataBuf = malloc(dataSize);
memcpy(dataBuf, data, dataSize); //重新拷贝一份数据,防止移除竞争码时对原数据造成影响
del_emulation_prevention(dataBuf, &dataSize);
sps_bit_stream bs = {0};
sps_bs_init(&bs, dataBuf, dataSize); //初始化SPS数据流结构体
u(&bs, 1); //forbidden_zero_bit
u(&bs, 2); //nal_ref_idc
UINT nal_unit_type = u(&bs, 5);
if (nal_unit_type == 0x7) { //Nal SPS Flag
info->profile_idc = u(&bs, 8);
u(&bs, 1); //constraint_set0_flag
u(&bs, 1); //constraint_set1_flag
u(&bs, 1); //constraint_set2_flag
u(&bs, 1); //constraint_set3_flag
u(&bs, 1); //constraint_set4_flag
u(&bs, 1); //constraint_set4_flag
u(&bs, 2); //reserved_zero_2bits
info->level_idc = u(&bs, 8);
ue(&bs); //seq_parameter_set_id
UINT chroma_format_idc = 1; //摄像机出图大部分格式是4:2:0
if (info->profile_idc == 100 || info->profile_idc == 110 || info->profile_idc == 122 ||
info->profile_idc == 244 || info->profile_idc == 44 || info->profile_idc == 83 ||
info->profile_idc == 86 || info->profile_idc == 118 || info->profile_idc == 128 ||
info->profile_idc == 138 || info->profile_idc == 139 || info->profile_idc == 134 || info->profile_idc == 135) {
chroma_format_idc = ue(&bs);
if (chroma_format_idc == 3) {
u(&bs, 1); //separate_colour_plane_flag
}
ue(&bs); //bit_depth_luma_minus8
ue(&bs); //bit_depth_chroma_minus8
u(&bs, 1); //qpprime_y_zero_transform_bypass_flag
UINT seq_scaling_matrix_present_flag = u(&bs, 1);
if (seq_scaling_matrix_present_flag) {
UINT seq_scaling_list_present_flag[8] = {0};
for (INT i=0; i<((chroma_format_idc != 3)?8:12); i++) {
seq_scaling_list_present_flag[i] = u(&bs, 1);
if (seq_scaling_list_present_flag[i]) {
if (i < 6) { //scaling_list(ScalingList4x4[i], 16, UseDefaultScalingMatrix4x4Flag[i])
} else { //scaling_list(ScalingList8x8[i − 6], 64, UseDefaultScalingMatrix8x8Flag[i − 6] )
}
}
}
}
}
ue(&bs); //log2_max_frame_num_minus4
UINT pic_order_cnt_type = ue(&bs);
if (pic_order_cnt_type == 0) {
ue(&bs); //log2_max_pic_order_cnt_lsb_minus4
} else if (pic_order_cnt_type == 1) {
u(&bs, 1); //delta_pic_order_always_zero_flag
se(&bs); //offset_for_non_ref_pic
se(&bs); //offset_for_top_to_bottom_field
UINT num_ref_frames_in_pic_order_cnt_cycle = ue(&bs);
INT *offset_for_ref_frame = (INT *)malloc((UINT)num_ref_frames_in_pic_order_cnt_cycle * sizeof(INT));
for (INT i = 0; iwidth = (INT)(pic_width_in_mbs_minus1 + 1) * 16;
info->height = (INT)(2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16;
if (!frame_mbs_only_flag) {
u(&bs, 1); //mb_adaptive_frame_field_flag
}
u(&bs, 1); //direct_8x8_inference_flag
UINT frame_cropping_flag = u(&bs, 1);
if (frame_cropping_flag) {
UINT frame_crop_left_offset = ue(&bs);
UINT frame_crop_right_offset = ue(&bs);
UINT frame_crop_top_offset = ue(&bs);
UINT frame_crop_bottom_offset= ue(&bs);
//See 6.2 Source, decoded, and output picture formats
INT crop_unit_x = 1;
INT crop_unit_y = 2 - frame_mbs_only_flag; //monochrome or 4:4:4
if (chroma_format_idc == 1) { //4:2:0
crop_unit_x = 2;
crop_unit_y = 2 * (2 - frame_mbs_only_flag);
} else if (chroma_format_idc == 2) { //4:2:2
crop_unit_x = 2;
crop_unit_y = 2 - frame_mbs_only_flag;
}
info->width -= crop_unit_x * (frame_crop_left_offset + frame_crop_right_offset);
info->height -= crop_unit_y * (frame_crop_top_offset + frame_crop_bottom_offset);
}
UINT vui_parameters_present_flag = u(&bs, 1);
if (vui_parameters_present_flag) {
vui_para_parse(&bs, info);
}
ret = 1;
}
free(dataBuf);
return ret;
}
代码中部分未用到的参数并没有获取并赋值,比如:u(&bs, 1); //forbidden_zero_bit。如需获取相应的参数,只需将注释后面对应的变量值进行赋值即可,如:UINT forbidden_zero_bit = u(&bs, 1);
h264_parse_sps接口中sps数据需要第一个字节以Nal类型为0x7作为开始,比如:67 42 00 28 ab 40 22 01 e3 cb cd c0 80 80 a9 02,解析得到的宽高为1080*1920,fps为0。
参考文档:T-REC-H.264-201704-I!!PDF-E.pdf、H264 SPS分析、编码算法之指数哥伦布编码等。