x264源码解读(三) - 编码前的准备工作(上)

今天开始进入到了x264的核心部分,编码。

编码总体分析

首先看一下encoder这个函数:

static int encode( x264_param_t *param, cli_opt_t *opt )
{
    ... ...
}

很多行,但是,就是阅读源码的时候要学会大刀切,不要限于细节,要了解代码的总体结构是怎么样的,其实也就是那么几种,后面可以慢慢体会,比如对于encoder而言,其实总体来分成三部分,编码前的准备,编码进行以及编码收尾工作,一旦你体会到这一点,那么几百行,几千行代码都不是问题,因为你已经可以对他们进行切块了。

首先我们来看一下编码前的工作,做了三件事情,分别是:初始化参数,初始化解码器,写入头信息。其中初始化编码器我们放在下一节详细介绍,本小节主要介绍初始化参数以及写入头信心两部分

初始化参数

先来看一下初始化参数这件事情,代码如下:

x264_t *h = NULL;
x264_picture_t pic;
cli_pic_t cli_pic;
const cli_pulldown_t *pulldown = NULL; // shut up gcc

int     i_frame = 0;
int     i_frame_output = 0;
int64_t i_end, i_previous = 0, i_start = 0;
int64_t i_file = 0;
int     i_frame_size;
int64_t last_dts = 0;
int64_t prev_dts = 0;
int64_t first_dts = 0;
#   define  MAX_PTS_WARNING 3 /* arbitrary */
int     pts_warning_cnt = 0;
int64_t largest_pts = -1;
int64_t second_largest_pts = -1;
int64_t ticks_per_frame;
double  duration;
double  pulldown_pts = 0;
int     retval = 0;

opt->b_progress &= param->i_log_level < X264_LOG_DEBUG;

/* set up pulldown */
if( opt->i_pulldown && !param->b_vfr_input )
{
	param->b_pulldown = 1;
	param->b_pic_struct = 1;
	pulldown = &pulldown_values[opt->i_pulldown];
	param->i_timebase_num = param->i_fps_den;
	FAIL_IF_ERROR2( fmod( param->i_fps_num * pulldown->fps_factor, 1 ),
					"unsupported framerate for chosen pulldown\n" );
	param->i_timebase_den = param->i_fps_num * pulldown->fps_factor;
}

DTS vs. PTS

看到初始化的变量中有以dts和pts做结尾的,比如last_dts,largest_pts等,这里介绍几个重要的概念,首先DTS和PTS,。

DTS(Decode Timestamp),解码时间戳,PTS(Presentation Timetamp),播放时间戳;每一帧都有这两个时间戳,DTS代表了解码顺序,PTS代表播放顺序;

播放和解码顺序不是一样的吗?这里就有了I片,B片以及P片区别,注意这里使用的是“片”,从H264开始,解码的最大单元就不再是帧,而是片。其中,I片(Intra Slice) 是帧内解码,解码只需要根据片内信息即可解码出来,对于P片(predictive slice)而言,需要根据前面片的信息才能够解码而言,对于B片(bi-directional interpolated prediction slice),从英文bi-directional中可见是双向的,即需要前后片的内容才能够解码出来,一般视频序列如下图所示,一个I帧跟随者B帧和P帧:

如果一个视频序列里面只有I帧和P帧,PTS和DTS是一致的;如果有了B帧,那么就不一致了,因为如果想要解出某个B帧,还需要解出B帧后面的帧内容,这就导致解码的顺序是B_pre_slice, B_after_slice, B_slice,按照这样的顺序才能够解出B帧,但是播放的顺序是:B_pre_slice,B_slice, B_after_slice, 所以需要PTS和DTS结合在一起来描述视频序列的解码以及播放。

H264对象体系

