VLC 0.1.99 源碼分析


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)程序運行後,根據程序的配置加載不同的模塊和模塊中不同的實現。

        具體源碼實現:

                在RequestPluginplugins.c)函數中通過dlopen系統調用打開動態鏈接庫對象。

                在GetPluginFunction(plugins.c)函數中通過dlsym系統調用獲得動態鏈接庫對象中的函數指針。


































































發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章