使用 DirectShow 播放音频和视频






4.56/5 (26投票s)
本项目演示了 DirectShow 及其相关概念的基础知识。
引言
本项目演示了一个简单的音频/视频播放器,它使用 DirectShow API 来播放音频和视频文件。它不支持所有文件格式,但可以处理 MP3、AVI 等 DirectShow 支持的常见格式。对于那些刚开始接触 DirectShow 并想了解 COM 和 DirectShow 相关基础知识的人来说,本项目是一个有用的指南。
背景
简单回顾一下,DirectX 是微软提供的一个先进的多媒体框架,它使执行所有多媒体任务变得容易。从播放音频文件到播放互联网流媒体视频,DirectX 都能够满足需求。
DirectX 是一个庞大的库集合,其中包括 DirectSound、Direct3D、DirectAnimation、DirectDraw、DirectShow 等组件。每个组件都专注于多媒体相关的特定领域。然而,DirectShow 一直被包含在微软随 Windows 发布的 DirectX 核心包中,也曾被移除。截至本文撰写之时,DirectX10 是随 Vista 一起发布的最新版本 DirectX。
整个 DirectX 库都基于 COM(组件对象模型)接口,因此在处理 DirectX 时,对 COM 有一些了解会很有帮助。
DirectShow
正如您可能已经猜到的,DirectShow 是一个基于 COM 的多媒体框架,它使得捕获、回放和操作媒体流的任务变得更加容易。它以前被称为 ActiveMovie,之后才更名为 DirectShow。DirectShow 在播放媒体时可能会在内部使用 DirectSound 和 DirectDraw,前提是硬件支持;否则,它可能会使用传统的 Wave API 和 GDI API。
DirectShow 概念
Filter(过滤器):从技术上讲,过滤器只是符合 COM 接口的 C++ 类,它们接收某种形式的输入,处理数据,并以相同或不同的形式产生某种输出。过滤器代表媒体类型处理中的每个阶段。
Pin(引脚):引脚本身就是一个接口,每个过滤器都必须实现至少一个引脚。引脚是过滤器与其他过滤器进行通信的方式。通常,一个过滤器可能有输入引脚和输出引脚。在媒体处理过程中,一个过滤器通过其输入引脚接收输入,处理数据,并通过输出引脚将输出传递给另一个过滤器。顾名思义,另一个过滤器通过其输入引脚接收处理后的数据。因此,基本上,一个过滤器的输出引脚连接到另一个过滤器的输入引脚,它们就是这样通信的。
Filter Graph(过滤器图)和 Filter Graph Manager(过滤器图管理器):过滤器图是连接过滤器的序列。任何类型的媒体处理都涉及一个具有最适合该媒体类型的特定过滤器集的过滤器图。过滤器图管理器是允许您创建这些过滤器图、将过滤器添加到过滤器图中、连接适当过滤器的引脚,并最终运行该图以完成我们目标的“神圣力量”,如果您还记得的话,那就是播放媒体。虽然上面听起来可能令人生畏,但实际上相当简单。只需要很少的 API 就可以实现所述功能。但是,如果您确实喜欢进行细粒度控制并确保细节正确,也提供了选项。但是,大多数事情都可以通过让图管理器完成繁重的工作来完成 :)。
下图显示了 MP3 媒体的示例过滤器图
(图片质量抱歉,我搞砸了。)
Source Filter(源过滤器)是过滤器图中的第一个过滤器。此过滤器从文件、URL 或捕获设备获取输入,并将数据处理成其他过滤器可以使用的形式。源过滤器的特点是它没有输入引脚,只有一个输出引脚。注意:引脚仅限于过滤器之间的秘密。Renderer Filter(渲染过滤器)是过滤器图中的最后一个过滤器。此过滤器从前一个过滤器接收输入,并将其渲染到声音设备、显示设备或文件(在捕获的情况下)。请记住,它可能在内部使用 DirectSound 或 DirectDraw。渲染过滤器的特点是它有一个输入引脚,没有输出引脚。源过滤器和渲染过滤器之间的所有内容都称为Transform Filter(转换过滤器)。转换过滤器可能有一个或多个输入引脚和一个或多个输出引脚。
有关 DirectShow 架构和相关概念的更多详细信息,请参阅此 MSDN 文档:http://msdn.microsoft.com/en-us/library/ms783323(VS.85).aspx。
您需要了解的 COM 相关知识
在您的应用程序可以使用 COM 之前,必须先初始化 COM 库。这可以通过使用 CoInitialize
或 CoIinitializeEx
API 来完成。CoCreateInstance
API 可用于获取特定 COM 类实例的接口指针。如果这听起来令人困惑,那么
//code snippet I
IGraphBuilder *m_pGraph;
HRESULT hr;
hr = CoCreateInstance(CLSID_FilterGraph,
NULL,
CLSCTX_INPROC_SERVER,
IID_IGraphBuilder,
(void **)&m_pGraph);
if (SUCCEEDED(hr))
{
//celebrate
...
}
第一个参数指定创建一个过滤器图类的实例。CLSID_FilterGraph
是唯一标识该类的类 ID。每个 COM 类都有一个唯一的 ID。倒数第二个参数指定您正在寻找 Graph Builder(还记得我们的过滤器图管理器吗,就是这个家伙)接口。IID_GraphBuilder
是唯一标识图构建器接口的接口 ID。最后,最后一个参数 m_pGraph
将指向/持有图构建器接口。
每个 COM 接口都实现了 IUnknown
接口,该接口包含最重要的 QueryInterface
方法。我们在接口上调用 QueryInterface
方法以获取该类可能实现的另一个接口的指针。例如(接上一个代码片段)
//code snippet II
IMediaControl m_pMediaControl;
hr = m_pGraph->QueryInterface(IID_IMediaControl,
(void **)& m_pMediaControl);
我们正在调用我们在之前获得的 m_pGraph
上的 QueryInterface
方法。第一个参数指定我们正在寻找接口 ID 为 IID_IMediaControl
的 IMediaControl
接口。并且,如果该类(CLSID_FilterGraph
)实现了 Media Control 接口,m_pMediaControl
变量将指向/持有 IMediaControl
接口。
最后,别忘了在应用程序退出时取消初始化 COM 库。CoUninitialize
API 用于执行此操作。
这绝不是 COM 的全部信息。我只提到了任何应用程序都需要使用的基本要点。COM 的内容远超您的想象,幸运的是,谷歌有一个相当不错的算法 :)。
应用程序必须做什么
任何想要使用 DirectShow 的应用程序都必须遵循以下步骤
- 创建过滤器图管理器
- 使用过滤器图管理器创建过滤器图
- 运行过滤器图(以播放媒体)
- 释放资源
创建过滤器图管理器
可以使用 CoCreateInstance
APP 创建过滤器图管理器。请参阅代码片段 I。
创建过滤器图
有三种方法可以创建过滤器图:自动图创建、手动图创建和半自动图创建。
自动图创建:这是所有方法中最简单的一种,但它没有给您的应用程序提供太多选择过滤器的灵活性。在这里,您只需调用 IGraphBuilder
接口上的 RenderFile
方法,传入文件名或其他源。图管理器将尽一切努力为您创建合适的图。图构建器有自己的算法来选择各种过滤器,然后连接它们的引脚以完成图。例如,如果您传入文件名“love.mp3”,图构建器将识别文件格式,然后选择合适的过滤器来构建适合 MP3 媒体的图。
手动图创建:此方法最麻烦,但为您选择过滤器提供了极大的灵活性。我不会详细介绍,但最抽象地说,首先,您必须创建所需单个过滤器的实例。然后,将每个过滤器添加到图中。此时,过滤器就像桌子上的豆子一样躺在图中。下一步是连接这些过滤器的引脚;这再次很麻烦,因为它涉及枚举过滤器的引脚,选择一个过滤器的输出引脚和另一个过滤器的输入引脚,然后将两者连接起来。一旦您连接了所有引脚,图就完成了,可以运行了,假设您没有弄乱引脚 :)。
半自动图创建:此方法在提供灵活性的同时,也试图减少麻烦。在这里,您只需选择一些优先级过滤器(您最想在图中包含的过滤器),然后将它们添加到图中。这些过滤器就像桌子上的豆子一样躺在图中。然后,您要求图管理器完成图。图管理器将添加缺失的部分(过滤器)并为您构建一个图。这里需要注意的一点是,图管理器会尽最大努力将您的优先级过滤器包含在最终图中,因此,通过这种方式,您可以控制覆盖图管理器原本会选择的一些默认过滤器。
运行过滤器图
一旦过滤器图准备就绪,就可以通过调用 IMediaControl
接口的 Run
方法来播放媒体。这将分别开始在默认音频和视频设备上播放音频或视频文件。
释放资源
在应用程序退出之前,请确保释放所有获取的资源/指针。
使用代码
该项目是使用 VC++6.0 构建的。我想提的一点是,项目在其设置中包含 STRSAFE_NO_DEPRECATE
预处理器指令。这是因为我在 sprintf
(以及可能所有与字符串相关的函数)上遇到了一些错误,提示“error C2065: 'sprintf_instead_use_StringCbPrintfA_or_StringCchPrintfA' : undeclared identifier”,稍微谷歌一下就找到了这个解决方案。
要在您的计算机上构建该项目,您需要安装“Platform SDK”和“DirectX SDK”。详细信息可以通过谷歌搜索找到。但是,如果遇到麻烦,请告诉我,我将提供链接和关于如何设置环境的简要说明。
播放器的 UI 如下所示
只需单击“Play File…”按钮打开文件,媒体就会开始播放。您可以暂停/播放或停止媒体。总时间和经过时间以及文件名也会显示出来。进度条的存在只是因为我想尝试一下。我知道滑块控件会更好;我将致力于将其替换为滑块控件,并且可能还会在旁边添加一个播放列表。
- //DSPlayer.cpp:此文件包含
main
函数,创建主对话框,并具有消息循环。main
函数使用CoInitialize(NULL)
初始化 COM 库。关于该文件唯一值得一提的是,它有一个类型为PlayerClass
的全局变量g_PlayerObject
。PlayerClass
类包含了真正的核心功能。此文件中的 main 函数创建了一个PlayerClass
类型的全局对象,并调用对象上的Initialise
来初始化成员变量。之后,在消息循环中,每个触发的事件(例如按下 UI 上的按钮)都会调用g_PlayerObject
对象的相应成员函数;这非常容易理解。 - //PlayerClass.cpp:此文件包含播放器类的实际实现。类的成员变量和成员函数都是不言自明的。
与 DirectShow 相关的成员变量是 IGraphBuilder
、IMediaControl
、IMediaSeeking
和 IMediaEventEx
。
IGraphBuilder
是图管理器,负责创建图。IMediaControl
使我们能够在图准备就绪后运行它。IMediaSeeking
允许我们对媒体执行查找操作。IMediaEventEx
向应用程序提供有关媒体状态的通知。例如,它会通知应用程序媒体已播放完毕。
在 Initialise
成员函数中,我们创建图构建器并获取其他接口。
CoCreateInstance(CLSID_FilterGraph,
NULL,
CLSCTX_INPROC_SERVER,
IID_IGraphBuilder,
(void**)&pGraphBuilder);
hr = pGraphBuilder->QueryInterface(IID_IMediaControl,
(void **)&pMediaControl);
hr = pGraphBuilder->QueryInterface(IID_IMediaSeeking,
(void**)&pMediaSeeking);
hr = pGraphBuilder->QueryInterface(IID_IMediaEventEx,
(void**)&pMediaEventEx);
我们设置了通知窗口,以便我们的应用程序知道媒体何时结束。
pMediaEventEx->SetNotifyWindow((OAHWND)hOwner,
WM_GRAPHNOTIFY,
0);
OpenFileDialog
成员函数处理文件浏览器界面,并将用户选择的文件名存储在 szFileName
成员变量中。然后,它调用 StartPlayingFile
私有成员函数来开始播放媒体。
StartPlayingFile
函数创建过滤器图并获取媒体的持续时间。
pGraphBuilder->RenderFile(wFileName, NULL);
pMediaSeeking->GetDuration(&lDuration100NanoSecs);
该函数还创建一个一秒计时器,以便我们可以跟踪经过的时间,然后使用 Run
方法播放文件。
SetTimer(hOwner,
MY_TIMEREVENT,
1000,
(TIMERPROC)NULL);
pMediaControl->Run();
其他 API 包括
pMediaControl->Pause(); //to pause the media
pMediaControl->Run() ; //resumes the media if it was previously paused
pMediaControl->Stop() ; //stops the media
非常简单。
要对媒体执行查找操作,我们使用 IMediaSeeking
接口的 SetPositions
函数。例如,我选择在媒体播放完毕后将其查找(seek)到其起始(零)位置。使用 IMediaEventEx
接口,我们可以知道媒体何时播放完毕,然后将其查找(seek)到开头。
pMediaSeeking->SetPositions(&rt,
AM_SEEKING_AbsolutePositioning,
NULL,
AM_SEEKING_NoPositioning);
DoTimerStuff
成员函数处理每秒触发一次的计时器事件,并在标签上显示经过的时间。
关注点
起初,进度条让我很烦恼。我认为使用进度条最简单的方法是将进度条的范围设置为从零到媒体持续时间(以秒为单位)。所以,每当一秒计时器触发时,我就会将进度条的值加一,这样就应该处理好所有事情了。但我在使用此方法时遇到了问题。出于某种原因,进度条的步进不正确,并且始终与媒体不同步。我的意思是,当媒体刚开始播放时,进度条已经到达了中间点。
因此,我决定反过来做,即设置进度条的范围从零到一百,然后将经过的时间在此尺度上归一化。例如,如果一个媒体长度为 300 秒,并且已经过去了 30 秒,那么这意味着媒体已经播放了 (30/300) *100 %,即 10%。因此,非常简单,我会将进度条设置为 10。这对我来说确实很顺利,尽管实现部分有点令人沮丧。但嘿,有什么管用就好 :)。
历史
- 更新:2008 年 6 月 21 日。
- 更新:2008 年 7 月 2 日。