VLC架构剖析

VLC架构剖析

1. VideoLan简介

1.1 videolan组成

Videolan有以下两部分组成:

VLC:一个最主要的部分,它可以播放各种类型的媒体文件和流媒体文件,并且可以创造媒体流并保存成各种格式的媒体文件,这些文件的质量要比没保存前的件好。videolan作为客户端可以播放本地文件,httP://,rtsp://。

VLS:是一种流服务器,专门用来解决流的各种问题,它也具有一些VLC的特征。videolan作为服务器可以输出httP,rtP,rtsp的流。

1.2 VLC优点

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,Mac OS X,Be OS,Solaris等等。支持带菜单的VCD,SVCD,和DVD,数字卫星频道、数字地球电视频道(digital terrestrial television channels),在这些操作系统下通过宽带IPv4、IPv6网络播放线上影片。此软件开发项目是由法国学生所发起的,参与者来自于世界各地,设计了多平台的支持,可以用于播放网络流媒体及本机多媒体文件,特别是它能直接播放未下载完整的多媒体文件。

下图表示出了VideoLan的解决方案:

VideoLan Client是VideoLan项目(一个完整的MPEG-2客户/服务器解决方案)的一个组成部分。不过VideoLan Client也可以作为一个独立的程序来播放来自硬盘或者DVDROM的MPEG数据流。它目前支持GTK+、GNOME、KDE和QT,并且可以使用X11、Xvideo、SDL或者DirectX作为视频输出。对于声音,VideoLan Client支持OSS、ALSA和ESD。要访问DVD,VideoLan Client使用的是Libdvdcss库。它是一个简单的专为DVD访问设计的库。它可以像访问块设备一样访问DVD,而不用考虑解密问题。

2. VLC整体架构分析

2.1 LibVLC

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.2 VLC

VLC是一个纯粹围绕着LibVLC写成的程序。它是非常小的,但是功能很齐全的媒体播放器,归功于LibVLC的动态组件支持。

2.3 组件

         组件位于modules\子目录,在运行时被加载。每一个组件提供不同的特征适应特定的文件的环境。另外,大量的不断编写的可移植功能位于audio_output\,vidco_output\和interface\组件,以支持新的平台(如:BeoS Mae OS X)。

组件中的插件被位于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_t date)等到一个确定的时间才被执行。也可以用msleep(mtime_t delay)休眠一段时间。如果有重要的事情要处理的话,那么应该在正常时间到来之前被唤醒(如色度变换)。例如在modules\codec\mpeg_vldeo\synchro.c中,通常的解码时间被记录,保证图像被即时解码。

3. VLC接口技术分析

3.1 VLC运行过程

