标签: directshowfilter开发文档 |
分类: 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要实现的功能。
处理、传递数据
大多数filter的主要功能就是处理和传递数据,不同类型filter的操作方式也有所不同:
支持COM
DirectShow filter是COM对象,基类中实现了支持COM标准的框架。
创建DirectShow filter
推荐使用基本类库来创建filter,可以按以下步骤执行:
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
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建立连接时,有以下几种方式来指定媒体类型:
一旦引脚之间建立了连接,那么媒体的完全类型就被确定。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:
在第一种情况下,Receive*方法应当返回S_FALSE,其他情况会返回一个错误码。没上级filter收到返回结果不是S_OK时,就应当停止发送sample。
可以把前3种情况视为“可预知的错误”,看作这时filter不是处于接收状态。而第四种情况就是即使现在filter是处于接收状态,但是仍然因为某种不可预知的错误而拒绝了sample,这时该输入引脚就应该向下发送一个“流结束通知”,而且发送一个EC_ERRORABORT事件给filter graph manager。
在DirectShow基本类库中,用CBaseInputPin::CheckStreaming来检查常见的错误:刷新数据、停止等。传递数据的类需要检查自定义filter中指定的错误。当一个错误发生,CBaseInputPin::Receive就发送一个流结束通知和EC_ERRORABORT事件。
拉模式:请求媒体采样
输入引脚通过调用以下方法来向输出引脚请求媒体采样:
其中Request方法是异步的,要调用IAsyncReader::WaitForNext来等待请求的完成。其他两个方法都是同步的。
传递数据的时机
filter通常在运行状态下会传递数据,暂停状态也会。这样可以提前调出数据,一旦调用Run方法运行就能立即开始回放。如果你的自定义filter在暂停状态下不想传递数据的话,那它的IMediaFilter::GetState方法在暂停状态应该返回一个VFW_S_CANT_CUE,这标识着在filter graph完成暂停转换前不会等待filter中完成数据调出。如果不返回这个标识的话,Pause方法就要无限期等下去了,等待调出数据。
下面是几个需要返回VFW_S_CANT_CUE的例子:
一个推模式的源filter或是一个解析filter会创建多个流线程来尽可能快地传递数据。而下级filter,比如解码filter、转换filter,经常是当其输入引脚上的Receive方法被调用时才会去执行传递数据的操作。
处理数据
解析媒体数据
如果你的自定义filter要解析媒体数据,千万不要信任内容header等其他一些自我描述数据。比如:不能相信AVI RIFF块、MPEG包中的size值。常见的这类错误有:
另一类常见的错误是不验证数据的格式描述,比如:
引脚连接过程中,filter应当先验证所有的格式结构体格式合法并且具体合理值。如果验证发现问题,则拒绝连接。在验证格式结构体的代码中,尤其要注意算术溢出,比如:在一个BITMAPINFOHEADER结构中,width、height都是32位long值,但imagesize却是个DWORD值。
如果从数据源中得到的格式值比分配的缓存大,千万不要截断拷贝进缓存,这样会造成隐含尺寸比实际尺寸大(因为实际尺寸只是原始的一截),比如,一个位图头信息中可能指定了一个调色板,但是却不存在了。所以只能重新分配适合大小的缓存或直接连接失败。
流中的错误
当graph运行时,filter可能会接收到有缺陷的数据内容,这时它应该按以下步骤终止数据流:
改变格式
有几种机制供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接口)而且有“提交输出引脚”来确定有几个流。通过两种方法来确定一个引脚是不是“提交引脚”:
拉模式下的流结束通知
源filter不发送流结束通知,而是由下级filter(如:解析filter)向下发送。
刷新数据(Flushing)
当filter graph在运行时,一些临时数据会在graph中移动,其中一些可能正在等待被传递。当filter graph需要删除这些待传递的数据并用新数据填充时,会需要一定时间。比如,应用程序发送定位命令后,源filter会从一个新位置来生成sample,为了达到最小延迟,下级filter应该丢弃所有定位命令之前的数据,这个过程就叫作“刷新数据”。当有改变发生在当前数据流上时,这个功能可以让graph响应更灵敏。
在推模式和拉模式下的刷新数据会稍有不同。
刷新数据有两个阶段:
在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,这些工作线程也被看作是流处理线程。
有些函数需要在应用程序上调用,而另一些需要在流处理线程来调用,比如:
使用独立的流处理线程可以保证graph中数据流动的同时,应用程序可以接收用户输入。但是,风险就是filter可能在暂停时在应用程序线程中创建资源,在流线程方法中使用,当停止时又在应用程序线程中销毁资源。一不小心,流线程中可能就会使用已经在应用程序线程中销毁的资源。对应的解决方案就是使用临界区,及使流处理函数和状态切换同步。
filter需要一个临界区来保护状态,CBaseFilter自身带有这么个变量:CBaseFilter::m_pLock。这个临界区被叫作“filter锁”。每个输入引脚也需要一个临界区来保护流线程要处理的资源,这种临界区叫作“流锁”,必须在自定义的引脚类中声明这种锁。最简单的就是使用CCritSec类,它封装了一个Windows CRITICAL_SECTION内核对象,这个类还提供了一些有用的调试函数。
当filter停止或刷新数据时,必须让应用程序线程和流处理线程同步。为了避免死锁,必须让流线程解阻塞。流线程阻塞的原因如下:
因此,当一个filter停止或刷新数据时,一定要做如下处理:
刷新数据和停止命令都发生在应用程序。调用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:
如果你的filter有一个自定义的分配器,该分配器会使用filter资源,那么GetBuffer函数应该和其他流函数一样启用流锁定:
当filter graph manager停止图表时,它要等待所有的流线程关闭。这对filter有如下影响:
filter graph manager使用临界区来同步各项操作,如果一个流线程试图控制这个临界区,可能导致死锁。比如:假如另一个线程停止了graph,这个线程占用了filter graph锁并等待你的filter停止传递数据。而你的filter中在等待这个锁,就造成死锁了。
如果filter通过AddRef、QueryInterface来控制filter graph manager的引用计数。可能会成为最后唯一一个引用filter graph manager的对象。当这个filter释放时,filter graph manager也要销毁自己。在销毁代码中,filter graph manager试图停止graph,于是等待流线程退出。但是在流线程内部也处于等待状态,于是死锁了。
DirectShow和COM
如何实现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。