源码阅读 x264 - 命令行工具

命令行工具简介

x264 命令行工具执行流程如下:

  1. 首先调用 parse() 解析输入的命令行参数
    1. parse()首先调用 x264_param_default() 为存储参数的结构体 x264_param_t 赋默认值
    2. 循环中调用 getopt_long() 逐个解析输入的参数,并作相应的处理
    3. 调用 select_output() 解析输出文件格式(例如 raw,flv,MP4…)
    4. 调用 select_input() 解析输入文件格式(例如 yuv,y4m…)
  2. 然后调用 encode() 进行编码
    1. encode() 首先调用 x264_encoder_open() 打开 H.264 编码器
    2. 然后调用 x264_encoder_headers() 输出 H.264 码流的头信息(例如 SPS、PPS、SEI)
    3. 循环调用 encode_frame() 逐帧编码视频
      1. encode_frame()中调用了x264_encoder_encode()完成了具体的编码工作
    4. 调用x264_encoder_close()关闭解码器

main 函数

main() 是 x264 控制台程序的入口函数,定义如下所示。

REALIGN_STACK int main(int argc, char **argv)
{
    if(argc == 4 && !strcmp( argv[1], "--autocomplete" ) )
        return x264_cli_autocomplete(argv[2], argv[3] );

    x264_param_t param;
    cli_opt_t opt = {0};
    int ret = 0;

    FAIL_IF_ERROR(x264_threading_init(), "unable to initialize threading\n" );

    x264_param_default(&param);
    /* Parse command line */
    if(parse( argc, argv, &param, &opt) < 0 )
        ret = -1;

    /* Control-C handler */
    signal(SIGINT, sigint_handler);

    if(!ret)
        ret = encode(&param, &opt);

    /* clean up handles */
    if(filter.free)
        filter.free(opt.hin);
    else if(opt.hin)
        cli_input.close_file(opt.hin);
    if(opt.hout)
        cli_output.close_file(opt.hout, 0, 0);
    if(opt.tcfile_out)
        fclose(opt.tcfile_out);
    if(opt.qpfile)
        fclose(opt.qpfile);
    x264_param_cleanup(&param);

    return ret;
}

可以看出 main() 的定义很简单,它主要调用了两个函数:parse() 和 encode()。

main() 首先调用 parse() 对命令行进行解析并进行参数设置,然后调用 encode() 进行编码压缩。下面分别分析这两个函数。

parse 函数

parse() 用于解析命令行输入的参数(存储于 argv[] 中),其定义如下:

/**
* @brief                    参数分析函数
* @param[in]        argc    参数个数
* @param[in]        argv    1 维数组指针,指向每个参数字符串首地址
* @param[in/out]    param   x264_param_t 结构体指针
* @param[in/out]    param   cli_opt_t 结构体指针
* @return                   # 0     执行成功
*                           # -1    执行失败
*/
static int parse(int argc, char **argv, x264_param_t *param, cli_opt_t *opt);

阅读源代码,可梳理出 parse() 函数的简单流程:

  1. 调用 x264_param_default() 为存储参数的结构体 x264_param_t 赋默认值
  2. 调用 x264_param_default_preset() 为 x264_param_t 赋值
  3. 在一个大循环中调用 getopt_long() 逐个解析输入的参数,并作相应的处理。举几个例子:
    1. 对于短选项,直接设置输出参数或调用其他函数进行处理。
    2. 对于长选项,统一调用 x264_param_parse() 进行处理。
  4. 调用 select_output() 解析输出文件格式(例如 raw,flv,MP4…)
  5. 调用 select_input() 解析输入文件格式(例如 yuv,y4m…)

关于 x264_param_t 结构体和 cli_opt_t 结构体的说明,详见:源码阅读 x264 - 常用结构体

parse() 中几个比较关键的函数:

x264_param_default

x264_param_default() 用于初始化 x264_param_t 类型变量

/**
* @brief                    x264_param_t 参数初始化函数
* @param[in/out]    param   x264_param_t 结构体指针
* @return                   void
* @note:    fill x264_param_t with default values and do CPU detection
*/
REALIGN_STACK void x264_param_default(x264_param_t *param);

