VBV学习记录

首先明确一点,VBV调整只针对I帧和P帧,B帧的q值是根据p帧来决定的

1、初始化

在encode_open--x264_ratecontrol_new--x264_ratecontrol_init_reconfigurable的时候会初始化VBV的参数,VBV_BUFFER_SIZE、VBV_MAX_Bitrate

int kilobit_size = h->param.i_avcintra_class ? 1024 : 1000;
int vbv_buffer_size = h->param.rc.i_vbv_buffer_size * kilobit_size;//对应--vbv-bufsize选项
int vbv_max_bitrate = h->param.rc.i_vbv_max_bitrate * kilobit_size;//对应vbv-maxrate
...
if( rc->b_vbv_min_rate )
    rc->bitrate = (double)h->param.rc.i_bitrate * kilobit_size;
rc->buffer_rate = vbv_max_bitrate / rc->fps;//最大码率除以帧数即一帧的码率
rc->vbv_max_rate = vbv_max_bitrate;
rc->buffer_size = vbv_buffer_size;

同时初始化buffer_fill_final、buffer_fill_final_min,表示buffer最后填充的字节数,以及至少要填充多少才行

rc->buffer_fill_final =
rc->buffer_fill_final_min = rc->buffer_size * h->param.rc.f_vbv_buffer_init * h->sps->vui.i_time_scale;
//buffersize乘以至少要填充满多少才能播放的系数,最后再乘以一个量纲(每个文件的量纲不一样,需要进行转化)
rc->b_vbv = 1;
rc->b_vbv_min_rate = !rc->b_2pass
       && h->param.rc.i_rc_method == X264_RC_ABR
       && h->param.rc.i_vbv_max_bitrate <= h->param.rc.i_bitrate;

2、逐帧参数初始化

encoder_encode编码帧在编码前,进入x264_ratecontrol_start控制函数,在这里面需要算出每一帧的qp值。
这里有几个变量,需要特别注意
rc->buffer_fill //计划缓冲区,规划的缓冲区大小,注意与buffer_fill_final量纲不同
rc->buffer_rate //每一帧编码后,加大缓冲区的大小,因为一帧是有时长的
代码如下

//时长乘以  1s可以加到buffer_fill的大小,并换算成以time_scale的量纲
rc->buffer_rate = h->fenc->i_cpb_duration * rc->vbv_max_rate * 
        h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale;

接下来,update_vbv_plan(从函数名可以看出事更新预算的,后面还有个updata_vbv函数)更新rc->buffer_fill,由于是多线程编码,因此在更新1s内的buffer缓存预算时,需要将各个线程所产生的比特数考虑在内。看代码

//buffer_fill_final_min的更新是编码完一帧后,在updata_vbv中用编码产生的大小来更新预算,与buffer_fill量纲不同
rcc->buffer_fill = h->thread[0]->rc->buffer_fill_final_min / h->sps->vui.i_time_scale;
    if( h->i_thread_frames > 1 )
    {
        int j = h->rc - h->thread[0]->rc;
        for( int i = 1; i < h->i_thread_frames; i++ )
        {
            x264_t *t = h->thread[ (j+i)%h->i_thread_frames ];
            double bits = t->rc->frame_size_planned;
            if( !t->b_thread_active )//该线程处于编码状态
                continue;
            bits = X264_MAX(bits, t->rc->frame_size_estimated);
            rcc->buffer_fill -= bits;//每个线程产生的位数用来填充缓冲区,所以预算减小
            rcc->buffer_fill = X264_MAX( rcc->buffer_fill, 0 );
            rcc->buffer_fill += t->rc->buffer_rate;//该帧时长能扩展的buffer大小
            rcc->buffer_fill = X264_MIN( rcc->buffer_fill, rcc->buffer_size );
        }
    }
    rcc->buffer_fill = X264_MIN( rcc->buffer_fill, rcc->buffer_size );
    rcc->buffer_fill -= overhead;//每个NAL的头部共40位

举个例子来说,如图所示是一个文件的码率图


image.png

那么在编码的过程中,通过将每一帧开始前的buffer_fill打出来可以看到如下变化。一开始初始状态下,约为80000的0.9倍,接着第一帧IDR帧编码后预算减少。


