Eric Rudolph
Microsoft Windows Digital Media Division
摘要:本文使用您自己的自定义 grabber 筛选器示例,说明了如何从 Microsoft DirectShow 中的媒体流检索数据。本文使用了 Microsoft DirectX 8.1 SDK 中的 GrabberSample 筛选器示例作为起始点;本代码对于 DirectX 的较新版本也同样能够正常运行。
简介
开发人员对于 Microsoft® DirectShow® 的一个常见问题为:“如何从 DirectShow 获取数据,并将其应用到我的应用程序中?”因为 DirectShow 使用的是插件体系结构,所以有几种方法可用来解决这个问题。按照复杂程度从低到高的顺序,您可以:
1. |
使用多媒体流的 API。这些 API 简单而且同步,并且确实能够发挥作用。 |
2. |
编写一个简单的、用来获取数据的 DirectShow 筛选器。在您的应用程序中处理这些数据。 |
3. |
编写一个 DirectShow 筛选器,让它来执行所有处理。在这种情况下,复杂性大部分包括在了筛选器内部,应用程序只需完成很少的操作。 |
本文讲述的是第二种方法。第三种方法是这三种方法中最常见的,因为它能使得其他应用程序使用您的筛选器,但同时也是最难的。
注 有关多媒体流的 API 的详细信息,请参阅 DirectShow 文档中的多媒体流。
本文包括以下主题:
您需要了解的 DirectShow 基本知识 | |
编写 Grabber 筛选器示例 | |
使用多线程处理 | |
应用程序代码示例 | |
加快连接速度 | |
强制筛选器传递到缓冲区 | |
处理格式更改 | |
DirectShow Grabber 示例的限制 |
首先我们简要概述一下 DirectShow 及其工作原理。DirectShow 是一个流式体系结构,它使得应用程序能够通过一系列连接的对象来流式处理数据,这些对象称为筛选器。DirectShow 筛选器的集合称为筛选器图形。
DirectShow 筛选器大致可以分为三个类别:源筛选器、转换筛选器和输出程序。源筛选器创建数据,并将其推入下一个筛选器。转换筛选器接收数据并传输数据,有时会在多个线程上完成这个操作。输出程序则只接收数据。
每个 DirectShow 筛选器都至少有一个连接点,称为针。筛选器在它们的针处与其他筛选器进行连接。元数据通过针连接在筛选器之间进行移动。
图形状态
筛选器图形具有四种可能的状态:已停止、已暂停、运行中和转换中。在处于转换中状态时,图形正在从一种状态更改为另一种状态,但是由于 DirectShow 的多线程特点,这种更改尚未完成。
对于大多数筛选器来说,已暂停和运行中状态是完全相同的:源筛选器生成新数据,转换筛选器接受新数据以进行处理。这种规则的例外是实时捕获筛选器和输出程序筛选器。实时捕获筛选器只在运行时发送数据,暂停时不会发送数据。输出程序筛选器在暂停时会停止呈现数据,不接受任何新数据。
筛选器停止时,不再处理数据,也不再接受新数据。它会关闭辅助线程,并释放正在使用的任何其他资源。
当筛选器图形从一种状态更改为另一种状态时,筛选器必须遵守一个既定的协议。有关详细信息,请参阅 DirectShow SDK 文档中的用于筛选器开发人员的数据流主题。
多线程处理
要使用 DirectShow,您必须了解有关多线程编程的一些知识。对于一个简单的 DirectShow 应用程序来说,只需了解一点就足够了,那就是数据在图形中移动所基于的线程与应用程序线程是分开的。但是,如果您计划编写所有类型的筛选器,则要准备使用线程、临界区、事件和其他概念。您可能想忽略这些问题,但是您的筛选器很可能因此而无法正确执行,甚至会导致应用程序中的死锁。了解了这些问题之后,编写筛选器就会变得容易多了。
注多媒体流的 API 在很大程度上已经使您能够远离多线程问题了,这是使用这些 API 的优点之一。
下面是用于 DirectShow 筛选器中线程处理的通用指南:
• | 源筛选器 |
• | 转换筛选器 |
• | 输出程序筛选器 |
筛选器在暂停或者运行时,会创建所需要的任意多个线程,在停止时则会关闭这些线程。
针连接协商
两个筛选器进行连接时,这些针会协商要建立哪种类型的连接。具体细节取决于所涉及的筛选器,但通常情况下,针必须决定下列内容:
• | 要传递数据的类型(例如,音频或视频),以及数据的格式。 |
• | 所要使用的缓冲区大小、所要创建的缓冲区数量,以及所需的内存对齐。 |
• | 哪个筛选器将分配缓冲区。 |
本文将对上述一些问题进行讲述。有关更多详细信息,请参阅 DirectShow 文档。
从 DirectShow 筛选器图形获取数据的一个简单方式是编写一个自定义“grabber”筛选器示例。将该筛选器与您要监视的数据流进行连接,然后运行筛选器图形。当数据通过筛选器进行传递时,应用程序会根据您的想法对数据进行操作。
这些 grabber 筛选器示例的可能用途包括:
• | 将整个文件解码到内存缓冲区。 |
• | 从视频文件获取一个贴帧。 |
• | 从实时视频流获取静态图像。 |
• | 将视频文件解码到 Microsoft DirectDraw® 缓冲区中。 |
DirectShow 8.0 SDK 包括了一个 grabber 筛选器示例,但是没有提供源代码。DirectShow 8.1 SDK 则包括了这个 grabber 筛选器示例改进版本的源代码,作为一个 SDK 示例,其名称为 GrabberSample Filter Sample。
从何处开始?
首先您要选择是编写转换筛选器还是输出程序。转换筛选器可以连接另一个下游筛选器,使您能够呈现数据、将其写入文件,以及执行其他操作。但是因为转换筛选器需要一个附加的下游连接,所以要使其正确实现可能更复杂一些。输出程序筛选器则只需要一个输入连接。
本文将说明如何编写一个转换筛选器,但是很多理念同样适用于输出程序筛选器。
本文讲述的这个筛选器是一个“就地转换”筛选器,这就表示它会直接在它接收的缓冲区中修改数据,而不会创建新缓冲区的副本。它使用 DirectShow 基类库。
要编写一个就地转换筛选器,请执行下列步骤:
1. |
定义一个从 CTransInPlaceFilter 类派生的新类。 |
2. |
您可以使筛选器成为一个能够执行自注册的真正的 COM 对象。为此,您需要一个带有 CLSID 定义的 IDL 文件或标头文件,一个导出 DLL 函数的 DEF 文件,还需要一个静态类方法,才能创建筛选器。有关详细信息,请参阅 DirectShow SDK 文档中有关如何创建 DLL 和如何注册 DirectShow 筛选器的主题。 |
3. |
重写 CTransInPlaceFilter 中的两个纯虚拟方法:Transform 方法和 CheckInputType 方法。 |
CTransInPlaceFilter 类会自动处理大量其他任务,例如:协商针连接和缓冲器,在必要时重新连接针,将数据从输入针移动到输出针,以及支持多个线程。阅读这些基类的 C++ 代码是了解 DirectShow 筛选器更多详细信息的一种很好的方法。如果您想执行一些更复杂的操作,可能需要重写 CTransInPlaceFilter 的其他方法。
重写 CheckInputType 方法
筛选器中的 CheckInputType 方法决定了要接受哪些媒体类型,要拒绝哪些媒体类型。在针连接过程中,上游针会提出各种媒体类型。您的筛选器可以接受任何媒体类型,也可以拒绝任何媒体类型。DirectShow 在构建筛选器图形时,会自动尝试找出注册表中列出的筛选器,以使得连接能够运行。例如,如果您的筛选器只接受未压缩的视频,则当应用程序尝试将其连接到一个 AVI 文件源时,DirectShow 会插入相应的视频解压缩程序。
格式类型
如果您的筛选器只接受带有子类型 MEDIASUBTYPE_RGB24 的 MEDIATYPE_Video,则无需与 FORMAT_VideoInfo 格式类型进行连接。还存在几个其他的视频格式类型,其中包括 Format_VideoInfo2 和 FORMAT_DvInfo。您必须决定筛选器要处理哪些格式,以及相应接受或拒绝哪些不同的格式类型。
格式块和反转的 DIB
对于未压缩的视频类型,上游筛选器可能要传递反转的设备无关位图 (DIB)。在连接时,它会在格式块的 BITMAPINFOHEADER 结构的 biHeight 成员中指定上述内容。因此,如果您的筛选器需要一种特定的 DIB 方向(反转或不反转),则请确保检查 biHeight 成员,并拒绝筛选器不会处理的任何类型。
很多解压缩器都可以使用两个方向进行解码,并且会同时建议两种类型。如果您不检查方向就接受媒体类型,这些针则会使用解压缩器提出的第一个方向进行连接。
在应用程序中设置媒体类型
在 grabber 筛选器示例中,让应用程序来控制筛选器接受哪些媒体类型比较有意义。应用程序使用这种方式可以执行下列步骤:
1. |
应用程序针对筛选器调用一个自定义方法,来指定您想要的数据类型。这可以是一种具体的格式,也可能是允许一些可能格式的通用说明(例如,任何大小的 24 位 RGB 视频)。 |
2. |
应用程序将该 grabber 示例与图形中的其他筛选器进行连接。在针协商过程中,CheckInput 方法尝试将建议的媒体类型与应用程序在第一步中指定的类型进行匹配。 |
3. |
该应用程序调用另一个自定义方法来检索真正用于该连接的媒体类型。 |
例如,在第一步中,应用程序可能指定了 24 位 RGB。在第二步中,这些针将要使用一个特定的视频大小进行连接,如 320 X 240 像素。在第三步中,应用程序检索媒体类型,以确定视频大小。如果没有此信息,应用程序则无法解释接收到的数据。
您必须在筛选器上定义一个包含这两个方法的自定义 COM 接口。DirectShow Grabber 筛选器示例使用的是 ISampleGrabber 接口;创建您自己的筛选器时,您可以将其用作指南。
重写转换方法
CTransInPlaceFilter 构造函数方法的其中一个参数是指定筛选器是否修改它所接收的数据的标志。如果您传递值 false,则不得以任何方式更改数据。否则,可以在 Transform 方法中任意修改数据。
Transform 方法会接收一个指向媒体示例 IMediaSample 接口的指针。这种方法称为 CTransInPlaceFilter::Receive 方法。在 Transform 方法返回之后,Receive 方法会针对输出针调用 CBaseOutputPin::Deliver,以传递该示例。
如果 Transform 方法返回 S_FALSE,该基类则会发出一个质量控制更改信号。但是,在本例中,Receive 方法返回了 S_OK(而不是 S_FALSE),因此上游筛选器继续传递。如果 Transform 方法返回错误代码,该基类则会向筛选器图形发出一个流式处理错误信号,而且筛选器图形停止。除非有真正的流式处理错误,否则不应返回错误代码。如果您只是想停止该流,则应该重写 Receive 方法,并从 Receive 返回 S_FALSE。
您的应用程序运行所基于的线程总是应该与向筛选器传递数据的线程不同。如果您想在应用程序中同步检索数据,则必须考虑这种多线程处理。下面是用于处理一些常见情况的建议:
解码整个文件
如果您想要解码整个压缩文件,并按照顺序获取每个未压缩的数据块,则可能不需要担心线程处理的问题。在应用程序中创建一个全局缓冲区,编写 Transform 方法,以便它写入该缓冲区。另外,还可以让 Transform 每当接收示例时都调用一个回调方法。在该回调方法中写入全局缓冲区。在您的应用程序中,设置该回调,运行图形直到停止,这样就完成了此过程。
解码部分文件
这种情况与解码整个文件相似,只是应用程序需要使用 IMediaSeeking::SetPositions 方法来设置开始和停止位置。还有一种方法是,从 Receive 方法返回 S_FALSE,以便向源筛选器发送停止传递数据的信号。
解码文件的随机部分
如果您想要解码文件的一部分,然后搜索另一个位置再进行解码,这个过程就变得更复杂了。当您搜索筛选器图形,或者从一种图形状态更改为另一种图形状态时,应用程序必须等待图形状态变为稳定状态。
当您搜索图形(使用 IMediaSeeking 或 IMediaPosition)时,首先会从输出程序筛选器启动调用,然后向上游进行同步移动,直到到达源筛选器。源筛选器会异步 停止推数据,向下游发送刷新,搜索新的位置,然后再次开始发送数据。
要获取单个数据帧,请重写 Receive 方法,以返回 S_FALSE。在应用程序中,暂停该图形,并搜索到所需的时间。该源将通过搜索进行响应,然后它会向下游发送一个示例。
如果您希望应用程序同步处理示例,而不是进行异步处理,则请使用事件。在 Transform 方法中设置事件,并在应用程序中等待该事件。例如,您可以使用一个如下所示的循环:
while (not done) Seek the filter graph. Wait for the event to be signaled.
此示例假设您完全在 Transform 方法内部或者回调方法内部处理数据。如果您希望在该应用程序循环中处理数据,则需要第二个事件。
while (not done) Seek the filter graph. Wait for event 1 to be signaled. Process the data. Signal event 2.
编写筛选器的 Transform 方法,如下所示:
Transform: Signal event 1. Wait for event 2. Return S_FALSE.
如果没有第二个事件,Transform 方法则会立即返回,这是因为它是在另外一个线程上运行的。然后,其他筛选器可能会在应用程序仍然处理旧数据的同时向该示例写入新的数据。
注 还有一个选择就是,在 Transform 方法中针对该示例调用 AddRef,然后在应用程序中针对该示例调用 Release。通过在示例中保留一个参考数,可以防止它返回到“可用”列表。但是,这样无法阻止下游示例修改该示例。有关参考计数和 IMediaSample 接口的详细信息,请参阅 SDK 文档中的“示例和分配器”主题。
下面的代码是一个使用 grabber 筛选器示例的控制台应用程序:
#include "stdafx.h" #include <atlbase.h> #include <streams.h> #include <qedit.h> // for Null Renderer #include <filfuncs.h> // for GetOutPin, GetInPin #include <filfuncs.cpp> // for GetOutPin, GetInPin #include "SampleGrabber.h" int test(int argc, char* argv[]); int main(int argc, char* argv[]) { CoInitialize( NULL ); int i = test( argc, argv ); CoUninitialize(); return i; } HANDLE gWaitEvent = NULL; HRESULT Callback(IMediaSample* pSample, REFERENCE_TIME* StartTime, REFERENCE_TIME* StopTime) { // Note: We cannot do anything with this sample until we call // GetConnectedMediaType on the filter to find out the format. DbgLog((LOG_TRACE, 0, "Callback with sample %lx for time %ld", pSample, long(*StartTime / 10000))); SetEvent(gWaitEvent); return S_FALSE; // Tell the source to stop delivering samples. } int test( int argc, char * argv[] ) { // Create an event, which is signaled when we get a sample. gWaitEvent = CreateEvent( NULL, FALSE, FALSE, NULL ); // The sample grabber is not in the registry, so create it with 'new'. HRESULT hr = S_OK; CSampleGrabber *pGrab = new CSampleGrabber(NULL, &hr, FALSE); pGrab->AddRef(); // Set the callback function of the filter. pGrab->SetCallback(&Callback); // Set up a partially specified media type. CMediaType mt; mt.SetType(&MEDIATYPE_Video); mt.SetSubtype(&MEDIASUBTYPE_RGB24); hr = pGrab->SetAcceptedMediaType(&mt); // Create the filter graph manager. CComPtr<IFilterGraph> pGraph; hr = pGraph.CoCreateInstance( CLSID_FilterGraph ); // Query for other useful interfaces. CComQIPtr<IGraphBuilder, &IID_IGraphBuilder> pBuilder(pGraph); CComQIPtr<IMediaSeeking, &IID_IMediaSeeking> pSeeking(pGraph); CComQIPtr<IMediaControl, &IID_IMediaControl> pControl(pGraph); CComQIPtr<IMediaFilter, &IID_IMediaFilter> pMediaFilter(pGraph); CComQIPtr<IMediaEvent, &IID_IMediaEvent> pEvent(pGraph); // Add a source filter to the graph. CComPtr<IBaseFilter> pSource; hr = pBuilder->AddSourceFilter(L"C:\\test.avi", L"Source", &pSource); // Add the sample grabber to the graph. hr = pBuilder->AddFilter(pGrab, L"Grabber"); // Find the input and output pins, and connect them. IPin *pSourceOut = GetOutPin(pSource, 0); IPin *pGrabIn = GetInPin(pGrab, 0); hr = pBuilder->Connect(pSourceOut, pGrabIn); // Create the Null Renderer filter and add it to the graph. CComPtr<IBaseFilter> pNull; hr = pNull.CoCreateInstance(CLSID_NullRenderer); hr = pBuilder->AddFilter(pNull, L"Renderer"); // Get the other input and output pins, and connect them. IPin *pGrabOut = GetOutPin(pGrab, 0); IPin *pNullIn = GetInPin(pNull, 0); hr = pBuilder->Connect(pGrabOut, pNullIn); // Show the graph in the debug output. DumpGraph(pGraph, 0); // Note: The graph is built, but we do not know the format yet. // To find the format, call GetConnectedMediaType. For this example, // we just write some information to the debug window. REFERENCE_TIME Duration = 0; hr = pSeeking->GetDuration(&Duration); BOOL Paused = FALSE; long t1 = timeGetTime(); for(int i = 0 ; i < 100 ; i++) { // Seek the graph. REFERENCE_TIME Seek = Duration * i / 100; hr = pSeeking->SetPositions(&Seek, AM_SEEKING_AbsolutePositioning, NULL, AM_SEEKING_NoPositioning ); // Pause the graph, if it is not paused yet. if( !Paused ) { hr = pControl->Pause(); ASSERT(!FAILED(hr)); Paused = TRUE; } // Wait for the source to deliver a sample. The callback returns // S_FALSE, so the source delivers one sample per seek. WaitForSingleObject(gWaitEvent, INFINITE); } long t2 = timeGetTime(); DbgLog((LOG_TRACE, 0, "Frames per second = %ld", i * 1000/(t2 - t1))); pGrab->Release(); return 0; }
如果您将 grabber 示例设置为接受音频类型,然后将文件源与输入针相连接,则该连接过程能够运行(假设该文件具有音频流),但是所需的时间比较长。这是因为 DirectShow Intelligent Connect 过程无法猜测筛选器接受哪些媒体类型,因此需要对所有类型都进行尝试。它会将所有视频和音频解码器都加载到您的系统上,然后尝试将每个解码器都放置在文件源和筛选器之间的图形中。它首先会尝试视频解码器,需要一些时间才能轮到音频解码器。
通过在 CBasePin::GetMediaType 方法中指定筛选器接受的媒体类型,您可以大大减少这样的问题。这样会为 DirectShow 连接逻辑提供一个提示,说明要尝试哪些 codec。
重写筛选器构造函数方法
CTransInPlaceFilter 类分别使用 CTransInPlaceInputPin 和 CTransInPlaceOutputPin 类来自动创建筛选器的输入和输出针。为了重写 GetMediaType 方法,您必须修改筛选器。首先,定义一个从 CTransInPlaceInputPin 派生的新类,称为 CSampleGrabberInPin。然后,在 CSampleGrabber 构造函数方法中,创建 CSampleGrabberInPin 的一个新实例,并将其分配给筛选器的 m_pInput 成员变量。
重写 EnumMediaType 方法
当上游筛选器连接该 grabber 示例时,它会针对该 grabber 示例的输入针调用 IPin::EnumMediaTypes。此时,输出针通常仍然处于未连接状态。如果是这样,CTransInPlaceInputPin 类(重写 CBasePin 类中的 EnumMediaTypes)会返回错误代码 VFW_E_NOT_CONNECTED。因此,永远都不会调用 GetMediaType。为了规避这个问题,请重写 EnumMediaTypes。如果输出针处于未连接状态,则创建一个枚举数对象,就好像在 CBasePin 方法中完成此操作一样。如果处于连接状态,则调用该方法的 CTransInPlaceInputPin 版本。
重写 GetMediaType 方法
在 GetMediaType 方法中,只填充媒体类型参数的主要类型。如果您还填充任何其他类型,则会使得一些第三方 codec 发生崩溃。
将下列代码添加到您的头文件中:
class CSampleGrabberInPin : public CTransInPlaceInputPin { public: CSampleGrabberInPin(CTransInPlaceFilter *pFilter, HRESULT *pHr) : CTransInPlaceInputPin(NAME("SGInputPin"), pFilter, pHr, L"Input") { } HRESULT GetMediaType( int iPosition, CMediaType *pMediaType ); STDMETHODIMP EnumMediaTypes( IEnumMediaTypes **ppEnum ); };
将下列代码添加到您的源文件中:
CSampleGrabber::CSampleGrabber(...) /* omitted */ { m_pInput = (CTransInPlaceInputPin*)new CSampleGrabberInPin(this, phr); if(!m_pInput) { *phr = E_OUTOFMEMORY; } } HRESULT CSampleGrabberInPin::GetMediaType(int iPosition, CMediaType *pMediaType) { if (iPosition < 0) { return E_INVALIDARG; } if (iPosition > 0) { return VFW_S_NO_MORE_ITEMS; } *pMediaType = CMediaType(); pMediaType->SetType( ((CSampleGrabber*)m_pFilter)->m_mtAccept.Type() ); return S_OK; } STDMETHODIMP CSampleGrabberInPin::EnumMediaTypes(IEnumMediaTypes **ppEnum) { CheckPointer(ppEnum,E_POINTER); ValidateReadWritePtr(ppEnum,sizeof(IEnumMediaTypes *)); // If the output pin is not connected, offer the media type that was // set by the application if( !((CSampleGrabber*)m_pTIPFilter)->OutputPin()->IsConnected() ) { // Create a new reference-counted enumerator. *ppEnum = new CEnumMediaTypes(this, NULL); return (*ppEnum) ? NOERROR : E_OUTOFMEMORY; } // If the output pin is connected, offer the full media type. return ((CSampleGrabber*)m_pTIPFilter)-> OutputPin()->GetConnected()->EnumMediaTypes(ppEnum); }
在某些情况下,您可以强制该 grabber 示例将示例传递到应用程序选择的缓冲区中。要了解这是如何运行的,您必须了解 DirectShow 中使用的分配器机制,并在一定程度上熟悉 Intelligent Connect 的运行方式。
下面简要列出了您必须完成的操作:
• | 定义一个从 CMemAllocator 类派生的新类,这个新类称为 CSampleGrabberAllocator。 |
• | 覆盖 GetAllocatorRequirements、Alloc 和 ReallyFree 方法,强制这些方法提供一个指向应用程序内存缓冲区的内存分配器。 |
• | 覆盖输入针上的 NotifyAllocator 方法,拒绝任何与自定义分配器不同的分配器。 |
• | 覆盖 GetAllocator 方法,以返回您的自定义分配器。 |
• | 在筛选器中提供一个受保护的方法,以确定应用程序是否已经指定了只读模式。 |
• | 在筛选器中提供一个公共方法,用于应用程序指定传递缓冲区。 |
分配器
两个针进行连接时,这两个针必须针对在其中向下游传递示例的内存缓冲区传输达成一致;这个内存缓冲区就称为分配器。每个已连接针对使用一个分配器。当转换筛选器将某个示例从其输入针复制到输出针时,它就是在两个不同的分配器中进行复制。当筛选器执行就地转换时,就是说它正在两个针上使用同一个分配器。如果整个过程中的每个针连接都使用相同的分配器,示例则有可能(但是一般不会)在不进行内存复制的情况下从源筛选器向下传递到输出程序。
分配器具有下列属性:
• | Prefix |
• | Alignment |
• | Buffer count |
• | Size |
在就地转换筛选器中,几乎可以保证输出针与输入针使用同一个分配器,因此对于输出针无需提供任何附加代码。
将下列代码添加到您的头文件中:
//---------------------------------------------------------------------- // Custom allocator class. // This object allocates CMediaSamples that reference a buffer location. //---------------------------------------------------------------------- class CSampleGrabberAllocator : public CMemAllocator { protected: CSampleGrabberInPin *m_pPin; // The pin that created this object. public: CSampleGrabberAllocator(CSampleGrabberInPin *pParent, HRESULT *phr) : CMemAllocator(NAME("SampleGrabberAllocator"), NULL, phr), m_pPin(pParent) { }; ~CSampleGrabberAllocator() { // Clear m_pBuffer. It is not an allocated buffer, and the // default destructor will try to free it. m_pBuffer = NULL; } // Inform the upstream pin of our required properties. HRESULT GetAllocatorRequirements( ALLOCATOR_PROPERTIES *pProps ); HRESULT Alloc(); void ReallyFree(); }; class CSampleGrabberInPin : public CTransInPlaceInputPin { CSampleGrabberAllocator *m_pPrivateAllocator; ALLOCATOR_PROPERTIES m_allocprops; BYTE *m_pBuffer; protected: HRESULT SetDeliveryBuffer(ALLOCATOR_PROPERTIES props, BYTE *pBuffer); public: // Refuse allocators other than the one specified by the user, if set. STDMETHODIMP NotifyAllocator(IMemAllocator *pAllocator, BOOL bReadOnly); // Return the special allocator, if necessary. STDMETHODIMP GetAllocator( IMemAllocator **ppAllocator ); }; class CSampleGrabber : public CTransInPlaceFilter { HRESULT SetDeliveryBuffer(ALLOCATOR_PROPERTIES props, BYTE *pBuffer); };
将下列代码添加到您的源文件中:
//------------------------------------------------------------------------ // SetDeliveryBuffer: Inform the input pin of the allocator buffer to use. // See the SetDeliveryBuffer method of the input pin for comments. //------------------------------------------------------------------------ HRESULT CSampleGrabber::SetDeliveryBuffer(ALLOCATOR_PROPERTIES props, BYTE *pBuffer ) { // Do not change delivery buffers while the pins are connected. if(InputPin()->IsConnected() || OutputPin()->IsConnected()) { return E_INVALIDARG; } return ((CSampleGrabberInPin*)m_pInput)-> SetDeliveryBuffer(props, pBuffer); } STDMETHODIMP CSampleGrabberInPin::NotifyAllocator( IMemAllocator *pAllocator, BOOL bReadOnly ) { if (m_pPrivateAllocator) { if (pAllocator != m_pPrivateAllocator) { return E_FAIL; } else { // Fail if the upstream filter wants a read-only buffer, // but we do not. It is OK if the upstream filter // does not request a read-only buffer, but we do. if (bReadOnly && !SampleGrabber()->IsReadOnly()) { return E_FAIL; } } } return CTransInPlaceInputPin::NotifyAllocator(pAllocator, bReadOnly); } STDMETHODIMP CSampleGrabberInPin::GetAllocator( IMemAllocator **ppAllocator) { if( m_pPrivateAllocator ) { *ppAllocator = m_pPrivateAllocator; m_pPrivateAllocator->AddRef(); return NOERROR; } else { return CTransInPlaceInputPin::GetAllocator( ppAllocator ); } } HRESULT CSampleGrabberInPin::SetDeliveryBuffer(ALLOCATOR_PROPERTIES props, BYTE *pBuffer ) { // Do not allow more than one buffer. if (props.cBuffers != 1) { return E_INVALIDARG; } if (!pBuffer) { return E_POINTER; } m_allocprops = props; m_pBuffer = pBuffer; HRESULT hr = S_OK; m_pPrivateAllocator = new CSampleGrabberAllocator(this, &hr); if (!m_pPrivateAllocator) { return E_OUTOFMEMORY; } m_pPrivateAllocator->AddRef(); return hr; } //------------------------------------------------------------------------ // GetAllocatorRequirements: Ask for the allocator properties. //------------------------------------------------------------------------ HRESULT CSampleGrabberAllocator::GetAllocatorRequirements( ALLOCATOR_PROPERTIES *pProps) { *pProps = m_pPin->m_allocprops; return NOERROR; } //------------------------------------------------------------------------ // Alloc: Do not allocate any memory, just use the buffer that the // application specified. //------------------------------------------------------------------------ HRESULT CSampleGrabberAllocator::Alloc() { // Most of this code comes directly from CMemAllocator::Alloc. CAutoLock lck(this); // Check that SetProperties was called. HRESULT hr = CBaseAllocator::Alloc(); if (FAILED(hr)) { return hr; } // If the requirements have not changed, donot reallocate. if (hr == S_FALSE) { ASSERT(m_pBuffer); return NOERROR; } ASSERT(hr == S_OK); // Free the old resources. if (m_pBuffer) { ReallyFree(); } // Compute the aligned size. LONG lAlignedSize = m_lSize + m_lPrefix; if (m_lAlignment > 1) { LONG lRemainder = lAlignedSize % m_lAlignment; if (lRemainder != 0) { lAlignedSize += (m_lAlignment - lRemainder); } } ASSERT(lAlignedSize % m_lAlignment == 0); // Do not allocate any memory. Use the buffer specified by // the application. m_pBuffer = m_pPin->m_pBuffer; if (m_pBuffer == NULL) { return E_OUTOFMEMORY; } LPBYTE pNext = m_pBuffer; CMediaSample *pSample; ASSERT(m_lAllocated == 0); // Create the new samples. We have allocated m_lSize bytes for each // sample, plus m_lPrefix bytes per sample as a prefix. Set the // pointer to the memory after the prefix, so that GetPointer returns // a pointer to m_lSize bytes. for (; m_lAllocated < m_lCount; m_lAllocated++, pNext += lAlignedSize) { pSample = new CMediaSample(NAME("Sample Grabber media sample"), this, &hr, pNext + m_lPrefix, m_lSize); ASSERT(SUCCEEDED(hr)); if (pSample == NULL) { return E_OUTOFMEMORY; } m_lFree.Add(pSample); // Cannot fail. } m_bChanged = FALSE; return S_OK; } //------------------------------------------------------------------------ // ReallyFree: Do not free any memory; it was allocated // by the application. //------------------------------------------------------------------------ void CSampleGrabberAllocator::ReallyFree() { // Most of this code comes directly from CMemAllocator::ReallyFree. ASSERT(m_lAllocated == m_lFree.GetCount()); CMediaSample *pSample; for (;;) { pSample = m_lFree.RemoveHead(); if (pSample != NULL) { delete pSample; } else { break; } } m_lAllocated = 0; // Do not free any memory; let the application do it. }
要使用这个自定义分配器,请将下列代码添加到前面所示的应用程序示例中:
// Set up a partially specified media type. CMediaType mt; #if 1 mt.SetType(&MEDIATYPE_Video ); mt.SetSubtype(&MEDIASUBTYPE_RGB24); #if 1 ALLOCATOR_PROPERTIES props; props.cBuffers = 1; props.cbBuffer = 320*240*3; props.cbAlign = 1; props.cbPrefix = 0; BYTE *pBuffer = new BYTE[320*240*3]; pGrab->SetDeliveryBuffer(props, pBuffer); memset(pBuffer, 0, 320*240*3); #endif #else mt.SetType(&MEDIATYPE_Audio); #endif ASSERT(hr == NOERROR); pGrab->SetAcceptedMediaType(&mt);
在 DirectShow 中,当图形在没有任何针连接的情况下运行时,流的格式可能会发生更改。上游筛选器可能会因为源媒体已经转换了格式而请求格式更改,下游筛选器也可能为了效率更高而请求格式更改。例如,视频呈现筛选器总是与一种 RGB 类型连接,这种类型可与 GDI 兼容。流式处理开始时,它会尝试转换为用于 DirectDraw 的 YUV 类型。
要请求格式更改,筛选器会执行下列操作:
1. |
针对其上游或下游筛选器调用 IPinConnection::DynamicQueryAccept 或 IPin::QueryAccept,同时指定新的媒体类型。 |
2. |
如果其他针返回 S_OK,筛选器则会通过调用 IMediaSample::SetMediaType 将这个新的媒体类型附加到下一个示例。 |
在 CTransInPlaceFilter 类中,调用 QueryAccept 最终会导致调用 CheckInputType。(即使请求来自下游筛选器也会发生这种情况;有关详细信息,请参阅源代码。)对于该 grabber 示例,这种实现可能会导致非预期的行为。假设您将该 grabber 示例配置为了接受很多媒体类型,例如任何视频类型。如果下游筛选器请求格式更改,如从 RGB 类型更改为 YUV 类型,该 grabber 示例则会接受这个新的类型,您接收的下一个示例将会具有您未曾预期的格式。
处理格式更改的可能方法有:
• | 筛选器拒绝新格式。 |
• | 筛选器在 Transform 方法中检查新格式,并通知应用程序格式已经发生了更改。 |
• | 应用程序在回调方法中检查新格式。 |
要检查格式更改,请针对每个示例调用 IMediaSample::GetMediaType。正常情况下,媒体类型为 NULL。格式更改之后的第一个示例将具有新的媒体类型;后面的示例则会再次具有 NULL 类型。
性能考虑事项
拒绝格式更改可能会影响性能。例如,一个开发人员发现该 grabber 示例会降低性能,即使他的回调方法并没有执行任何操作也是如此。他的问题在于,将该 Grabber 示例配置为仅接受 RGB 类型,这样就使得视频输出程序无法转换为 YUV 类型。当他从图形删除了该 Grabber 示例时,视频输出程序则直接与一个接受 YUV 类型的解码器进行了连接。呈现 YUV 类型比很多视频卡速度都快,因此没有该 Grabber 示例的情况下他发现性能得到了大大提高。
另一方面,如果您的应用程序不呈现它所接收的示例,这些考虑事项则不适用。
DirectShow 附带的 Grabber 示例具有几个限制。如果您了解这些限制,则可以对该 GrabberSample 筛选器源代码进行修改,使其适应您的应用程序需求。
一次性模式
正如本文前面部分所说的那样,在一次性模式中,当该 Grabber 示例接收示例时,它会返回 S_FALSE。S_FALSE 返回值会通知上游筛选器停止发送数据。此机制有一些缺点:
• | 虽然上游筛选器停止了发送数据,但是它不会向筛选器图形管理器发送 EC_COMPLETE 事件。因此,当筛选器接收示例时,应用程序无法得到通知。(但是如果应用程序设置了回调的话,它会等待调用该回调。) |
• | 如果上游筛选器使用一个工作线程向输出队列传递示例的话,它会继续执行此操作。 |
一个更好的方法是让应用程序在回调方法中返回 S_FALSE,让筛选器使用该值作为 Receive 方法中的返回值。按照这种方法设计该筛选器则不再需要单独的一次性模式。
视频格式
对于视频类型,该 Grabber 示例需要 VIDEOINFOHEADER 格式。它不能连接到需要其他格式类型的筛选器,如 VIDEOINFOHEADER2 或 DVINFO。因此,它不兼容 MPEG-2 或 DV 视频,也不兼容基于场(隔行扫描)的视频。
缓冲模式
该 Grabber 示例的缓冲模式不是特别有用。如果应用程序需要将示例复制到缓冲区的话,它可以在回调中执行此操作。
格式更改
应用程序可以为该 Grabber 示例指定一个部分媒体类型或一个完整的媒体类型,但是却无法指定两个不同的媒体类型,如“MEDIASUBTYPE_RGB24 or MEDIASUBTYPE_UYVY”。这种实现限制了该 Grabber 示例响应格式更改的方式。