x264_param_default(x264_param_t*),在 parse() 中声明了一个 x264_param_t 类型的局部变量 default,并调用此函数进行初始化,但是目前还不知道 default 的用途。

x264_param_default_preset

/**
* @brief                    根据 preset 和 tune 对 x264_param_t 参数进行初始化
* @param[in/out]    param   x264_param_t 结构体指针
* @param[in]        preset  字符串,指示预设编码类型
* @param[in]        tune    字符串,指示视频类型或视觉优化类型
* @return                   # 0          执行成功
*                           # negative   执行失败 (e.g. invalid preset/tune name)
* @note:    Multiple tunings can be used if separated by a delimiter in ",./-+",
*           however multiple psy tunings cannot be used.
*           film, animation, grain, stillimage, psnr, and ssim are psy tunings.
*/
X264_API int x264_param_default_preset(x264_param_t *param, const char *preset, const char *tune);

--preset 的参数主要调节编码速度和质量的平衡,有 ultrafast、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、placebo 这 10 个选项,从快到慢。 --tune 的参数主要配合视频类型和视觉优化的参数,或特别的情况。如果视频的内容符合其中一个可用的调整值又或者有其中需要,则可以使用此选项,否则建议不使用。 tune 的值有: film、animation、grain、stillimage、psnr、ssim、fastdecode、zerolatency、touhou 这 9 个选项,部分选项可同时设置,可详见 param_apply_tune() 函数。

x264_param_default_preset(),会首先调用 x264_param_default() 对 param 进行初始化,然后根据 preset 和 tune 的值再对 param 进行初始化。

x264_param_parse

/**
* @brief                    根据 name 和 value 对 x264_param_t 参数进行设置
* @param[in/out]    param   x264_param_t 结构体指针
* @param[in]        name    字符串,指示参数名称
* @param[in]        value   字符串,指示参数值
* @return                   # 0                                         执行成功
*                           #define X264_PARAM_BAD_NAME         (-1)    执行失败
*                           #define X264_PARAM_BAD_VALUE        (-2)    执行失败
*                           #define X264_PARAM_ALLOC_FAILED     (-3)    执行失败
* @note:    BAD_VALUE occurs only if it can't even parse the value,
*           numerical range is not checked until x264_encoder_open() or x264_encoder_reconfig().
*           value=NULL means "true" for boolean options, but is a BAD_VALUE for non-booleans.
*           can allocate memory which should be freed by call of x264_param_cleanup.
*/
X264_API int x264_param_parse(x264_param_t *param, const char *name, const char *value);

x264_param_parse() 用来对命令行中的长选项进行解析,并完成 param 的设置。

x264_param_apply_fastfirstpass

/* x264_param_apply_fastfirstpass:
 *      If first-pass mode is set (rc.b_stat_read == 0, rc.b_stat_write == 1),
 *      modify the encoder settings to disable options generally not useful on
 *      the first pass.
 */
X264_API void x264_param_apply_fastfirstpass(x264_param_t *param);

x264_param_apply_profile

static const char * const x264_profile_names[] = {
    "baseline", "main", "high", "high10", "high422", "high444", 0
};
/**
* @brief                    根据 profile 对 x264_param_t 参数进行设置
* @param[in/out]    param   x264_param_t 结构体指针
* @param[in]        profile 字符串,指示 H264 配置文件
* @return                   # 0         执行成功
*                           # negative  执行失败 (e.g. invalid profile name)
* @note:    (can be NULL, in which case the function will do nothing)
*           Does NOT guarantee that the given profile will be used: if the restrictions
*           of "High" are applied to settings that are already Baseline-compatible, the
*           stream will remain baseline.  In short, it does not increase settings, only
*           decrease them.

*/
X264_API int x264_param_apply_profile(x264_param_t *param, const char *profile);

x264_param_apply_profile() 根据 profile 对 param 进行设置,这里 profile 有 6 种选项,分别是 baseline/main/high/high10/high422/high444,如果选择无损压缩的话,profile 会被限制为 high444。

select_output

