您的位置:首页 > 理论基础 > 计算机网络

如何用nginx+ffmpeg实现苹果HLS协议 (http living stream)

2012-09-01 12:16 621 查看
什么是?
[b]HLS (HTTP Live Streaming)  Apple的动态码率自适应技术。主要用于PC和Apple终端的音视频服务。包括一个m3u(8)的索引文件,TS媒体分片文件和key加密串文件。

[/b]
FFmpeg是一个开源免费跨平台的视音频方案,采用LGPL或GPL许可证(依据你选择的组件)。它提供了录制、转换以及流化音视频的完整解决方案。
Nginx ("engine x") 是一个高性能的 HTTP 和 反向代理服务器,也是一个 IMAP/POP3/SMTP代理服务器。

————————————————————————————————————————————————————————————————

本文档所描述的是 基于ffmpeg 开发一个nginx模块,使其具有支持HLS协议的能力。
同时,视频源文件为mp4格式,不能做命令行形式(手工或者shell)的提前转化ts或者切片
据我所知,目前网上的开源项目中没有实现此功能的代码开发。基本上都是需要预处理视频文件,将其转化成ts之后,再进行虚拟切片的。
此文档对nginx模块开发、ffmepg的使用(不要求理解到api层面)以及HLS协议的理解有一定要求。开发语言当然是C。

————————————————————————————————————————————————————————————————

今年用做了一个支持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:10output_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:10output_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(对应structSpecifierOpt)和 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(对应structSpecifierOpt)和 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_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_mp4toannexb –ss00:00:00 –t00:00:10output_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:10output_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设置之外,还对输入输出的对应结构和变量进行了初始化,其功能和重要性已经超出了简单的命令行解析的范围,因此,将在后继章节中分析。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: