一、VBV码率控制模型图
将vbv buff比做水桶,编码后帧的bits比做水瓶的水;vbv的码率控制过程可以看做往水桶中加水以从水桶中用水的过程;vbv码率控制原理图如下:
上图中可用水量buffer_fill_final初始量为水桶总容量vbv_buffer_size; 流入的流速固定为bitrate/fps,即平均每帧的目标bits; 当用水过多(enc_bits过大),导致流出水过大,使得可用水量低于下溢阈值线,则需要减少流出,用小瓶接水,即降低enc_bits; 当用水量过少(enc_bits过小),使得可用水量高于上溢阈值线,需要增加流出,用大瓶接水,即加大enc_bits; enc_bits的大小是通过调节qscale来调节qp从而影响enc_bits。
二、VBV码率控核心参数
vbv码率控制的主要参数如下图。
buffer_size可以看作是水桶的总容量; buffer_fill_final看作是可用水量; buffer_fill看作是未来一段时间内可用水量; buffer_rate可以看作是一次加水量,理想状态下一次加水和一次出水量是相等的。
1、vbv参数初始化
vbv主要参数的初始化在x264_ratecontrol_init_reconfigurable()函数中完成;
//代码有删减
void x264_ratecontrol_init_reconfigurable( x264_t *h, int b_init )
{
x264_ratecontrol_t *rc = h->rc;
if( !b_init && rc->b_2pass )
return;
rc->buffer_rate = vbv_max_bitrate / rc->fps;
rc->vbv_max_rate = vbv_max_bitrate;
rc->buffer_size = vbv_buffer_size;
rc->single_frame_vbv = rc->buffer_rate * 1.1 > rc->buffer_size;//单帧码率控制的vbv,即vbv buff size只有一帧编码数据的大小
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;//这里乘上一个i_time_scale系数,方便后面码率控制按照帧时长来处理
rc->b_vbv = 1;
}
x264_ratecontrol_init_reconfigurable()函数在x264_ratecontrol_new()函数里调用。
h264的帧率计算的说明,下图中由(1)式可以推出(2)式,代码中多用(2)式等号右边来计算时长。
2、vbv码率控制过程
在x264_ratecontrol_start()首先计算vbv buff的buffer_rate,即往水桶中注水的速率,也就是平均每帧的bits;代码如下:
//代码有删减。
void x264_ratecontrol_start( x264_t *h, int i_force_qp, int overhead )
{
if( rc->b_vbv )
{
memset( h->fdec->i_row_bits, 0, h->mb.i_mb_height * sizeof(int) );
memset( h->fdec->f_row_qp, 0, h->mb.i_mb_height * sizeof(float) );
memset( h->fdec->f_row_qscale, 0, h->mb.i_mb_height * sizeof(float) );
rc->row_pred = rc->row_preds[h->sh.i_type];
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( h, overhead );//更新vbv
}
update_vbv_plan()主要是用于计算buffer_fill,帧级多线程的时候使用预测的码率来更新buffer_fill,代码如下:
//代码有删减
static void update_vbv_plan( x264_t *h, int overhead )
{
rcc->buffer_fill = h->thread[0]->rc->buffer_fill_final_min / h->sps->vui.i_time_scale;
//因为buffer_fill_final_min乘上了i_time_scale这里除上i_time_scale;
if( h->i_thread_frames > 1 )//帧级多线程编码
{
int j = rcc - 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 )//b_thread_active为1表示线程编码未完成,0表示线程没有编码 continue;
bits = X264_MAX(bits, t->rc->frame_size_estimated);//没有编码完成的使用预测的bits
rcc->buffer_fill -= bits; //相当于水桶出水
rcc->buffer_fill = X264_MAX( rcc->buffer_fill, 0 );
rcc->buffer_fill += t->rc->buffer_rate;//相当于水桶加水
rcc->buffer_fill = X264_MIN( rcc->buffer_fill, rcc->buffer_size );
}
}
rcc->buffer_fill -= overhead;//减去帧头占用的bits(如pps,sps,slice header占用的bits)
}
然后在vbv_pass1()函数中完成vbv的码率调节,如果lookahead开启则会使用未来的一些帧来调整当前帧的qscale;在lookahaead中会预测未来若干帧的帧类型和未来帧的satd值,就可以使用predict_size()函数来预测未来帧的编码bits,根据未来帧的编码bits对vbv buff的影响来调整当前帧的qscale。
//代码有删减
static double vbv_pass1( x264_t *h, int pict_type, double q )
{
double fenc_cpb_duration = (double)h->fenc->i_cpb_duration * h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale;//计算当前帧的时长
if( h->param.rc.i_lookahead )//lookahead 开启
{
//iterations 限制最大循环次数,terminate 调整失败容错处理
for(int iterations = 0; iterations < 1000 && terminate != 3; iterations++ )
{
double cur_bits = predict_size( &rcc->pred[h->sh.i_type], q, rcc->last_satd );
double buffer_fill_cur = rcc->buffer_fill - cur_bits;//出水之后可用水量的更新,
double last_duration = fenc_cpb_duration;
//根据当前帧qscale计算I/P/B帧的qscale
frame_q[0] = h->sh.i_type == SLICE_TYPE_I ? q * h->param.rc.f_ip_factor : q;//P帧qscale
frame_q[1] = frame_q[0] * h->param.rc.f_pb_factor;//B帧qscale
frame_q[2] = frame_q[0] / h->param.rc.f_ip_factor; //I帧qscale
//根据lookahead里面计算的信息来预估未来帧对vbv buff的消耗
for( int j = 0; buffer_fill_cur >= 0 && buffer_fill_cur <= rcc->buffer_size; j++ )
{
buffer_fill_cur += rcc->vbv_max_rate * last_duration;//根据帧的时长计算该帧对vbvbuff的补充,可以看做是水桶的加水
int i_type = h->fenc->i_planned_type[j];//lookahead中对未来帧的帧类型预测
int i_satd = h->fenc->i_planned_satd[j];//lookahead中对未来帧的satd预测
if( i_type == X264_TYPE_AUTO )//帧类型未确定,也受lookahead的深度影响
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 );//根据satd计算编码预测bits
buffer_fill_cur -= cur_bits;//出水之后可用水量的更新
last_duration = h->fenc->f_planned_cpb_duration[j]; //更新帧时长
}
//到这buffer_fill_cur就表示当前帧以及未来帧一起对vbv消耗后可用的水量,
//target_fill 最大不超过buffer_size 的50%,
//X264_MIN()的作用主要是历史帧编码压不住导致码率高上去,可用的buffer_fill 很低,且未来帧补充来的也比较低(如视频的末尾)
//可能导致buffer_fill_cur会一直低于buffer_size 的50%
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 )//预测出的可用水量低于目标可用水量的阈值,则需要降低当前帧的bits,即降低出水量
{
q *= 1.01;//增大qscale
terminate |= 1;
continue;//继续用增大后的qsacle来预测剩余可用水量是否在阈值范围
}
target_fill = x264_clip3f( rcc->buffer_fill - total_duration * rcc->vbv_max_rate * 0.5, rcc->buffer_size * 0.8, rcc->buffer_size );//上溢的阈值
//b_vbv_min_rate 当vbv的最大码率不高于目标码率才会为1,不上溢防止码率高于目标码率
f( rcc->b_vbv_min_rate && buffer_fill_cur > target_fill )//预测出的可用水量高于目标可用水量的阈值,则需要提高当前帧的bits,即增大出水量
{
q /= 1.01;//降低qscale,来降低qp
terminate |= 2;
continue;
}
}
}
esle lookahead 关闭
{
//p帧或连续的I帧且剩余可用输出的大小不足则降低当前帧的大小
if( ( pict_type == SLICE_TYPE_P ||
( pict_type == SLICE_TYPE_I && rcc->last_non_b_pict_type == SLICE_TYPE_I ) ) &&//连续出现两个I帧
rcc->buffer_fill/rcc->buffer_size < 0.5 )//可用于输出的大小不足总buff的一半
{
q /= x264_clip3f( 2.0*rcc->buffer_fill/rcc->buffer_size, 0.5, 1.0 );//加大qscale来增大qp,来降低编码码率
}
double bits = predict_size( &rcc->pred[h->sh.i_type], q, rcc->last_satd );//根据satd预测帧编码的大小
//vbvbuffsize比较小的时候运行当前编码帧占用完剩余可用的大小,否只允许占用不大于剩余可用大小的50%,
double max_fill_factor = h->param.rc.i_vbv_buffer_size >= 5*h->param.rc.i_vbv_max_bitrate / rcc->fps ? 2 : 1;
double min_fill_factor = rcc->single_frame_vbv ? 1 : 2;
//防止下溢的判断
if( bits > rcc->buffer_fill/max_fill_factor )//预测编码的bit比剩余可用大小还要大则,增大qscale来增大qp,进而降低编码bits
{
double qf = x264_clip3f( rcc->buffer_fill/(max_fill_factor*bits), 0.2, 1.0 );
q /= qf;//增大qscale
bits *= qf;
}
//防止上溢的判断
if( bits < rcc->buffer_rate/min_fill_factor )//预测编码bites比加水的量的一半还要低则降低qscale来降低qp,进而提升编码bits
{
double qf = x264_clip3f( bits*min_fill_factor/rcc->buffer_rate, 0.001, 1.0 );
q *= qf;
}
}
//防溢出处理完毕后
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 );//预测当前编码帧的编码bits
double pbbits = bits;
double bbits = predict_size( rcc->pred_b_from_p, q * h->param.rc.f_pb_factor, rcc->last_satd );//预测b帧的bits
double space;
double bframe_cpb_duration = 0;//mingop中所有B帧的时长
double minigop_cpb_duration;//mingop中所有帧的时长
for( int i = 0; i < nb; i++ )//统计mingop中B帧的总时长
bframe_cpb_duration += h->fenc->f_planned_cpb_duration[i];
if( bbits * nb > bframe_cpb_duration * rcc->vbv_max_rate )//B帧总的预测bits大于B帧总的补充buff(总的进水量)
{
nb = 0;
bframe_cpb_duration = 0;
}
pbbits += nb * bbits;//P帧和所有B帧的bits和
minigop_cpb_duration = bframe_cpb_duration + fenc_cpb_duration;
space = rcc->buffer_fill + minigop_cpb_duration*rcc->vbv_max_rate - rcc->buffer_size;//当前剩余可用buff加上mingop编码完成后新补充的buff减去vbv大小得到溢出大小(上溢),即mingop全部编码完成后溢出的大小
if( pbbits < space )//mingop全部编码完成后溢出的大小比mingop预测出的编码bits要大则说明当前当前mingop的bits太小了
{
q *= X264_MAX( pbbits / space, bits / (0.5 * rcc->buffer_size) );//降低qscale来降低qp,提升bits
}
q = X264_MAX( q0/2, q );
}
}
3、vbv的参数更新
vbv参数在update_vbv( x264_t *h, int bits )函数里面更新;vbv参数更新代码如下:
//代码有删减
static int update_vbv( x264_t *h, int bits )
{
uint64_t buffer_diff = (uint64_t)bits * h->sps->vui.i_time_scale;//bits为实际编码的大小
rct->buffer_fill_final -= buffer_diff;//减去实际编码的大小,可以看作是出水
rct->buffer_fill_final_min -= buffer_diff;
//下面的代码等于价于buffer_diff = (uint64_t)bitrate *h->sps->vui.i_time_scale* (h->sps->vui.i_num_units_in_tick * h->fenc->i_cpb_duration/h->sps->vui.i_time_scale)
//即buffer_diff = bitrate *i_time_scale*单帧时长
buffer_diff = (uint64_t)bitrate * h->sps->vui.i_num_units_in_tick * h->fenc->i_cpb_duration;
rct->buffer_fill_final += buffer_diff;//剩余可用buff的大小,可以看作是加水
rct->buffer_fill_final_min += buffer_diff;
}
三、vbv行级(宏块)码率控制
1、vbv行级码率控制预测过程的主要参数
2、vbv行级(宏块)码率控制原理
每编码完一行,计算前面编码完所有行的总的bits,使用前一行qpm预测剩余未编码行的所有bits,将已经编码完行的实际bits和剩余未编码行预测的bits进行累加得到总的bits(预测帧的编码bits),然后和帧级码控预测出来的帧bit做比较,如果比帧级预测的bits大则提升qpm,如果小则降低qpm。
3、vbv行级(宏块)码率控制代码
在int x264_ratecontrol_mb( x264_t *h, int bits )函数里进行vbv的行级(宏块)码率控制,主要代码代码如下:
h->fdec->i_row_bits[y] += bits;//累加一行中每一个宏块的编码bits
rc->qpa_aq += h->mb.i_qp;
if( h->mb.i_mb_x != h->mb.i_mb_width - 1 )//只有一行宏块编码完成才在做行级码率控制
return 0;
//更新行级编码bits预测的参数
update_predictor( &rc->row_pred[0], qscale, h->fdec->i_row_satd[y], h->fdec->i_row_bits[y] );
//如果是P帧或者B帧,且当前行的qp比第一个前向参考帧中对应行的qp低(当前行很可能采用帧内压缩)
if( h->sh.i_type != SLICE_TYPE_I && rc->qpm < h->fref[0][0]->f_row_qp[y] )//更新使用帧内satd的预测参数
update_predictor( &rc->row_pred[1], qscale, h->fdec->i_row_satds[0][0][y], h->fdec->i_row_bits[y] );
float step_size = 0.5f;//qpm调整的步长
float slice_size_planned = h->param.b_sliced_threads ? rc->slice_size_planned : rc->frame_size_planned;//多slice下其他slice的预测bits或者帧的预测bits
float bits_so_far = row_bits_so_far( h, y );//计算已经完成编码行的总bits
//vbv剩余可用缓存的大小
float buffer_left_planned = rc->buffer_fill - rc->frame_size_planned;
//当前线程(帧)从剩余可用缓存中分配到的可用缓存大小,作为b1的弹性值,即b1和rame_size_planned的差异在rc_tol内都不做处理,超过才处理
float rc_tol = buffer_left_planned / h->param.i_threads * rc->rate_tolerance;
//predict_row_size_to_end()用于预测剩余未编码行的总bits,b1就是根据已经编码和未编码的行计算出来的帧的bits
float b1 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;
//vbv下溢判断和处理
while( rc->qpm < qp_max
&& ((b1 > rc->frame_size_planned + rc_tol) ||b1和帧级预测出的bits差异大于弹性阈值rc_tol了
(b1 > rc->frame_size_planned && rc->qpm < rc->qp_novbv) ||//b1比帧级预测出的bits且qpm的值比帧级qp还低
(b1 > rc->buffer_fill - buffer_left_planned * 0.5f)) )//vbv下溢了
{
rc->qpm += step_size;//增大qp
b1 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;//根据新的qpm重新计算当前帧的编码bits
}
float b_max = b1 + ((rc->buffer_fill - rc->buffer_size + rc->buffer_rate) * 0.90f - b1) * trust_coeff;
rc->qpm -= step_size;
float b2 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;//根据新的qpm重新计算当前帧编码的bits
//vbv上溢判断和处理
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;//降低qp
b2 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;//根据新的qpm重新计算当前帧编码的bits
}
rc->frame_size_estimated = b1 - size_of_other_slices;//将当前已经编码和剩余未编码bits的总和更新到frame_size_estimated
4、vbv行级(宏块)码率控制bits的计算函数
//统计0到y行的所有bits
static int row_bits_so_far( x264_t *h, int y )
{
int bits = 0;
//如果是多slice的话计算当前slice的已经编码的bits
for( int i = h->i_threadslice_start; i <= y; i++ )
bits += h->fdec->i_row_bits[i];
return bits;
}
//预测从y行到最后一行所有行的bits和
static float predict_row_size_to_end( x264_t *h, int y, float qp )
{
float qscale = qp2qscale( qp );
float bits = 0;
//如果是多slice的话只计算当前slice的未编码的bits
for( int i = y+1; i < h->i_threadslice_end; i++ )
bits += predict_row_size( h, i, qscale );//预测当前行的bits
return bits;
}
static float predict_row_size( x264_t *h, int y, float qscale )
{
//预测当前行的bits
float pred_s = predict_size( &rc->row_pred[0], qscale, h->fdec->i_row_satd[y] );//当前帧为I帧的时候i_row_satd为帧内代价,P/B帧则为帧间代价
if( h->sh.i_type == SLICE_TYPE_I || qscale >= h->fref[0][0]->f_row_qscale[y] )
//P帧且qscale比参考帧的qscale大则大概率是帧间块,只需要帧间代价来计算bits
{
if( h->sh.i_type == SLICE_TYPE_P
&& h->fref[0][0]->i_type == h->fdec->i_type
&& h->fref[0][0]->f_row_qscale[y] > 0
&& h->fref[0][0]->i_row_satd[y] > 0
&& (abs(h->fref[0][0]->i_row_satd[y] - h->fdec->i_row_satd[y]) < h->fdec->i_row_satd[y]/2))
{
float pred_t = h->fref[0][0]->i_row_bits[y] * h->fdec->i_row_satd[y] / h->fref[0][0]->i_row_satd[y]
* h->fref[0][0]->f_row_qscale[y] / qscale;
return (pred_s + pred_t) * 0.5f;
}
return pred_s;
}
else //当前QP比ref中的qp小则大概率是帧内压缩,需要加上根据帧内代价预测出的bits
{
float pred_intra = predict_size( &rc->row_pred[1], qscale, h->fdec->i_row_satds[0][0][y] );//i_row_satds[0][0]是帧内的statd代价
/* Sum: better to overestimate than underestimate by using only one of the two predictors. */
return pred_intra + pred_s;
}
}
5、vbv行级(宏块)satd的介绍
和帧级一样行级的bits估计也是需要使用satd的,行级satd的参数如下:
int *i_row_satds[X264_BFRAME_MAX+2][X264_BFRAME_MAX+2];
int *i_row_satd;
i_row_satds是存储的是当前帧在lookahead阶段与前向、后向参考帧计算的satd代价,存储的是每一行的代价。 i_row_satd是当前帧与某一参考帧的代价,存储的是每一行的代价。 i_row_satds[0][0]存储的是帧内的satd代价,其他是帧间的代价。 当为I帧则i_row_satd=i_row_satds[0][0]; 当为P/B帧则i_row_satd是i_row_satds[b-p0][p1-b]的satd的代价。satd的获取在 x264_rc_analyse_slice( x264_t *h )函数中实现:
int x264_rc_analyse_slice( x264_t *h )
{
int p0 = 0, p1, b;
if( IS_X264_TYPE_I(h->fenc->i_type) )
p1 = b = 0;
else if( h->fenc->i_type == X264_TYPE_P )
p1 = b = h->fenc->i_bframes + 1;
else //B
{
p1 = (h->fref_nearest[1]->i_poc - h->fref_nearest[0]->i_poc)/2;
b = (h->fenc->i_poc - h->fref_nearest[0]->i_poc)/2;
}
h->fenc->i_row_satd = h->fenc->i_row_satds[b-p0][p1-b];//I帧的时候b=p0=p1=0
h->fdec->i_row_satd = h->fdec->i_row_satds[b-p0][p1-b];
//将fenc的结果拷贝到fedc中后面码率控制使用的是fdec中的结果
memcpy( h->fdec->i_row_satd, h->fenc->i_row_satd, h->mb.i_mb_height * sizeof(int) );
if( !IS_X264_TYPE_I(h->fenc->i_type) )//帧内压缩代价
memcpy( h->fdec->i_row_satds[0][0], h->fenc->i_row_satds[0][0], h->mb.i_mb_height * sizeof(int) );
}