您的位置:首页 > 其它

转载,写的不错的一个关于dshow的文章

2015-10-09 10:12 393 查看

转载地址:http://blog.sina.com.cn/s/blog_a2e5bcda01019gkg.html

六、自己写一个“filter”(1)

(2012-12-04 09:40:59)


转载▼

标签:

directshow

filter

开发文档

分类:
DirectShow开发文档翻译
DirectShow filter开发介绍

DirectShow基本类库

DirctShow开发包中包含了用来写一个filter要用到的一套C++类,推荐使用这些类来写自定义的filter,当然这也不是必须的,如果你想所有东西都自己实现。使用这些基本的类库,你要将源文件编译为静态库然后在项目中连接生成的.lib文件。

在基本类库中定义了一个根类:CBaseFilter,还有其他一些为了完成特定功能的继承自它的派生类,比如CTransformFilter,它继承自CBaseFilter,用来生成一个转换filter。创建一个filter,你要根据类库中提供的这些继承自CBaseFilter的类派生出自定义的类,然后完成它的功能,就像下面这样:

class CMyFilter : public
CTransformFilter


{

private :

// 声明自定义的变量和方法

public :

// 重写CTransformFilter的方法

}

创建引脚

一个filter必须包含至少一个引脚,引脚的数量可以在设计filter的时候指定,也可以在需要时filter在创建引脚。引脚类从CBasePin类或其派生类派生,如从CBaseInputPin类派生。引脚应该在filter类中被声明为变量。有些filter类中已经定义好了引脚,所以会直接继承这些引脚,但是如果是从CBaseFilter继承,就必须要自己声明引脚。

引脚协商连接

当filter graph manager试图连接两个filter时,引脚必须在一些方面达成一致,如果不能达成一致就会连接失败。通常,引脚会按照下面规则进行协商:

传输机制:相连的filter是按照什么机制从输出引脚向输入引脚传输media sample的。是使用IMemInputPin接口(推模式)还是IAsyncReader接口(拉模式)。
媒体类型:几乎所有引脚都使用媒体类型来描述它们可以传递的数据格式。
分配器:引脚之间必须协商由那个引脚提供分配器,及分配的缓存大小、缓存数量等属性。

类库提供的基类中已经提供了协商的框架,而你在派生类要做的就是重写这些方法,完成所以协商细节。到底要重写哪些方法取决于继承的类、自定义filter要实现的功能。

处理、传递数据

大多数filter的主要功能就是处理和传递数据,不同类型filter的操作方式也有所不同:

使用推模式的源filter会使用一个工作线程来持续不断地填充media sample并向下级传递。
使用拉模式的源filter会等地其下级filter来请求一个sample,然后它才会写数据到一个空闲sample并向下级传递。驱动数据流动的线程由下级filter提供。
转换filter接收sample、处理、向下传递。
提交filter接收sample、按时间戳呈现。

支持COM

DirectShow filter是COM对象,基类中实现了支持COM标准的框架。

创建DirectShow filter

推荐使用基本类库来创建filter,可以按以下步骤执行:

编译基本类库,添加环境(头文件、库文件)到项目中
包含头文件:Streams.h
使用__stdcall调用约定
使用"多线程C运行时库"(debug/retail)
包含.def文件(导出DLL接口方法)。下面是一个.def文件的例子,假定输出文件名为MyFilter.dll:

LIBRARY MYFILTER.DLL

EXPORTS

DllMain PRIVATE

DllGetClassObject PRIVATE

DllCanUnloadNow PRIVATE

DllRegisterServer PRIVATE

DllUnregisterServer PRIVATE

连接以下文件:

debug:Strmbasd.lib、Msvcrtd.lib、Winmm.lib

retail:Strmbase.lib、Msvcrt.lib、Winmm.lib

在连接设置中选择“忽略默认类库”
源代码中声明DLL入口点:

extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE, ULONG, LPVOID);

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)

{

return DllEntryPoint((HINSTANCE)hModule,dwReason,lpReserved);

}

Filter如何连接

引脚连接

filter通过引脚连接,使用引脚上的IPin接口,输出引脚连接到输入引脚。引脚连接要协商媒体类型,媒体类型由AM_MEDIA_TYPE结构给出。

