如何用nginx+ffmpeg实现苹果HLS协议

openkk 11年前

今年用三个月时间做了一个支持HLS的视频服务,用了三个月时间,对于一个视频处理的门外汉来说,是一个相当痛苦和漫长的过程,因此想抽时间将开发过程重新梳理一边,顺边形成一个不多篇幅但是足够细致的回顾和说明文档。

当前只是一个草稿,不做任何整理,所以不会强调文章的连贯和呼应关系。

大致叙述的内容包括:

HLS协议的理解

nginx ffmpeg的编译 安装 调试,以及工具的选择 使用 gdb等

nginx模块开发

ffmpeg的开发

重点将集中在 ffmpeg 的开发上。

HLS协议的实现有很多的细节,比如我在实际的开发过程中,就面临将多种不同格式的视频源文件(来源于不同的编码器以及有不同的profile)动态切片输出。而现有能在网上找到的方式基本都是对视频文件做了预先处理,比如用ffmpeg将视频文件先转换成物理存储的mpeg2ts文件,然后用nginx进行动态切片输出。这对开发带来了很大的困难。

如果我们将问题简化的话,即 输入文件为 mp4 (isom512 , 2 channels stereo),那么最简单的实现方式是如下命令行:

avconv -i input_file.mp4 -vcodeccopy -acodeccopy -vbsfh264_mp4toannexbss00:00:00t00:00:10 output_file.ts

然后通过 对 参数ss00:00:00t00:00:10 的调整,获得多个物理切片,提供给nginx输出。

这里需要提供一个细节,即 处理的性能。 所以在上述的命令行中,仅仅进行了 remux 而没有任何 ecode 和 decode 的操作。

我们要做的,就是将这行命令变成 可供 nginx 调用的 api。

当然,任然可以选择最简单的作法,nginx模块里面调用系统命令。不过这样子,貌似有点儿寒碜吧。呵呵。

所以,我们需要的是这样一个接口:

int segment(byte** output, int *output_len, int start, int end, const char * inputfile)


从命令行到接口方法,第一步就是弄懂ffmpeg如何解析命令行参数并赋值

ffmpeg参数解析

——此文档为《如何用nginx+ffmpeg实现苹果HLS协议》的一个章节。

谢绝对非技术问题的修改,转载请注明来源

继续以命令行

avconv -i input_file.mp4 -vcodeccopy -acodeccopy -vbsfh264_mp4toannexbss00:00:00t00:00:10 output_file.ts

为例说明ffmpeg如何将命令行参数解析处理。

 

int main(int argc,char**argv)

{

    //初始化参数容器

    OptionsContext o={0};

    //重置参数

    reset_options(&o);

    //解析参数

    parse_options(&o, argc, argv, options,opt_output_file);

}

 

1.重置参数

staticvoid reset_options(OptionsContext*o)

依次进行了以下步骤:

1.1第一步:释放特殊类型

释放所有的 OPT_SPEC(对应struct SpecifierOpt)和 OPT_STRING (对应 char*)类型的 OptionDef

代码如下:

       //指向全局变量options

    const OptionDef*po= options;

    //遍历options

    while(po->name){

       //dest指针指向当前option对应的OptionContext中的位置

        void*dst=(uint8_t*)o+ po->u.off;

       //判断是否是SpecifierOpt类型

        if(po->flags& OPT_SPEC){

          //so指向SpecifierOpt*的首地址

            SpecifierOpt **so= dst;

          //获得数组长度

            int i,*count=(int*)(so+1);

          //循环遍历SpecifierOpt*数组

            for(i=0; i<*count; i++){

             //释放SpecifierOptspecifierchar*类型

               av_freep(&(*so)[i].specifier);

             //如果OPT类型是字符串,释放SpecifierOptu.strchar*类型

               if(po->flags& OPT_STRING)

                   av_freep(&(*so)[i].u.str);

            }

          //释放SpecifierOpt*指针数组

            av_freep(so);

          //重置计数器

            *count=0;

        }

       //判断是否是char*类型

elseif(po->flags& OPT_OFFSET&& po->flags& OPT_STRING)

            av_freep(dst);

        po++;

    }

 

这里需要对OptionContext的内容做一些说明:

 

OptionContext 包含了在视频编转码过程中需要用到的参数,这些参数来自于命令行的输入。

参数在OptionContext中的存储形式有:

#defineOPT_INT   0x0080

#defineOPT_FLOAT 0x0100

#defineOPT_INT64 0x0400

#defineOPT_TIME 0x10000

#defineOPT_DOUBLE 0x20000

等,详情参见 structOptionDef

在上述代码中,主要循环释放的是OPT_SPEC(对应struct SpecifierOpt)和 OPT_STRING

OptionContext中,OPT_SPEC类型是成对出现的,如下:

typedefstructOptionsContext{

    int64_t start_time;

    constchar*format;

    SpecifierOpt *codec_names;

    int       nb_codec_names;

    SpecifierOpt *audio_channels;

    int       nb_audio_channels;

 

即:

    SpecifierOpt *xxx_vars;

    int       nb_xxx_vars;   //nb_读作number_意思是xxx_vars数组的长度

 

然后我们来分析对SpecifierOpt*数组的遍历:

            SpecifierOpt **so= dst;

            int i,*count=(int*)(so+1);

            for(i=0; i<*count; i++){

 

这里可以这么理解:

so  —指向—>   SpecifierOpt *xxx_vars;

so+1—指向—>   int       nb_xxx_vars;

so+1 的含义:so是个SpecifierOpt指针,指针+1则移动了sizeofSpecifierOpt)的位置,即跳到nb_xxx_vars的位置。

1.2释放其他类型

    av_freep(&o->stream_maps);

    av_freep(&o->meta_data_maps);

    av_freep(&o->streamid_map);

 

这里说一下 av_freep 的用法。

void av_freep(void*arg)

{

    void**ptr=(void**)arg;

    av_free(*ptr);

    *ptr=NULL;

}

相比传统的free方法,这里主要多做了一步工作:将释放free之后,指针设置为NULL

同时,要注意到:

Object *obj;

free(obj);

等价用法为:

av_freep(&obj);

 

在ffmpeg中,封装了对应free的方法为:

void av_free(void*ptr)

{

#ifCONFIG_MEMALIGN_HACK

    if(ptr)

        free((char*)ptr-((char*)ptr)[-1]);

#else

    free(ptr);

#endif

}

这里除了考虑内存对齐之外,跟传统的free方法没有任何变化。

1.3第三步:设置初始值

    memset(o,0,sizeof(*o));

 

    o->mux_max_delay =0.7;

    o->recording_time= INT64_MAX;

    o->limit_filesize= UINT64_MAX;

    o->chapters_input_file= INT_MAX;

 

不需要过多解释。

o->mux_max_delay =0.7;

这一行内容以后在视频切片中会用到。可以调整到更小。

 

1.4重新初始化特殊参数

    uninit_opts();

    init_opts();

 

这两行代码对应cmdutils.c 文件中的代码段:

struct SwsContext*sws_opts;

AVDictionary*format_opts,*codec_opts;

 

void init_opts(void)

{

#if CONFIG_SWSCALE

    sws_opts= sws_getContext(16,16,0,16,16,0, SWS_BICUBIC,

                             NULL,NULL,NULL);

#endif

}

 

void uninit_opts(void)

{

#ifCONFIG_SWSCALE

    sws_freeContext(sws_opts);

    sws_opts=NULL;

#endif

    av_dict_free(&format_opts);

    av_dict_free(&codec_opts);

}

主要进行: SwsContext*sws_optsAVDictionary*format_opts,*codec_opts三个全局变量的创建和释放工作。

2.解析命令行参数

void parse_options(void*optctx,int argc,char**argv,const OptionDef *options,void(*parse_arg_function)(void*,constchar*))

 

void*optctx,——OptionContext

int argc,——命令行参数个数

char**argv,——命令行参数列表

const OptionDef*options,——选项列表

void(*parse_arg_function)(void*,constchar*)——自定义的解析方法

2.1总览

    constchar*opt;

    int optindex, handleoptions=1, ret;

    //处理window的情况

    prepare_app_arguments(&argc,&argv);

   

    optindex=1;

    //循环处理命令行参数

    while(optindex< argc){

        opt = argv[optindex++];

       //如果传入的参数是-”打头

        if(handleoptions&& opt[0]=='-'&& opt[1]!='\0'){

          //如果传入的参数是“--”打头

            if(opt[1]=='-'&& opt[2]=='\0'){

                handleoptions =0;

             //略过

               continue;

            }

          //丢弃第一个字符”-”

            opt++;

           //解析命令行参数

          //eg–acodec copy

//对应的 opt argv[optindex] “acodec” “copy”

            if((ret= parse_option(optctx, opt, argv[optindex], options))<0)

               exit_program(1);

            optindex += ret;

        }else{

          //此时 opt的值为输出文件名 test.ts

            if(parse_arg_function)

             //处理输出文件的相关内容,如 struct OutputFile的初始化

               parse_arg_function(optctx, opt);

        }

    }

 

在此,ffmpeg 默认的处理输出文件名参数为:

staticvoid opt_output_file(void*optctx,constchar*filename)

 

2.2处理命令行参数

int parse_option(void*optctx,constchar*opt,constchar*arg,                 const OptionDef*options)

2.2.1查找匹配的Option

    const OptionDef*po;

    int bool_val=1;

    int*dstcount;

    void*dst;

   

    //从全局变量options数组中查找opt对应的OptionDef

    po = find_option(options, opt);

   

    //如果未找到且以”no”打头

    //不需要传递参数的选项是bool类型的选项,默认为true

    //如果需要设置为false,则需要加上”no”,以下的if则是处理这种情况

    if(!po->name&& opt[0]=='n'&& opt[1]=='o'){

       //去掉开头的”no”重新查找

        po = find_option(options, opt +2);

       //如果仍未找到或者找到的选项不是bool类型

        if(!(po->name&&(po->flags& OPT_BOOL)))

          //报错

            goto unknown_opt;

        bool_val =0;

    }

    //如果未找到且不是以上的”no”打头情况

    if(!po->name)

       //寻找默认配置进行处理

        po = find_option(options,"default");

    //default配置也未找到,报错

    if(!po->name){

unknown_opt:

        av_log(NULL, AV_LOG_ERROR,"Unrecognizedoption '%s'\n", opt);

        return AVERROR(EINVAL);

    }

    //如果选项必须有参数但是没有可用的参数,报错

    if(po->flags& HAS_ARG&&!arg){

        av_log(NULL, AV_LOG_ERROR,"Missingargument for option '%s'\n", opt);

        return AVERROR(EINVAL);

    }

 

现在来查看一下find_option方法的实现:

staticconst OptionDef*find_option(const OptionDef*po,constchar*name)

根据name在全局变量options数组中查找OptionDef

 

    //这里先处理参数带有冒号的情况。比如 codec:a codec:v

    constchar*p= strchr(name,':'); 

    int len= p? p- name: strlen(name);

    //遍历options

    while(po->name!=NULL){

       //比较option的名称与name是否相符。

       //这里 codec codec:a相匹配

        if(!strncmp(name, po->name, len)&& strlen(po->name)== len)

            break;

        po++;

    }

    return po;

2.2.2寻找选项地址

以下的代码用于将 void*dst变量赋值。让dst指向需要赋值的选项地址。

    //如果选项在OptionContext中是以偏移量定位或者是 SpecifierOpt*数组的类型

    dst= po->flags&(OPT_OFFSET| OPT_SPEC)?

       //dst指向从 optctx地址偏移u.off的位置

(uint8_t*)optctx+ po->u.off:

//否则直接指向 OptionDef结构中定义的位置

po->u.dst_ptr;

 

    //如果选项是SpecifierOpt*数组

    if(po->flags& OPT_SPEC){

       //数组首地址

        SpecifierOpt **so= dst;

        char*p= strchr(opt,':');

       //这里是取得数组的当前长度+1

       //请回顾 1.1中的描述:

    //SpecifierOpt *xxx;

//int nb_xxx;

//so指向xxx时刻,so+1指向nb_xxx

        dstcount =(int*)(so+1);

       //动态增长数组

        *so = grow_array(*so,sizeof(**so), dstcount,*dstcount+1);

       //将创建的SpecifierOpt结构体中的specifier赋值

       //codec:v specifier值为 “v”

        (*so)[*dstcount-1].specifier= av_strdup(p? p+1:"");

       //dst指针指向数组新增的SpecifierOpt中的 u地址

       //此时dstcount的值已经变作新数组的长度,亦即原数组长度+1

        dst =&(*so)[*dstcount-1].u;

    }

    //日志输出

    av_log(NULL, AV_LOG_ERROR,"parse_option->'%s' '%s' %d %d %d\n", opt, arg,

        po->flags& OPT_SPEC,

        po->flags& OPT_STRING,

        (po->u.func_arg?1:0)

        );

 

在此做出一些说明:

    dst= po->flags&(OPT_OFFSET| OPT_SPEC)?

       //dst指向从 optctx地址偏移u.off的位置

(uint8_t*)optctx+ po->u.off:

//否则直接指向 OptionDef结构中定义的位置

po->u.dst_ptr;

 

关于po->u.dst_ptr的指向,在ffmpeg中都是用来设置全局变量使用。如以下代码:

staticint exit_on_error=0;

staticconst OptionDef options[]={

    {"xerror", OPT_BOOL,{(void*)&exit_on_error},"exit on error","error"},

};

也就是:

po->u.dst_ptr== ((void*)&exit_on_error)

所以之后的对po->u.dst_ptr赋值也就是对avconv.c中定义的全局变量赋值。

 

关于*so= grow_array(*so,sizeof(**so), dstcount,*dstcount+1);

用户数组动态增长方法签名如下:

void*grow_array(void*array,int elem_size,int*size,int new_size);

其内在处理逻辑如下:

        uint8_t *tmp= av_realloc(array, new_size*elem_size);

        memset(tmp+*size*elem_size,0,(new_size-*size)* elem_size);

        *size = new_size;

        return tmp;

需要注意到的是int elem_size在当前的上下文中指的是sizeof(**so)==sizeof(structSpecifierOpt)

2.2.3选项赋值

       在获得需要赋值的变量地址void *dst之后,接下来的代码流程用于赋值处理,主要是根据变量类型进行赋值:

       //如果是字符型

    if(po->flags& OPT_STRING){

        char*str;

        str = av_strdup(arg);

        *(char**)dst= str;

    //bool

    }elseif(po->flags& OPT_BOOL){

        *(int*)dst= bool_val;

    //整型

    }elseif(po->flags& OPT_INT){

        *(int*)dst= parse_number_or_die(opt, arg, OPT_INT64, INT_MIN, INT_MAX);

    //长整型

    }elseif(po->flags& OPT_INT64){

        *(int64_t*)dst= parse_number_or_die(opt, arg, OPT_INT64, INT64_MIN, INT64_MAX);

    //时间型

    }elseif(po->flags& OPT_TIME){

        *(int64_t*)dst= parse_time_or_die(opt, arg,1);

    //浮点型

    }elseif(po->flags& OPT_FLOAT){

        *(float*)dst= parse_number_or_die(opt, arg, OPT_FLOAT,-INFINITY, INFINITY);

    //双精度浮点型

    }elseif(po->flags& OPT_DOUBLE){

        *(double*)dst= parse_number_or_die(opt, arg, OPT_DOUBLE,-INFINITY, INFINITY);

    //方法指针

    }elseif(po->u.func_arg){

       //调用方法

        int ret = po->flags& OPT_FUNC2? po->u.func2_arg(optctx, opt, arg)

                                       : po->u.func_arg(opt, arg);

        if(ret<0){

            av_log(NULL, AV_LOG_ERROR,"Failed to set value '%s' for option '%s'\n", arg, opt);

            return ret;

        }

    }

    if(po->flags& OPT_EXIT)

        exit_program(0);

    return!!(po->flags& HAS_ARG);

}

 