/**
* @brief                        根据文件名的后缀确定输出的文件格式(raw H264,flv,mp4...)
* @param[in]        muxer       字符串,指示输出文件的 muxer 格式
* @param[in]        filename    字符串,指示输出文件的文件名
* @param[in/out]    param       x264_param_t 结构体指针
* @return                       # 0         执行成功
*                               # negative  执行失败
* @note:    在指定 muxer 时以 muxer 为准,muxer 为 auto 时,以输出文件拓展名为准
*/
static int select_output(const char *muxer, char *filename, x264_param_t *param);

select_output() 会从输入文件路径的字符串中提取出了扩展名,然后根据不同的扩展名设定不同的输出格式。

select_input

/**
* @brief                            根据文件名的后缀确定输出的文件格式(raw H264,flv,mp4...)
* @param[in]        demuxer         字符串,指示输入文件的 demuxer 格式
* @param[in]        used_demuxer    字符串,指示输入文件的 demuxer 格式
* @param[in]        filename        字符串,指示输入文件的文件名
* @param[out]       p_handle        hnd_t 结构体指针
* @param[out]       info            video_info_t 结构体指针,保存输入文件信息
* @param[out]       opt             cli_input_opt_t 结构体指针
* @return                           # 0         执行成功
*                                   # negative  执行失败
* @note:    在指定 demuxer 时以 demuxer 为准,demuxer 为 auto 时,以输入文件拓展名为准
*/
static int select_input( const char *demuxer, char *used_demuxer, char *filename,
                         hnd_t *p_handle, video_info_t *info, cli_input_opt_t *opt )

select_input() 首先获取输入文件名的扩展名;然后根据扩展名不同调用不用的 open_file 函数来设置不同的输入格式。

encode 函数

/**
* @brief                    开始进行编码
* @param[in]        param   x264_param_t 结构体指针
* @param[in]        opt     cli_opt_t 结构体指针
* @return                   # 0         执行成功
*                           # negative  执行失败
*/
static int encode( x264_param_t *param, cli_opt_t *opt );

从源代码可以梳理出来 encode() 的流程:

  1. 调用 x264_encoder_open() 打开 H.264 编码器。
  2. 调用 x264_encoder_parameters() 获得当前的参数集 x264_param_t,用于后续步骤中的一些配置。
  3. 调用输出格式(H.264裸流、FLV、mp4 等)对应 cli_output_t 结构体的 set_param() 方法,为输出格式的封装器设定参数。其中参数源自于上一步骤得到的 x264_param_t。
  4. 如果不是在每个 keyframe 前面都增加 SPS/PPS/SEI 的话,就调用 x264_encoder_headers() 在整个码流前面加 SPS/PPS/SEI。
  5. 进入一个循环中进行一帧一帧的将 YUV 编码为 H.264:
    1. 调用输入格式(YUV、Y4M 等)对应的 cli_vid_filter_t 结构体 get_frame() 方法,获取一帧 YUV 数据。
    2. 调用 encode_frame() 编码该帧 YUV 数据为 H.264 数据,并且输出出来。该函数内部调用 x264_encoder_encode() 完成编码工作,调用输出格式对应 cli_output_t 结构体的 write_frame() 完成了输出工作。
    3. 调用输入格式(YUV、Y4M 等)对应的 cli_vid_filter_t 结构体 release_frame() 方法,释放刚才获取的YUV 数据。
    4. 调用 print_status() 输出一些统计信息。
  6. 编码即将结束的时候,进入另一个循环,输出编码器中缓存的视频帧:
    1. 不再传递新的 YUV 数据,直接调用 encode_frame(),将编码器中缓存的剩余几帧数据编码输出出来。
    2. 调用 print_status() 输出一些统计信息。
  7. 调用 x264_encoder_close() 关闭 H.264 编码器。

encode() 的流程中涉及到 libx264 的几个关键的API,这些函数较为复杂,后续再进行分析:

  • x264_encoder_open():打开H.264编码器。
  • x264_encoder_headers():输出SPS/PPS/SEI。
  • x264_encoder_encode():编码一帧数据。
  • x264_encoder_close():关闭H.264编码器。

参考资料