通过对相关资料和自己的分析,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没被定义的话,调用线程将会获得一个print lock(用来防止在同一时刻有两个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:A Sam- ism,目前还没有用到。

MODULE_CAPABILITY_PUT:一个输入插件比如说PS和DVD的播放要用到。

MODULE_CAPABILITY_DECAPS: A Sam-ism,unused at present。

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和远程服务器时,服务器时钟的漂流策略是必不可少的,这是由于如果在流文件传输过程中,两端的时钟有一个哪怕是一点的混乱,那么利用漂流策略则可以让流文件说明这个时钟的偏差。这也就意味着在每一个基本流给出的频率中,由输入线程显示的日期将会有某种程度的不同步,输出线程(通常说的是解码线程)将会处理这个问题。不同步的问题也可以出现在读取己连接上的设备文件上,比如说把这些文件读取到视频编码盘上。由于cat foo.mpg|vlc没任何显示时钟问题的功能,仅仅从简单的cat foo.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_t structure,the stream_descriptor_t和ES描述符(可看成树的结构)。

注意到input_thread_t structure的特色是有两个void型的指针,这两个指针是p_method_data和p_plugin_data,创门分别用于缓冲管理数据和插件数据。并且文件流的描述放在了一个树形结构的程序描述符里,这个程序描述符里包含了几种基本流的描述符。

下图表示出了这个树形结构,这里一个流启动了两个线程。而在多数情况下只能有一个线程,目前只有TS流下可以有启动多个线程,比如说一个电影和一场足球比赛可以同时播放,这对于卫星和光纤广播是足够用的了。

 

VLC输入组件和界面之间的API通信的树形结构图

这里需要注意的是在对p_input->stream结构进行修改和存取时,必须先对其加锁。

Es被一个ID(这个ID适合于多路信号分离器来寻找),一个stream_id(the real MPEG stream ID),一个模式(在ISO/IEC 13818-1 table 2-29里被定义)和一些描述符来标示,它同样提供其他有用的信息给多路信号分离器。如果需要读取的流文件不是MPEG系统层的流(比如AVI或者RTP),那么需要写一个规范的多路信号分离器,在这种情况下,如果要携带额外的信息时,就可能要用到一个void型的p_demux_data的指针,这个指针在不传输流时会自动的被释放掉。

下面说明为什么要用ID而不是简单的用stream_id:当一个信息包(可以是TS包,PS包或者其他类型的包)被读取时,多路信号分离器将会从这个包里寻找这个ID来找到相关的基本流,并且如果用户选择了基本流的话就分离这个流。对于TS包,我们能知道的唯一信息就是ES PID,因此我们保存的参考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(program stream map)结构,因此在大多说境况下我们需要预解析(也就是说读取数据的第一个兆字节,然后又回到数据的开始处)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,int i_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,这个值越小,读取得速度就会越快,速度的改变要考虑到工input_ClockManageRef。暂停是通过简单的停止输入线程来获得的(此时被一个

pthread信号唤醒),在这种情况下,解码器也会停止。当统计解码时间(src/video_out/vout_synchro.c来统计)时需注意到这一点,如果p_input->b_pace_control==0时则不要调用这个函数。

(2)input_Seek(input_thread_t*p_input,off_t i_position):改变读取速度的偏移量。如果p_input->stream.b_seekable=0时不能调用这个函数。它的位置在p_input->p_selected_area->i_start和p_input->p_selected_area->i_size之间。(当前值是在p_input->p_selected_area->i_tell)。由于多媒体文件可能会很大(尤其当我们读取像DVD这样的设备时),因此偏移量必须要有64bit这么大。在很多系统下(比如FreeBSD),off_t的默认值就是64bit,但是在GNU libc 2.x下并不是这样,这也是为什么需要用-D_FILE_OFFSETBITS=64-D_USE_UNIX98来编译VLC的原因。随机的改变读取流的位置会导致流的混乱,读取流的解码器也会出错,为了避免这种情况的发生,在改变读取流位置的之前,需要先发送一些空包(全零)。事实上,对于大多数的音视频格式,足够长的零流就是流的溢出并且解码器也可以不出错。

(3)input_OffsetToTime(input_thread_t*p_input,char *psz_buffer,off_t i_offset):把偏移值转换成和时间一致(用于界面播放),通常在播放MPEG-2文件时这个一致性会受到破坏。

(4) input_ChangeEs(input_thread_t*p_input,es_descriptor_t*p_es,u8 i_cat):选择部分基本流的i_cat类型和p_es,用于更改语言和字幕路径。

(5)input_ToggleEs(input_thread_t*p_input,es_descriptor_t*p-es,boolean_t b_select):用来清除界面上的被选择的基本流。

4.5 缓冲器的管理

输入插件必须要执行分配和删除分配包的功能,这个功能的实现要用到以下四个基本函数:

(1)pf_new_packet(void*p_private_data,size_t i_buffer_size):关联到缓冲器的i_buffer_size的大小,分配一个新的data_packet_t。

(2) pf_new_es(void*p_private_data):分配一个新的pes_packet_t。

(3)pf_delete_packet(void*p_private_data,data_packet_t*pes_data):删除p_data。

(4) pf_delete_es(void*p_private_data,pes_packet_t*p_pes):删除p_pes。

以上四个函数都把p_input->p_method_data(即*p_private_data)作为第一个参数,因此可以保存这个分配纪录和释放包。缓冲器的管理可以有以下三种方法:

(l)传统的libc分配:在PS插件中每次经常要用到malloc()和free()函数来分配和删除分配包,这并不像想象中的那么慢。

(2)Netlist:在包有问题的开头处用这种方法来分配一个非常大的缓冲空间,然后通过管理指针列表来释放包(这就是netlist)。这种方法只有当所有的包都是同样大小时效果才会很好,它长时间用在TS输入上。DVD的插件也会用到它,但是要把加入的标志符也考虑进去,这是因为缓冲器(2048bytes)通常被若干个包所共享。现在这个方法己经不被使用而且也没有文档记录。

(3)缓冲器的cache:这种方法是目前在发展中的最新方法,它己经用在了PS的插件中。它的思想是调用malloc()和free()来获得流的不规则规律,然后经过cache系统后重新使用所有已被分配好的缓冲空间。现在的工作就是要扩展它在性能没妨碍的前提下可以用到任何插件里,但是目前还没有这方面的参考文档。

4.6 流的分路

流文件被pf_read读取后,插件必须把一个函数指针给分路函数,这个分路函数用来解析包,收集PES并把包送到解码器里。这个分路函数是专门为标准的MPEG结构(PS和TS)来写的,必须对pf_demux表明input_DemuxPS和input_DemuxTS,当然也可以自己写这个函数。

5. 解码器技术

解码器做播放流的数学处理部分。它从输入组件的多路分解器(modules/demux目录下,管理包以重新编译连续的基本流)和输出线程(包的样本被解码器重构并且播放它)分离出来。基本上解码器只是纯粹的算法,不和硬件打交道。

5.1 解码器结构分析

         输入线程调用Input_DecoderNew(src\input\decoder.c)派生合适的解码器,通过CreateDecoder选择更恰当的解码器组件,每一个解码器组件都要检查decoder_config.i_type然后返回一个记录,然后和decoder_config_t一起运行module.pf_run()。一般的decoder_config_t的结构给出了解码器的ES ID和其类型,一个指向stream_control_t结构(给出播放状态的信息)的指针,decoder_fifo_t和pf_init_bit_stream函数,然后运行DecoderThread(解码器主循环)来解码。CreateDecoder:将解码器结构中的输出回调函数指针(音频,视频,SPU)附值,这些函数建立或者销毁这些输出设备,解码后的数据由此被正确的传递出去;调用Module_Need将特定的解码器组件加载进解码器结构,组件的回调函数指针pf_activate(通常是open函数)完成对pf_decode_XXX函数指针的附值(其中的音频,视频函数指针用刚才解码器的相应输出回调函数的包裹函数附值)。DecoderThread:调用被赋予特定函数值的pf_decode_XXX函数指针进行解码工作。

5.2 数据流包结构分析

         这个输入组件(包)为传递到解码器的流数据提供高级的API。首先先看这个包的结构,它被定义在include/input_ext-dec.h里。

data_packet_t包含一个指向数据物理地址的指针,解码器读取数据只能从p_payload_start开始到p_payload_end结束,然后如果p_next不空的话,解码器就跳转到下一个包。如果b_discard_payload的标志位显示时则说明包地内容是混乱的因此这个包就会被丢弃。data_packet_t被包含进pes_packet_t里,pes_packet_t表示出了可以表明一个完全的pes包的data_packet_t链表目录。对于PS流,一个pes_packet_t通常只包含一个data_packet_t。而对于TS流,一个PES被分解在许多的TS包里。PES包里含有PTS日期(在MPEG规范里有详细说明)和目前的读取速度,这个可以用于修改日期(i_rate)。

b_data_alignment(如果在系统层里可用的话)表明假如这个包是随机的接入点的话,那么b_discontinuity就会知道先前的包是不是被丢弃过。

PES Packet 对于节目流的PES包结构图

对于节目流,PES包只含有一个数据包,它的缓冲器包括PS头,PES头和数据的有效负载。

对于传输流的PES包结构图

对于传输流,PES包可以含有无限制的数据包(上图是三个),它的缓冲器包括TS头,PES头和数据的有效负载。

这个结构由输入和decoder_fifo_t解码器共同组成,它的功能是PES包的循环FIFO被解码。输入为FIFO提供了一些宏指令:DECODER_FIFO_ISEMPTY,DECODER_FIFO_ISFULL,DECODER_FIFO_START,DECODER_FIFO_INCSTART,DECODER_FIFO_END,DECODER_FIFO_INCEND。在对FIFO进行任何操作前一定要用p_decoder_fifo->date_lock。下一个包被DECODER_FIFO_START(*p_decoder_fifo)所解码。当这些完成后,要先调用p_decoder_fifo->pf_delete_pes(p_decoder_fifo->p_packets_mgt,decoder_fifo_start(*p_decoder_fifo))然后调用ecoder_fifo_nestart(*p_decoder_fifo),使得把PES返回到缓冲器管理。如果FIFO是空的话(DECODER_FIFO_ISEMPTY),可以在携带着条件信号(Vlc_cond_wait(&p_fifo->data_wait,&p_fifo->data_lock))的新的信息包到达之前先中断它(加锁)。当文件被播放完或者用户退出时,p_fifo->b_die要设置成1,这意味着要释放所有的数据结构并且要尽快调用到vic_thread_exit()。

5.3 比特流分析

由于基本流可以任意地别分割,因此传统的读包方法并不方便。这个输入组件就提供了容易读取比特流的方法。这个可选可不选,如果选了话就不必再进入缓冲器了。

这个比特流允许调用GetBits(),当有必要时这个函数在不干扰用户的情况下读取包的缓存、修改数据包和pes包。因此对于读取连续的基本流会非常方便,用户不必去处理包的边界和FIFO,这些由比特流来处理。

下面着重分析一个32位的缓冲器:bit_fifo_t。它包括字缓冲和一些重要的bit(高位),这个输入组件提供了以下五个内联函数来管理这个缓冲器:

(1)GetBits(bit_stream_t*p_bit_stream,unsigned_int i_bits):

从比特缓冲返回到下一个1bit。如果没有足够多的比特时,这个函数就从decoder_fifo_t里抓取一个字的大小来补充。这个函数只能保证工作在24bits,有时也会工作在31bits,但这是副作用。要是使其可以读取32bit,就必须修改这个函数。

(2)RemoveBits(bit_stream_t*p_bit_stream,unsigned_int i_bits):

这个函数除了比特不返回外(为了节省CPU循环)和GetBits()一样,它同样有其的局限,因此在必要时也得修改这个函数。

(3)ShowBits(bit_stream_t*p_bit_stream,unsigned_int i_bits):

除了在读取完没获得多的比特外和GetBits()一样,因此接着需要调用RemoveBits()。要注意的是除非对字节进行排列的话那么这个函数不能工作在高于24bits的情况下

(4)RealignBits(bit_stream_t*p_bit_stream):

为了使缓冲的前几个比特排列成字节的边界这个函数丢弃掉比特流的高n(n<8)位,这有利于找到排列好的开始代码(MPEG)。

(5)GetChunk(bit_stream_t*p_bit_stream,byte_t*p_buffer,size_t i_buf_len):

它和函数memcpy())类似,只不过它把比特流作为第一个参数。P_buffer必须被分配并且至少要i_buf_len这么大,这有利于用户要保存路径时备份数据。所有以上的函数重新构造了连续的基本流范例。当比特缓冲器是空的话,这些函数就在当前包里取随后的字;当包是空的话,就转到下一个data_packet_t,如果转不到的话就转到下一个pes_packet_t(见

p_bit_stream->pf_next_data_packet),所有的这些都是明晰的。为了能用到比特流,必须要调用p_decoder_config->pf_init_bit_stream(bit_stream_t*p_bit_stream,decoder_fifo_t*p_fifo)来设置所有的变量。这可能需要从包里获得规范的信息(比如说对于PTS)。如果p_bit_stream->p_bit_stream_callback不空的话,那么包必须要改变(在较低版本中的video_parser.c有例子说明),调用的函数的第二个参数表明它是个新的data_packet还是新的pes_Packet_t,这个结构可以保存到p_bit_sream->p_callback_arg。

当要调用pf_init_bit_stream时,pf_bitstream_callback还没有被定义,因此只能先转到第一个包,这个可以在调用pf_init_bit_stream后通过指针再来回调比特流。

5.4 VLC内置解码器

         VLC内置了MPEGI,2层的音频解码器,一个MPEG MP@ML的视频解码器,一个AC3(来自LiViD)解码器,一个DVD SPU解码器和一个LPCM解码器。可以仿照视频解析器用户可以自己写特定功能的解码器。

MPEG音频解码器虽然是本地化的,但是并不支持第三层解码(这个非常不方便),AC3解码器是来自于Aaron Holtzman’s libac3的端口,SPU解码器也是本地化的。这些可以在AC3解码器里的比特流的回调里有说明。这这种情况下,必须要跳过PES的前三个字节(不是基本流的部分)。

对于MPEG视频解码器,VLC媒体播放器在其解码器的主层上提供了MPEG-1,MPEG-2的主要框架,这对于VLC来说己经非常成熟。既然这个主要框架被视频解析器和视频解码器这两个逻辑实体所分开,因此它是面向比特的,它最初的目的是把比特流解析功能从高并行的数学算法中分离出来。在理论上,这里必须只有一个视频解析线程(否则在读取比特流时会产生混乱)和视频解码线程池,这些用来做即时IDCT和在一些块上做动态补偿。它不支持也不会支持MPEG-4或者DivX解码。它不是一个编码器。尽管还有部分没被测试(比如说差分动态向量)但是它完全支持MPEG-2 MP@ML规范。对于这个解码器最有趣的文件是vpar_synchro.c,它解释了dropping算法的全部框架。在nutshell里,如果它足够强大的话就能对全部的IPB进行解码,或者在用户有足够的时间的话就能对全部的IP和B进行解码(这个基于on-the-fly时间统计)。另一个有趣的文件vpar_blocks.c,它描述了全部的块(包括系数和动态向量)解析算法,在这个文件的最后需要产生一个对普通图片类型的最优化函数和一个减缓类属函数。这里同样有不同程度的最优化函数(可以慢速编辑文件但快速解码)为VPAR_OPTIM_LEVEL,level 0表示没优化,level 1表示优化MPEG-l和MPEG-2图片框架,level 2表示优化MPEG-1和MPEG-2域和图片框架。