最后对if(po->u.func_arg)的方法调用再次说明:

acodec选项定义:

{"acodec", HAS_ARG| OPT_AUDIO| OPT_FUNC2,{(void*)opt_audio_codec},"force audio codec ('copy' to copy stream)","codec"},

我们可以看到,在全局变量options中注册的解析方法为:opt_audio_codec

 

3实例分析

现在回到文档开头提到的

avconv -i input_file.mp4 -vcodeccopy -acodeccopy -vbsfh264_mp4toannexbss00:00:00t00:00:10 output_file.ts

的解析上来。下面,将以比较特殊的 -acodec copy 说明。

 

首先,在全局变量options中定义了acodec选项的相关信息:

{"acodec", HAS_ARG| OPT_AUDIO| OPT_FUNC2,{(void*)opt_audio_codec},"force audio codec ('copy' to copy stream)","codec"},

 

可以看到:此选项:

1.      有参数需要传入

2.      处理的是音频数据

3.      解析方式是自定义方法

4.      解析方法为: opt_audio_codec

5.      其功能是:"forceaudio codec ('copy' to copy stream)"

6.      其对应的命令行名称为codec

 

因此,在parse_option的调用中,对于acodec选项,将用opt_audio_codec解析方法进行处理。

opt_audio_codec(optctx,“acodec”,“copy”)

 

