vlc 是通过模块来扩展它的功能,插件一般就是实现一个模块。
vlc的模块有很多类型:
Access提供输入功能,比如HTTP输入、文件输入
Demux提供解封装功能,比如Asf、Mp4、Ts的解封装
Access_Demux当然是Access、Demux两者的组合功能
其他的与我们不太相关,就不废话了,那个Interface是界面插件。
对应到我们的库提供的功能,应该既有输入,又有解封装,vlc正好提供了Access_Demux这种类型,说明vlc已经遇到或者考虑过这种情形了,查看vlc的源码,发现在access目录有不少模块是access_demux类型的,其中我听说过的格式有rtp、dshow。
下面开始实现我们的vlc插件,没必要重头开始写,拷贝一个现有的插件代码修改修改就行了,比如我拷贝demux/asf/asf.c,其他的也行,我只是选了一个我比较熟悉的格式。
static int Open ( vlc_object_t * ); static void Close ( vlc_object_t * ); vlc_module_begin () set_category( CAT_INPUT ) set_subcategory( SUBCAT_INPUT_DEMUX ) set_description( "Mylibdemuxer" ) set_capability( "access_demux", 200 ) set_callbacks( Open, Close ) add_shortcut("vod") vlc_module_end ()
vlc注册模块用的是一套标准模板,从asf.c拷贝过来,改变一下字符串描述,类型改成"access_demux",其他没什么要修改的。需要说明的是set_capability的200参数表示该插件的优先级,越大越可能被使用,如果是0的话就不会自动使用,只能手动强制使用。注意set_callbacks( Open, Close ),这个很关键,其实是告诉vlc开始播放的时候调用Open,结束播放的时候调用Close。
typedef struct { int i_cat; es_out_id_t *p_es; Mylib_StreamInfoEx *p_si; mtime_t i_time; } mylib_track_t; struct demux_sys_t { mtime_t i_time; /* s */ mtime_t i_length; /* length of file file */ int64_t i_bitrate; /* global file bitrate */ bool b_seek; unsigned int i_track; mylib_track_t *track[128]; /* track number is stored on 7 bits */ };
定义自己的结构体是用来保存一些自己的私有数据。我这里定义了两个结构体mylib_track_t和demux_sys_t。
mylib_track_t对应一个track,在demux之后,音视频已经分离,音频、视频都是一个track。这里的es_out_id_t可以看做是音视频数据处理的下一个模块,所以每个track都会有一个es_out_id_t *,我们自己的音视频流信息保存在Mylib_StreamInfoEx,这个是我们链接库中定义的结构体(都是Mylib开头的);另外还有一个时间戳mtime_t,保存已经demux的最后一帧时间。
vlc里面的时间戳好像都是微妙单位,用64为整形表示。
demux_sys_t保存全局信息,比如影片总时长i_length,当前位置i_time,能否拖动b_seek,另外还有一个track数组。i_bitrate没有用到。这里全局当前位置其实是所有track的当前位置的最小值,当前位置在显示进度条位置时候用到。
static int Open( vlc_object_t * p_this ) { demux_t *p_demux = (demux_t *)p_this; demux_sys_t *p_sys; my_int32 ret; char playlink[1024] = {0}; strcat(playlink, p_demux->psz_access); strcat(playlink, "://"); #if PACKAGE_VERSION_MAJOR == 1 strcat(playlink, p_demux->psz_path); #elif PACKAGE_VERSION_MAJOR == 2 strcat(playlink, p_demux->psz_location); #endif msg_Dbg(p_demux, "Mylib Open %s", playlink); ret = Mylib_Open(playlink); if (ret != mylib_success) { Mylib_Close()(); return VLC_EGENERIC; } /* Set p_demux fields */ p_demux->pf_demux = Demux; p_demux->pf_control = Control; p_demux->p_sys = p_sys = (demux_sys_t *)calloc(1, sizeof(demux_sys_t)); /* Load the headers */ if( DemuxInit( p_demux ) ) { free( p_sys ); Mylib_Close()(); return VLC_EGENERIC; } return VLC_SUCCESS; } static void Close( vlc_object_t * p_this ) { demux_t *p_demux = (demux_t *)p_this; DemuxEnd( p_demux ); Mylib_Close(); free( p_demux->p_sys ); }
Open里面主要的工作就是设置另外几个回调函数以及绑定上面定义的私有数据结构。vlc中对应一次播放,所需要的信息都是存放在一个大结构体中,其中access_demux模块需要的信息在demux_t中,设置回调函数、绑定私有数据都是直接修改demux_t的成员:pf_demux、pf_control和p_sys。pf_demux在驱动处理下一个sample时调用,pf_control在做一些播放控制时调用,如拖动、暂停,也用于获取信息:如能否拖动、当前位置、总时长。
demux类型模块的Open还需要向vlc核心注册音视频流(track),这个我们放在我们的另一个函数DemuxInit里面。
Close比较简单,主要工作是释放自己的私有数据。
static int DemuxInit( demux_t *p_demux ) { demux_sys_t *p_sys = p_demux->p_sys; /* init context */ p_sys->i_time = -1; p_sys->i_length = 0; p_sys->i_bitrate = 0; for( int i = 0; i < 128; i++ ) { p_sys->track[i] = NULL; } p_sys->i_length = (mtime_t)Mylib_GetDuration()() * 1000; p_sys->b_seek = p_sys->i_length > 0; p_sys->i_track = Mylib_GetStreamCount(); if( p_sys->i_track <= 0 ) { msg_Warn( p_demux, "Mylib plugin discarded (cannot find any stream!)" ); return VLC_EGENERIC; } msg_Dbg( p_demux, "found %d streams", p_sys->i_track ); for( unsigned i_stream = 0; i_stream < p_sys->i_track; i_stream++ ) { mylib_track_t *tk; tk = p_sys->track[i_stream] = (mylib_track_t *)malloc(sizeof(mylib_track_t)); memset(tk, 0, sizeof(mylib_track_t)); tk->i_time = -1; tk->p_es = NULL; Mylib_StreamInfoEx * p_si = (Mylib_StreamInfoEx *)malloc(sizeof(Mylib_StreamInfoEx)); Mylib_GetStreamInfoEx(i_stream, p_si); tk->p_si = p_si; es_format_t fmt; if (p_si->type == mylib_audio) { es_format_Init(&fmt, AUDIO_ES, 0); fmt.i_codec = VLC_FOURCC( 'm', 'p', '4', 'a' ); fmt.audio.i_channels = p_si->audio_format.channel_count; fmt.audio.i_rate = p_si->audio_format.sample_rate; fmt.audio.i_bitspersample = p_si->audio_format.sample_size; fmt.i_extra = p_si->format_size; fmt.p_extra = malloc( fmt.i_extra ); memcpy( fmt.p_extra, p_si->format_buffer, fmt.i_extra ); msg_Dbg(p_demux, "added new audio stream(codec:0x%x,ID:%d)", fmt.i_codec, i_stream); } else if (p_si->type == mylib_video) { es_format_Init( &fmt, VIDEO_ES, 0); fmt.i_codec = VLC_FOURCC( 'a', 'v', 'c', '1' ); fmt.video.i_width = p_si->video_format.width; fmt.video.i_height = p_si->video_format.height; fmt.video.i_frame_rate = 10000000; fmt.video.i_frame_rate_base = 1; fmt.i_extra = p_si->format_size; fmt.p_extra = malloc( fmt.i_extra ); memcpy( fmt.p_extra, p_si->format_buffer, fmt.i_extra ); msg_Dbg(p_demux, "added new video stream(ID:%d)", i_stream); } else { es_format_Init( &fmt, UNKNOWN_ES, 0 ); } tk->i_cat = fmt.i_cat; if( fmt.i_cat != UNKNOWN_ES ) { tk->p_es = es_out_Add( p_demux->out, &fmt ); } else { msg_Dbg(p_demux, "ignoring unknown stream(ID:%d)", i_stream); } es_format_Clean( &fmt ); } return VLC_SUCCESS; }
注册音视频流时,先填充es_format_t 结构,然后调用vlc的函数es_out_Add完成注册。
es_format_t 结构中,i_codec就是那个四个字节的标准定义值,如AVC1、H264,MP4V等等,i_extra、p_extra是音视频的配置数据,像AVC1就是AvcConfigRecord结构。
还有几个字段是音频和视频不同的,音频有采样率、位宽、声道数,视频有宽、高、帧率,其实还有很多字段,不填也无所谓,vlc会自动处理的。
static int Demux( demux_t *p_demux ) { demux_sys_t *p_sys = p_demux->p_sys; for( ;; ) { my_int32 ret; if( !vlc_object_alive (p_demux) ) break; Mylib_SampleEx2 sample; ret = Mylib_ReadSampleEx2(&sample); /* Read and demux a packet */ if (ret == mylib_success) { //msg_Dbg( p_demux, "Mylib_ReadSampleEx2 stream_index: %lu, start_time: %llu", // sample.stream_index, sample.start_time); if (p_sys->i_time < 0) es_out_Control(p_demux->out, ES_OUT_SET_PCR, sample.start_time + 1); mylib_track_t * tk = p_sys->track[sample.stream_index]; block_t * p_block = block_Alloc(sample.buffer_length); p_block->i_pts = sample.start_time + sample.composite_time_delta; p_block->i_dts = sample.start_time; memcpy(p_block->p_buffer, sample.buffer, sample.buffer_length); tk->i_time = sample.start_time; p_sys->i_time = GetMoviePTS(p_sys); es_out_Send(p_demux->out, tk->p_es, p_block); es_out_Control(p_demux->out, ES_OUT_SET_PCR, sample.start_time + 1); return 1; } else if (ret == mylib_would_block) { msg_Dbg(p_demux, "Mylib_ReadSampleEx2 mylib_would_block"); usleep(10000); } else if (ret == mylib_stream_end) { msg_Dbg(p_demux, "Mylib_ReadSampleEx2 mylib_stream_end"); return 0; } else { msg_Dbg(p_demux, "Mylib_ReadSampleEx2 %s", Mylib_GetLastErrorMsg()()); return -1; } } return 1; }
这里使用了循环,但不是为了一直解出所有的帧,而是为了处理网络延时。按vlc的定义这个回调函数只需要解出一个帧就可以了,当然多个也可以,但是这个函数最好不要长时间阻塞调用者。但是vlc又规定必须返回成功或者失败,不存在暂时无数据的错误码,对于网络卡的时候不好处理,暂时只能阻塞了。
Demux解出的帧不需要返回给调用者,但需要push给后续处理模块。还记得前面在私有数据里面保存的es_out_id_t 吧,对了,用es_out_Send函数push给它就可以了。注意每个track有各自的es_out_id_t。
除此之外,Demux还有一个比较重要的事情要做,就是更新vlc的pts(其实就是当前时间),pts通过取各个track的当前时间的最小值获得,实现如下:
static mtime_t GetMoviePTS(demux_sys_t *p_sys) { mtime_t i_time = -1; int i; for( i = 0; i < 128 ; i++ ) { mylib_track_t *tk = p_sys->track[i]; if( tk && tk->p_es && tk->i_time > 0) { if( i_time < 0 ) i_time = tk->i_time; else i_time = __MIN( i_time, tk->i_time ); } } return i_time; }
pts计算出来后,通过es_out_Control更新pts。
static int Control( demux_t *p_demux, int i_query, va_list args ) { demux_sys_t *p_sys = p_demux->p_sys; bool *pb_bool; int64_t i64, *pi64; double f, *pf; switch (i_query) { case DEMUX_CAN_SEEK: pb_bool = (bool*)va_arg(args, bool*); *pb_bool = p_sys->b_seek; return VLC_SUCCESS; case DEMUX_CAN_CONTROL_PACE: pb_bool = (bool*)va_arg(args, bool*); *pb_bool = false; return VLC_SUCCESS; case DEMUX_GET_LENGTH: pi64 = (int64_t*)va_arg(args, int64_t *); *pi64 = p_sys->i_length; return VLC_SUCCESS; case DEMUX_GET_TIME: if (p_sys->i_time < 0) return VLC_EGENERIC; pi64 = (int64_t*)va_arg(args, int64_t *); *pi64 = p_sys->i_time; return VLC_SUCCESS; case DEMUX_SET_TIME: SeekPrepare(p_demux); { va_list acpy; va_copy(acpy, args); i64 = (int64_t)va_arg(acpy, int64_t); va_end(acpy); if( !SeekIndex( p_demux, i64, -1 ) ) return VLC_SUCCESS; } return VLC_EGENERIC; case DEMUX_GET_POSITION: if (p_sys->i_time < 0) return VLC_EGENERIC; if (p_sys->i_length > 0) { pf = (double*)va_arg(args, double *); *pf = p_sys->i_time / (double)p_sys->i_length; return VLC_SUCCESS; } return VLC_EGENERIC; case DEMUX_SET_POSITION: SeekPrepare(p_demux); if (p_sys->i_length > 0) { va_list acpy; va_copy(acpy, args); f = (double)va_arg(acpy, double); va_end(acpy); if (!SeekIndex(p_demux, -1, f)) return VLC_SUCCESS; } return VLC_EGENERIC; default: return VLC_EGENERIC; } }
Control函数内部就是一个大的switch语句,根据控制命令类型i_query,解释控制参数args。
access_demux模块需要处理的控制命令,主要有以下几个:
DEMUX_CAN_SEEK 询问能否seek
DEMUX_CAN_CONTROL_PACE 询问能否速率控制
DEMUX_GET_LENGTH 获取总时长
DEMUX_GET_TIME 获取当前时间位置
DEMUX_GET_POSITION 获取当前位置(百分比)
DEMUX_SET_TIME 设置当前时间位置,相当于拖动到某个时间点
DEMUX_SET_POSITION 设置当前位置(百分比),相当于拖动到某个百分比
这里position可以通过总时长和当前时间算出来的,但是POSITION相关的两个命令还必须实现,否则进度条不会动,拖动进度条也没有效果。
拖动时用了两个自己的函数,我只列出来,不详细说明了。
static int SeekIndex( demux_t *p_demux, mtime_t i_date, float f_pos ) { demux_sys_t *p_sys = p_demux->p_sys; my_int32 ret; msg_Dbg( p_demux, "seek with index: %i seconds, position %f", i_date >= 0 ? (int)(i_date/1000000) : -1, f_pos ); if( i_date < 0 ) i_date = p_sys->i_length * f_pos; ret = mylib_Seek()(i_date / 1000); return (ret == mylib_success || ret == mylib_would_block) ? VLC_SUCCESS : VLC_EGENERIC; } static void SeekPrepare( demux_t *p_demux ) { demux_sys_t *p_sys = p_demux->p_sys; p_sys->i_time = -1; for( int i = 0; i < 128 ; i++ ) { mylib_track_t *tk = p_sys->track[i]; if( !tk ) continue; tk->i_time = 1; } }
static void DemuxEnd( demux_t *p_demux ) { demux_sys_t *p_sys = p_demux->p_sys; int i; for( i = 0; i < 128; i++ ) { mylib_track_t *tk = p_sys->track[i]; if( tk ) { if( tk->p_es ) { es_out_Del( p_demux->out, tk->p_es ); } free( tk ); } p_sys->track[i] = NULL; } }
在DemuxInit中,通过es_out_Add注册了track,在DemuxEnd中,通过es_out_Del反注册,DemuxEnd还删除了私有的track修改数据。
vlc使用autotools管理makefile,我对autotools不是很熟悉,这部分看似简单的工作反而花了比较长的时间。一开始自己搞了一个简单的makefile编译,链接都没问题,可是一运行就crash,后来只好把自己的代码工程加到vlc的configure体系。为了方便像我一样不熟悉autotools体系的程序员,我这里加了一节说一下加入自己的插件工程需要的步骤:
1、在VLC_ROOT/module/access/下创建自己的目录
mkdir module/access/mylib
(按照vlc的开发说明,简单的插件(只有一个c文件)不创建目录也可以,但是方式与我下面的不一样,我觉得尽量独立比较好,还是创建自己的一个目录)
2、把实现文件放到该目录
3、在该目录下增加一个Module.am文件,方法还是照抄别人的代码,内容如下:
SOURCES_mylib = \ mylib.c \ $(NULL) libmylib_plugin_la_CFLAGS = $(AM_CFLAGS) \ -fno-strict-aliasing libmylib_plugin_la_LIBADD = $(AM_LIBADD) libmylib_plugin_la_DEPENDENCIES = libvlc_LTLIBRARIES += libmylib_plugin.la
4、修改VLC_ROOT/module/access/Module.am,在BASE_SUBDIRS最后加上自己的目录
BASE_SUBDIRS = dvb mms cdda rtp rtsp vcd vcdx screen bd zip mylib
5、修改VLC_ROOT/configure.ac,在AC_CONFIG_FILES里面增加自己的一行
AC_CONFIG_FILES([
modules/access/mylib/Makefile
extras/package/win32/vlc.win32.nsi
......
就这样,然后运行下列命令编译、安装
./bootstrap
./configure
make
make install