VLC 0.1.99 源碼分析
super.raymond.lu[at]gmail[dot]com
(轉載請註明出處http://blog.csdn.net/raymond_lu_rl/article/details/7336038)
最近在學軟件構架,找些開源軟件來學習一下。
由於VLC和自己要搞的項目有些接近,因此首先從VLC開刀,但是VLC經過10多年的發展,現在的2.0版本已經非常龐大了。磨刀不誤砍柴工,還是先花兩天時間學習一下最初的0.1.99版本,先摸清個大概,再往高版本學習。本文就是這兩天的學習記錄。
帶着下面幾個問題,開始閱讀源碼:
1、 程序模塊怎麼劃分?
2、 各個模塊之間怎麼進行通信?
3、 內存如何管理?
4、 怎麼做日誌?
5、 VLC可以根據用戶的配置動態加載不同的用戶界面和輸入輸出模塊,是怎麼做到的?
從程序目錄來看程序模塊劃分:
從源碼目錄來看,整個應用程序分成:媒體數據輸入、媒體數據解析、媒體數據解碼、視頻顯示、音頻輸出、用戶界面六個模塊。
下面再來看看主要的數據結構:
/*****************************************************************************
* main_t, p_main (global variable)
*****************************************************************************
* This structure has an unique instance, declared in main and pointed by the
* only global variable of the program. It should allow access to any variable
* of the program, for user-interface purposes or more easier call of interface
* and common functions (example: the intf_*Msg functions). Please avoid using
* it when you can access the members you need in an other way. In fact, it
* should only be used by interface thread.
*****************************************************************************/
typedef struct
{
/* Global properties */
int i_argc; /* command line arguments count */
char ** ppsz_argv; /* command line arguments */
char ** ppsz_env; /* environment variables */
/* Generic settings */
boolean_t b_audio; /* is audio output allowed ? */
boolean_t b_video; /* is video output allowed ? */
boolean_t b_vlans; /* are vlans supported ? */
/* Unique threads */
p_aout_thread_t p_aout; /* audio output thread */
p_intf_thread_t p_intf; /* main interface thread */
/* Shared data - these structures are accessed directly from p_main by
* several modules */
p_intf_msg_t p_msg; /* messages interface data */
p_input_vlan_t p_vlan; /* vlan library data */
} main_t;
這個是主程序數據結構,該數據結構包含各個模塊用到的所有數據,很多東西註釋都說得很清楚了,就不詳述了。需要注意的是音頻輸出p_aout和界面p_intf兩個模塊的數據結構。
好像看不到比如視頻輸入、解碼器等的數據結構? 對,該版本把這些結構都放到intf_thread_s中了,下面便是該結構:
typedef struct intf_thread_s
{
boolean_t b_die; /* `die' flag */
/* Specific interfaces */
p_intf_console_t p_console; /* console */
p_intf_sys_t p_sys; /* system interface */
/* Plugin */
plugin_id_t intf_plugin; /* interface plugin */
intf_sys_create_t * p_sys_create; /* create interface thread */
intf_sys_manage_t * p_sys_manage; /* main loop */
intf_sys_destroy_t * p_sys_destroy; /* destroy interface */
/* XXX: Channels array - new API */
//p_intf_channel_t * p_channel[INTF_MAX_CHANNELS];/* channel descriptions */
/* file list - quick hack */
char **p_playlist;
int i_list_index;
/* Channels array - NULL if not used */
p_intf_channel_t p_channel; /* description of channels */
/* Main threads - NULL if not active */
p_vout_thread_t p_vout;
p_input_thread_t p_input;
} intf_thread_t;
發現了吧,intf_thread_s裏面包含了視頻輸出模塊p_vout和媒體輸入模塊的數據結構p_input。
下面開始查看程序的主要運行流程吧,還是從interface/main.c文件中的main函數看起。
由於程序通過用戶配置的方式來加載不同的模塊,因此以下程序跟蹤對用戶的配置進行了假設:
1、假設程序使用gnome界面。
2、使用文件輸入的方式。
3、媒體輸入爲TS流,視頻顯示使用gnome(X11)的方式。
下面從main函數開始跟蹤了:
1. 調用intf_MsgCreate(intf_msg.c)初始化消息數據結構p_main->p_msg,
2. 調用GetConfiguration(main.c)根據命令行參數設置環境變量,後面的模塊通過讀取環境變量還獲得配置。
3. 通過命令行參數初始化界面模塊中的播放文件列表數據結構main_data.p_intf->p_playlist和main_data.p_intf->i_list_index。
4. 如果配置了網絡模塊,則調用input_VlanCreate(input_vlan.c)加載網絡模塊,初始化網絡模塊數據結構main_data.b_vlans
5. 如果配置了音頻輸出模塊,則調用aout_CreateThread(audio_output.c)加載音頻輸出模塊,初始化網絡模塊數據結構main_data.b_audio
6. 調用intf_Create(interface.c)加載界面模塊,初始化界面模塊數據結構main_data.p_intf
6.1 初始化界面模塊函數指針:
/* Get plugins */
p_intf->p_sys_create
= GetPluginFunction( p_intf->intf_plugin, "intf_SysCreate" );
p_intf->p_sys_manage
= GetPluginFunction( p_intf->intf_plugin, "intf_SysManage" );
p_intf->p_sys_destroy
= GetPluginFunction( p_intf->intf_plugin, "intf_SysDestroy" );
6.2 調用p_intf->p_sys_create函數創建UI,實際上是調用了intf_SysCreate(intf_gnome.c)函數:
6.2.1 調用GnomeCreateWindow創建gnome界面。
6.2.2 調用vout_CreateThread函數初始化視頻輸出模塊p_intf->p_vout(video_output.c):
6.2.2.1 初始化視頻輸出模塊函數指針:
/* Get plugins */
p_vout->p_sys_create =
GetPluginFunction( p_vout->vout_plugin, "vout_SysCreate" );
p_vout->p_sys_init =
GetPluginFunction( p_vout->vout_plugin, "vout_SysInit" );
p_vout->p_sys_end =
GetPluginFunction( p_vout->vout_plugin, "vout_SysEnd" );
p_vout->p_sys_destroy =
GetPluginFunction( p_vout->vout_plugin, "vout_SysDestroy" );
p_vout->p_sys_manage =
GetPluginFunction( p_vout->vout_plugin, "vout_SysManage" );
p_vout->p_sys_display =
GetPluginFunction( p_vout->vout_plugin, "vout_SysDisplay" );
6.2.2.2 調用p_vout->p_sys_create函數創建視頻顯示模塊。
6.2.2.3 創建RunThread(video_output.c)線程,初始化顯示雙緩衝區(InitThread)並進入視頻輸出模塊事件循環:
l 檢查p_vout->p_picture緩衝區中是否有已經準備好顯示的圖片。
l 進行色彩空間轉換、圖片OSD信息輸出等。
l 根據PTS或者幀率計算顯示後等待事件並等待。
l 調用p_vout->p_sys_display(vout_SysDisplay)函數進行顯示(X11)。
l 調用p_vout->p_sys_manage函數或者Manage函數處理界面模塊對視頻輸出所進行的參數改變,比如檢查p_vout->i_changes變量。
6.2.3 創建GnomeThread線程,在線程中
6.2.3.1 使用定時器調用GnomeManageMain函數檢查主程序是否退出,以便退出gtk事件循環。
6.2.3.2 調用gtk_main();進入gtk界面事件循環。
7. 調用InitSignalHandler函數註冊系統信號處理函數,比如通過鍵盤中斷退出。
8. 調用intf_Run(interface.c)運行界面模塊
8.1 如果p_intf->p_playlist中包含播放對象,則調用input_CreateThread(input.c)函數,以文件輸入的方式初始化輸入模塊p_intf->p_input。
8.1.1 初始化輸入模塊主要接口函數:
case INPUT_METHOD_TS_FILE: /* file methods */
p_input->p_Open = input_FileOpen;
p_input->p_Read = input_FileRead;
p_input->p_Close = input_FileClose;
break;
這三個input_File*函數主要定義在input_file.c文件中。
8.1.2調用p_input->p_Open函數( p_input )打開文件,調用ps_thread函數初始化信號量信息,並打開input_DiskThread(input_file.c)線程進入文件源輸入事件循環(ps_fill函數):
l 等待包處理隊列非滿vlc_cond_wait(&p_in_data->notfull, &p_in_data->lock);
l 調用ps_read函數(input_file.c)從文件讀入TS包數據。
l 置包隊列非空信號vlc_cond_signal(&p_in_data->notempty);
8.1.3調用RunThread(input.c)函數進入包處理模塊事件循環
while( !p_input->b_die && !p_input->b_error )
{
/* Scatter read the UDP packet from the network or the file. */
if( (input_ReadPacket( p_input )) == (-1) )
{
/* FIXME??: Normally, a thread can't kill itself, but we don't have
* any method in case of an error condition ... */
p_input->b_error = 1;
}
#ifdef STATS
p_input->c_loops++;
#endif
}
//input_ReadPacket函數:
8.1.3.1 調用p_input->p_Read從TS包隊列讀取包數據並對包進行解析、排序和重組,這裏讀取的數據流主要是TS流,對TS流不瞭解的可以Google,這裏不詳述。p_Read函數實際上就是input_FileRead函數(input_file.c),該函數執行以下操作:
1) 等待包隊列非空vlc_cond_wait( &p_in_data->notempty, &p_in_data->lock);
2) 調整PCR時鐘,複製包數據。
3) 將解析後的TS包放入包隊列中並置包隊列非空信號:
p_in_data->end++;
p_in_data->end %= BUF_SIZE+1;
vlc_cond_signal(&p_in_data->notempty);
8.1.3.2 調用input_SortPacket函數(input.c)處理讀取的TS包,input_SortPacket函數調用input_DemuxTS函數解析TS包(input.c),input_DemuxTS函數將TS包解析並判斷其中是PSI數據還是PES數據,如果是PSI數據則調用input_DemuxPSI函數進行處理,如果是PES數據則調用input_DemuxPES函數處理,下面分別說明這兩個函數的處理流程:
1) input_DemuxPES函數:
----- input_ParsePES函數組成完整的PES包後,將PES包送到解碼器fifo隊列:
p_fifo->buffer[p_fifo->i_end] = p_pes;
DECODER_FIFO_INCEND(*p_fifo );
然後通知視頻事件解析循環開始啓動vlc_cond_signal( &p_fifo->data_wait );通知解析事件循環開始解析ES包。
2) input_DemuxPSI函數:
----- 調用input_PsiDecode函數(input_psi.c)對PSI包進行解析,並分別對PAT\PMT\NIT表進行解析。
----- PMT表的解析在DecodePgrmMapSection函數中進行。通過解析PMT表中包含的節目的媒體信息,根據不同的媒體格式調用input_AddPgrmElem函數(input_ctrl.c)進行處理。
----- input_AddPgrmElem函數中根據解碼方式打開不同的解碼器線程,如果不定義OLD_DECODER宏,vdec_CreateThread(video_decoder.c)並進入解碼事件循環;如果定義OLD_DECODER宏,則通過vpar_CreateThread函數(video_parser.c)打開視頻解析線程,InitThread(video_parser.c)調用vdec_CreateThread創建解碼事件循環後進入視頻解析事件循環。
----- 解碼事件循環等待FIFO隊列信號,vlc_cond_wait( &p_fifo->wait,&p_fifo->lock );隊列中有數據後變讀取數據進行解碼。
----- 視頻解析事件循環等待data_wait 信號,接收到信號後開始初始化解析函數,並進入解析事件循環RunThread(video_parser.c)。解析事件循環對ES流進行解析,提取出視頻序列,然後發送信號到通知解碼時間循環進行解碼PictureHeader(vpar_headers.c):vlc_cond_signal( &p_vpar->vfifo.wait );
8.2進入界面模塊事件循環
/* Main loop */
while(!p_intf->b_die)
{
/* Flush waiting messages */
intf_FlushMsg();
/* Manage specific interface */
p_intf->p_sys_manage( p_intf );
/* Check attached threads status */
if( (p_intf->p_vout != NULL) && p_intf->p_vout->b_error )
{
/* FIXME: add aout error detection ?? */
p_intf->b_die = 1;
}
if( (p_intf->p_input != NULL) && p_intf->p_input->b_error )
{
input_DestroyThread( p_intf->p_input, NULL );
p_intf->p_input = NULL;
intf_DbgMsg("Input thread destroyed\n");
}
/* Sleep to avoid using all CPU - since some interfaces needs to access
* keyboard events, a 100ms delay is a good compromise */
msleep( INTF_IDLE_SLEEP );
}
p_intf->p_sys_manage函數指針在intf_Create中被初始化,假定加載的UI模塊是Gnome模塊,則該指針實際調用了intf_SysManage(intf_gnome.c)函數,該函數接收GUI菜單、鍵盤和鼠標事件並處理,其中包括改變音量、改變節目頻道等操作。比如其中的視頻窗口大小參數改變是通過設置p_intf->p_vout->i_changes |= VOUT_GAMMA_CHANGE;變量,來通知視頻輸出線程。
現在開始來回答文章開始提出的問題。
1、程序模塊怎麼劃分?
見文章中的模塊圖。
2、各個模塊之間怎麼進行通信?
每個模塊都由一個主要線程運行一個事件循環,線程之間以生產者-消費者的模式進行線程通信,生產者等待隊列非滿時開始生產,消費者等待隊列有數據開始進行消費,生產者和消費者通過信號量的方式進行通信。
比如:
文件輸入線程等待TS數據包隊列非滿。
文件輸入線程發現TS數據包隊列非滿,從文件中讀取數據,解析出TS數據包,將TS數據包放到隊列中,並置TS數據包隊列非空信號。
TS包解析線程等待TS數據包隊列有非空。
TS包解析線程發現TS數據包隊列非空,從隊列中讀取並解析TS數據包,將解析出的PSI\PES數據包放到PES隊列中,並通知視頻解碼線程PES隊列非空。
視頻解碼線程等待PES數據包非空,如果非空則讀取數據進行解碼,並把解碼後的數據放到視頻顯示隊列中。
視頻顯示隊列線程等待視頻顯示隊列非空,如果非空則讀取當前視頻幀進行顯示。
圖形界面事件循環等待用戶操作,並對用戶的操作事件進行相應,修改相關模塊數據結構。
相關模塊線程在事件循環中檢查配置信息是否被修改,如果被修改則進行相應的操作。
3、內存如何管理?
該版本沒有做專門的內存管理模塊,都是直接使用malloc和free對相關的數據結構和內存緩衝地址進行內存分配和釋放。
4、怎麼做日誌?
打開一個文件作爲日誌記錄,方便程序的跟蹤和調式。PrintMsg函數(intf_msg.c)負責打印輸出信息、調式信息。
5、 VLC可以根據用戶的配置動態加載不同的用戶界面和輸入輸出模塊,是怎麼做到的?
1)將各個模塊的不同實現編譯成動態鏈接庫的形式。
2)程序運行後,根據程序的配置加載不同的模塊和模塊中不同的實現。
具體源碼實現:
在RequestPlugin(plugins.c)函數中通過dlopen系統調用打開動態鏈接庫對象。
在GetPluginFunction(plugins.c)函數中通過dlsym系統調用獲得動態鏈接庫對象中的函數指針。