http://blog.csdn.net/xingyu19871124/article/details/7671634

对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[];//结构体的数组
x264_sps_t *sps;
x264_pps_t pps_array[];
x264_pps_t *pps;
int i_idr_pic_id; ...... struct
{
//这个结构体涉及到X264编码过程中的帧管理,理解这个结构体中的变量在编码标准的理论意义是非常重要的
x264_frame_t *current[X264_BFRAME_MAX*+];/*已确定帧类型,待编码帧,每一个GOP在编码前,每一帧的类型在编码前已经确定。当进行编码时,从这里取出一帧数据。*/
x264_frame_t *next[X264_BFRAME_MAX*+];//尚未确定帧类型的待编码帧,当确定后,会将此数组中的帧转移到current数组中去。
x264_frame_t *unused[X264_BFRAME_MAX* + X264_THREAD_MAX* + +];/*这个数组用于回收那些在编码中分配的frame空间,当有新的需要时,直接拿过来用,不用重新分配新的空间,提高效率*/
/* For adaptive B decision */
x264_frame_t *last_nonb; /* frames used for reference + sentinels */
x264_frame_t *reference[+];//参考帧队列,注意参考帧都是重建帧 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[+]; /* 存放前向参考帧的数组(注意参考帧均是重建帧) */
int i_ref1;//后向参考帧的数量
x264_frame_t *fref1[+]; /* 存放后向参考帧的数组*/
int b_ref_reorder[];
........
};

