一. DVD基础知识
首先,我们来增加一点感性认识,看一下DVD光盘的文件系统(采用MicroUDF标准)。我们可以看到,典型情况下光盘上有两个文件夹:VIDEO_TS和AUDIO_TS(通常是空的,这里不作介绍)。VIDEO_TS下面一般包含三种类型的文件:.VOB文件、.IFO文件和.BUP文件。这些文件都是作什么用的呢?其实,.VOB文件是用来保存所有MPEG2格式的音视频数据的,这些数据包括影片内容、供菜单(Menu)和按钮(Button)以及多种语言字幕用的子图片(Sub-picture)流;.IFO文件则是控制.VOB文件播放用的,这个文件中可以找到有关何时以及如何播放.VOB文件数据的控制信息;而.BUP文件则是.IFO文件内容的一个备份(因为.IFO文件对于保证DVD光盘的正确播放起着至关重要的作用)。根据这三种文件的特性,我们也就可以理解它们在光盘介质上的排列顺序了:IFO-VOB-VOB...-BUP。
我们再来看一下各个具体的文件。VIDEO_TS.IFO文件,保存DVD光盘的视频管理器 (Video Manager,简称VMG)信息,主要是光盘的一些全局信息,比如光盘指定的播放区域、如何显示菜单等。VIDEO_TS.VOB文件,保存显示菜单的数据。类似VTS_xx_y.VOB的文件,保存各个视频节目。(注:这里"xx"是节目编号,从01到99,"y"是从0到9的编号;由于MicroUDF文件系统中一个文件最大只能1 GB,因此大多数影片不得不保存在多个VOB文件中。)VTS_xx_y.IFO文件,保存对应编号的VOB文件的音视频格式信息。
大致了解了文件系统,我们再来看一下DVD其他方面的基础知识。大家知道,一张DVD光盘主要包括三种媒体数据:视频(Video)、音频(Audio)和子图片。视频一般采用MPEG2压缩算法,视频流支持最多9个视角(Angle),支持Line 21 Closed Caption(模拟电视中的概念,主要是些文字信息);音频最多支持8条不同的流(即8种不同语言的配音),支持最多6声道的声音格式(具体格式可以是AC3、MPEG、LPCM、DTS、SDDS等),支持卡拉OK;子图片流最多可以支持32个(即提供多种语言的字幕)。DVD内容最主要的逻辑分类叫做“标题”(Title)(一个标题通常代表了一部电影,或者一段视频节目,一张DVD光盘最多可以有99个标题),每个标题又可以 最多分成999个“章节”(Chapter)(章节是便于用户随机访问的节点,每播放完一个章节的内容会自动播放下一个章节,或者跳回菜单)。
用户与DVD的交互,最主要是通过菜单来完成的。菜单有两种:视频管理器菜单(Video Manager Menu,简称VMGM,也叫做Top Menu或Title Menu)和视频标题集菜单(Video Title Set Menu,简称VTSM,也叫做Root Menu,尽管它不是真正意义上的“根”菜单)。VMGM允许用户进入主要的标题或者标题集(包含有一组标题)。如果进入标题集,就显示VTSM子菜单。VTSM菜单可以包含进入当前标题集中各个标题的按钮,还有音频、视角、字幕、章节等选择子菜单。VTSM菜单是可选的,可以不实现;当用户开始播放一张DVD,一般首先看到的就是VMGM菜单,除非这张光盘在制作时被设置成了自动播放第一个标题(此时DVD也可以根本不制作VMGM菜单)。
DVD还有父母锁功能。DVD中的视频内容可以打上父母管理级别(Parental Management Level,简称PML);级别可以从1到8,其中1是无限制级别,8是最高限制级别。这样,可以禁止孩子在未经父母同意的情况下观看成人电影。注意:父母锁功能需要播放器的支持。
DVD导航(Navigation)还有一个非常重要的抽象概念,就是域(Domain)。通俗地说,也就是DVD在特定播放状态下的操作许可。比如“选择按钮”命令必须在菜单状态下才有效,“快进”命令在停止或者菜单状态下是无效的。播放器必须使用域的概念,阻止用户向DVD驱动器发送无效的命令。下表列出了五个主要的域以及对应的操作说明:
Domain(域)
DVD导航器的工作内容
First Play(开始播放DVD时第一个播放的一段视频内容)
读取光盘上初始部分
Video Manager Menu(显示VMGM菜单时)
读取光盘的主菜单,菜单相关的指令都是有效的
Video Title Set Menu(显示VTSM菜单时)
读取VTSM菜单,或者设置音频、字幕、视角等的子菜单,菜单相关的指令都是有效的
Title(播放标题内容时)
菜单相关的指令都是无效的
Stop(停止时)
导航器不在工作,此时可以执行播放指令
另外,DVD的制作者还可以利用“用户操作控制”功能(User Operation Controls,简称UOPs)来限制用户的操作。这需要在光盘上记录这些限制记号。比如,大多数光盘都不允许用户在DVD播放处于First Play域时执行快进或者显示菜单命令。
二. DirectShow对DVD的支持
DirectShow对DVD播放提供了强力的支持。(DirectShow为支持DVD播放作了大量的工作,除了不提供MPEG2 Video Decoder Filter外。换句话说,必须提供第三方的MPEG2 Decoder,否则我们写的播放程序还是跑不起来的!)编写DVD播放程序,我们无须研究DVD的规格说明书;了解上面的这些基础知识就已经足够了。因为微软提供了一个叫DVD Navigator的Filter,帮我们完成了繁琐的DVD导航的任务;提供一个专门用于建立播放DVD的Filter Graph的COM组件(CLSID_DvdGraphBuilder)。我们所要做的,主要就是使用DVD Navigator的两个接口:IDvdInfo2,获得光盘的属性和导航状态;以及IDvdControl2,设置属性和执行播放操作。典型的播放DVD的Filter Graph如下:
典型的Filter Graph创建过程如下:
// Create an instance of the DVD Graph Builder object.
HRESULT hr;
hr = CoCreateInstance(CLSID_DvdGraphBuilder,
NULL,
CLSCTX_INPROC_SERVER,
IID_IDvdGraphBuilder,
reinterpret_cast<void**>(&m_pIDvdGB));
// Build the DVD filter graph.
AM_DVD_RENDERSTATUS buildStatus;
hr = m_pIDvdGB->RenderDvdVideoVolume(pszwDiscPath, m_dwRenderFlags, &buildStatus);
// Get the pointers to the DVD Navigator interfaces.
hr = m_pIDvdGB->GetDvdInterface(IID_IDvdInfo2, reinterpret_cast<void**>(&m_pIDvdI2));
hr = m_pIDvdGB->GetDvdInterface(IID_IDvdControl2, reinterpret_cast<void**>(&m_pIDvdC2));
...
// Get a pointer to the filter graph manager.
hr = m_pDvdGB->GetFiltergraph(&m_pGraph);
...
// Use the graph pointer to get a pointer to IMediaControl,
// for controlling the filter graph as a whole.
hr = m_pGraph->QueryInterface(IID_IMediaControl, reinterpret_cast<void**>(&m_pIMC));
...
// Get a pointer to IMediaEventEx,
// used for handling DVD and other filter graph events.
hr = m_pGraph->QueryInterface(IID_IMediaEventEx, reinterpret_cast<void**>(&m_pME));
...
// Use the graph builder pointer again to get the IVideoWindow interface,
// to set the window style and message-handling behavior of the video renderer filter.
hr = m_pIDvdGB->GetDvdInterface(IID_IVideoWindow, reinterpret_cast<void**>(&m_pIVW));
hr = m_pDvdGB->GetDvdInterface(IID_IAMLine21Decoder, reinterpret_cast<void**>(&pL21Dec));
关于IDvdInfo2和IDvdControl2的各个接口方法的详细说明和用法,请参见DirectShow SDK文档,以及SDK提供的例子代码DVDSample。下面,仅就编写DVD播放程序需要注意的地方,进行一些简单的罗列:
1. 为了得到DVD Navigator在创建时发出的事件,一般在RenderDvdVideoVolume 调用之前获得ImediaEventEx接口。
2. 菜单命令实际上有两种,一种是选中(Select),一种是激活(Activate)。前者如IDvdControl2::SelectAtPosition,IDvdControl2::SelectButton,IDvdControl2::SelectRelativeButton等,效果是高亮度显示被选中的菜单;后者如IDvdControl2::ActivateAtPosition,IDvdControl2::ActivateButton等,效果是产生相应的动作。也有选中加激活的命令,如IDvdControl2::SelectAndActivateButton。
3. DVD最多支持8条音频流、32条子图片流,但在同一时刻,都只能各自选中某一条。
4. DVD Navigator本身并不强调父母锁功能,而只是把光盘上的PML信息发送给应用程序。因此,父母锁功能需要在应用程序上完成。
5. 通过IDvdInfo2::GetState调用,可以得到IDvdState对象,用以实现“书签”的功能。具体实现参见DVDSample的CDvdCore::SaveBookmark和CDvdCore::RestoreBookmark两个函数。
6. 注意DVD播放时候的Filter Graph状态,需要同时考虑DVD Navigator的状态。
7. 支持卡拉OK的DVD,要求Audio Decoder实现AM_KSPROPSETID_DvdKaraoke属性集(即实现IKsPropertySet接口)。
8. IDvdControl2关于播放的接口方法,一般都有异步和同步两种调用方式。如果是异步方式,调用这些接口方法后,会立即返回,而并不等到实际的操作完成。有时候,这样操作会引起DVD Navigator的状态混乱。对于这些接口方法,微软推荐了有5种调用方法,下面列出常用的3种:
(1)异步方式(以PlayTitle调用为例,下同)
HRESULT hr = pDVDControl2->PlayTitle( uTitle,
DVD_CMD_FLAG_None, // = 0
NULL);
(2)阻塞方式
HRESULT hr = pDVDControl2->PlayTitle( uTitle,
EC_DVD_CMD_FLAG_Block,
NULL);
(3)使用同步对象
IDvdCmd* pObj;
HRESULT hr = pDVDControl2->PlayTitle(uTitle, 0, &pObj);
if(SUCCEEDED(hr))
{
pObj->WaitToEnd();
pObj->Release();
}
三. 总结
总之,DirectShow使我们从DVD的专业知识中解放出来;有了DirectShow的支持,编写一个DVD播放程序还是比较轻松的!再次提醒读者朋友们,必须要提供第三方的Video Decoder和Audio Decoder,否则,我们的DVD播放程序会陷入“万事具备,只欠东风”的窘境!