X264編碼流程

對H.264編碼標準一直停留在理解原理的基礎上,對於一個實際投入使用的編碼器是如何構建起來一直感覺很神祕,於是決定在理解理論的基礎上潛心於編碼器實現框架。關於開源的H264編碼器有很多,JMVC,T264、X264,這裏選擇X264,因爲網上關於X264源碼分析資源很多。X264編碼器是一個開源的經過優化的高性能H.264編碼器,目前最新的源碼在本人的I5處理器的PC機上,編碼1920x1080分辨率的視頻序列在使用ultrafast配置的情況下,可以實現160fps左右的編碼速度。這裏對源碼分析並沒有選擇最新的源碼,而是選用2009年2月份的版本,原因有二:一是這個版本是從網上下載下來的,已經是一個建立好的VS2008工程,對於像我這種用慣IDE調試的程序員來說,這可以大大提高源碼閱讀速度;二是雖然X264源碼雖然幾乎每天都有更新,但是從這個版本以後,最大的改動基本是針對多Slice編碼的支持,其他都是在輸入輸出方面的一些改動,從這個版本學習可以較快進入編碼部分的學習;三是這個版本當中已經有別人的很多的註釋,便於自己理解;一般將編碼分爲幀級、片級、宏塊級編碼,依次從上到下。下面就從這三個級分析X264編碼流程:一、幀級編碼分析定位到x264_encoder_encode這個函數,這個函數應該是H264編碼最上層的函數,實現編碼一幀視頻。在進行下一步分析之前有必要了解,控制X264編碼的全局性結構體x264_t,這個結構體控制着視頻一幀一幀的編碼,包括中間參考幀管理、碼率控制、全局參數等一些重要參數和結構體。下面是x264_t這個結構體的定義(這裏僅對幾個關鍵的結構和變量進行分析):
struct x264_t  
{  
    x264_param_t    param;//編碼器編碼參數,包括量化步長、編碼級別等等一些參數  
  
...........  
    int             i_frame;//編碼幀號,用於計算POC(picture of count 標識視頻幀的解碼順序)  
...........  
    int             i_nal_type;     /* Nal 單元的類型,可以查看編碼標準,有哪幾種類型,需要理解的類型有:不分區(一幀作爲一個片)非IDR圖像的片;片分區A、片分區B、片分區C、IDR圖像中的片、序列參數集、圖像參數集 */  
  
    int             i_nal_ref_idc;  /* Nal 單元的優先級別<span style="background-color: rgb(255, 255, 255); ">取值範圍[0,1,2,3],值越大表示優先級越高,此Nal單元就越重要</span>*/  
  
    /* We use only one SPS(序列參數集) and one PPS(圖像參數集) */  
    x264_sps_t      sps_array[1];//結構體的數組  
    x264_sps_t      *sps;  
    x264_pps_t      pps_array[1];  
    x264_pps_t      *pps;  
    int             i_idr_pic_id;  
  
......  
  
    struct  
    {  
//這個結構體涉及到X264編碼過程中的幀管理,理解這個結構體中的變量在編碼標準的理論意義是非常重要的  
        x264_frame_t *current[X264_BFRAME_MAX*4+3];/*已確定幀類型,待編碼幀,每一個GOP在編碼前,每一幀的類型在編碼前已經確定。當進行編碼時,從這裏取出一幀數據。*/  
        x264_frame_t *next[X264_BFRAME_MAX*4+3];//尚未確定幀類型的待編碼幀,當確定後,會將此數組中的幀轉移到current數組中去。  
        x264_frame_t *unused[X264_BFRAME_MAX*4 + X264_THREAD_MAX*2 + 16+4];/*這個數組用於回收那些在編碼中分配的frame空間,當有新的需要時,直接拿過來用,不用重新分配新的空間,提高效率*/  
        /* For adaptive B decision */  
        x264_frame_t *last_nonb;  
  
        /* frames used for reference + sentinels */  
        x264_frame_t *reference[16+2];//參考幀隊列,注意參考幀都是重建幀  
  
        int i_last_idr; /* 上一次刷新關鍵幀的幀號,配合前面的i_frame,可以用來計算POC */  
  
        int i_input;    /* Number of input frames already accepted *///frames結構體中i_input指示當前輸入的幀的(播放順序)序號。  
  
        int i_max_dpb;  /* 分配解碼圖像緩衝的最大數量(DPB) */  
        int i_max_ref0;//最大前向參考幀數量  
        int i_max_ref1;//最大後向參考幀數量  
        int i_delay;    /* Number of frames buffered for B reordering */  
        //i_delay設置爲由B幀個數(線程個數)確定的幀緩衝延遲,在多線程情況下爲i_delay = i_bframe + i_threads - 1。  
        //而判斷B幀緩衝填充是否足夠則通過條件判斷:h->frames.i_input <= h->frames.i_delay + 1 - h->param.i_threads。  
        int b_have_lowres;  /* Whether 1/2 resolution luma planes are being used */  
        int b_have_sub8x8_esa;  
    } frames;//指示和控制幀編碼過程的結構  
  
    /* current frame being encoded */  
    x264_frame_t    *fenc;//指向當前編碼幀  
  
    /* frame being reconstructed */  
    x264_frame_t    *fdec;//指向當前重建幀,重建幀的幀號要比當前編碼幀的幀號小1  
  
    /* references lists */  
    int             i_ref0;//前向參考幀的數量  
    x264_frame_t    *fref0[16+3];     /* 存放前向參考幀的數組(注意參考幀均是重建幀) */  
    int             i_ref1;//後向參考幀的數量  
    x264_frame_t    *fref1[16+3];     /* 存放後向參考幀的數組*/  
    int             b_ref_reorder[2];  
........  
};  

定位到x264_encoder_encode這個函數,這個函數應該是H264編碼最上層的函數,實現編碼的幀級處理(如何進行參考幀管理、幀類型確定等等)。
 下面對x264_encoder_encode中幾個關鍵函數以及關鍵部分進行分析:

1、x264_reference_update這個函數主要完成參考幀的更新,H.264的幀間預測需要使用參考幀,參考幀使用的都是已編碼後的重建幀,每編碼一幀的同時會重建此幀作爲參考幀,在編碼下一幀時,將此重建幀加入到參考幀隊列中。函數實現如下:

static inline void x264_reference_update( x264_t *h )  
{  
    int i;  
  
    if( h->fdec->i_frame >= 0 )//重建幀幀數大於等於零時  
        h->i_frame++;//當前編碼幀的幀號要比重建幀的幀號大1  
  
    if( !h->fdec->b_kept_as_ref )/*如果重建幀不作爲參考幀(不作爲參考幀,當然不用加入參考幀隊列了)*/  
    {//when b frame is not used as reference frame  
        if( h->param.i_threads > 1 )  
        {  
            x264_frame_push_unused( h, h->fdec );  
            h->fdec = x264_frame_pop_unused( h );  
        }  
        return;//if b-frame is not used as reference, return   
    }  
  
    /* move lowres(低分辨率) copy of the image to the ref frame */  
    for( i = 0; i < 4; i++)  
    {/*暫時還不知道幹嘛的*/  
        XCHG( uint8_t*, h->fdec->lowres[i], h->fenc->lowres[i] );  
        XCHG( uint8_t*, h->fdec->buffer_lowres[i], h->fenc->buffer_lowres[i] );  
    }  
  
    /* adaptive B decision needs a pointer, since it can't use the ref lists */  
    if( h->sh.i_type != SLICE_TYPE_B )  
        h->frames.last_nonb = h->fdec;  
  
    /* move frame in the buffer */  
    x264_frame_push( h->frames.reference, h->fdec );/*把重建幀放入參考隊列中*/  
    if( h->frames.reference[h->frames.i_max_dpb] )/*如果參考幀的個數大於解碼圖像緩存的最大數(decoded picture buffer(DPB))*/  
        x264_frame_push_unused( h, x264_frame_shift( h->frames.reference ) );/*取出參考隊列中第一個參考重建幀,並放入暫時不用幀隊列中*/  
    h->fdec = x264_frame_pop_unused( h );/*從暫時不用幀隊列中,取出一幀作爲新的重建幀buf*/  
}  

