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