VLC架构剖析.docx
- 文档编号:11607250
- 上传时间:2023-03-28
- 格式:DOCX
- 页数:22
- 大小:258.13KB
VLC架构剖析.docx
《VLC架构剖析.docx》由会员分享,可在线阅读,更多相关《VLC架构剖析.docx(22页珍藏版)》请在冰豆网上搜索。
VLC架构剖析
VLC架构剖析
1.VideoLan简介
1.1videolan组成
Videolan有以下两部分组成:
VLC:
一个最主要的部分,它可以播放各种类型的媒体文件和流媒体文件,并且可以创造媒体流并保存成各种格式的媒体文件,这些文件的质量要比没保存前的件好。
videolan作为客户端可以播放本地文件,httP:
//,rtsp:
//。
VLS:
是一种流服务器,专门用来解决流的各种问题,它也具有一些VLC的特征。
videolan作为服务器可以输出httP,rtP,rtsp的流。
1.2VLC优点
VLC是一种跨平台的媒体播放器和流媒体服务器,最初为videolan的客户端,它是一种非常简便的多媒体播放器,它可以用来播放各种各样的音视频的格式文件(MPEG-1、MPEG-2、MPEG-4、DivX、WMV、mp3、OGG、Vorbis、AC3、AAC等等)流媒体协议,最具特色的功能是可以边下载边观看Divx媒体文件,并可以播放不完全的AVI文件。
并且支持界面的更改。
VLC支持多种的操作系统,linux(rh9,Debian,Mandrake,Gentoo),BSD,windows,MacOSX,BeOS,Solaris等等。
支持带菜单的VCD,SVCD,和DVD,数字卫星频道、数字地球电视频道(digitalterrestrialtelevisionchannels),在这些操作系统下通过宽带IPv4、IPv6网络播放线上影片。
此软件开发项目是由法国学生所发起的,参与者来自于世界各地,设计了多平台的支持,可以用于播放网络流媒体及本机多媒体文件,特别是它能直接播放未下载完整的多媒体文件。
下图表示出了VideoLan的解决方案:
VideoLanClient是VideoLan项目(一个完整的MPEG-2客户/服务器解决方案)的一个组成部分。
不过VideoLanClient也可以作为一个独立的程序来播放来自硬盘或者DVDROM的MPEG数据流。
它目前支持GTK+、GNOME、KDE和QT,并且可以使用X11、Xvideo、SDL或者DirectX作为视频输出。
对于声音,VideoLanClient支持OSS、ALSA和ESD。
要访问DVD,VideoLanClient使用的是Libdvdcss库。
它是一个简单的专为DVD访问设计的库。
它可以像访问块设备一样访问DVD,而不用考虑解密问题。
2.VLC整体架构分析
2.1LibVLC
LibVLC是VLC的核心部分。
它是一个提供接口的库,比如给VLC提供些功能接口:
流的接入,音频和视频输出,插件管理,线程系统。
所有的LibVLC源码位于src\及其子目录:
Interface/:
包含与用户交互的代码如按键和设备弹出。
Playlist/:
管理播放列表的交互,如停止,播放,下一个,或者随机播放。
Input/:
打开一个输入组件,读包,解析它们并且将被还原的基本流传递给解器。
Video_output/:
初始化video显示器,从解码器得到所有的图片和子图片(如subtitles)。
随意将它们转换为其它格式(如:
YUV到RGB)并且播放。
Audio_output/:
初始化音频mixer(混合器)。
如:
发现正确的播放频率,然后重新制作从解码器接收过来的音频帧。
Stream_output/:
类似Audio_output。
Misc/:
被libvlc其它部分使用的杂项,如线程系统,消息队列,CPU探测,对象查询系统,或者特定平台代码。
2.2VLC
VLC是一个纯粹围绕着LibVLC写成的程序。
它是非常小的,但是功能很齐全的媒体播放器,归功于LibVLC的动态组件支持。
2.3组件
组件位于modules\子目录,在运行时被加载。
每一个组件提供不同的特征适应特定的文件的环境。
另外,大量的不断编写的可移植功能位于audio_output\,vidco_output\和interface\组件,以支持新的平台(如:
BeoSMaeOSX)。
组件中的插件被位于src\misc\modules.c和include\modules*.h中的函数动态加载和卸载。
写组件的API描述如下,共3种:
(l)组件描述宏:
声明组件具有哪种优先级的能力(接口,demux2等等),还有GUI组件的实现参数,特定组件的配置变量,快捷方式,子组件等等;
(2)Open(vlc_objeet_t*p_object):
被VLC调用初始化这个组件,它被组件描述宏赋值给了结构体module_t中的pf_activate函数指针,被Module_Need调用;
(3)Close(vlc_objeet_t*p_object):
被VLC调用负初始化这个组件,保证消耗Open分配的所有资源。
它被组件描述宏赋值给了结构体module_t中的pf_deactivate函数指针,被Module_Unneed调用。
用LibVLC写的组件能够直接被编译进VLC,因为有的OS不支持动态加载代码。
被静态编译进VLC的组件叫做内置组件。
2.4线程分析
(l)线程管理:
VLC是一个密集的多线程应用。
由于解码器必须预先清空和播放工序必须预先做好流程(比如说解码器和输出必须被分开使用,否则无法保证在要求的时间里播放文件),因此VLC不采用单线程方法。
目前不支持单线程的客户端,多线程的解码器通常就意味着更多的开销(各线程共享内存的问题等),进程间的通信也会比较复杂。
VLC的线程结构基于pthreads线程模型。
为了可移植的目的,没有直接使用pthreads函数,而是做了一系列类似的包裹函数:
vlc_thread_create,vlc_thread_exit,vlc_thread_join,vlc_mutex_init,vlc_mutex_lock,vlc_mutex_unlock,vlc_mutex_destroy,vlc_cond_init,vlc_cond_signal,vlc_cond_broadcast,vlc_cond_wait,vlc_cond_destroy和类似结构:
vlc_thread_t,vlc_mutex_t,and vlc_cond_t。
(2)线程同步:
VLC的另一个关键特征就是解码和播放是异步的:
解码由一个解码器线程工作,播放由音频输出线程或者视频输出线程工作。
这个设计的主要目的是不会阻塞任何解码器线程,能够及时播放正确的音频帧或者视频帧。
这样实现也导致产生了在接口,输入,解码器和输出之间的一个复杂的通讯结构。
虽然当前接口并不允许,但是让若干个输入和视频输出线程在同一时刻读取多个文件是可行的(这是VLC未来改进的主要方向)。
现在的客户端就是用这种思想实现的,这就意味着如果没有用到全局锁的话那么一个不能重入的库是不能被使用的(尤其是liba52库)。
VLC输出的流里包含时间戳,被传递给解码器,所有有时间戳标记的流也均被记录,这样输出层可以正确及时的播放这些流。
时间mtime_t是一个有符号的64-bit整形变量,单位是百万分之一秒,是从1970年7月1日以来的绝对时间。
当前时间能够被mdate()函数恢复。
一个线程可以被阻塞到mwait(mtime_tdate)等到一个确定的时间才被执行。
也可以用msleep(mtime_tdelay)休眠一段时间。
如果有重要的事情要处理的话,那么应该在正常时间到来之前被唤醒(如色度变换)。
例如在modules\codec\mpeg_vldeo\synchro.c中,通常的解码时间被记录,保证图像被即时解码。
3.VLC接口技术分析
3.1VLC运行过程
通过对相关资料和自己的分析,VLC的运行过程如下:
ELF(Linux下可执行文件的格式)先被动态加载,然后主线程就变成了接口线程并且在src/interface/interface.c中开始。
它执行下列步骤:
1.cpu探测:
什么型号?
所有能力(MMX,MMXEXT,3DNow,AltiVec等等)
2.消息接口初始化;
3.命令行选项解析组件
4.创建播放列表
5.仓库初始化
6.加载所有内置和动态组件
7.打开接口
8.安装信号处理器:
SIGHUP,SIGINT和SIGQUIT(捕获一个,忽略后来的并退出)。
9.派生音频输出线程;
10.派生视频输出线程;
11.主循环:
事件管理;
下图表示了这些步骤的执行过程:
VLC的运行过程图
3.2消息接口
由于printf()函数不是线程安全的,因此在调用printf()函数时一个线程的执行将会受到干扰,当这个线程被另一个函数所调用时就会其状态被破坏而退出程序。
所以VLC构造了自己的线程安全的消息接口。
VLC的线程安全的消息接口有两种实现方式:
如果在config.h里定义了INTF_MSG_QUEUE的话,每一个类似printf()的函数将会把排队的消息放到链表里,这个链表将会在事件循环中被线程接口用红色标记的方式打印出来。
如果INTF_MSG_QUEUE没被定义的话,调用线程将会获得一个printlock(用来防止在同一时刻有两个printf操作被执行)同时直接打印出消息(默认操作)。
以下为VLC线程安全消息的API:
QueueMsg:
添加一条消息到消息队列,如果消息队列满了,先打印所有的消息;
FlushMsg:
打印所有在消息队列里的消息,特别的,消息队列必须被提前加锁,因为该函数不检查锁。
PrintMsg:
打印一条消息到stderr,可以打印彩色消息。
3.3命令行选项
VLC用GNU的getopt解析命令行选项。
Getopt结构定义在src\extras\getopt.h里。
所有的配置也可以用环境变量改变:
调用函数main_Put*Variable和main_Get*Variable。
所以,.\vlc--height=240和.\vic_height=240./vlc(这种方式用于所有地方,包括插件)是一样的。
但是为了线程安全的考虑,当第二个线程派生了,main_Put*Variable便不能被使用了。
3.4播放列表管理
当VLC得到输入媒体文件的时候播放列表被创建。
一个合适的接口插件能够从这个播放列表添加和删除文件。
在src/Playlist目录下的这些被使用的函数被描述。
播放列表既不是动态组件也不是内置组件,只是可以被外部调用的API:
Playlist_Create:
初始化播放列表,派生两个线程。
一个是播放列表主线程RunThread调用Input_CreateThread为每个被读的文件派生输入线程。
一个是播放列表里的项目排队预解析线程RunPreparse。
Intf_playlistadd和intf_playlistdelete是两个典型的最常用的添加和删除播放列表的命令函数。
此时接口主循环函数inif_manage将被启动同时在必要的时候终止输入的线程。
3.5组建仓库
在启动的时候,VLC创建一个包含所有插件接口(.so和内置插件)的仓库,每一个插件都会被检查其实现的功能,这些功能如下:
MODULE_CAPABILITY_INTF:
一个接口插件。
MODULE_CAPABILITY_ACCESS:
ASam-ism,目前还没有用到。
MODULE_CAPABILITY_PUT:
一个输入插件比如说PS和DVD的播放要用到。
MODULE_CAPABILITY_DECAPS:
ASam-ism,unusedatpresent。
MODULE_CAPABILITY_ADEC:
音频解码器。
MODULE_CAPABILITY_VDEC:
视频解码器。
MODULE_CAPABILITY_MOTION:
视频解码器的补充动态组件。
MODULE_CAPABILITY_IDCT:
视频解码器的IDCT组件。
MODULE_CAPABILITY_AOUT:
一个音频输出组件。
MODULE_CAPABILITY_VOUT:
一个视频输出组件。
MODULE_CAPABILITY_YUV:
视频输出的YUV组件。
MODULE_CAPABILITY_AFX:
音频输出的音频效果插件,目前还没实现。
MODULE_CAPABILITY_VFX:
视频输出的音频效果插件,目前还没实现。
管理这些插件的API如下:
Module_InitBank:
创建组件仓库,然后调用module_LoadMain将主程序信息导入组件银行。
Module_LoadMain:
将主程序信息导入组件仓库。
Module_LoadBulltins:
加载所有内置组件。
Module_Loadplugins:
加载所有动态组件。
Module_EndBank:
清空组件仓库。
Module_ReSetBank:
通过卸载所有无用的动态(插件)组件,重置组件仓库。
Module_EndBank:
卸载所有动态(插件)组件,清空模仓库。
Module_Need:
得到能力最符合要求的组件。
Module_Unneed:
减少一个组件的引用计数,必须被Module_Need的同一个线程调用。
3.6接口主循环
这个接口线程首先选取合适的接口动态插件,然后和这个插件的pf_run()函数一起进入主接口循环。
pf_run()函数将实行其该实现的功能并且每隔100ms调用intf_Manage一次(典型的为用户图形界面的时间回调)。
intf_Manage通过卸载不必要的组件来清空组件仓库,并且管理播放列表和当消息队列正在用时对排队的消息进行红色标记。
如果在linux下编译有图形界面,那么这个动态插件是modules\gui\wxwindows\xwindows.cpp。
3.7接口动态组件
这两种组件都位于modules\目录,接口动态组件除了具有普通动态组件的定义外,还需要定义以下标准API:
Run或者Runlntf:
这个函数就是pf_run,履行接口动态组件的一切功能(等待用户输入并且显示信息)。
4.功能组件分解
4.1复合的多层输入技术
输入组件的中心思想就是处理包,但又不必知道包里的具体内容。
它只是读包的ID,在包头(在MPEG中是SCR和PCR字段)的指示的正确时刻将包投递到解码器。
输入组件不需要具体细节。
如,不需要知道怎样播放一帧或者几帧,也不需要知道什么是“帧”。
像视频图像一样,基本流没有优先级。
经过分析发现输入组件对一个媒体文件做了以下这些事情:
为每一个读取的文件派生输入线程。
实际上,由于流是不一样的,因此输入文件的结构和解码器需要重新被初始化。
这些工作由接口线程(播放列表组件)调用input_CreateThread来完成:
首先寻找一个能读取这个文件的输入插件(首先我们要打开文件的socket,然后探测文件流的开始处以确定哪个插件可以读取这个文件),文件的socket是被input_FileOpen,input_NetworkOpen或者input_DvdOpen函数所打开的,这些函数需要设置两个非常重要的参数:
b_pace_control和b_seekable。
然后我们可以运行输入插件的pf_init函数和一个无限循环来实现pf_read和pf_demux函数的功能,这个插件是用来初始化流结构(p—input->stream),管理文件包缓存,读取文件包并且使这些文件包分路。
但是这些最重要的任务是在输入的API高级函数的协助下完成的。
4.2文件流的管理
这个己经打开输入socket的功能模块必须先规范两个属性如下:
(1)p_input->stream.b_pace_control:
不管文件流是否以客户端所要求的速率被读取(这个由流本身的频率和客户电脑系统的时钟所确定),比如说如果客户端不能读取文件足够快得话,这时一个文件或者一个管道(包括TCP/IP连接)可以以客户端速率被读取,那么这个管道的另一端将会被write()阻塞掉;相反,如果客户端不能读取文件足够快得话,UDP流(比如说被VLS所使用的流)以服务器端的速率被读取,当内核缓冲器满的时候文件包就会丢失,因此服务器时钟的漂移策略将会用来弥补这种包的丢失。
不管文件流是怎样的速率,这个属性是用来控制时钟的管理。
时钟管理的子菜单:
当用到UDPsocket和远程服务器时,服务器时钟的漂流策略是必不可少的,这是由于如果在流文件传输过程中,两端的时钟有一个哪怕是一点的混乱,那么利用漂流策略则可以让流文件说明这个时钟的偏差。
这也就意味着在每一个基本流给出的频率中,由输入线程显示的日期将会有某种程度的不同步,输出线程(通常说的是解码线程)将会处理这个问题。
不同步的问题也可以出现在读取己连接上的设备文件上,比如说把这些文件读取到视频编码盘上。
由于catfoo.mpg|vlc没任何显示时钟问题的功能,仅仅从简单的catfoo.mpg|vlc上客户端是不可能觉察出时间的不同步的,因此此时应该知道用户的b_pace_control值。
总之,当客户端和服务器都能同步于同一个CPU时钟时,服务器时钟的漂流策略就可以被忽略掉。
(2)p_input->stream.b_seekable:
不管lseek()函数在文件中有没有被描述,我们都要调用它来管理流。
基本上我们要么跳转到文件流的任何地方(在滚动栏上显示),要么就一字节一字节的读取文件流。
这个属性没有第一个属性对流的管理来的重要,但是它也不是多余的,这是因为catfoo.mpg1vlc中b_pace_control和b_seekable有关联,比如当b_pace_control=1时b_seekable=0,相反就是当b_pace_control=0时b_seekable=1是不可能成立的。
如果流是可以被搜寻的,p_input->stream.p_selectede_area->i_size必须被设置(比如说在任意的字节单位中,p_input->stream.p_selectede_area->i_size必须设置成和p_input->i_tell一样,这是因为p_input->i_tell表示当前从流读取的字节)。
时间转换的偏移:
时钟管理函数位于src\input\input_clock.c目录下。
客户端所能知道的就是文件开头和结尾的偏移量(p_input->stream.p_selectede_area->i_size),目前这个偏移量是用字节来表示的而且是依赖于插件的。
例如如何在界面上以秒来显示hell这个文件的时间信息呢?
那就是客户端要获得PS流中的可以表明每秒读取多少字节的mux_rate属性,这个属性可以随时更改但它在整个流传输过程中是不变的,这样可以用它来确定时间的偏移量。
4.3输出到接口的结构分析
这里着重说明输入组件和界面之间的API通信。
最重要的文件是include\input_ext-intf.h,它定义了input_thread_tstructure,thestream_descriptor_t和ES描述符(可看成树的结构)。
注意到input_thread_tstructure的特色是有两个void型的指针,这两个指针是p_method_data和p_plugin_data,创门分别用于缓冲管理数据和插件数据。
并且文件流的描述放在了一个树形结构的程序描述符里,这个程序描述符里包含了几种基本流的描述符。
下图表示出了这个树形结构,这里一个流启动了两个线程。
而在多数情况下只能有一个线程,目前只有TS流下可以有启动多个线程,比如说一个电影和一场足球比赛可以同时播放,这对于卫星和光纤广播是足够用的了。
VLC输入组件和界面之间的API通信的树形结构图
这里需要注意的是在对p_input->stream结构进行修改和存取时,必须先对其加锁。
Es被一个ID(这个ID适合于多路信号分离器来寻找),一个stream_id(therealMPEGstreamID),一个模式(在ISO/IEC13818-1table2-29里被定义)和一些描述符来标示,它同样提供其他有用的信息给多路信号分离器。
如果需要读取的流文件不是MPEG系统层的流(比如AVI或者RTP),那么需要写一个规范的多路信号分离器,在这种情况下,如果要携带额外的信息时,就可能要用到一个void型的p_demux_data的指针,这个指针在不传输流时会自动的被释放掉。
下面说明为什么要用ID而不是简单的用stream_id:
当一个信息包(可以是TS包,PS包或者其他类型的包)被读取时,多路信号分离器将会从这个包里寻找这个ID来找到相关的基本流,并且如果用户选择了基本流的话就分离这个流。
对于TS包,我们能知道的唯一信息就是ESPID,因此我们保存的参考ID就是PID,PID在PS流里并不存在,因此需要写这个PID。
当然所有在PS包里能找到的信息都是基于stream_id的,但是既然每个私有流(AC3,SPU,LPCM的等等)都在使用同一个stream_id,因此仅仅基于stream_id是不够的,在这种情况下,PES有效负载的第一个字节就是流的私有ID,把这个私有ID和stream_id经过某种形式的合并来获得想要的唯一ID。
流程序和ES的结构都写在了插件里的pf_init()函数里,这个函数可以随时被更改(在vic-0.8.0版本前都放在了src\input\input_programs.c中)。
DVD插件解析.ifo文件后知道是哪个ES在信息流里,TS插件读取流里的PAT和PMT结构,PS插件也可以解析PSM结构(目前很少见),或者通过预解析数据的第一个兆字节来编译树结构上不工作的部分。
这里需要注意的是:
因为几乎从来没有PSM(programstreammap)结构,因此在大多说境况下我们需要预解析(也就是说读取数据的第一个兆字节,然后又回到数据的开始处)PS流,虽然并不适合这样做,但是这是唯一的选择。
这样做会出现以下两个问题:
首先不能解析不可被搜索到的流,因此ES树在不工作的时候被编译;再者,如果一个新的ES流在上一个数据包的第一个兆字节后出现的话(比如说在节目字幕中不会出现对其的说明字幕),在没遇到第一个数据包前是不会出现在菜单里的。
由于要花费很长的时间(即使包没被解码),因此无法解析全部的流。
通常输入插件的任务是派生出必要的解码线程,它必须在其选择的ES流里调用input_seleetES(input_thread_t*p_input,es_descriptor_t*p_es),流的描述符也包含一些区域(比如说DVD里的章目和标题),这些区域在流里呈现出逻辑的不连续性。
尽管当PSM(或者PAT/PMT)改变时要用到这些区域,但是在TS和PS流里只有一个这样的区域。
这个区域的目标是当寻找另一个区域时,输入插件要导入一个新的流描述符的树结构(这时选择流的ID可能是不正确的)。
4.4接口用到的方法
Input_ext-intf.c提供了一些函数来控制读取流:
(l)input_SetStatus(input_thread_t*p_input,inti_mode):
改变读取流的速度。
i_mode可以是INPUT_STATUS_END,INPUT_STATUS_PLAY,INPUT_STATUS_PAUSE,INPUT_STATUS_FASTER,INPUT_STATUS_SLOWER的一种。
读取流速度的主要是由变量p_input->stream.control.i_rate来决定。
它的默认值为DEFAULT_RATE,这
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- VLC 架构 剖析