帧内解码以及帧间解码是H264核心的知识点,解码的方式和过程到源码相关部分介绍,这里需要建立这样一个概念即可;刚才提到了在H264开始,编解码的最大单元是片,至于I帧,B帧这类说法是H263时代的说法了,那么在H264里面他们的对象体系是如下图所示:

序列:一组图像,例如一个GOP就是一组序列;

图像:顶场(top field),底场(bottom field),帧(frame);一个图像有1个片组组成;

片组:片组是片的集合;每个片组包含一个或者多个片;片组里面片的常规的顺序是按照扫描次序进行编码的;

片:片编码的最大单元;即编码操作也是从片这个级别开始进行的;

NALU:NAL Unit,是抽象的传输层的单元,一个片将会拆分为1个或者多个NALU进行传输;NALU这个层级比较特殊,因为是NAL层级别的概念,其他的对象都是VCL层操作的对象,不过NALU是VCL层输出,从这个意义上面讲NALU还是属于VCL层;

宏块:每个NALU至少有一个宏块;预测的时候,对于片的分割都是基于宏块的;虽然在所属层次在片和宏块之间夹了一个NALU,但是其实实际应用和实现中,宏块和片直接相关,后面再讲预测的时候进行宏块分割都是从片上面进行16*16,8*8之类的分割;

亚宏块:对于宏块细分;

块:对于亚宏块进行细分;

像素/亚像素:图像的最小粒度;像素是我们比较熟悉的单位,在基于运动估计(预测)的时候,为了提高精度,都是与1/2或者1/4像素的,此时需要在像素之间插入亚像素来实现精度;

片组顺序

在上面提到的对象片组,除了按照正常的扫描顺序之外,还有还有两种比较特殊的片组顺序,第一个是ASO(Arbitrary Slice Order, ASO),描述的是片在传输的时候可以不按照编码顺序来传输,接收端在解码的时候也可以不按照编码顺序来进行解码;

第二种是FMO(Flexible Macroblock Ordering, FMO),通过映射表的方式记录宏块和片组的映射关系,灵活宏块传输的次序;片:是最大的解码单元,为什么要分片,目的是放置误差扩散;片是宏块的集合;

简单讲,ASO描述了在片层级上可以不按照扫描顺序进行传输,FMO则是描述了在宏块级别上可以不按照顺序进行传输;

NALU

NALU:NAL的概念,在前一节x264源码解读(二)中已经有描述;这小节,我们主要讲一下NALU,如果指定了片的进行数据分割(DP,Data Partition),那么一个片将会拆分到三类NALU(A,B,C三类),拆分为三类NALU之后,源编码器将会为每一类分片建立一个缓冲区,同时要保证每一个分片/分区(partition)的数据大小不超过MTU(Maximum Transmission Unit,网络传输包的最大大小);

一个片(I,P,B片)是由NALU的nal_unit_type决定的,即使所有的片为I片,nal_unit_type不是5(代表帧是I帧),那么由该片组构成的图像也不是I帧;nal_unit_type的类型是由图像的primary_pic_type属性决定;

NALU的unit type定义如下:

 

 

A型分割 :A型分割是头信息划分,包括宏块类型、量化参数和运动矢量,这个信息是最重要的。Unit_Type=2

B型分割 :B型分割是帧内信息划分,包括帧内CBPs和帧内系数。帧内信息可以阻止错误的传播,该型数据分割要求给定分片的A型分割有效,相对于帧间信息,帧内信息能更好地阻止漂移效应,因此它比帧间分割更为重要。Unit_Type=3

C型分割:C型分割是帧间信息划分,包括帧间CBPs和帧间系数,一般情况下它是编码分片的最大分区。帧间分割是最不重要的,它的使用要求A型分割有效。Unit_Type=4

虽然在标准中,为SPS,PPS以及SEI单独分配了unit type,但是在实际的实现上并不会真正单独产生一个NALU专门放置;会放在ABC型的NALU里面中;

对于NALU,在传输的时候,他的组织结构如下:

每个NALU,他的内部结构是由NAL Header和RBSP(Raw Byte Sequence Payload)如下:

其中NAL Header主要描述RBSP里面的数据的元数据,结构如下:

 

 

其中第一位F是Forbit位,当这个位的值是1的时候,表示语法错误;

接着两位是NRI(nal_ref_idc)位,参考级别,值越大NAL越重要;比如如果数据是SPS/ PPS,那么Unit的NRI值是一定要大于1,因为里面信息是关乎全局的,非常重要;

最后的5位是单元数据类型,NAL Unit Type,上面列表给出的32种类型,注意这里采用的是5位,2^5=32,其实并没有那么多的NALU Type,所以会出现13~31为保留和未占用,即:你用不用,他都是那么多种类别,因为bit位数在那里;

对于RBSP,我们就要提到SODB(String Of DataBit),原始数据bit流,成功不一定是8的倍数,为了处理方便,将会进行补齐,即添加trailing bits,确保是码流为整数字节。故,RBSP和SODB,NAL Header的的关系如下所示:

SODB + RBSP trailing bits = RBSP 
NAL header(1 byte) + RBSP = NALU

手头没有h264的文件,借用h264 NALU的获取与分析_xiaoluer的专栏-CSDN博客_h264 nalu type里面的一个例子来一起分析一下

00 00 00 01是start code,关注startcode的后两位,分别是06,67,68以及65:

0x06 & 0x1f = 6,nalu为辅助增强信息 (SEI);

0x67 & 0x1f = 7,nalu为SPS;

0x68 & 0x1f = 8,nalu为PPS;

0x65 & 0x1f = 5,nalu为I帧。

为什么是要&上1f呢?我们拿0x06&0x1f举例:首先开头是0x,代表了这是16进制的表现,每位16进制是2^4,所以两位16进制正好是1 Byte(这个也是为什么16进制这么风靡的原因);1对应二进制就是1,f对应二进制是1111(16进制的最大表示边界值),1f就是11111,五个1,上面介绍了在NAL Header中Type就是5bit的;06对应的二进制是0110,在和1f进行“与”运算的时候,即00110和11111进行运算,注意06是四位,前面加0进行补位;最终结果是110,即6,查表得到对应的NALU Type是SEI。

关于pulldown

if( opt->i_pulldown && !param->b_vfr_input )
{
	param->b_pulldown = 1;
	param->b_pic_struct = 1;
	pulldown = &pulldown_values[opt->i_pulldown];
	param->i_timebase_num = param->i_fps_den;
	FAIL_IF_ERROR2( fmod( param->i_fps_num * pulldown->fps_factor, 1 ),
					"unsupported framerate for chosen pulldown\n" );
	param->i_timebase_den = param->i_fps_num * pulldown->fps_factor;
}

pulldown是一种视频/电影帧率软同步(soft telecine)的信号,信号的值是预设好的,包括:none, 22, 32, 64, double, triple and euro;

注意telecine是一个组合词,television和cinema组合;对于telccine的概念我们举一个例子:例如,NTSC制式的电影(NTSC Film)是采用逐行扫描的方式来渲染画面,通用的帧率是24fps;NTSC的视频(NTSC Vision)是采用隔行扫描的方式来渲染画面,帧率是30fps,即60场/s;

如果现在如果想要采用NTFS Vision的制式来播放NTSC File的节目怎么办,即如何用30fps来播放24fps;最直接的方式就是将24fps→转化为30fps,这个就是telecine;即两个制式的节目之间的兼容播放的处理,或者说帧率对齐的处理;

这种转化有两种方式,分别是硬性转化(hard telecine)以及软转化(soft telecine),影星转化就是通过物理插帧的方式来进行帧率对齐,pulldown就是这样的实现了帧率对齐,我们通过下面展示硬转化的过程:

软转化(soft telecine)不同于物理插入值,而只是标记为某一帧的某个场要播放时间长一些,让播放器在播放的时候动态处理插帧。这里软转化,即为pulldown信号要做的事情。

encoder初始化

h = x264_encoder_open( param );
... ...
x264_encoder_parameters( h, param );

x264_encoder_open()里面做了大量的encode的初始化工作,返回的h就是一个encode操作相关的结构体,我们下一节将会详细介绍编码器的初始化工作;x264_encoder_parameters()则是将入口参数信息拷贝到encoder对象中;

ticks_per_frame的计算

/* ticks/frame = ticks/second / frames/second */
ticks_per_frame = (int64_t)param->i_timebase_den * param->i_fps_den / param->i_timebase_num / param->i_fps_num;
ticks_per_frame = X264_MAX( ticks_per_frame, 1 );

tickes_per_frame的计算非常重要,因为它和一个tamebase的概念相关,我们其实习惯了秒,毫秒,微妙这些既定的时间单位,但是在音视频的世界里面,并不是都是严格按照这些既定时间来进行的,比如我们上面提到的pulldown处理场景,当需要根据情况来对N帧同步到M帧的场景,每帧之间的时间虽然可以做到间隔一致,但是却并不再遵循既定的时间单位;timebase就是用于表达时间单位,即时间最小单元和秒的关系,ticks_per_frame描述就是每一帧播放时间占用几个(最小)单位(timebase);

注意上面的注释,经过约分之后,就剩下了ticks/frame;另外注意_den后缀,是指denominator,分母;在下一节介绍x264_encoder_open函数的时候,还会看到很多这个后缀的变量。

 

参数信息写入头文件

if(!param->b_repeat_headers){
    // Write SPS/PPS/SEI
    x264_nal_t *headers;
    int i_nal;

    FAIL_IF_ERROR2( x264_encoder_headers( h, &headers, &i_nal ) < 0, "x264_encoder_headers failed\n" );
    FAIL_IF_ERROR2( (i_file = cli_output.write_headers( opt->hout, headers )) < 0, "error writing headers to output file\n" );
}

repeat_header,代表每个NALU都会携带公共参数;虽然会导致数据冗余,但是相比于只是在第一个NALU携带头信息相比更加安全,即使第一个NALU丢失了,后面的NALU信息的处理并不会收到影响;这个段代码涵义是如果只是第一个NALU携带参数信息,那么就调用x264_encoder_headers来写入参数信息。

参数种类大致是分为三类,分别是SPS,PPS以及SEI三类;

SPS

SPS:Sequence Paramater Set,序列参数集,保存了一组视频编码序列(Codec Video Sequence)的全局参数,如下表所示:

No.

SSP Parameter

Description

1

profile_idc

标识当前H.264码流的profile

2

level_idc

标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。

3

seq_parameter_set_id

表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数

4

log2_max_frame_num_minus4

用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 +4)。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。

5

pic_order_cnt_type

表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2

6

log2_max_pic_order_cnt_lsb_minus4

用于计算MaxPicOrderCntLsb的值,该值表示POC的上限。计算方法为MaxPicOrderCntLsb = 2^(log2_max_pic_order_cnt_lsb_minus4 + 4)

7

max_num_ref_frames

用于表示参考帧的最大数目

8

gaps_in_frame_num_value_allowed_flag

标识位,说明frame_num中是否允许不连续的值

9

pic_width_in_mbs_minus1

用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:

frame_width = 16 × (pic_width_in_mbs_minus1 + 1);

10

pic_height_in_map_units_minus1

使用PicHeightInMapUnits来度量视频中一帧图像的高度。PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码

11

frame_mbs_only_flag

标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度

按照宏块计算的图像实际高度FrameHeightInMbs的计算方法为:

FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) * PicHeightInMapUnits

12

mb_adaptive_frame_field_flag

标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择

13

direct_8x8_inference_flag

标识位,用于B_Skip、B_Direct模式运动矢量的推导计算

