这是很久以前的东西了,那些地方不准确的还请见谅
DVD程序的开发
本节介绍一种编写桌面DVD解码应用程序的简单方法。描述了用C++创建 DVD应用程序的主要步骤。
1、DVD程序基本概念
当编写一个DVD程序时,开发者不仅需要提供用户界面代码还需要调用微软DirectShow应用程序接口来控制DVD回放和导航命令。此方法涉及调用组件对象模型和DVDGraphBuilder对象的CoCreateInstance方法,DVDGraphBuilder将实现创建DVD过滤图形,当创建过滤图形后,将会获得指向接口IDVDControl2和IDVDInfo2的指针,有了接口指针,就可以连接你的用户界面到DVD的导航命令,如播放,暂停等。此处理非常简单和创建别的DirectShow应用程序十分相似。如果读者如果对DirectShow不清楚,可以参阅DirectX8.0开发文档。
2、配置DVD的过滤图形
通常,应用程序开发者不需要关心DVD过滤图形的细节,因为它是自动被创建的。但如果需要建立个人过滤可参阅DirectX8.0参考,这里不作详尽描述。下图解显示了典型的DVD过滤图形。
DVD导航者是DVD回放过滤图形的第一个过滤者,主要处理以下一些任务:
· 从碟片读取导航信息和视频数据。
· 把视频与音频分离,把子图数据分离到单独的数据流中。
· 将提取的流数据应用到图表以供将来处理和最终的渲染。
· 通知DVD应用程序的相关事件。
在音频流中,DVD导航者连接到底层音频解码,而音频解码连接到默认DirectShow过滤设备的默认音频处理。在视频和子图流中,底层过滤是第三方视频解码、Overlay Mixer和视频渲染。如果应用程序处理line21解码过滤关闭标题数据,就必须增加DirectShow line21 解码过滤到相应的图形,这包括单一方法调用,过滤器将会自动连接视频解码和Overlay Mixer。
应用程序通过定制DVD导航中IDVDControl2的“set”成员函数和IDVDInfo2中的“get”成员函数来通讯和控制DVD导航。过滤图形管理必需通过IMediaControl接口来控制停止、播放等其它图形控制。开发者可能会需要控制其它的单独过滤,例如Overlay Mixer过滤在窗口与全屏间切换,可以参阅IMixerPinConfig2用法。图形的准确配置会随安装何种类型的mpeg2解码来判断是否处理line21 closed-captioned数据和其它因素。
3、建立DVD过滤图形
创建回调过滤图形的一种简单方法是让DvdGraphBuilder 对象自动进行创建,此方法比较接近下面演示的例子DVD 应用程序。如果需要手动建立DVD过滤图形,则要遵循DirectShow的基本规则,通常情况下,在由DvdGraphBuilder建立的图形中,不应手动加载、删除、连接或断开连接单独的过滤,因为这样可能会使其清除代码混乱。
在创建DvdGraphBuilder对象实例之后,应用程序可以简单的调用IDvdGraphBuilder::RenderDvdVideoVolume成员函数,该成员函数将从本地的有效DirectShow过滤和mpeg2解码中建立过滤图形。
建立过滤图形之后,应用程序即可获得DVD向导控制、过滤图形管理和视频窗口的指针。以下代码是除过错误效验和其它无关代码的最简编写过程、基本步骤。完整代码可在DVD例程中找到(DirectX8 SDK DXF\samples\multimedia\dshow\DVD 目录下)
// 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,
// used 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, (LPVOID *)&m_pME) ;
...
// Use the graph builder pointer again to get the IVideoWindow interface,
// used 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, (LPVOID *)&pL21Dec) ;
4、处理DVD通报事件
当某种事件发生时,DVD导航将发送通报事件到应用程序的指定窗口,如: DVD区码改变,遇到一个新的父级管理层等。由事件参数可以获得事件的附加信息,错误信息和警告通常也采用这种方法来发送。应用程序的指定窗口会通过ImdeiaEventEx指针调用SetNotifyWindow来处理通报事件。如:
hr = m_pIME->SetNotifyWindow(reinterpret_cast<OAHWND>(m_hWnd), WM_DVD_EVENT, 0);
在上面的例子中,m_hWnd是接收消息和WM_DVD_EVENT的窗口句柄,WM_DVD_EVENT是应用程序定义的标记DVD发生事件的消息。事件自身是通过应用程序调用IMediaEvent::GetEvent来接收的。因为消息循环中可能会有超过一个的事件,而且随时都会增加,所以应用程序必需调用GetEvent循环进行接收,直到接收所有事件,如下代码所示:
while (SUCCEEDED(m_pIME->GetEvent(&lEvent, &lParam1, &lParam2, lTimeOut)))
{
HRESULT hr;
switch(lEvent)
{
case EC_DVD_CURRENT_HMSF_TIME:
{
DVD_HMSF_TIMECODE * pTC = reinterpret_cast<DVD_HMSF_TIMECODE *>(&lParam1);
m_CurTime = *pTC;
...
}
break;
...
}
DVD事件可能会在lParam1或 lParam2中包含附加信息,如上例子中所示,当前时间存放于lParam1中。上面的代码在DVD例程中的DVDcore.cpp中。想要得到完整的DVD 事件列表和别的参数,可参阅 DVD 事件通报码。
5、用DVD菜单进行工作
DVD菜单逻辑结构在DVD导航基础里有详细的讨论。应用程序要显示DVD菜单是通过调用IDvdControl2::ShowMenu成员函数,然后通过想要得到的菜单的索引来确定的。
要记住选择某个按钮只是使其边缘成为高亮状态,为了让关联命令起作用,按钮必需被激活。可用鼠标在DVD例子中的CDvdCore::OnMouseEvent函数里选择一个激活的DVD菜单的例子。
与IDvdControl2菜单相关的成员函数被应用程序用来编程控制DVD菜单。应用程序通过鼠标移动或者用户动作或其它编程逻辑来控制高亮或选择按钮。应用程序可以实现控制一个定制的菜单或者bmp绘制的按钮的控制。在DVD例子程序中,相关的菜单按钮命令被连接到键盘的方向键。(参阅CDvdCore::OnKeyEvent)可以用不同的方法,来编程激活一个按钮,但按钮在被激活之前必需一直选定。
音频和子图流
DVD碟片可以支持到8路音频流,标号是从0―7,每一个可以有不连续的6路通道。(注意音频和子图流标号是从0-7,但标题,angles,和父层标记是从1开始。)在同一时间里,这些流只能有一个可以被选中。对于子图,可以有32个有效数据流,但只能有一个在同一时间被选激活。碟片一般会使用默认的音频和子图流,但应用程序可以通过有效流的列表让用户进行选择一个他们喜欢的风格。基本的处理步骤和音频和子图流是相同的。
6、决定有效流数目。
可以通过流来获得每个流的品质状态。获得本地标识符返回的语言码后翻译为可读字符串。还可以通过列表或其它用户接口控制来允许用户选择一个首选的流。基本步骤在DVD例子应用程序Dialogs.cpp中的CAudioLangDlg::MakeAudioStreamList函数中。
7、执行父级管理层
DVD视频碟片中的任何标题或标题的一部分都能被赋予一般父级管理层(PML),标记从1-8。当DVD导航读到的内容含有PML,即意味在父层块中。
父层块可以由部分章节、多个章节或多个标题组成。DVD导航本身不能执行PML,它只是当在碟片中遇到PML时通告应用程序。默认方式会忽略此信息而直接处理最高等级的内容。为了执行PMLs,应用程序必需执行一些与用户联系的密码控制逻辑层,通知DVD导航发送PML通报消息,然后才响应事件,允许或拒绝。(在启动时应调用IDVDControl2::SetOption方法,参数DVD_NotifyParentalLevel变为真)
一个DVD标题可以拥有一个完全的PML,但碟片制作者可以赋予标题的某一部分更高或更多局限性的PMLs,这就需要调用临时PML命令。这些命令通常包含两个分支结构:一个是临时PML命令被应用程序接受,另外一个则是命令被拒绝。
事件应遵循以下顺序。当遇到临时PML命令时,DVD导航读取视频目录。然后检测内部标志以决定应用程序是否请求通报这个事件。如果没有设定标志,则继续播放DVD,以后的父级层改变将被拒绝。如果标志存在,DVD发送一个EC_DVD_PARENTAL_LEVEL_CHANGE事件给应用程序,然后处于暂停状态直到收到响应。当应用程序接收到事件,将使用自身的逻辑决定是否接收此命令,然后调用设置成为“真”或“假”的IDvdControl2::AcceptParentalLevelChange。如果设置为真,DVD导航继续在父层接受改变的情况下进行播放,反之,则继续在父层拒绝改变的情况下进行播放。
8、存储和恢复DvdState对象
DvdState对象允许应用程序保存用户任务的快照,如当前碟片的位置、个人特征父层被选择音频与子图流等信息。故用户可以保存其位置、状态以便今后从定制的地方继续播放。
应用程序不能创建DvdState对象。这些对象是当应用程序调用IDvdInfo2::GetState时,DVD导航在其内部创建的。DvdState对象揭示了IDvdState接口,其目的主要是允许应用程序随意的查询某种信息。
在DVD例子程序中,CDvdCore::RestoreBookmark和CDvdCore::SaveBookmark函数演示了怎样保存和恢复DvdState对象。
9、DVD的文字管理
DVD碟片,特别是卡拉ok碟片,可能包含一个支持视频和音频的文本信息数据库。例如在卡拉ok碟片能包含歌曲名称,记录文字等。这些文字可以呈现超过一种的语言信息。这些字符串可以是随意的,碟片本身并不要求其存在,目前是由逻辑层的DVD卷标镜像方式来组织的。每个字符串都有一个表示符,用来表示其所属关系即在碟片哪部分,或给出这些字符内容的索引以供查询。字符串类型的子集定义为在DVD_TextStringType中列举。
文字有两种基本类型:结构标识和索引字符。类型值在0x01~0x20间的是结构标识,用数字编码来识别索引字符串属于何种逻辑结构。此结构非常类似DVD碟片的逻辑结构索引:如卷标,标题,章节等。索引字符则保留用户接口显示信息。准确使用索引字符的方法很难定义,因此DVD的制作者可以通过各种方法使用索引字符。
10、播放卡拉ok音频流
DVD导航可以播放DVD碟片的卡拉ok的音频流,但卡拉ok回放要求支持多通道卡拉ok混音解码。解码器必需支持DVD 卡拉ok属性设置(AM_PROPERTY_DVDKARAOKE)。
卡拉ok碟片是一种DVD视频碟片,有同样的导航结构。歌曲一般会被格式化为标题,各个标题的设置是基于表演者、歌曲类型、或者别的标准。卡拉ok和其它碟片的区别主要在音频流上。卡拉ok碟片包括所有的多同道音频流,通常为杜比、AC-3。通道0和1通常包含背景音乐,通道2到5包含结合的嗓音、音调、声音效果。一个卡拉ok应用程序可以控制每个辅助声道的音量和单独发音单元。
当DVD导航检测到卡拉ok索引并进入卡拉ok模式时,会通知解码器三个辅助通道,哪个应该静音,直到任何一个或所有都被明确的通过应用程序打开。基本卡拉ok应用程序处理任务步骤如下:
l 决定辅助通道数及其索引使用的IDVDInfo2方法。
l 提供用户接口:显示通道索引和允许用户任何时候控制辅助通道开或关闭。
l 使用IDVDControl2::SelectKaraokeAudioPresentationMode成员函数。
以上步骤的例子在DVDCore.app的GetAudioAttributes方法中。
11、同步DVD命令
熟悉DVD导航和要求应用程序有特殊功能的开发人员可使用命令同步。对于仅仅使用DVD导航或者应用程序不要求带有同步命令,可以在以后讨论此功能。
IDvdControl2中不同的回放方法,PlayTitle即是一个非同步表示。当方法被调用时,会立即在其执行完所有必要的步骤之前返回结果。在方法调用被成功返回和最终完成操作之间,其它命令可能会将DVD导航引导到其它DVD处理方式或者新的过虑器状态,这将阻碍方法调用完成所有必要的步骤的过程。例如,假定应用程序调用PlayTitle,用户立即点击停止按钮。如果命令不同步那么DVD导航将会在碟片播放到指定位置之前处理停止操作。这因为PlayTitle在彻底完成操作之前PlayTitle已成功的返回,所以当碟片开始播放时,DVD导航已经不再是正确的状态,随即产生错误。
因为许多方法的调用要依靠DVD导航的具体状态,因此应用程序需要一个同步命令。这就涉及以下两个功能。
重所周知,当一个异步操作被完成,最终结果和方法调用的返回值是截然不同的。要求可以处理并发命令,直到其操作完成前DVD导航都处于阻塞状态。DVD导航赋予应用程序这些能力以及一些同步命令选项。每个回放方法在IDvdControl2里都有两个参数被用来处理同步:一个是同步对象接口指针,一个设置DVD命令标识。应用程序可以使用这些参数,通过不同方法来实现同步。
步骤 1:非同步
为了保持非同步性质的方法调用,可以使用以下的语法,pDvdControl2是一个IDvdControl2的指针。
HRESULT hres = pDvdControl2->PlayTitle( uTitle,
DVD_CMD_FLAG_None, // = 0
NULL);
步骤 2:简单的非同步阻断
此步骤使用DVD命令标识来实现没有同步命令对象管理的简单的阻断。这是最简单的实现同步的方法,因为将不用管理同步对象或者处理通报事件,但该方法不能确定任何特殊命令的返回状态。
HRESULT hres = pDvdControl2->PlayTitle( uTitle,
EC_DVD_CMD_FLAG_Block,
NULL);
步骤 3:使用同步对象
此步骤的功能上相当于步骤2。提供另一种方法来处理阻塞,DVD导航不会直接处理通报事件。当应用程序传递该地址的指针时,将收到一个关联到具体命令的同步对象命令。调用对象的WaitToEnd函数将阻塞DVD导航,直到此成员函数彻底成功完成。在此期间,新的命令可能会被加到命令循环。确保当处理完成时释放对象。
IDvdCmd* pObj;
HRESULT hr = pDvdControl2->PlayTitle( uTitle, 0, &pObj);
if(SUCCEEDED(hr))
{
pObj->WaitToEnd();
pObj->Release();
}
步骤 4:使用事件在同一时间处理一个命令的同步
如果通过调用IDvdControl2::SetOption来设定EC_DVD_CMD_FLAG_SendEvents标志,则DVD导航在调用play方法时不被阻塞。反之,当命令开始和异步操作完成时,会分别给应用程序发送EC_DVD_CMD_START和EC_DVD_CMD_END通报事件。这就允许同步单个事件,而不需要管理IDvdCmd对象。但如果不使用IDvdCmd对象,将不能确定的知道是那个命令事件被关联。
在许多情况下,是不需要这样的信息,因此理解到这一层就已经可以了。
// Call PlayChapterInTitle using "SendEvents".
HRESULT hres = pDvdControl2->PlayChapterInTitle( uTitle, 0x2,
DVD_CMD_FLAG_SendEvents, NULL);
…
// In the event notification handler function
switch (lEvent)
{
case EC_DVD_CMD_END:
DoSomethingGeneric();
break;
}
步骤 5:同步使用事件和IDvdCmd对象
此方法提供了对命令同步的最大控制,但实现起来也颇复杂。允许应用程序获得关联的事件命令,因此应用程序可以定制对EC_DVD_CMD_END事件的响应。第一个例子演示单线程解决方案,第二个例子演示多线程解决方案。
// 全局变量
IDvdCmd* pGlobalObj=0;
…
// pGlobalObj is assigned by the Navigator BEFORE the event
// is issued; otherwise the event can occur at (*1) before
// pGlobalObj is assigned.
HRESULT hres = pDvdControl2->PlayTitle( uTitle,
DVD_CMD_FLAG_SendEvents, &pGlobalObj );
// (*1)
If( FAILED( hres)) {
pGlobalObj = NULL;
}
…
switch (lEvent)
{
case EC_DVD_CMD_END:
IDvdCmd* pObj = GetDvdCmd( lParam1 );
HRESULT hres = HRESULT( lParam2 );
If( NULL != pObj ) {
If(pGlobalObj == pObj ) {
…
pGlobalObj->Release();
pGlobalObj = NULL;
}
pObj->Release();
}
break;
Use this approach when your event loop is on a separate thread.
// In global code
IDVDCmd* pGlobalObj=0;
CcritSect globalCritSect;
…
{
CautoLock(globalCritSect );
HRESULT hres = pDvdControl2->PlayTitle( uTitle,
DVD_CMD_FLAG_SendEvents, &pGlobalObj );
If( FAILED( hres)) {
pGlobalObj = NULL;
}
}
…
switch (lEvent)
{
case EC_DVD_CMD_COMPLETE:
case EC_DVD_CMD_CANCEL:
{
CautoLock(globalCritSect );
IDvdCmd* pObj = GetDvdCmd( lParam1 );
HRESULT hres = HRESULT( lParam2 );
If( NULL != pObj ) {
If(pGlobalObj == obj ) {
…
pGlobalObj->Release();
pGlobalObj = NULL;
}
pObj->Release();
}
break;
}
12、DVD命令标志
DVD 命令标志位通过SetOption的dwFlags 参数来除去或代替同步对象时依据所使用特殊方法调用的步骤。DVD_CMD_FLAG_Block的使用如步骤2中所示,DVD_CMD_FLAG_SendEvents则在步骤4和步骤6例示。这些标志位均可同其它标志位进行逐位的OR运算。DVD_CMD_FLAG_Flush导致该方法被立即执行,无需等候视频在图形过虑器缓冲的一、两秒的延时。指定这个标志位会加快对用户输入的响应。在需要视频段顺次完成时,最好不要使用这个标志位。四个标志位及其意义如下表:
名称
值
注释
DVD_CMD_FLAG_None
0x00000000
无标志,即缓冲区在新命令到来之前不被刷新,当操作完成时没有事件被丢弃,DVD导航在该方法返回之后不被阻塞。
DVD_CMD_FLAG_Flush
0x00000001
清空缓冲然后立即开始在新的位置播放。
DVD_CMD_FLAG_SendEvents
0x00000002
通过给应用程序发送事件来通知DVD导航完成异步操作及其返回值。
DVD_CMD_FLAG_Block
0x00000004
阻塞DVD导航直到异步操作完成或被放弃。
13、处理碟片弹出
当用户将DVD从光驱中弹出,DVD导航将发送给应用程序一个EC_DVD_DISC_EJECTED消息。应用程序能安全的忽略这个消息,让DVD导航管理图形的状态。当碟片弹出时,应用程序也能同样处理EC_DVD_DISC_EJECTED方法实现定制行为,如果处理消息,必需通过SetOption方法设置DVD_ResetOnStop标志为TRUE,然后在应用程序关闭之前调用IMediaControl::Stop或者播放另外的碟片。
以上就是编制过程,希望大家参阅源代码进行理解,下面我们就讲解WebDvd程序和纯DVD解码部分的编程。