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系统调用获得动态链接库对象中的函数指针。



 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

                
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值