image.png

更新完缓冲区大小后,就开始评估出qscale,qscale的评估是根据复杂度(ABR)、crf值来确定的(具体下篇再说)。再由qscale换算出qp值,到这里得到的是一个未经过VBV控制的QP值,因此还需要经过VBV进行调整。调整的过程将分成两个过程:帧级、宏块级码率控制。

3、帧级码率控制

上一步得到的qp值还未经过vbv的调整,因此可能存在上溢、或者下溢(部分算法不负责下溢),因此还需要通过VBV进行帧层级的调整,调整主要在函数clip_qscale内实现。
帧级控制在内部又分为经典算法和lookahead两种。经典较为简单,不细说。lookahead的做法则是通过评估出该q值下的帧大小,进行扩大或者缩小qp值。
里面几个重要的变量:
buffer_fill_cur只实现预算的缓存大小扣去当前帧的预测大小后,剩下的空间,即剩下的预算。
target_fill即花掉预算后,你得剩下多少空间,这个空间不能太大,也不能太小。
看代码

for( int j = 0; buffer_fill_cur >= 0 && buffer_fill_cur <= rcc->buffer_size; j++ )
{
    total_duration += last_duration;
    buffer_fill_cur += rcc->vbv_max_rate * last_duration;//上一帧所增加的预算
    int i_type = h->fenc->i_planned_type[j];
    int i_satd = h->fenc->i_planned_satd[j];
    if( i_type == X264_TYPE_AUTO )
        break;
    i_type = IS_X264_TYPE_I( i_type ) ? SLICE_TYPE_I : IS_X264_TYPE_B( i_type ) ? SLICE_TYPE_B : SLICE_TYPE_P;
    cur_bits = predict_size( &rcc->pred[i_type], frame_q[i_type], i_satd );//将该帧拿去编码,所花费的空间
    buffer_fill_cur -= cur_bits;//扣除花费,即剩下的预算
    last_duration = h->fenc->f_planned_cpb_duration[j];
}
//目标上限,至少要保证buffer50%填满
//但是如果buffer里可用数据量少的话,50%的条件太苛刻
//那么在原来基础上新增数据预算的50%即可,公式中的两个0.5都是经验值
target_fill = X264_MIN( rcc->buffer_fill + total_duration * rcc->vbv_max_rate * 0.5, rcc->buffer_size * 0.5 );
if( buffer_fill_cur < target_fill )
{
//当前剩下的预算小于目标可用预算
//说明用掉太多数据了,所以应该增大q值
    q *= 1.01;
    terminate |= 1;
    continue;
}
//目标下限,不要剩太多的预算。
//源代码中是控制在50%到80%的区间。
target_fill = x264_clip3f( rcc->buffer_fill - total_duration * rcc->vbv_max_rate * 0.5, rcc->buffer_size * 0.8, rcc->buffer_size );
//对于不要求控制码率下限的情况下,不需要调小q值
if( rcc->b_vbv_min_rate&& buffer_fill_cur > target_fill )
{
//大的话说明数据太少,因此需要调小q值
    q /= 1.01;
    terminate |= 2;
    continue;
}

按上个例子来说,把每帧调整过程中当前剩余预算的大小打印出来,


image.png

根据设置,至少要剩8000*0.5=4000以上,同时最多只能64000到8000中的一个数字。
从局部放大图可以看出,一开始q值较大的时候,预算用掉比较少,所以剩的比较多,因此需要调小q值,经过多次微调,要吗刚好落在8000要吗,预算全部用掉且还不够(这个时候需要回调)。


image.png

而在最后阶段,q值使得剩下的预算太少了低于4000了,就意味着数据量太多,所以需要调大q值,直至调整到4000的水平。
image.png

调整结束后,看一张buffer_fill的预算图,可以看出实现了码率的控制。预算80000就将码率限制在80000左右,图中第二个点,预算很少,这是因为那一帧刚好是第一帧的IDR帧,因此信息量比较多。另外也可以看出总共调整了140多次,而源文件共有480帧,因此只有I、P帧进入到VBV的调整阶段。


image.png

4、minGOP vbv 调整

因为B帧的大小是根据P帧决定的,因此,当收到P帧时,还需要将B帧考虑进预算,防止超出预算。

if( h->sh.i_type == SLICE_TYPE_P && !rcc->single_frame_vbv )
{
    int nb = rcc->bframes;
    double bits = predict_size( &rcc->pred[h->sh.i_type], q, rcc->last_satd );
    double pbbits = bits;
    double bbits = predict_size( rcc->pred_b_from_p, q * h->param.rc.f_pb_factor, rcc->last_satd );
    double space;
    double bframe_cpb_duration = 0;
    double minigop_cpb_duration;
    for( int i = 0; i < nb; i++ )
    {
        bframe_cpb_duration += h->fenc->f_planned_cpb_duration[i];
    }

    if( bbits * nb > bframe_cpb_duration * rcc->vbv_max_rate )
        nb = 0;
    pbbits += nb * bbits;//这个minigop消耗的位数
    //b帧和p帧带来的预算
    minigop_cpb_duration = bframe_cpb_duration + fenc_cpb_duration;
    
    space = rcc->buffer_fill + minigop_cpb_duration*rcc->vbv_max_rate - rcc->buffer_size;
    //预算超出buffersize的部分,仍然不够存放这个MiniGop,这时需要增大预算,防止上溢
    if( pbbits < space )
    {//适当调低qscale (调高码率预算), 使得本minGOP过后,缓存区没有上溢。
        q *= X264_MAX( pbbits / space, bits / (0.5 * rcc->buffer_size) );
    }
    q = X264_MAX( q0/2, q );
}

到这里结束后,还有根据整帧大小,再进一步微调q值,然后根据最后的q值去算出整帧预算的大小rcc->frame_size_planned,同时将这个q值更新到h->rc->accum_p_qp中。

5、宏块级码率控制

只在VBV模式下才有效,以行为控制单元。控制方式与帧级控制类似,预测大小大了,就增大q值,反之减小。代码如下

//对于多线程slice编码的情况需要将各个线程的slice加入统计
//采用多线程帧编码的情况下不使用
if( h->param.b_sliced_threads )
{
    float size_of_other_slices_planned = 0;
    for( int i = 0; i < h->param.i_threads; i++ )
        if( h != h->thread[i] )
        {
            size_of_other_slices += h->thread[i]->rc->frame_size_estimated;
            size_of_other_slices_planned += h->thread[i]->rc->slice_size_planned;
        }
    float weight = rc->slice_size_planned / rc->frame_size_planned;
    size_of_other_slices = (size_of_other_slices - size_of_other_slices_planned) * weight + size_of_other_slices_planned;
}
//剩下的预算
float buffer_left_planned = rc->buffer_fill - rc->frame_size_planned;
buffer_left_planned = X264_MAX( buffer_left_planned, 0.f );
//计算出当前slice的大小
float b1 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;
...
//控制上溢
//判断逻辑如下:
// 1 &&(2||3||4)
//1、处于qp_MAX的低通滤波器下
//2、当前row加入后,会使得大小超出,整帧预算(包含容忍度)
//3、当前row加入后,已经超出了预算,并且量化值小于未经过vbv调整后的量化值
//4、当前row加入后,预算缓冲区不足(写成rc->buffer_fill - b1 < buffer_left_planned * 0.5,比较好理解)
while( rc->qpm < qp_max&& ((b1 > rc->frame_size_planned + rc_tol) ||
           (b1 > rc->frame_size_planned && rc->qpm < rc->qp_novbv) ||
           (b1 > rc->buffer_fill - buffer_left_planned * 0.5f)) )
{
    rc->qpm += step_size;//加大q值
    b1 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;
}
...
//控制下溢
// 1&& 2 && 3 && 4 && 5
//1、qp_min 的高通滤波器中
//2、比前一行的qp值小
//3、调整后的qp值大于未调整的qp值,才有回调的必要
//4、预估大小不上溢
//5、预估大小占用率没有达到80%,与上面的0.5对应控制在50%和80%之间,这两个取值来源于经验值
while( rc->qpm > qp_min && rc->qpm < prev_row_qp
               && (rc->qpm > h->fdec->f_row_qp[0] || rc->single_frame_vbv)
               && (b2 < max_frame_size)
               && ((b2 < rc->frame_size_planned * 0.8f) || (b2 < b_max)) )
{
    b1 = b2;
    rc->qpm -= step_size;
    b2 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;
}
//调小之后,仍然要防止上溢
while( rc->qpm < qp_absolute_max && (b1 > max_frame_size) )
{
    rc->qpm += step_size;
    b1 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;
}

