如何用nginx+ffmpeg實現蘋果HLS協議

openkk 12年前發布 | 115K 次閱讀 Nginx 多媒體處理

今年用三個月時間做了一個支持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

 本文由用戶 openkk 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!