2、幀排序部分,在H.264標準中採用編碼順序與顯示順序不同的編碼方式,對於一個已經確定幀類型的待編碼序列:IBBPBBP在編碼時需要先排序爲IPBBPBB,然後進行編碼。在X264代碼中,實現在如下部分:

//確定幀的類型  
      x264_stack_align( x264_slicetype_decide, h );/*通過x264_slicetype_decide函數來確定決定h-frames.next[]中每一幀的類型*/  
  
      /* 3: move some B-frames and 1 non-B to encode queue 這裏來完成幀排序,還是有點巧妙的*/  
      while( IS_X264_TYPE_B( h->frames.next[bframes]->i_type ) )  
          bframes++;/*注意這個循環查找的作用,一方面可以確定第一個非B幀之前B幀的數量,也可以定位出第一個非B幀的位置*/  
      x264_frame_push( h->frames.current, x264_frame_shift( &h->frames.next[bframes] ) );/*取出第一個非B幀,並放到current指針第一個位置:通過這兩步完成幀排序的(例如BBP->PBB)*/  
      /* FIXME: when max B-frames > 3, BREF may no longer be centered after GOP closing */  
      if( h->param.b_bframe_pyramid && bframes > 1 )  
      {  
          x264_frame_t *mid = x264_frame_shift( &h->frames.next[bframes/2] );  
          mid->i_type = X264_TYPE_BREF;  
          x264_frame_push( h->frames.current, mid );  
          bframes--;  
      }  
      while( bframes-- )  
          x264_frame_push( h->frames.current, x264_frame_shift( h->frames.next ) ); /*然後依次取出B幀,並放到current隊列中*/  

下面就可以從h->frames.current隊列中取出第一幀放入h->fenc中

h->fenc = x264_frame_shift( h->frames.current );//從當前編碼幀中取出第一幀,作爲當前編碼幀  

然後就開始編碼,首先當前編碼幀(h->fenc)的類型,設定slice類型,這裏就不解釋了,關於IDR幀,執行了x264_reference_reset這個函數將參考幀隊列清空。
接着進行相關參數的賦值,這裏主要對POC的計算強調一下:

h->fenc->i_poc = 2 * (h->fenc->i_frame - h->frames.i_last_idr);//考慮到場編碼,POC每幀增長爲2,如果是場編碼POC增長爲1  

3、重建參考幀列表(x264_reference_build_list),即將參考幀列表中的參考幀分爲前向參考幀和後向參考幀,並根據POC進行參考幀排序。函數具體實現如下:

static inline void x264_reference_build_list( x264_t *h, int i_poc )  
{  
    int i;  
    int b_ok;  
  
    /* build ref list 0/1 */  
    h->i_ref0 = 0;//前向參考幀索引  
    h->i_ref1 = 0;//後向參考幀索引  
    for( i = 0; h->frames.reference[i]; i++ )  
    {//注意這裏都是指針操作  
        if( h->frames.reference[i]->i_poc < i_poc )  
        {//小於當前幀POC的,放到前向參考幀列表中  
            h->fref0[h->i_ref0++] = h->frames.reference[i];  
        }  
        else if( h->frames.reference[i]->i_poc > i_poc )  
        {//大於當前幀POC的,放到後向參考幀列表中  
            h->fref1[h->i_ref1++] = h->frames.reference[i];  
        }  
    }  
  
    /* Order ref0 from higher to lower poc */  
    do  
    {/*採用冒泡排序(不知道使用dowhile+for循環與雙重for循環有什麼優勢),對參考幀按照POC從高到低進行排序*/  
        b_ok = 1;  
        for( i = 0; i < h->i_ref0 - 1; i++ )  
        {  
            if( h->fref0[i]->i_poc < h->fref0[i+1]->i_poc )  
            {  
                XCHG( x264_frame_t*, h->fref0[i], h->fref0[i+1] );  
                b_ok = 0;  
                break;  
            }  
        }  
    } while( !b_ok );  
    /* Order ref1 from lower to higher poc (bubble sort) for B-frame */  
    do  
    {  
        b_ok = 1;  
        for( i = 0; i < h->i_ref1 - 1; i++ )  
        {  
            if( h->fref1[i]->i_poc > h->fref1[i+1]->i_poc )  
            {  
                XCHG( x264_frame_t*, h->fref1[i], h->fref1[i+1] );  
                b_ok = 0;  
                break;  
            }  
        }  
    } while( !b_ok );  
  
    /* In the standard, a P-frame's ref list is sorted by frame_num. 
     * We use POC, but check whether explicit reordering is needed */  
    h->b_ref_reorder[0] =  
    h->b_ref_reorder[1] = 0;  
    if( h->sh.i_type == SLICE_TYPE_P )  
    {  
        for( i = 0; i < h->i_ref0 - 1; i++ )  
            if( h->fref0[i]->i_frame_num < h->fref0[i+1]->i_frame_num )  
            {  
                h->b_ref_reorder[0] = 1;  
                break;  
            }  
    }  
  
    h->i_ref1 = X264_MIN( h->i_ref1, h->frames.i_max_ref1 );  
    h->i_ref0 = X264_MIN( h->i_ref0, h->frames.i_max_ref0 );  
    h->i_ref0 = X264_MIN( h->i_ref0, h->param.i_frame_reference ); // if reconfig() has lowered the limit  
    assert( h->i_ref0 + h->i_ref1 <= 16 );  
    h->mb.pic.i_fref[0] = h->i_ref0;//爲什麼參考幀選擇這兩個,還沒有搞懂  
    h->mb.pic.i_fref[1] = h->i_ref1;  
}  

4、初始化比特流,寫入SPS以及PPS信息後就開始進行片級編碼。

if( i_nal_type == NAL_SLICE_IDR && h->param.b_repeat_headers )  
    {/*SPS和PPS是解碼需要用到的信息,因此只有解碼器解析了SPS和PPS信息 
     才能進行解碼,這就是爲什麼在每個IDR幀前寫入這些信息*/  
        if( h->fenc->i_frame == 0 )  
        {//僅僅在第一針寫入sei信息  
            /* identify ourself */  
    <span style="white-space:pre">  </span>x264_nal_start( h, NAL_SEI, NAL_PRIORITY_DISPOSABLE );/*開始整理nal。*/  
            x264_sei_version_write( h, &h->out.bs );//寫sei信息  
            x264_nal_end( h );  
        }  
  
        /* generate sequence parameters */  
        x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST );/*開始整理nal。一個nal單元的首地址被賦值, 
<span style="white-space:pre">          </span>將要處理此新的nal單元;設置nal的優先權和類型。*/  
        x264_sps_write( &h->out.bs, h->sps );/*寫SPS信息。 
            將序列參數集sps寫進位流結構中h->out.bs 
            不是每次都要寫SPS and PPS,只有碰見立即刷新片(NAL_SLICE_IDR)時才寫*/  
        x264_nal_end( h );/*結束nal,整理nal 
            (1)輸出新nal單元的地址 
            (2)自增表示下一個新nal單元的序號*/  
  
        /* generate picture parameters */  
        x264_nal_start( h, NAL_PPS, NAL_PRIORITY_HIGHEST );/*開始整理nal。 
                一個nal單元的首地址被賦值,將要處理此新的nal單元;設置nal的優先權和類型。*/  
        x264_pps_write( &h->out.bs, h->pps );/*寫PPS信息。 
            將序列參數集sps寫進位流結構中h->out.bs 
            不是每次都要寫SPS and PPS,只有碰見立即刷新片(NAL_SLICE_IDR)時才寫*/  
        x264_nal_end( h );/*結束nal,整理nal 
            (1)輸出新nal單元的地址 
            (2)自增表示下一個新nal單元的序號*/  
    }  

接着就開始片級編碼x264_slices_write( h );

二、片級編碼分析


轉載文章鏈接轉載文章鏈接







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章