VLC源码分析总结
1. 概述
VLC属于Video LAN开源项目组织中的一款全开源的流媒体服务器和多媒体播放器。作为流媒体服务器,VLC跨平台,支持多操作系统和计算机体系结构;作为多媒体播放器,VLC可以播放多种格式的媒体文件。主要包括有:WMV、ASF、MPG、MP、AVI、H.264等多种常见媒体格式。
VLC采用全模块化结构,在系统内部,通过动态的载入所需的模块,放入一个module_bank的结构体中统一管理,连VLC的Main模块也是通过插件的方式动态载入的(通过module_InitBank函数在初始化建立module_bank时)。对于不支持动态载入插件的系统环境中,VLC也可以采用builtin的方式,在VLC启动的时候静态载入所需要的插件,并放入module_bank统一管理。
VLC的模块分成很多类别主要有:access、access_filter、access_output、audio_filter、audio_mixer、audio_output、codec、control、demux、gui、misc、mux、packetizer、stream_output、video_filter、video_output、interface、input、playlist等(其中黑体为核心模块)。VLC无论是作为流媒体服务器还是多媒体播放器,它的实质思路就是一个“播放器”,之所以这么形象描述,是因为(The core gives a framework to do the media processing, from input (files, network streams) to output (audio or video, on ascreen or a network), going through various muxers, demuxers, decoders and filters. Even the interfaces are plugins for LibVLC. It is up to the developer to choose which module will be loaded. 摘于官网说明)它实质处理的是ES、PES、PS、TS等流间的转换、传输与显示。对于流媒体服务器,如果从文件作为输入即:PS->DEMUX->ES->MUX->TS;对于多媒体播放器如果采用UDP方式传输即:TS->DEMUX->ES。
2. 插件管理框架
在VLC中每种类型的模块中都有一个抽象层/结构体,在抽象层或结构体中定义了若干操作的函数指针,通过这些函数指针就能实现模块的动态载入,赋值相关的函数指针的函数地址,最后通过调用函数指针能调用实际模块的操作。
对于VLC所有的模块中,有且仅有一个导出函数:vlc_entry__(MODULE_NAME)。(其中MODULE_NAME为宏定义,对于main模块,在\include\modules_inner.h中定义为main)动态载入模块的过程是:使用module_Need函数,在module_bank中根据各个插件的capability等相关属性,寻找第一个能满足要求并激活的模块。所谓激活是指,调用插件的初始化函数成功。对于各个插件的初始化函数和析构函数均在vlc_entry__(MODULE_NAME)函数中指定了相关函数地址。因此载入各个插件(动态库)的过程,就成为了解析动态库文件,并找到其中vlc_entry__函数的地址,然后运行。这样各个模块的激活函数就会赋值各个操作的函数地址,以待后面函数动态调用。
具体函数调用过程如下:
l Main模块的载入过程:
int main( int i_argc, char *ppsz_argv[] )(src\vlc.c)->i_ret = VLC_Init( 0, i_argc, ppsz_argv )->module_InitBank( p_vlc )(src\libvlc.c void __module_InitBank( vlc_object_t *p_this ))-> module_LoadMain( p_this )(src\misc\modules.c)->AllocateBuiltinModule( p_this, vlc_entry__main )->pf_entry( p_module )(激活了main模块,以上为main模块的载入过程,对于main模块调用的实际函数为导出函数vlc_entry__main,其它模块导出的均为vlc_entry__0_8_6)
l Module_Need函数实现载入任意模块的过程:
module_t * __module_Need( vlc_object_t *p_this, const char *psz_capability,
const char *psz_name, vlc_bool_t b_strict )(src\misc\modules.c)-> vlc_list_find(将所有已经载入的模块查询出来)->然后循环,根据capability查找第一个最合适的module->AllocatePlugin(动态载入所需要的插件,该函数会在动态库所在目录,遍历所有动态库文件,)->p_module->pf_activate(调用激活函数)
l VLC_Init函数流程:
module_InitBank->module_LoadBuiltins(载入静态插件)->module_LoadPlugins(载入动态插件->VLC_AddIntf(添加interface插件,VLC会静态载入hotkeys模块)
在VLC中根据处理任务不同,会静态载入不同的模块,main、memcpy、hotkeys等;动态载入的模块根据处理任务不同,差异很大。
3. VLC流媒体服务器体系结构
以下主要讨论VLC作为流媒体服务器时的体系结构。针对一个节目单文件,调试其运行过程,并最后给出总结。
该实例的播放节目单为如下:
New br broadcast enabled
Setup br input /mnt/hgfs/movie/caiyan.mpg
Setup br output #standard{mux=ts,access=udp,url=234.0.1.4,sap,name=ch1}
在例子中,通过VLC提供API:libvlc_new,libvlc_vlm_new,libvlc_vlm_play_media,libvlc_vlm_load_file等(有些API是自己添加的)可以完成对广播节目br的播放。
下面让我们仔细看看通过这几个接口,VLC内部到底是怎么工作完成了流媒体发布的。
1. 首先程序调用libvlc_new(\src\control\core.c)接口,实现创建一个VLC运行实例libvlc_instance_t,该实例在程序运行过程中唯一。
2. 在libvlc_new接口中,调用了VLC_Init函数实现具体的初始化工作。
3. VLC_Init(\src\libvlc.c)函数中,首先通过system_Init函数完成传入参数对系统的相关初始化,接着通过module_InitBank(\src\misc\modules.c)函数初始化module_bank结构体,并创建了main模块,然后通过module_LoadBuiltins载入静态模块,通过module_LoadPlugins(\src\misc\modules.c)函数载入动态模块,通过module_Need(\src\misc\modules.c)函数载入并激活memcpy模块,通过playlist_Create(\src\playlist\playlist.c)函数,创建了一个playlist播放管理的线程,其线程处理函数为RunThread(\src\playlist\playlist.c),通过VLC_AddIntf(\src\libvlc.c)函数添加并激活hotkeys模块,最后根据系统设置定义了宏HAVE_X11_XLIB_H,因此还需要添加screensaver模块。
4. 总结:此时加载的模块有main,hotkeys,screensaver,memcpy;多创建了一个线程,用于管理playlist,该线程无限循环,直到p_playlist->b_die状态为止。
5. 其次程序中调用libvlc_vlm_new接口,创建VLM对象(该接口为自己添加的)。
6. 该接口调用的是vlm_New(\src\misc\vlm.c)函数,实现VLM对象的创建,函数返回值是指向vlm_t的指针。
7. Vlm_new函数中,创建了一个vlm管理线程,线程处理函数为Manage(\src\misc\vlm.c)。该函数循环处理当前各种媒体(vod、broadcast、schedule)的播放实例,控制其每个播放细节(如:从一个input切换到下一个input;schedule周期循环调度等)。与playlist线程不同的是,Manage主要针对播放实例的操作,而RunThread主要针对播放列表的管理,也就是说VLC管理是分级的,播放列表级和播放列表中媒体播放实例级。
8. 其次程序调用libvlc_vlm_load_file接口,载入播放节目单(该接口也为自己添加,播放节目单如上所述)。
9. 该接口调用的是vlm_Load(\src\misc\vlm.c)函数,在该函数中,依次调用如下函数:stream_UrlNew、stream_Seek、stream_Read、Load,以下详细介绍各个函数作用。
a) 首先是stream_UrlNew(\src\input\stream.c)函数。先调MRLSplit(\src\input\input.c)函数完成对access、demux和path的解析。具体对于本例解析的结果为:access="",demux="",path="aa"。然后调用access2_New(\src\input\access.c)函数创建一个access_t结构体并初始化。具体运行时载入模块的相关参数是:capability="access2",name="access_file",psz_filename=access/libaccess_file_plugin.so。最后调用stream_AccessNew(\src\input\stream.c)函数,创建stream_t结构体对象,并初始化对象中所有函数指针;
b) 再调用stream_Seek(\include\vlc_stream.h)内联函数,设置起始位置;
c) 再调用stream_Size(\include\vlc_stream.h)获得大小;
d) 再调用stream_Read(\include\vlc_stream.h),读取到缓冲区;
e) 最后调用Load(\src\misc\vlm.c),完成实际的载入节目单。对于节目单文件,是一行行解析,并调用ExecuteCommand(\src\misc\vlm.c)完成解析的。Load函数的调用仅仅是设置了相关参数,如:设置input字符串值,设置output字符串值,设置mux的值及与播放相关的enabled、loop等参数。Load工作仅仅是为了下一步发布流做准备的。
10. 程序中调用libvlc_vlm_play_media接口,将节目流发布出去。(自己添加接口)
11. 在libvlc_vlm_play_media接口中,实质是创建了命令“control br play”再调用vlm_ExecuteCommand(\src\misc\vlm.c),完成对命令的执行,根据命令类型,由vlm_MediaControl(\src\misc\vlm.c)函数处理。
12. 在vlm_MediaControl函数中,会调用vlc_input_item_Init(\include\vlc_input.h)函数完成播放实例的初始化,并调用input_CreateThread2(\src\input\input.c)函数完成播放线程的创建。该线程的处理函数为Run(\src\input\input.c)。
13. Run线程是整个VLC作为流媒体服务器的核心。其主要分为如下几个步骤:Init、MainLoop和End。其中MainLoop是一个无限循环,是完成流媒体的整个发布过程。
a) 首先调用Init(\src\input\input.c)函数,初始化相关统计参数;
b) 其次再调用input_EsOutNew(\src\input\es_out.c)函数,初始化es_out_t结构体对象和es_out_sys_t结构体对象,并设置相关函数指针;
c) 再调用InputSourceInit(\src\input\input.c)函数,初始化input_thread_t对象中的input_source_t对象,主要有access_t、stream_t、demux_t三个结构体对象;
d) 总结此时各个模块实际载入的情况:
1) (access_t)type="access",name="access_filter",capability="access2",psz_filename="access/libaccess_file_plugin.so";
2) (stream_t)type="stream",pf_read="AStreamReadStream",pf_seek="AStreamPeekStream",pf_control="AStreamControl",pf_destory="AStreamDestory";
3) (demux_t)type="demux",capability="demux2",shortcuts="ps";
4) (sout_instance_t)type="stream out",psz_capability="sout stream",shortcut="stream_out_standard",psz_filename="/stream_out/libstream_out_standard_plugin.so";
5) (es_out_t)pf_add="ESOutAdd",pf_send="ESOutSend",pf_del="ESOutDel",pf_control="ESOutControl";
e) 再调用MainLoop(\src\input\input.c)函数,完成读取、解复用、解码、复用和传输;
f) MainLoop函数为无限循环,直到input_thread_t对象存在b_die、b_error、b_eof时为止。在该函数中,存在如下行代码:
i_ret=p_input->input.p_demux->pf_demux(p_input->input.p_demux);
它就是流媒体服务器运行的起点,所有的后续操作都会在该函数中继续衍生。
g) Pf_demux调用的是(\modules\demux\ps.c)中的Demux函数,在该函数中主要完成如下操作:
1) 先调用ps_pkt_resynch(\modules\demux\ps.c)函数,完成PS流中数据包重新同步(这里应该涉及到多媒体相关知识,需要补补);
2) 再调用ps_pkt_read(\modules\demux\ps.c)函数,最终调用stream_Block函数,这个函数内部会根据实际情况,调用stream_t模块中的pf_read或pf_block函数,函数结果会返回一个读取的buffer;
3) 根据数据包的i_code的值,做不同的处理,对于音视频数据流,调用es_out_Send(\include\vlc_es_out.h)函数处理;
4) es_out_Send一个抽象层函数,其通过函数指针,实际调用的是EsOutSend(\src\input\es_out.c)函数;
5) EsOutSend函数最终会调用input_DecoderDecode(\src\input\decoder.c)函数;
6) input_DecoderDecode函数会调用DecoderDecode(\src\input\decoder.c)函数完成解码;
7) DecoderDecode函数会调用pf_packetize(\modules\packetizer\mpegvideo.c)函数实现PES的打包;
8) DecoderDecode函数会调用sout_InputSendBuffer(\src\stream_output\stream_output.c)函数,实现发送;
9) sout_InputSendBuffer函数中的pf_send指针,指向的是(\modules\stream_out\standard.c)Send函数;
10) Send函数调用的是流化输出(stream_output)的抽象层(\src\stream_output\stream_output.c)中的sout_MuxSendBuffer函数,首先将要发送的数据放入fifo队列中,然后调用pf_mux函数指针,完成多路复用;
11) Pf_mux函数指针指向的是(\modules\mux\mpeg\ts.c)的Mux函数,完成多路复用后,最终调用(\modules\mux\mpeg\ts.c)TSSchedule函数,准备调度发送了;
12) TSSchedule函数中调用了TSDate(\modules\mux\mpeg\ts.c)函数;
13) TSDate函数中调用了流化输出(stream_output)的抽象层(\src\stream_output\stream_output.c)中的sout_AccessOutWrite函数,最终调用pf_write函数完成数据输出;
14) pf_write函数指向的是(\modules\access_output\udp.c)中的Write函数,完成数据UDP发送,这样数据就转换称TS流输出了;
15) 总结:pf_demux函数为流媒体所有操作的起点,通过该处衍生了很多其他模块的处理,从上面的分析可以看出,系统实质就是PS、ES、PES和TS几种流间的转换,针对应用场合(主要指做服务器或客户端)的不同,转换的方式不同。