如何用nginx+ffmpeg實現蘋果HLS協議
今年用三個月時間做了一個支持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_mp4toannexb –ss00:00:00 –t00:00:10 output_file.ts
然后通過 對 參數 –ss00:00:00 –t00: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_mp4toannexb –ss00:00:00 –t00: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++){
//釋放SpecifierOpt的specifier(char*類型)
av_freep(&(*so)[i].specifier);
//如果OPT類型是字符串,釋放SpecifierOpt的u.str(char*類型)
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則移動了sizeof(SpecifierOpt)的位置,即跳到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_opts,AVDictionary*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_mp4toannexb –ss00:00:00 –t00: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_STRING(char *)
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_mp4toannexb –ss00:00:00 –t00: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