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.
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幾種流間的轉換,針對應用場合(主要指做伺服器或客戶端)的不同,轉換的方式不同。