H.264笔记之一
H.264标准写得比较繁复,所以考虑在浏览完Whitepaper之后就开始研读X264代码。X264代码风格还是比较清晰简洁的。
根据对标准的理解,Picture Order Count在Slice解码的一开始就被提及:
I0 B1 B2 P3 B4 B5 P6
I0 P3 B1 B2 P6 B4 B5
于是I0的POC是0,P3的POC是3,B1是1……
为了支持H264复杂的帧存机制,X264以专门的一个模块frame.c进行处理。
common/frame.c中包括一组帧缓冲操作函数。包括对帧进行FILO和FIFO存取,空闲帧队列的相应操作等。
以下逐个函数分析encoder.c中编码一帧的函数x264_encoder_encode中有关frame的调用:
x264_reference_update
这个函数里最主要的工作的是将上一个参考帧放入参考帧队列,并从空闲帧队列中取出一帧作为当前的参考工作帧(即解码操作的目的帧),即h->fdec。
x264_t结构体维护着CODEC的诸多重要信息,其中成员frames是一个指示和控制帧编码过程的结构。其中current是已经准备就绪可以编码的帧,其类型已经确定;next是尚未确定类型的帧;unused用于回收不使用的frame结构体以备今后再次使用。frames结构体中i_input指示当前输入的帧的(播放顺序)序号。i_delay设置为由B帧个数(和线程个数)确定的帧缓冲延迟,在多线程情况下为i_delay = i_bframe + i_threads
- 1(显然当线程数为1的单线程情形,该缓冲延迟就是B帧个数,至于多线程如何工作已经超出本文讨论范围)。而判断B帧缓冲填充是否足够则通过条件判断:h->frames.i_input <= h->frames.i_delay + 1 - h->param.i_threads。
x264_encoder_encode每次会以参数送入一帧待编码的帧pic_in,函数首先会从空闲队列中取出一帧用于承载该新帧,而它的i_frame被设定为播放顺序计数,如:fenc->i_frame = h->frames.i_input++。
x264_encoder_encode在根据上述判据确定B帧缓冲充满的情况下才进行后续编码工作。
当当前队列(current队列)可用帧为0时,需要对next队列中的帧进行判决,需要进行如下过程:
1. 调用x264_slicetype_decide
这个函数确定当前条带(帧)的类型
其中首先调用x264_ratecontrol_slice_type,依据码率控制逐个求出next列表中所有帧的类型(虽然在当前并不全部用到,见后)。
随后统计审查并调整next列表,保证IDR帧满足有关最大关键帧间隔的要求的正常出现:即针对frm->i_frame - h->frames.i_last_idr >= h->param.i_keyint_max作判断。审查按顺序针对所有被判定为B系或AUTO类型的帧进行(这些帧在审核过程中被确认为B帧),直到遇到第一个不是这样的帧。
如果某个帧被指定为IDR,则一个GOP在它之前结束。
2. 而后,即将next列表中已经判定的一系列帧(先后是一些B帧和一个非B帧)转移到current列表中。在这个过程中:
原始序列(播放顺序)B0, B1, B2, P,转移后的顺序为P, B0, B1, B2。在使用bframe_pyramid模式时,中间的B帧要前置,即上述顺序变为:P, B1, B0, B2。
此时,就可以从current队列中取出一帧,进行编码,现在记这帧叫h->fenc。
首先做几项和帧有关的设置工作:
1. 如果f_enc是IDR,则将最近IDR序号标记h->frames.i_last_idr设置为i_frame。
2. 根据f_enc的类型确定NAL和SLICE类型相关参数。
3. 设置POC为2 * (h->fenc->i_frame - h->frames.i_last_idr)。并使得h->fdec和h->fenc的主要帧参数一致。
随后进行以下一些过程:
x264_reference_build_list
在这个函数中,我们将遇到参考帧列表h->frames.reference和H.264很有特色的双列表(h->fref0、h->fref1)。前者中放置了所有可用于参考的参考帧。
首先将所有reference列表中的帧按照POC和h->fenc的POC的大小关系不同复制到双列表中,其中h->fref0放置POC较小的那些。然后对双列表进行排序,使得h->fref0中的帧POC按从大到小排列,h->fref1按从小到大排列,于是这两个列表中靠前的帧的POC比较接近当前帧h->fenc。在这里,一个特殊的情况是,对于P帧(只需要用到列表h->fref0做参考),其排序依据是i_frame_num而不是POC,因此需要对这种情况的列表h->fref0置位需重排序标记。最后对双列表的长度做限制。
x264_ratecontrol_start
码率控制初始化,在以后专门的话题中论述。
在初始化后,从码率控制体中获得全局QP值:i_global_qp = x264_ratecontrol_qp( h )。
如果当前是B条带(帧),则调用x264_macroblock_bipred_init
该函数设置一些参考帧关系矩阵,关系矩阵以fref0的索引为行坐标,以fref1的索引为列坐标。主要包括:dist_scale_factor,bipred_weight,矩阵值主要由相应帧的POC决定。后续讨论。
x264_slice_init
内部调用x264_slice_header_init,它对Header进行配置,含义以后讨论。
bs_init
初始化码流工作上下文。
随后将帧类型表现到码流中。接着写SEI,SPS和PPS。
然后就是最关键的写条带(即主要的编码工作)开始的地方:
x264_slices_write
这里通过x264_stack_align调用x264_slice_write,为了将栈做4字节对齐,以提高运行效率。
x264_slice_write函数是编码一帧的关键函数,将在下一章中论述。