应用程序通过调用filter graph manager上的方法来连接filter,而不是调用filter或是pin上的方法。可以调用IFilterGraph::ConnectDirect或IGraphBuilder::Connect方法来连接指定的filter,也可以调用IGraphBuilder::RenderFile来间接连接filter。

连接filter首先要调用IFilterGraph::AddFilter来添加filter到filter graph中,添加后,filter graph manager会调用IBaseFilter::JoinFilterGraph来通知filter。

连接的大体过程如下:

1. filter graph manager调用输出引脚上的IPin::Connect,传递一个指针到输入引脚

2. 如果输出引脚接受连接,会调用输入引脚上的IPin::ReceiveConnection

3. 如果输入引脚接受连接,连接成功

个别引脚支持“动态重连”,暂时不予讨论。

通常filter自上而下建立连接,也就是一个filter的输入引脚会先建立连接,然后是输出引脚(这里说的是对同一个filter而言,并非建立连接的一对引脚)。个别filter会使用相反的顺序建立连接,比如:MUX filter。

当一个引脚的Connect、ReceiveConnection方法被调用,它会验证是否支持该连接,不同的filter过程会有所不同,下面的一些通用的过程:

检测媒体类型是否支持
协商分配器属性
在配对的引脚上查找需要的接口

协商媒体类型

当filter graph manager调用IPin::Connect建立连接时,有以下几种方式来指定媒体类型:

完全类型:如果媒体类型被完全指定,那么引脚连接时媒体类型稍有出入,连接就会失败
不完全媒体类型:如果mayortype、subtype、formattype之一是GUID_NULL,那么媒体类型为不完全类型,这时GUID_NULL作为通配符
无限定媒体类型:如果filter graph manager使用空指针作为媒体类型,那么引脚连接时只要双方可就任意一种媒体类型达成一致,就可建立连接

一旦引脚之间建立了连接,那么媒体的完全类型就被确定。filter graph manager使用媒体类型来限定连接类型。

在协商过程中,输出引脚指定一种媒体类型并调用输入引脚上的IPin::ReceiveConnection。输入引脚可以接受或拒绝这种请求的媒体类型。会重复这种操作直到输入引脚接受了一种媒体类型的请求,或是输出引脚已遍历完所有支持的媒体类型,连接失败。

到底输出引脚是如何选择一种媒体类型向输入引脚请求的,这要依赖实际运行情况。在DirectShow基础类库中,输出引脚调用输入引脚上的IPin::EnumMediaTypes,它会返回一个遍历器,这个遍历器可以遍历出输入pin可支持的媒体类型。如果不行,输出引脚就要遍历自身支持的媒体类型。

处理媒体类型

在任何接收AM_MEDIA_TYPE类型参数的函数中,在对pbFormat变量取值前,都要先验证cbFormat、formattype的有效性。