14

frame_cropping_flag

标识位,说明是否需要对输出的图像帧进行裁剪

15

vui_parameters_present_flag

标识位,说明SPS中是否存在VUI信息

 

PPS

PPS:Picture Parameter Set,图像参数集合,如下表所示:

No.

SSP Parameter

Description

1

pic_parameter_set_id

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

2

seq_parameter_set_id

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

3

entropy_coding_mode_flag

熵编码模式标识,该标识位表示码流中熵编码/解码选择的算法。对于部分语法元素,在不同的编码配置下,选择的熵编码方式不同。例如在一个宏块语法元素中,宏块类型mb_type的语法元素描述符为“ue(v)| ae(v)”,在baseline profile等设置下采用指数哥伦布编码,在main profile等设置下采用CABAC编码。

标识位entropy_coding_mode_flag的作用就是控制这种算法选择。当该值为0时,选择左边的算法,通常为指数哥伦布编码或者CAVLC;当该值为1时,选择右边的算法,通常为CABAC。

4

bottom_field_pic_order_in_frame_present_flag

标识位,用于表示另外条带头中的两个语法元素delta_pic_order_cnt_bottom和delta_pic_order_cn是否存在的标识。这两个语法元素表示了某一帧的底场的POC的计算方法

5

num_slice_groups_minus1

表示某一帧中slice group的个数。当该值为0时,一帧中所有的slice都属于一个slice group。slice group是一帧中宏块的组合方式,定义在协议文档的3.141部分

6

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的默认值

7

weighted_pred_flag

标识位,表示在P/SP slice中是否开启加权预测。

8

weighted_bipred_idc

表示在B Slice中加权预测的方法,取值范围为[0,2]。0表示默认加权预测,1表示显式加权预测,2表示隐式加权预测

9

pic_init_qp_minus26和pic_init_qs_minus26

表示初始的量化参数。实际的量化参数由该参数、slice header中的slice_qp_delta/slice_qs_delta计算得到

10

chroma_qp_index_offset

用于计算色度分量的量化参数,取值范围为[-12,12]

11

deblocking_filter_control_present_flag

标识位,用于表示Slice header中是否存在用于去块滤波器控制的信息。当该标志位为1时,slice header中包含去块滤波相应的信息;当该标识位为0时,slice header中没有相应的信息

12

constrained_intra_pred_flag

若该标识为1,表示I宏块在进行帧内预测时只能使用来自I和SI类型宏块的信息;若该标识位0,表示I宏块可以使用来自Inter类型宏块的信息

13

redundant_pic_cnt_present_flag

标识位,用于表示Slice header中是否存在redundant_pic_cnt语法元素。当该标志位为1时,slice header中包含redundant_pic_cnt;当该标识位为0时,slice header中没有相应的信息

通过上面的参数列表可以看到其实两者所包含的参数并没有本质区别和分类,SPS和PPS可以统一的作为通用参数来进行理解。

SEI

SEI,Supplemental Enhancement Information,补充增强信息,提供了码流相关额外的信息,这部分参数信息为自定义信息,是音视频解码开发商根据自己的需要,记录一些私有的处理和标记。

 

OK,这次encoder的准备阶段就先讲到这里,下一回我们详细讲一下准备阶段一个重要环节x264_encoder_open函数里面的内容。

 

参考:

H264码流中SPS PPS详解 - 知乎 (zhihu.com)

h264 NALU的获取与分析_xiaoluer的专栏-CSDN博客_h264 nalu type

H.264中NALU、RBSP、SODB的关系 (弄清码流结构)_认知 行动 坚持-CSDN博客

笔记---H.264里的SEI - 简书 (jianshu.com)

H.264 数据分割片_chinabinlang的专栏-CSDN博客

《新一代视频压缩编码标准(第二版)》 毕节厚 王建主编

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张叫兽的技术研究院

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值