转自http://blog.lmtw.com/b/guanyhu/archives/2006/38868.html
x264源代码分析(一) | |
相关说明: 1. 使用版本: x264-cvs- 2004-05-11 2. 这次的分析基本上已经将代码中最难理解的部分做了阐释,对代码的主线也做了剖析,如果这个主线理解了,就容易设置几个区间,进行分工阅读,将各个区间击破了. 3. 需要学习的知识: a) 编码器的工作流程. b) H.264的码流结构,像x264_sps_t,x264_pps_t等参数的定义基本上都完全符合标准文档中参数集的定义,抓住主要参数,次要参数也应该有所了解. c) 数学知识,对dct变换等与数学相关的知识的编程实现要有较好理解. d) C语言的知识.涉及到c语言的较多不经常用的特性,如函数指针数组,移位运算,结构体的嵌套定义等. e) 耐心,对h.264的复杂性要有清醒的认识. 3.参考资料: a) 新一代视频压缩编码标准-h.264/avc 毕厚杰主编,人民邮电出版社. b) 网上的流媒体论坛,百度,google等搜索引擎. 4. 阅读代码的方法: a) 较好的方法是利用vc的调试器,如果对某个函数感兴趣,可以将断点设置在它的前面.然后采用step into,step over等方法进去该函数一步步分析.当然本身要对程序执行流程要有较清楚认识,不然不知道何时step into,何时step over. b) 建议应该先对照标准弄清各个结构体成员的意义. 源代码主要过程分析:
1. 进入x264.c中的main函数. 刚开始是读取默认参数,如果你设置了参数的话会修改param的. i_ret = Encode( ¶m, fin, fout ); 这条语句使过程进入x264.c中的Encode函数. 2. X.264的encode函数. A. i_frame_total = 0; if( !fseek( fyuv, 0, SEEK_END ) ) { int64_t i_size = ftell( fyuv ); fseek( fyuv, 0, SEEK_SET ); i_frame_total = i_size / ( param->i_width * param->i_height * 3 / 2 ) } 上面这段计算出输入文件的总帧数. B. h = x264_encoder_open( param )这个函数是对不正确的参数进行修改,并对各结构体参数和cabac编码,预测等需要的参数进行初始化. C. pic = x264_picture_new( h ); 该函数定义在/CORE/common.c中.首先分给能容纳sizeof(x264_picture_t)字节数的空间,然后进行初始化. 这里看一下x264_picture_t和x264_frame_t的区别.前者是说明一个视频序列中每帧的特点.后者存放每帧实际的象素值.注意区分. D. for( i_frame = 0, i_file = 0; i_ctrl_c == 0 ; i_frame++ ) { int i_nal; x264_nal_t *nal; int i; /* read a frame */ if( fread( pic->plane[0], 1, param->i_width * param->i_height, fyuv ) <= 0 || fread( pic->plane[1], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 || fread( pic->plane[2], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 ) { break; } //文件位置指示器自己变化了.
if( x264_encoder_encode( h, &nal, &i_nal, pic ) < 0 ) { fprintf( stderr, "x264_encoder_encode failed/n" ); } …… } 凡是出现for循环的地方都应该认真对待,这是我的一个体会,也是进入分层结构认真分析的好方法. fread()函数一次读入一帧,分亮度和色度分别读取.这里要看到c语言中的File文件有一个文件位置指示器,调用fread()函数会使文件指示器自动移位,这就是一帧一帧读取的实现过程. E. 然后进入x264_encoder_encode( h, &nal, &i_nal, pic )函数,该函数定义在/Enc/encoder.c中. 开始进入比较复杂的地方了. |
这个函数前面有一段注释(如下):
****************************************************************************
* x264_encoder_encode:
* XXX: i_poc : is the poc of the current given picture
* i_frame : is the number of the frame being coded
* ex: type frame poc
* I 0 2*0//poc是实际的帧的位置.
* P 1 2*3//frame是编码的顺序.
* B 2 2*1
* B 3 2*2
* P 4 2*6
* B 5 2*4
* B 6 2*5
****************************************************************************/
要搞清poc和frame的区别.
假设一个视频序列如下:
I B B P B B P
我们编码是按I P B B P B B的顺序,这就是frame的编号.
而我们视频序列的播放序号是POC的序号,这里是乘以了2.
函数中先定义了如下三个参数:
int i_nal_type;
nal存放的数据类型, 可以是sps,pps等多种.
int i_nal_ref_idc;
nal的优先级,nal重要性的标志位.
前面两个参数虽然简单,但如果不参照标准,也不容易理解,所以标准中的句法表是很重要的,可以说是最关键的.
int i_slice_type;
slice的类型,在x264中我的感觉好像一帧只有一个slice.如果确定了帧的类型,slice的类型也就确定了.
我们来看看编码器是如何区分读入的一帧是I帧,P帧,或者B帧,这个过程需要好好理解.
还以I B B P B B P为例.
if( h->i_frame % (h->param.i_iframe * h->param.i_idrframe) == 0 ){
确定这是立即刷新片.
}
这里很好理解.
但到了if( h->param.i_bframe > 0 )//可以B帧编码时.
就有问题了.
注意我们编完I帧后碰到了一个B帧,这时我们先不对它进编码.而是采用frame = x264_encoder_frame_put_from_picture( h, h->frame_next, pic )函数将这个B帧放进h->frame_next中.
好,这里出现了h->frame_next,在h中同时定义了下面几个帧数组用以实现帧的管理.
x264_frame_t *bframe_current[X264_BFRAME_MAX]; /* store the sequence of b frame being encoded */
x264_frame_t *frame_next[X264_BFRAME_MAX+1]; /* store the next sequence of frames to be encoded *///搞清意义,下一个帧,而不一定是B帧.
x264_frame_t *frame_unused[X264_BFRAME_MAX+1]; /* store unused frames */
注意区分这3个数组.
同时还有下面4个函数(定义在/ENCODER/encoder.c中).
x264_encoder_frame_put_from_picture();
x264_encoder_frame_put();
x264_encoder_frame_get();
x264_frame_copy_picture();
这3个数组和4个函数可以说完成了整个帧的类型的判定问题.这个里面if ,else语句较多,容易使人迷惑.但我们只要把握下面一个观点就可以看清实质:在不对P帧进行编码之前,我们不对B帧进行编码,只是把B帧放进缓冲区(就是前面提到的数组).
比如视频序列:I B B P B B P
先确立第一个帧的类型,然后进行编码.然后是2个B帧,我们把它放进缓冲区数组.然后是P帧,我们可以判定它的类型并进行编码.同时,我们将缓冲区的B帧放进h->bframe_current[i],不过这时P帧前的两个B帧并没有编码.当读到P帧后面的第一个B帧时,我们实际上才将h->bframe_current数组中的第一个B帧编码,也就是将在I帧后面的第一个B帧(说成P帧前面的第一个B帧容易误解J)编码.
依此类推,把握好上面4个函数的调用流程和指针操作的用法,就可以将帧的类型判定这个问题搞明白了.
F. 然后是速率控制(先不说这个,因为它对编码的流程影响不大),看看建立参考帧列表的操作,也就是
x264_reference_build_list( h, h->fdec->i_poc ); (定义在/ENCODER/encoder.c中).
光看这个函数是不行的,它是和后面的这个函数(如下)一起配合工作的.
if( i_nal_ref_idc != NAL_PRIORITY_DISPOSABLE )//B帧时.
{
x264_reference_update( h );
}
If条件是判断当前帧是否是B帧,如果是的话就不更新参考列表,因为B帧本来就不能作为参考帧嘛!如果是I帧或P帧的话,我们就更新参考帧列表.
我们看到了一个for循环,两个do—while循环.这是实现的关键,具体看代码,不好用语言说明白.
G. 进入另一个复杂的领域:写slice的操作,刚开使挺简单,如我下面的注释.
/* ---------------------- Write the bitstream -------------------------- */
/* Init bitstream context */
h->out.i_nal = 0;//out的声明在bs.h中.
bs_init( &h->out.bs, h->out.p_bitstream, h->out.i_bitstream );//空出8位.
/* Write SPS and PPS */
if( i_nal_type == NAL_SLICE_IDR )//不是每次都要写SPS and PPS,只有碰见立即刷新片时才写.
{
/* generate sequence parameters */
x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST );
x264_sps_write( &h->out.bs, h->sps );
x264_nal_end( h );
/* generate picture parameters */
x264_nal_start( h, NAL_PPS, NAL_PRIORITY_HIGHEST );
x264_pps_write( &h->out.bs, h->pps );
x264_nal_end( h );
}
不过看下面那个函数(就进入了复杂的领域).
x264源代码分析(三) | |
H. x264_slice_write()(定义在/ENCODER/encoder.c中),这里面是编码的最主要部分,下面仔细分析. 前面不说,看下面这个循环,它是采用for循环对一帧图像的所有块依次进行编码. for( mb_xy = 0, i_skip = 0; mb_xy < h->sps->i_mb_width * h->sps->i_mb_height; mb_xy++ )//h->sps->i_mb_width指的是从宽度上说有多少个宏快.对于宽度也就是288 / 16 = 18 { const int i_mb_y = mb_xy / h->sps->i_mb_width; const int i_mb_x = mb_xy % h->sps->i_mb_width;//这两个变量是定义宏块的位置.而不是指宏块中元素的位置.
/* load cache */ x264_macroblock_cache_load( h, i_mb_x, i_mb_y );//是把当前宏块的up宏块和left宏块的intra4x4_pred_mode,non_zero_count加载进来,放到一个数组里面,这个数组用来直接得到当前宏块的左侧和上面宏块的相关值.要想得到当前块的预测值,要先知道上面,左面的预测值,它的目的是替代getneighbour函数. /* analyse parameters * Slice I: choose I_4x4 or I_16x16 mode * Slice P: choose between using P mode or intra (4x4 or 16x16) * */ TIMER_START( i_mtime_analyse ); x264_macroblock_analyse( h );//定义在analyse.h中. TIMER_STOP( i_mtime_analyse );
/* encode this macrobock -> be carefull it can change the mb type to P_SKIP if needed */ TIMER_START( i_mtime_encode ); x264_macroblock_encode( h );//定义在Enc/encoder.c中. TIMER_STOP( i_mtime_encode ); 截止到这就已经完成编码的主要过程了,后面就是熵编码的过程了(我也没看到那,但认为前面才是编码的主要过程).下面对这个过程进行分析. A. x264_macroblock_cache_load( h, i_mb_x, i_mb_y );它是将要编码的宏块的周围的宏块的值读进来, 要想得到当前块的预测值,要先知道上面,左面的预测值,它的作用相当于jm93中的getneighbour函数. B. 进入x264_macroblock_analyse( h )函数(定义在/Enc/analyse.c中,这里涉及到了函数指针数组,需要好好复习,个人认为这也是x264代码最为复杂的一个地方了).既然已经将该宏块周围的宏块的值读了出来,我们就可以对该宏块进行分析了(其实主要就是通过计算sad值分析是否要将16*16的宏块进行分割和采用哪种分割方式合适). 看似很复杂,但我们只要把握一个东西就有利于理解了: 举个生活中的例子来说: 如果你有2元钱,你可以去买2袋1元钱的瓜子,也可以买一袋2元钱的瓜子,如果2袋1元钱的瓜子数量加起来比1袋2元钱的瓜子数量多,你肯定会买2袋1元的.反之你会去买那2元1袋的. 具体来说,对于一个16*16的块, 如果它是I帧的块,我们可以将它分割成16个4*4的块,如果这16个块的sad加起来小于按16*16的方式计算出来的sad值,我们就将这个16*16的块分成16个4*4的块进行编码(在计算每个4*4的块的最小sad值时已经知道它采用何种编码方式最佳了),否则采用16*16的方式编码(同样我们也已知道对它采用哪种编码方式最为合适了. 如果它是P帧或B帧的块,同样是循环套循环,但更为复杂了,可以看我在analyse.c中的注释. 这里还要注意的是提到了 x264_predict_t predict_16x16[4+3]; typedef void (*x264_predict_t)( uint8_t *src, int i_stride ); 这是函数指针数组,有很多对它的调用. C. 退出x264_macroblock_analyse( h )函数,进入x264_macroblock_encode( )函数(定义在/ENCODER/macroblock.c中). 我拿宏块类型为I_16*16为例. if( h->mb.i_type == I_16x16 ) { const int i_mode = h->mb.i_intra16x16_pred_mode; /* do the right prediction */ h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );//这两个参数的关系. //涉及到x264_predict_t(函数指针数组),声明在core/predict.h中,core/predict.c里有不同定义. /* encode the 16x16 macroblock */ x264_mb_encode_i16x16( h, i_qscale );// /* fix the pred mode value */ … } 我们看到h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );只调用了一次,这是因为在x264_macroblock_analyse( )中我们已经确定了采用4种方式中的哪种最合适.而在x264_macroblock_analyse( )中判定一个块是否为I_16*16,我们调用了四次.这是因为当时我们需要拿最小的sad值进行比较. 继续,是x264_mb_encode_i16x16( h, i_qscale )函数(定义在/ENCODER/macroblock.c中).在这个函数中我们就可以看到量化,zig-扫描等函数了,这些都是直来直去的,需要的只是我们的细心和对数学知识的掌握了 |
|