通过来来回回的3次调整,完成宏块级码率的控制。将上面例子qp值的变化过程打印出来。可见qp值处于不断的完善调整过程中


image.png

6、编码结束,更新信息

得到每个宏块的qp值,x264就用这个pq值去量化DCT变化后的数据。最后,以上的一些信息都是通过预估得到的,因此编码后需要用实际值去更新。更新的操作在x264_rate control_end函数中完成,对于VBV来说,最主要的得到下次控制时VBV Buffer fill这个缓存预算的值,在程序中也就是buffer fill final这个变量(它只是与buffer fill的量纲不同,实际是一个性质)。

//编码后的数据量乘以量纲
uint64_t buffer_diff = (uint64_t)bits * h->sps->vui.i_time_scale;
rct->buffer_fill_final -= buffer_diff;
rct->buffer_fill_final_min -= buffer_diff;
//溢出的话,即花光了预算,所以就要初始化预算为0
if( rct->buffer_fill_final_min < 0 )
{
    double underflow = (double)rct->buffer_fill_final_min / h->sps->vui.i_time_scale;
    if( rcc->rate_factor_max_increment && rcc->qpm >= rcc->qp_novbv + rcc->rate_factor_max_increment )
        x264_log( h, X264_LOG_DEBUG, "VBV underflow due to CRF-max (frame %d, %.0f bits)\n", h->i_frame, underflow );
    else
        x264_log( h, X264_LOG_WARNING, "VBV underflow (frame %d, %.0f bits)\n", h->i_frame, underflow );
    rct->buffer_fill_final =
    rct->buffer_fill_final_min = 0;
}
//新增预算量
if( h->param.i_avcintra_class )
    buffer_diff = buffer_size;
else
    buffer_diff = (uint64_t)bitrate * h->sps->vui.i_num_units_in_tick * h->fenc->i_cpb_duration;
rct->buffer_fill_final += buffer_diff;
rct->buffer_fill_final_min += buffer_diff;
//更新后的预算量如果超出边界,就要进行裁剪
if( rct->buffer_fill_final > buffer_size )
{
    if( h->param.rc.b_filler )
    {
        //使用filler选项
        //buffer fill final 和buffer size都是以i_time_scale为单位的,同时换算成bit需要乘以8
        int64_t scale = (int64_t)h->sps->vui.i_time_scale * 8;   
        //要换算成字节的话,需要再除8,
        //就是(buffer_fill_final-buffer_size)/(h->sps->vui.i_time_scale bits*8)
        //+scale-1 因为后面要除以scale,就是实现普通的向上取整的方法
        filler = (rct->buffer_fill_final - buffer_size + scale - 1) / scale;     
        bits = h->param.i_avcintra_class ? filler * 8 : X264_MAX( (FILLER_OVERHEAD - h->param.b_annexb), filler ) * 8;
        buffer_diff = (uint64_t)bits * h->sps->vui.i_time_scale;
        rct->buffer_fill_final -= buffer_diff;
        rct->buffer_fill_final_min -= buffer_diff;
    }
    else
    {
        //直接取最小
        rct->buffer_fill_final = X264_MIN( rct->buffer_fill_final, buffer_size );
        rct->buffer_fill_final_min = X264_MIN( rct->buffer_fill_final_min, buffer_size );
    }
}

[1]、http://www.pianshen.com/article/4198342118/
[2]、https://onlivetest.wordpress.com/2015/02/20/x264-rate-control-part-one/
[3]、https://blog.csdn.net/nonmarking/article/details/50737198

你可能感兴趣的:(VBV学习记录)