方法代码如下:

staticint opt_audio_codec(OptionsContext*o,constchar*opt,constchar*arg)

{

    return parse_option(o,"codec:a", arg, options);

}

 

可以看到,在这里,没有做更多的工作,只是对命令行选项acodec进行了一个转换,使用"codec:a"的解析器进行重新解析:

opt_audio_codec(optctx,“codec:a”,“copy”)

 

这里需要回顾一下方法

staticconst OptionDef*find_option(const OptionDef*po,constchar*name)

此方法是在查找name为“codec:a” 的option 时,实际是寻找的 “codec”

 

{"codec", HAS_ARG| OPT_STRING| OPT_SPEC,{.off= OFFSET(codec_names)},"codec name","codec"},

 

可以看到:此选项:

1.      有参数需要传入

2.      处理的是OPT_SPEC类型的数组(SpecifierOpt*

3.      SpecifierOpt 结构体存储的是OPT_STRINGchar *

4.      赋值方式是直接赋值,偏移位是:{.off= OFFSET(codec_names)},亦即:

typedefstruct OptionsContext{

    /* input/output options */

   int64_t start_time;

    constchar*format;

 

    SpecifierOpt*codec_names;   ß----------------此行位置

    int       nb_codec_names;

 

5.       其功能是"codec name"

6.      其对应的命令行名称为codec

 

因此,在调用

parse_option(o,"codec:a","copy",options)

之后,获得的结果是:

 

typedefstruct SpecifierOpt{

    //值为”a”

    char*specifier;

    //值为”copy”

    union{

        uint8_t *str;

        int       i;

        int64_t i64;

        float      f;

        double   dbl;

    } u;

} SpecifierOpt;

 

而在OptionsContext中,

typedefstruct OptionsContext{

    /* input/output options */

    int64_t start_time;

    constchar*format;

 

    SpecifierOpt*codec_names;  ß----------------增加一个数组元素

    int       nb_codec_names;ß----------------计数器+1

 

4总结

通过本篇的分析,基本可以明了ffmpeg在输入参数的解析流程。这对我们之后想要把命令行

avconv -i input_file.mp4 -vcodeccopy -acodeccopy -vbsfh264_mp4toannexbss00:00:00t00:00:10 output_file.ts

转换为可用的内嵌代码提供了一个很好的入口和分析点。

在之后的章节中,我们会从此全面进入avconv.c 的世界。

 

同时,需要指出的是,本章节没有描述两个重要的解析部分:

staticconst OptionDef options[]={

    {"i", HAS_ARG| OPT_FUNC2,{(void*)opt_input_file},"input file name","filename"},

以及

parse_options(&o, argc, argv, options,opt_output_file);

 

这里涉及到的两个解析方法为:

//输入文件的分析处理

staticint opt_input_file(OptionsContext*o,constchar*opt,constchar*filename)

//输出文件的分析处理

staticvoid opt_output_file(void*optctx,constchar*filename)

 

这两个方法除了进行Option设置之外,还对输入输出的对应结构和变量进行了初始化,其功能和重要性已经超出了简单的命令行解析的范围,因此,将在后继章节中分析。

转自:http://blog.csdn.net/deltatang/article/details/7931827