5.5 动态补偿技术分析

         动态补偿技术是非常依赖于平台的(比如MMX或者AltiVec版本),因此必须把它放plugins\Motion目录下。这对于视频解码器来说是非常方便的,这样的插件可以用到其他的视频解码器中。一个动态的插件必须定义6个函数(直接来自于规范): vdec_MotionFieldField420,vdec_MotionField16x8420,vdec_MotionFieldDMV420,vdec_MotionFrameFrame420,vdec_MotionFrameField420,vdec_MotionFrameDMV420。而对于在MP@ML标准里被禁止的格式自然是不能用(因为需要的编译的时间长)。

5.6 IDCT技术分析

         像动态补偿一样,IDCT技术也是一种平台规范。因此需要把它放到phigins\idct目录下。这个模块是用来做IDCT运算和把数据复制到最后的图片上,这里需要定义以下7种方法:

(1)vdec_IDCT(decode_config_t*p_config,dctelem_t*p_clock,int):

做完全的2-D IDCT运算。64个系数在p_block里。

(2)vdec_SparseIDCT(vdec_thread_t*p_vdec,dctelem_t*p_clock,int i_sparse_pos):

只用一个非零系数(有i_sparse_pos来设计)在块上做IDCT运算。可以把这个函数定义在phigins\idct\idct_common.c(在初始化时间段里对这些64位的矩阵进行预计算)里。

(3)vdec_InitIDCT(vdec_thread_t*p_vdec):

对vdec_SparselDCT做初始化的填充。

(4)vdec_NormScan(ppi_scan):

通常情况下,这个函数不做任何运算。在做一些较小的优化时,用它来对MPEG导航矩阵(在ISO/IEC 13818-2里有详细介绍)做某些系数的求逆运算。

(5)vdec_InitDecode(struct vdec_thread_s*p_vdec):

初始化IDCT和选择配置列表。

(6)vdec_DecodeMacroblockC(struct vdec_thread_s*p_vdec,struct macroblock_s*p_mb):

对整个宏块解码并把这个数据复制到最终要的图片里(包括彩色信息)。

(7) vdec_DecodeMacroblockBW(struct vdec_thread_s*p_vdec, struct macroblock_s*p_mb):

对整个宏块解码并把这个数据复制到最终要的图片里(除了彩色信息),它用在gayscale模式里。

5.7 对称多处理插件技术分析

在必要情况下,VLC的MPEG视频解码器将会利用到多处理器。这个思想来自于解码器的池,这个池可以在多个宏块上同时实现IDCT/动态补偿功能。这个管理池的模块在src\video——decoder\vpar_pool.c里。但是这个模块的处理速度要慢一些。

6 视频输出层技术

6.1 数据结构和主循环

重要的数据结构被定义在inchide\vlc--vidco.h和indude\video-output.h里。事实上VLC的SPU解码器只能解析SPU报头和把SPU图片数据转化成VLC的格式,这样做是为了为能处理的快一点。部分类似SPU解码器在src\video_output\video\spu.c里。

picture_t:是主要的数据结构,描述了视频解码器线程需要的一切。其中的p_data是一个YUV平面图的指针;

subpicture_t:存储subtitle部分(一个视频文件包括audio,video,subtiiles);

vout_thread_t:一个很复杂的合成结构。

基本上video输出线程管理一堆图像和子图像(默认5个)。每一个图像有一个状态(显示,消耗,清空等等)和确定的播放时间。视频输出的主要工作是一个无限循环:

(1)在堆上找到下一个要显示的图像。

(2)找到当前要显示的子图像。

(3)翻译图像(如:视频输出插件不支持YIJV):调用最佳的YUV插件,做缩放比例,添加subtitles和一个可选的图像字段。

(4)在特定的时间到来之前休眠。

(5)视频插件显示图像(通常在缓冲器转换时输出)。p_vout->p_buffer是两个缓冲器(用于YUV的转换)的排列,p_vout->i_buffer_index表明当前显示的缓冲器。

(6)管理事件。

6.2视频解码器的方法

视频输出导出了一些函数,以便解码器能够发送它们已经解码过的数据。

(1)picture_t*vout_CreatePicture(vout_thread_t*p_vout,vlc_bool_t b_progressive,vlc_bool_t b_top_field_first,unsigned int i_nb_fields):

这个是最重要的函数,它分配视频解码器指示的缓冲器,然后用解码后的数据反馈到(void*)p_picture->p_data里,必要时调用vout_-DisplayPicture和vout_DatePicture。最后返回已分配好的视频缓冲器。比如i_type表示YUV_420_PICTURE,i_width和I_height表示像素。如果堆里没有图像的话,那么这个函数将返回为空。

(2)vout_Lillkpieture(vout_thread_t*p_vout,pieture_t*p_pic):

增加图像的引用计数,以便在解码器还需要它的时候不会意外将它释放。比如说一个I图片或者P图片在已经被解码和交叉存取为B图片后可能还要用到。

(3)vout_UnlinkPicture(vout_thread_t*p_vout,picture_t*p_pic):

减少图像的引用计数,等于0时就可以将图像释放。

(4)vout_DatePicture(vout_thread_t*p_vout,picture*p_pic):

赋予图像的播放时间,可以在图像在开始播放时就知道它什么时候播放结束。比如说当赋予I或则P图片时间时,必须等到先前所有的B图片全部被解码完。

(5)vout_DisplayPieture(vout_thread_t*p_vout,picture_t*p_pic):

通知视频输出端一个图像已经被解码等候显示。可以在vout_DatePicture前后调用。

(6)vout_DestroyPicture(vout_thread_t*p_vout,picture_t*p_pic):

为图片做空标记,减小视频输出的内存堆的尺寸(在流解析错误时非常有用)。

(7)subpieture_t*vout_CreateSubPicture(vout_thread_t*p_vout,int i_channel,inti_type):

返回到一个已分配好的子图片缓冲器。i_channel表示子图片信道的ID,i_type表示是DVDSUBPICTURE还是TEXTSUBPICTURE,i_size表示包的字节长度。

(8)vout_DisplaySubPicture(vout_thread_t*p_vout,subpicture_t*p_subpic):

通知视频输出一个子图象已被完全解码,废除解码前的子图象。

(9)vout_DestroySubPicture(vout_thread_t*p_vout,subpicture_t*p_subpic):

为子图片做空标记。

7 音频输出层技术

7.1 音频输出概况

音频输出的主要目的是从一个或者几个解码器(也叫“输入流“)得到声音采样,然后混合它们并且写到输出设备(也叫“输出流”)。在这个过程中,转换可能需要,也可能被用户要求,由音频过滤器来完成这个过程。

以下为常用的音频术语:

采样:采样是音频信息的基本部分,包括所有信道的值。例如不管有多少信道被编码也不管系数的编码类型,流工作在44100HZ就是表明流每秒采样44100个音频元素。

帧:任意大小的一组采样。编解码通常有固定的帧大小(比如一个A/52帧含有1536个采样元素)。既然帧以任意大小的方式管理缓冲器,因此它对于音频输出不是很重要。但是对于还没被解码的文件格式,由于要依赖于流的压缩率,因此必须要指明一个有n个采样元素的帧所需要的字节数。

系数:一采样元素对于每个信道都含有一个系数。例如一个立体声流的每一个元素里有两个系数。许多音频项(例如float32位的音频混音器)直接处理系数。由于一采样元素不能单独的在流里被事例化,因此一个没被解码的元素格式当然就没有系数的概念。

重采样:改变音频流每秒的采样数。

上下混频:改变信道的配置。

7.2 音频采样格式

整个音频输出可以别看成以连续的步骤来把一种音频格式转换成另一种音频格式的管道,这对于理解音频采样格式非常重要。audio_sample_format_t结构被定义在incfude/audio_output.h里,它包括以下的成员:

(1)i_format:

定义了系数的格式。比如:’fl32’(float32),’fi32’(fixed32),’s16b’(有符号的16bit字节以big-endian方式存储),‘s161’(有符号的16bit字节以little-endian方式存储),AOUT_FMT_S16_NE(‘sl6b’或者’s16l’的截短),’u16b’,’u16l’,’s8’,’u8’,’ac3’,’spdi’(S/PDIF)。没被解码的采样格式包括:’a52’,’dts’,’spdi’,’mpga’(MPEG音频I,II层),’mpg3’(MPEG音频III层)。音频过滤器(允许从一种格式到另一种格式)被定义为转换器,有些转换器扮演着解码器的角色(比如a52tonoat32.c),但是实际上它是音频过滤器。

(2)i_rate:

定义了音频输出每秒要处理的采样数。通常这个值为22050,24000,44100,48000,单位为HZ。

(3)i_physical_channels:

定义在缓冲器里被自然编码的信道。它掩盖了在audio_output.h定义的比特值(比如AOUT_CHAN_CENTER,AOUT_CHAN_LEFT等)。要注意的是:这个数字值并不代表每个采样含有多少个系数(aout_FormatNbChannels()里有说明),由于对于混音器处理交叉存取的系数是比较容易的,因此每个信道的系数也总是交叉存取的,从而一个来可以输出平面数据的解码器必须能实现交叉存取的功能。

(4)i_original_channels:

定义用于组成缓冲器的原始流的信道。比如说只有单一输出的插件但要输出的音频流是立体声时,那么可以同时使用流的信道(i_original_channels == AOUT_CHAN_LEFT| AOUT_CHAN_RIGHT)或者选择一个信道。

i_original_channels和i_physical_channels使用同一个比特掩码,并且表明了专用的比特流AOUT_CHAN_DOLBYSTEREO(表明输入流是否下行混音成杜比环绕声)和AOUT_CHAN_DUALMONO(表明立体声流实际上是由两个单一的流所组成的),这两个专用的比特流只能选择一个(就像在一个VCD上可以有两种语言)。

对于16位的整型格式的音频类型,可以区分开big-endian和little-endian的存储类型。但对于浮点型的音频类型(可以以big-endian存储或者用little-endian存储)却无法分别开来。这是因为采样的数据是在文件里被强制储存成32位的浮点类型并且从一个机器转移到另一个机器中,因此这32位的浮点类型数据以本地机器的endian来默认储存。然而采样的数据一般都是以16位的整型类型big-endian方式来储存的(比如DVD的LPCM格式),因此LPCM解码器分配’s16b’,给输入流,在little-endian机器上也是这样,’s16b’->’s16l’的转换被输入管道自动的激活。(在大多数情况下,AOUT_FMT_S16_NE和AOUT_FMT_U16_NE要被用到)。音频输出内核提供了宏来比较两种音频采样格式。AOUT_FMT_IDENTICAL)用来验证是否i_format,i_rate,i_physical _channels和i_original_channels一样,AOUT_FMT_SIMILAR()用来验证i_rate和i_channels是否一样(有利于写一个纯粹的转换过滤器)。

audio_sample_format_t结构包含两个额外的参数(除非处理没被解码的音频格式就不需要有)。对于PCM格式这两个参数被自动添充到aout_FormatPrepare()(必要时被内核模块所调用)。下面说明这两个额外的参数:

i_frame_lenth:定义普通帧采样的数目。对于A\52,1536个采样是压缩后在没被解码的缓冲器里的数目,那么i_frame_length=1536;对于PCM格式,帧的大小为1(在缓冲器里的每一个采样值都能独立的获得)。

i_bytes_per_frame:定义帧的大小(字节)。对于A\52它取决于输入流(同步读取)的比特率,例如对于32位浮点类型的立体声采样,i_bytes_per_frame == 8(i_frame_length=1l)。

以上两个参数(对于调用aout_FormatPrepare()非常有意义)方便计算音频过滤器(i_nb_samples*i_bytes_per_frame/i_frame_length)的大小。

       7.3 音频运行过程

输入派生了一个新的音频解码器(比如A\52解码器)。A\52解码器为格式信息解析同步信息并且用aout_InPutNew()来创造出输入流的新的音频输出,这个采样格式为:

i_format=’a52’

i_rate=48000

i_physical_channels=i_original_channels=AOUT_CHAN_LEFT|AOUT_CHAN_RIGHT|AOUT_CHAN_CENTER|AOUT_CHAN_REARLEFT|AOUT_CHAN_REARRIGHT|AOUT_CHAN_LFE

i_frame_length=1536

i_bytes_per_frame=24000

这个输入格式不能被修改且存储在aout_input_t structure里和p_aout->pp_input[0]->input这个输入流一致。既然这个是输入的第一个流,音频输出将会用这个音频采样格式(p_aout->output.output),来配置输出设备以避免不必要的转换。

音频内核用通常的方式来探测输出模块,输出模块的行为取决于输出设备,如果输出设备有S/PDIF能力的话,那么就需要设置p_aout->output,把output .i_format转换成’spdi’格式,如果只是PCM设备的话就需要知道本地的采样格式(比如用于Darwin CoreAudio的’fl32’或者用于OSS的AOUT_FMT_S16_NE)。输出设备也可能因信道的数目和流的速率不同而受到限制(例如p_aout->output)。输出结构可以如下:

i_format =AOUT_FMT_S16_NE

i_rate =44100

i_channels=AOUT_CHAN_LEFT|AOUT_CHAN_RIGHT

i_frame_length=1

i_bytes_per_frame=4

一旦获得了输出的格式,那么需要根据它来推出混音器的格式。除了i_format以外,禁止在混音器和输出之间来改变音频采样格式(所有的转换在输入管道里发生),这是因为需要开发出三个混音器(内置设备的float32 and S/PDIF和plus fixed32),所有其它的类型必须转化成这三个中的一个。下面以p_aout->mixer为例,混音器的结构如下:

i_format =’fl32’

i_rate=4410

i_channels=AOUT_CHAN_EFT|AOUT_CHAN_RIGHT

i_frame_length=1

i_bytes_per_frame=8

音频输出内核因此要分配音频过滤器来把’fl32’转换成AOUT_FMT_S16_NE,这是在输出管道里唯一的音频过滤器,同时也要分配一个32位浮点类型的混音器。由于只是一个输入流,因此用一个很小的混音器就好了(只需从第一个输入流里复制采样数据即可),否则要用到更准确的32位浮点类型的混音器。

初始化的最后一步是编译输入管道。当需要更改几个属性时,音频输出内核将会优先搜索音频过滤器可以更改的属性:

(l)所有的参数

(2)i_format和i_physical_channels/i_original_channels

(3)i_format

如果整个的转换不能用一个音频过滤器来完成的话,音频内核就需要分配第二个甚至第三个过滤器来处理剩余的转换。接着上面的例子,分配两个过滤器:

a52到float32(用来处理这种转换和下行混音)和重采样器。出于对效率的考虑,通常对于没被解码的格式转换器也要处理下行混音。

当初始化完成后,解码器插件模块就会运行自己的主循环。通常解码器需要i_nb_samples大小的缓冲器并且复制没被解码的采样值到这个缓冲器里(用到GetChunk()函数)。然后这个缓冲器和输入管道一起编码(为’fl32’格式),下行混音和重采样。如果输出层需要暂时的调节其快慢速率以获得和输入流完美的同步(由每一个缓冲器的基层来确定)时就需要重采样。在输入管道的底端,这个缓冲器将放置到FIFO上,同时解码器线程运行音频混音器。

这时音频混音器就会计算采样数是否足够多来编译一个新的输出缓冲器。如果足够的话,它就和输入流混合经过缓冲器到达输出层。为了输出设备缓冲器就顺着输出管道(在上面的例子里只含有转换过滤器)到达输出FIFO。这时输出设备就会从输出FIFO上取出下一个缓冲器,取出的方法为通过回调音频子系统(Mac OS X’CoreAudio,SDL)或者通过音频输出线程(OSS,ALSA…)来达到。这种机制用到了aout_OutputNextBuffer()函数,并且给出了缓冲器大致的播放时间。如果计算出的播放时间和估计的播放时间不一致时(小的差别),输出层就会在音频输出模块上修改所有的缓冲器时间,当下一个从解码器出来的缓冲到来的时候就会在输入管道的开始处触发再采样。用这种方法需要重同步音视频流,当缓冲播放时,就会最终释放它。

7.4 互斥排除机制

posted on 2012-02-09 16:15  zzwworld  阅读(8737)  评论(1编辑  收藏  举报

导航