定位到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 >= )//重建帧帧数大于等于零时
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 > )
{
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 = ; i < ; 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 > )
{
x264_frame_t *mid = x264_frame_shift( &h->frames.next[bframes/] );
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 =  * (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 = ;//前向参考帧索引
h->i_ref1 = ;//后向参考帧索引
for( i = ; 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 = ;
for( i = ; i < h->i_ref0 - ; i++ )
{
if( h->fref0[i]->i_poc < h->fref0[i+]->i_poc )
{
XCHG( x264_frame_t*, h->fref0[i], h->fref0[i+] );
b_ok = ;
break;
}
}
} while( !b_ok );
/* Order ref1 from lower to higher poc (bubble sort) for B-frame */
do
{
b_ok = ;
for( i = ; i < h->i_ref1 - ; i++ )
{
if( h->fref1[i]->i_poc > h->fref1[i+]->i_poc )
{
XCHG( x264_frame_t*, h->fref1[i], h->fref1[i+] );
b_ok = ;
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[] =
h->b_ref_reorder[] = ;
if( h->sh.i_type == SLICE_TYPE_P )
{
for( i = ; i < h->i_ref0 - ; i++ )
if( h->fref0[i]->i_frame_num < h->fref0[i+]->i_frame_num )
{
h->b_ref_reorder[] = ;
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 <= );
h->mb.pic.i_fref[] = h->i_ref0;//为什么参考帧选择这两个,还没有搞懂
h->mb.pic.i_fref[] = 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 == )
{//仅仅在第一针写入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 );

二、片级编码分析

.......................

X264编码流程详解(转)的更多相关文章

  1. JPEG图像压缩算法流程详解

    JPEG图像压缩算法流程详解 JPEG代表Joint Photographic Experts Group(联合图像专家小组).此团队创立于1986年,1992年发布了JPEG的标准而在1994年获得 ...

  2. FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  3. C++的性能C#的产能?! - .Net Native 系列《二》:.NET Native开发流程详解

    之前一文<c++的性能, c#的产能?!鱼和熊掌可以兼得,.NET NATIVE初窥> 获得很多朋友支持和鼓励,也更让我坚定做这项技术的推广者,希望能让更多的朋友了解这项技术,于是先从官方 ...

  4. [nRF51822] 5、 霸屏了——详解nRF51 SDK中的GPIOTE(从GPIO电平变化到产生中断事件的流程详解)

    :由于在大多数情况下GPIO的状态变化都会触发应用程序执行一些动作.为了方便nRF51官方把该流程封装成了GPIOTE,全称:The GPIO Tasks and Events (GPIOTE) . ...

  5. 迅为4412开发板Linux驱动教程——总线_设备_驱动注册流程详解

    本文转自:http://www.topeetboard.com 视频下载地址: 驱动注册:http://pan.baidu.com/s/1i34HcDB 设备注册:http://pan.baidu.c ...

  6. Linux启动流程详解【转载】

    在BIOS阶段,计算机的行为基本上被写死了,可以做的事情并不多:一般就是通电.BIOS.主引导记录.操作系统这四步.所以我们一般认为加载内核是linux启动流程的第一步. 第一步.加载内核 操作系统接 ...

  7. iOS 组件化流程详解(git创建流程)

    [链接]组件化流程详解(一)https://www.jianshu.com/p/2deca619ff7e

  8. git概念及工作流程详解

    git概念及工作流程详解 既然我们已经把gitlab安装完毕[当然这是非必要条件],我们就可以使用git来管理自己的项目了,前文也多多少少提及到git的基本命令,本文就先简单对比下SVN与git的区别 ...

  9. Lucene系列六:Lucene搜索详解(Lucene搜索流程详解、搜索核心API详解、基本查询详解、QueryParser详解)

    一.搜索流程详解 1. 先看一下Lucene的架构图 由图可知搜索的过程如下: 用户输入搜索的关键字.对关键字进行分词.根据分词结果去索引库里面找到对应的文章id.根据文章id找到对应的文章 2. L ...

随机推荐

  1. 【git】提交到github不显示贡献小绿点问题的解决

    问题描述: 最近一直在用github来写博客,但是今天发现github上的contributions记录并没有我的提交记录. 经过一番百度和自行捣鼓发现了问题所在. 原因: 最近实习,公司给配电脑.原 ...

  2. Class.getResource()方法的使用

    我们之前使用路径总是有点不知道怎么用,发现别人使用Class.getResource()方法,好像挺不错的样子.于是看看博客,简单学习下. 参考链接:http://blog.csdn.net/lcj8 ...

  3. Python基础学习----拆包

    拆包,多用在多值参数种. 1.多值参数. 有时候,在函数的参数转递时,不单只传输单个字符的参数,比如有元组和字典的参数,这时候我们就使用多值参数. *args 代表元组的多值参数 *kwargs 代表 ...

  4. TF随笔-12

    #!/usr/bin/env python2 # -*- coding: utf-8 -*- """ Created on Tue Aug 1 08:14:30 2017 ...

  5. 《DSP using MATLAB》Problem 2.16

    先由脉冲响应序列h(n)得到差分方程系数,过程如下: 代码: %% ------------------------------------------------------------------ ...

  6. Gym 100712L Alternating Strings II(单调队列)

    题目链接 Alternating Strings II 题意是指给出一个长度为n的01串,和一个整数k,要求将这个01串划分为很多子串(切很多刀),使得每个子串长度不超过k,且每个字串不是01交替出现 ...

  7. lua 二进制函数使用

    由于 Lua 脚本语言本身不支持对数字的二进制操作(例如 与,或,非 等操作),MUSHclient 为此提供了一套专门用于二进制操作的函数,它们都定义在一个"bit"表中,使用时 ...

  8. 对ashx请求用Gzip,Deflated压缩

    //GZIP压缩 //查看请求头部 string acceptEncoding = context.Request.Headers["Accept-Encoding"].ToStr ...

  9. Ant build.xml相关属性详解

    关键字: ant build.xml Ant的概念 可能有些读者并不连接什么是Ant以及入可使用它,但只要使用通过Linux系统得读者,应该知道make这个命令.当编译Linux内核及一些软件的源程序 ...

  10. pthread访问调用信号线程的掩码(pthread_sigmask )

    掩码: 信号掩码 在POSIX下,每个进程有一个信号掩码(signal mask).简单地说,信号掩码是一个"位图",其中每一位都对应着一种信号.如果位图中的某一位为1,就表示在执 ...