if((pmt->formattype == FORMAT_VideoInfo)&&

(pmt->cbFormat> sizeof(VIDEOINFOHEADER)&&

(pbFormat != NULL))

{

VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;

}

协商分配器

当引脚建立连接,它们需要一种机制来交换数据,这种机制叫作“传输机制”。两个相连的filter可以协商使用任一种它们都支持的传输机制。

最通用的传输机制是:本地内存传输。在这种机制中,媒体数据存储在主内存中。这种传输机制又分成两种模式:推模式和拉模式。在推模式中,源filter通过调用下级filter的输入引脚上的IMemInputPin接口来向下传递数据。而在拉模式下,下级filter通过调用上级filter的输出引脚上的IAsyncReader接口来请求数据。

在本地内存传输机制下,使用分配器来负责分配缓存,分配器支持IMemAllocator接口。连接的两个引脚共享用一根分配器。任意一个引脚都可以提供一个分配器,而输出引脚最终决定使用哪个分配器。

同样由输出引脚设置分配器的相关属性,比如创建多少缓存、缓存大小、内存对齐方式。输出引脚也可以根据输入引脚的需求来设置这些属性值。

在使用IMemInputPin建立连接时,即在推模式下,分配器按照下面的步骤完成协商过程:

1. 根据具体情况,输出引脚决定是否调用IMemInputPin::GetAllocatorRequirements,这个方法会检查输入引脚对缓存的属性需求。通常输出引脚会尊重输入引脚的要求,除非有绝对理由不这么做。

2. 根据具体情况,输出引脚绝对是否调用IMemInputPin::GetAllocator,向输入引脚请求一个分配器。输入引脚提供一个分配器,或返回错误。

3. 输出引脚选择一个分配器:使用输入引脚提供的,或是自己创建一个。

4. 输出引脚调用IMemAllocator::SetProperties设置分配器的属性(分配器可以不回应对属性的更改,这会发生在分配器由输入引脚提供的情况下)。分配器会将最终的实际属性作为输出参数返回。

5. 输出引脚调用IMemInputPin::NotifyAllocator来通知输入引脚它对分配器的最终选择。

6. 输入引脚调用IMemAllocator::GetProperties来验证分配器的属性是否可用。

7. 输出引脚负责提交和反提交分配器。(当媒体流开始和停止时)

在使用IAsyncReader建立连接时,即在推模式下,分配器按照下面的步骤完成协商过程:

1. 输入引脚调用输出引脚上的IAsyncReader::RequestAllocator。输入引脚要指定它对缓存的需求,当然,它可以自己提供一个分配器。

2. 输出引脚选择一个分配器:有输入引脚提供的,或是自己创建的。

3. 输出引脚将分配器作为IAsyncReader::RequestAllocator的输出参数返回。输入引脚要检查分配器的属性。

4. 输入引脚负责分配器的提交和反提交。

5. 在分配器协商过程的任何时候,任意引脚可以停止连接。

6. 如果输出引脚使用了输入引脚提供的分配器,那么它只能传递数据到那个对应的输入引脚。

提供一个通用的分配器

这一节描述如何给一个filter提供自定义的分配器。这里只给出了推模式的描述,拉模式下,步骤类似。

首先,为自定义的分配器定义一个C++类,可以继承自一个标准分配器类,如CBaseAllocator、CMemAllocator。当然也可以完全自己实现,而且必须实现IMemAllocator接口。

剩下的步骤要取决于分配器属于filter中的输入引脚还是输出引脚。因为在分配器协商阶段,输入输出引脚扮演着不同的角色。

为输入引脚定义分配器

为输入引脚提供分配器,要重写输入引脚上的CBaseInputPin::GetAllocator函数,在这个函数内,检查m_pAllocator变量是否为空,如果不为空,说明已经选择了分配器,函数返回分配器指针。如果为空,说明还未选择分配器,要返回输入pin优先支持的分配器指针,这种情况下,创建自定义分配器的实例并返回IMemAllocator接口指针。下面代码给出了GetAllocator方法的实现:

STDMETHODIMP CMyInputPin::GetAllocator(IMemAllocator **ppAllocator)

{

CheckPointer(ppAllocator,E_POINTER);

if(m_pAllocator)

{

// 已经有了分配器,返回吧

*ppAllocator = m_pAllocator;

(*ppAllocator)->AddRef();

return S_OK;

}

// 还没有分配器的话,就返回一个我们自定义的分配器

HRESULT hr = S_OK;

CMyAllocator *pAlloc = new CMyAllocator(&hr);

if(!pAlloc)

{

return E_OUTOFMEMORY;

}

if(FAILED(hr))

{

delete pAlloc;

return hr;

}

// 返回IMemAllocator接口

return pAlloc->QueryInterface(IID_IMemAllocator,(void**)ppAllocator);

}

为输出引脚定义分配器

为输出引脚提供分配器,要重写CBaseOutputPin::InitAllocator函数来创建分配器的实例:

HRESULT MyOutputPin::InitAllocator(IMemAllocator **ppAlloc)

{

HRESULT hr = S_OK;

CMyAllocator *pAlloc = new CMyAllocator(&hr);

if(!pAlloc)

{

return E_OUTOFMEMORY;

}

if(FAILED(hr))

{

delete pAlloc;

return hr;

}

// 返回IMemAllocator接口

return pAlloc->QueryInterface(IID_IMemAllocator,(void**)ppAllocator);

}

默认的,CBaseOutputPin类会先向输入引脚请求分配器,如果返回的分配器不适合,输出引脚就创建自己的分配器。如果强制使用自定义的分配器,你要重写CBaseOutputPin::DecideAllocator函数。但是这会导致一个问题,就是可能会影响到filter的连接,因为另一个filter可能也在请求使用它自己创建的allocator。

重连引脚

开发一个filter,除了要关心filter的连接过程,第二个要关心的就是数据的流动。

这一节就要详细描述数据如何在filter graph中流动,主要是针对“本地内存传输机制”,由前面章节可知,这种机制下是通过IMemInputPin或IAsyncReader接口来完成连接。

贯穿filter graph的数据主要分为两类:媒体数据、控制数据。媒体数据从上向下移动,主要包括视频帧、音频采样、MPEG包、流结束通知等。控制数据移动方向则相反,控制命令有定位命令、质量控制请求等。

传递采样

下面描述filter在推模式和拉模式下分别如何传递媒体采样。

推模式:传递媒体采样

输出引脚调用输入引脚上的IMemInputPin::Reveive或IMemInputPin::ReceiveMultiple来传递采样,后者比前者唯一的区别就是传递的数量,可以传递一个采样数组。输入引脚可以在上述方法中阻塞。如果希望阻塞呢,那么在重写IMemInputPin::ReceiveCanBlock方法时,要返回S_OK,否则返回S_FALSE。当然返回S_OK也并不是每次一直都阻塞,只是说明有发生阻塞的可能,到底会不会发生和具体运行过程相关。

尽管在Receive*方法内会发生阻塞去等待需要的资源,但是不可以是从上级filter等待数据或资源,因为上级filter可能也同时在等待下级filter释放一个媒体采样,这样就造成了死锁。如果一个filter上有多个输入引脚,那么一个输入引脚可以等待另一个输入引脚完成接收数据,比如AVI Mux filter就是这样来交替视频和音频数据的。

输入引脚可能因为以下原因而拒绝接收一个sample:

引脚刷新数据
引脚未连接
filter已停止
其他错误发生

在第一种情况下,Receive*方法应当返回S_FALSE,其他情况会返回一个错误码。没上级filter收到返回结果不是S_OK时,就应当停止发送sample。

可以把前3种情况视为“可预知的错误”,看作这时filter不是处于接收状态。而第四种情况就是即使现在filter是处于接收状态,但是仍然因为某种不可预知的错误而拒绝了sample,这时该输入引脚就应该向下发送一个“流结束通知”,而且发送一个EC_ERRORABORT事件给filter graph manager。

在DirectShow基本类库中,用CBaseInputPin::CheckStreaming来检查常见的错误:刷新数据、停止等。传递数据的类需要检查自定义filter中指定的错误。当一个错误发生,CBaseInputPin::Receive就发送一个流结束通知和EC_ERRORABORT事件。

拉模式:请求媒体采样

输入引脚通过调用以下方法来向输出引脚请求媒体采样:

IAsyncReader::Request
IAsyncReader::SyncRead
IAsyncReader::SyncReadAliqned

其中Request方法是异步的,要调用IAsyncReader::WaitForNext来等待请求的完成。其他两个方法都是同步的。

传递数据的时机

filter通常在运行状态下会传递数据,暂停状态也会。这样可以提前调出数据,一旦调用Run方法运行就能立即开始回放。如果你的自定义filter在暂停状态下不想传递数据的话,那它的IMediaFilter::GetState方法在暂停状态应该返回一个VFW_S_CANT_CUE,这标识着在filter graph完成暂停转换前不会等待filter中完成数据调出。如果不返回这个标识的话,Pause方法就要无限期等下去了,等待调出数据。

下面是几个需要返回VFW_S_CANT_CUE的例子:

现场采集源filter,在暂停时不应继续发送数据,提前调出的数据到下次运行时都是过时的数据。
一个分离filter如果使用单独的线程来使各个输出引脚传递数据,那么在暂停时,它可以继续调出数据。但是当分离filter只使用一个线程传递数据,线程可能在其中一个引脚上调用Receive时阻塞,这时其他引脚就不能发送数据了,应该在这里返回VFW_S_CANT_CUE。
一个filter可能会零零散散的传递数据。比如解析接收的数据,然后过滤掉一些内容,发送剩下的数据。这种情况下,将不能保证暂停时传递数据。

一个推模式的源filter或是一个解析filter会创建多个流线程来尽可能快地传递数据。而下级filter,比如解码filter、转换filter,经常是当其输入引脚上的Receive方法被调用时才会去执行传递数据的操作。

处理数据

解析媒体数据

如果你的自定义filter要解析媒体数据,千万不要信任内容header等其他一些自我描述数据。比如:不能相信AVI RIFF块、MPEG包中的size值。常见的这类错误有:

从数据内容中获取了数据大小为N字节,然后不检查实际大小就直接读取N字节。
不检查是否过界,直接在缓存数据中定位指针。

另一类常见的错误是不验证数据的格式描述,比如:

在位图中,BITMAPINFO结构由颜色表和BITMAPINFOHEADER构成,紧跟着颜色表RGBQUAD数组的是一个BITMAPINFOHEADER结构。RGBQUAD数组的大小由biClrUsed指定,所以拷贝一个颜色表到BITMAPINFO之前要验证分配给它的内存大小。
一个WAVEFORMATEX结构可能会紧跟着一些扩展格式信息,cbSize变量指定了扩展信息的大小。

引脚连接过程中,filter应当先验证所有的格式结构体格式合法并且具体合理值。如果验证发现问题,则拒绝连接。在验证格式结构体的代码中,尤其要注意算术溢出,比如:在一个BITMAPINFOHEADER结构中,width、height都是32位long值,但imagesize却是个DWORD值。

如果从数据源中得到的格式值比分配的缓存大,千万不要截断拷贝进缓存,这样会造成隐含尺寸比实际尺寸大(因为实际尺寸只是原始的一截),比如,一个位图头信息中可能指定了一个调色板,但是却不存在了。所以只能重新分配适合大小的缓存或直接连接失败。

流中的错误

当graph运行时,filter可能会接收到有缺陷的数据内容,这时它应该按以下步骤终止数据流:

从Receive方法中返回一个错误码
调用下级filter上的IPin::EndOfStream
调用CBaseFilter::NotifyEvent发送EC_ERRORABORT事件

改变格式

有几种机制供filter在流传递过程中改变媒体格式,这造成了误接收的可能性。当你的自定义filter接收到一个动态转换格式的请求,它要么拒绝,要么以新格式来处理接收到的数据。同样,在另一个filter同意格式转换之后,当前filter才能切换数据格式并发送。

流结束通知

当一个源filter完成数据的发送后,要调用下级filter的输入引脚上的IPin::EndOfStream方法,然后下级filter继续向下重复这一过程。当“流结束通知”到达提交filter,它会向filter graph manager发送EC_COMPLETE事件。如果提交filter有多路输入,就会等待所有输入引脚接收到“流结束通知”后发送EC_COMPLETE。

filter应该序列化所有的流调用,也就是下级filter必须有序地接收这些流调用。

有时,下级filter可能会比源filter先察觉到流的结束。这时,下级filter会向下发送流结束通知,而对IMemInputPin::Receive要返回S_FALSE直到graph停止或是刷新数据结束。S_FALSE可以通知源filter停止发送数据。

对EC_COMPLETE的默认处理

默认的,filter graph manager并不会传递每一个EC_COMPLETE给应用程序,它只会等待所有流都发送了EC_COMPLETE事件,才会向应用程序发送一个EC_COMPLETE。

filter graph manager通过检查filter是否支持定位(IMediaSeeking/IMediaPosition接口)而且有“提交输出引脚”来确定有几个流。通过两种方法来确定一个引脚是不是“提交引脚”:

IPin::QueryInternalConnections函数的nPin参数返回0。
filter提供了IAMFilterMiscFlags接口且返回AM_FILTER_MISC_FLAGS_IS_RENDERER标识。

拉模式下的流结束通知

源filter不发送流结束通知,而是由下级filter(如:解析filter)向下发送。

刷新数据(Flushing)

当filter graph在运行时,一些临时数据会在graph中移动,其中一些可能正在等待被传递。当filter graph需要删除这些待传递的数据并用新数据填充时,会需要一定时间。比如,应用程序发送定位命令后,源filter会从一个新位置来生成sample,为了达到最小延迟,下级filter应该丢弃所有定位命令之前的数据,这个过程就叫作“刷新数据”。当有改变发生在当前数据流上时,这个功能可以让graph响应更灵敏。

在推模式和拉模式下的刷新数据会稍有不同。

刷新数据有两个阶段:

首先,源filter调用下级filter的输入引脚上的IPin::BeginFlush方法,然后下级filter就开始拒绝结束sample,并丢弃所有现有的sample,然后向下逐级调用BeginFlush。
当源filter准备好发送新数据后,调用输入引脚上的IPin::EndFlush,通知下级filter可以接收新sample了,同样是会逐级向下调用EndFlush。

在BeginFlush中,输入引脚会做以下事情:

1. 调用下级的BeginFlush。

2. 拒绝所有的流数据调用请求,包括Receive、EndOfStream。

3. 对等待allocator分配空闲sample的上级filter的阻塞调用进行解阻塞。一些filter是通过“反提交分配器”来实现的。

4. 从所有阻塞流的等待中退出。比如,提交filter当暂停时会阻塞,当等待准确时间呈现sample时也在阻塞,它必须解阻塞,然后上级filter中等待被传递的sample才能被传递进而被拒绝。这一步保证了上级filter的解阻塞。

在EndFlush中,输入引脚做如下操作:

1. 等待所有在队列中的sample被丢弃。

2. 释放缓存数据,这个操作有时在BeginFlush中执行,但是,BeginFlush和流处理线程不同步。filter在BeginFlush和EndFlush的调用之间不能继续处理或缓存任何数据。

3. 清除所有挂起的EC_COMPLETE通知。

4. 调用下级EndFlush函数。

这时,filter就可以继续接收新sample了,可以保证所有sample都是新的。

在拉模式下,解析filter启动刷新数据,而不是源filter。它不仅要调用下级的IPin::BeginFlush和IPin::EndFlush,还要调用源filter输出引脚上的IAsyncReader::BeginFlush和IAsyncReader::EndFlush。如果源filter中有尚未处理的读数据请求,会全部丢弃。

定位

filter通过IMediaSeeking接口支持定位功能。应用程序中从filter graph manager上询问IMediaSeeking接口然后用它来处理定位命令。filter graph manager会把定位命令分发到graph中的所有提交filter。每个提交filter再依次通过输出引脚向上级传递定位命令,直到到达一个可以执行定位的filter。典型的执行定位filter就是源filter和解析filter,比如AVI Splitter。

当filter执行定位操作,它会刷新所有未处理的数据,来最小化定位延迟。定位命令执行后,“流时间”会重置为零。

下图说明了操作执行的顺序:





如果一个解析filter不止一个输出引脚,通常指定其中一个来接收定位命令,其他输出引脚要拒绝并忽略定位命令。这样,解析filter就可以维持所有流的同步。但是,所有的输出引脚都应该执行IMediaSeeking::GetCapabilities和IMediaSeeking::CheckCapabilities来返回filter的定位能力,来保证filter graph manager返回正确的值到应用程序。

IMediaPosition接口已经被弃用了。一些自动化的客户端依然调用这个接口,filter graph manager会自动把对它的调用转换到IMediaSeeking的调用。

动态格式转换

线程和临界区

下面描述DirectShow filter中的线程,及开发中为避免冲突和死锁要做的处理。

流处理线程和应用程序线程

每个DirectShow应用程序都至少包括两个重要的线程:应用程序线程、一个或多个流处理线程。媒体采样在流处理线程上传递,而状态转换发生在应用程序线程上。源filter和解析filter创建“主流处理线程”,其他filter也会创建自己的工作线程来传递sample,这些工作线程也被看作是流处理线程。

有些函数需要在应用程序上调用,而另一些需要在流处理线程来调用,比如:

流处理线程:IMemInputPin::Receive,IMemInputPin::ReceiveMultiple,IPin::EndOfStream,IMemAllocator::GetBuffer等
应用程序线程:IMediaFilter::Pause,IMediaFilter::Run,IMediaFilter::Stp,IMediaSeeking::SetPositions,IPin::BeginFlush等
任意线程:IPin::NewSegment等

使用独立的流处理线程可以保证graph中数据流动的同时,应用程序可以接收用户输入。但是,风险就是filter可能在暂停时在应用程序线程中创建资源,在流线程方法中使用,当停止时又在应用程序线程中销毁资源。一不小心,流线程中可能就会使用已经在应用程序线程中销毁的资源。对应的解决方案就是使用临界区,及使流处理函数和状态切换同步。

filter需要一个临界区来保护状态,CBaseFilter自身带有这么个变量:CBaseFilter::m_pLock。这个临界区被叫作“filter锁”。每个输入引脚也需要一个临界区来保护流线程要处理的资源,这种临界区叫作“流锁”,必须在自定义的引脚类中声明这种锁。最简单的就是使用CCritSec类,它封装了一个Windows CRITICAL_SECTION内核对象,这个类还提供了一些有用的调试函数。

当filter停止或刷新数据时,必须让应用程序线程和流处理线程同步。为了避免死锁,必须让流线程解阻塞。流线程阻塞的原因如下:

在IMemAllocator::GetBuffer函数中等待sample,当分配器的sample暂时都被使用时。
等待另一个filter从流处理函数中返回。像Receive函数。
在自身的流处理线程中等待,比如等待某个资源可以被使用(当前被其他线程使用)。
如果是提交filter,要等待准确时间来呈现sample。
如果是提交filter,当暂停时,在Receive函数中等待。

因此,当一个filter停止或刷新数据时,一定要做如下处理:

无条件释放它所持有的所有sample,可以让GetBuffer函数返回。
迅速从流处理函数中返回。如果一个流函数在等待一个资源,必须立即停止。
在Receive函数中拒绝接收sample,这样流处理线程就不会继续去占用新资源了。
stop函数中必须反提交所有filter的分配器。(CBaseInputPin类自动处理)

刷新数据和停止命令都发生在应用程序。调用IMediaControl::Stop后,filter graph manager会从提交filter开始向上级传递停止命令。在CBaseFilter::Stop中执行停止操作,该函数返回后,filter应该处于停止状态。

刷新数据通常是由定位命令引起。刷新命令从源filter或解析filter开始,向下传递。刷新信息分两个阶段:IPin::BeginFlush函数通知filter丢弃待处理的数据并拒绝到来的数据。IPin::EndFlush函数通知filter开始接收新数据。刷新需要两个阶段是因为BeginFlush的调用是在应用程序线程,这时流处理线程仍然在继续传递数据。因此,在BeginFlush调用后,依然有一些sample到达了filter而没来得及传递,filter应该丢弃它们。所有EndFlush调用后到来的sample可以保证都是新的,应该被传递。

后面的内容会包含一些重要函数的示例代码,有Pause、Receive函数等。这些示例中会考虑到死锁和资源竞争的问题。每个filter都不同,所以要小心把这些示例代码移植到你的自定义filter中。

注意:CTransformFilter和CTransInPlaceFilter基类会处理以下描述的部分操作,如果要写一个转换filter,这两个类已经可以实现一些基本的功能。

暂停

所有filter在状态转换时都必须被锁定,来创建filter需要的资源:





CBaseFilter::Pause函数设置了filter的正确状态(State_Paused)并调用CBasePin::Active函数来通知filter上的引脚这个filter已经激活了。如果引脚要创建资源,就重写Active方法:





接收和传递媒体采样

下面是IMemInput::Receive函数的伪代码:





Receive方法保持流锁定,而不是filter锁定。filter可能在处理数据之前要像上面代码中那样等待某个事件WaitForSingleObject,但这不是必须的。CBaseInutPin::Receive方法验证流状态,如果流停止则返回VFW_E_WRONG_STATE,如果是在刷新数据则返回S_FALSE。只要返回的不是S_OK,就说明函数要立刻失败返回。sample被处理后,要调用CBaseOutputPin::Deliver向下传递。一个filter可以把数据传递到多个引脚。

传递“流结束通知”

当输入引脚接收到流结束通知,它要向下传递该通知。任何一个从该引脚接收数据的下级filter都应该获得该通知。这里同样要设置流锁定,而不是filter锁定。当一个filter接收到流结束通知,而它还有些数据没有来得及传递,应该马上传递下去,然后发送流结束通知到下级,之后就不能再发送任何数据。





CBaseOutputPin::DeliverEndOfStream方法会调用下级filter输入引脚上的IPin::EndOfStream。

刷新数据

下面是IPin::BeginFlush的伪代码:





当刷新开始,BeginFlush函数启用filter锁定。现在还不能启用流锁定,因为刷新数据发生在应用程序线程,流线程可能在Receive方法中。需要先保证Receive方法没有阻塞且下面的调用都会失败。CBaseInputPin::BeginFlush函数设置一个内部标识m_bFlushing,当为TRUE时,Receive会失败。

通过向下级传递BeginFlush,保证所有的下级filter都释放sample并从Receive返回。首先是保证输入引脚从GetBuffer、Receive解阻塞。如果引脚仍在Receive方法上等待,BeginFlush就强制其返回。并且m_bFlushing标识会阻止后续的对Receive的调用。

对一些filter来说,上面就是它的全部工作了,然后EndFlush方法会通知filter重新开始接收新数据。另一些filter可能要在BeginFlush中要使用到一些数据,而这些数据在Receive中也会使用。这时,要先启用流锁定,不然有可能造成死锁。

EndFlush函数启用filter锁定,并依次向下传递:





CBaseInputPin::EndFlush函数将m_bFlushing重置为FALSE。这样Receive才能重新接收sample。当然这要在EndFlush的最后才做,因为刷新数据完成之前还不能让filter能接收新数据。

停止

Stop方法应该解阻塞Receive方法并反提交filter的分配器。反提交分配器会强制GetBuffer的调用返回,会解阻塞上级filter的等待。Stop函数启用filter锁定,然后调用CBaseFilter::Stop方法,这个方法会调用所有引脚的CBasePin::Inactive:





重写Inactive方法:





获取缓存

如果你的filter有一个自定义的分配器,该分配器会使用filter资源,那么GetBuffer函数应该和其他流函数一样启用流锁定:





流线程和滤镜图表管理器

当filter graph manager停止图表时,它要等待所有的流线程关闭。这对filter有如下影响:

filter不能在流线程中调用filter graph manager上的方法。

filter graph manager使用临界区来同步各项操作,如果一个流线程试图控制这个临界区,可能导致死锁。比如:假如另一个线程停止了graph,这个线程占用了filter graph锁并等待你的filter停止传递数据。而你的filter中在等待这个锁,就造成死锁了。

filter不能在流线程中对filter graph manager增加引用、询问接口。

如果filter通过AddRef、QueryInterface来控制filter graph manager的引用计数。可能会成为最后唯一一个引用filter graph manager的对象。当这个filter释放时,filter graph manager也要销毁自己。在销毁代码中,filter graph manager试图停止graph,于是等待流线程退出。但是在流线程内部也处于等待状态,于是死锁了。

DirectShowCOM

如何实现IUnknow接口

DirectShow基于组件对象模型,所以你要些自己的filter,就必须把它实现成一个COM对象。DirectShow基本类库提供了相关的框架,它可以简化开发过程。下面会描述一些COM对象知识和在DirectShow基本类库中是如何实现的。

下面的文章假定读者能够编写COM客户端程序---即,读者直到如何使用IUnknown接口。但并不需要读者一定会独立编写一个COM对象。DirectShow处理了开发COM对象的许多细节。

COM是一种规范,而不是任何实现代码。它定义了开发组件要遵守的规则,具体怎样来开发符合规则的应用要开发人员自己处理。在DirectShow中,所有对象都是从一系列C++基类派生。基类的构造函数和方法做了大部分COM工作,比如保持引用计数。当你的filter从基类派生,要继承基类的方法。为了更有效的使用基类,你需要大概理解这些基类如何实现COM规范。

IUnknown接口如何工作

通过IUnknown接口,可以询问组件上支持的接口,还可以管理组件的引用计数。

引用计数

引用计数是一个内部变量,通过AddRef、Release来增加、减小引用计数。基类管理引用计数并控制多线程中引用计数的同步使用。

询问接口

询问接口也很简单,调用者传递两个参数:接口ID、接口指针地址,如果组件支持这种接口,就会返回接口指针到传入的指针地址,并增加接口引用计数,然后返回S_OK。否则,返回E_NOINTERFACE。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: