转载地址:http://blog.sina.com.cn/s/blog_a2e5bcda01019gkg.html
六、自己写一个“filter”(1)
(2012-12-04 09:40:59)标签: directshowfilter开发文档 | 分类: DirectShow开发文档翻译 |
DirectShow filter开发介绍
DirectShow基本类库
DirctShow开发包中包含了用来写一个filter要用到的一套C++类,推荐使用这些类来写自定义的filter,当然这也不是必须的,如果你想所有东西都自己实现。使用这些基本的类库,你要将源文件编译为静态库然后在项目中连接生成的.lib文件。
在基本类库中定义了一个根类:CBaseFilter,还有其他一些为了完成特定功能的继承自它的派生类,比如CTransformFilter,它继承自CBaseFilter,用来生成一个转换filter。创建一个filter,你要根据类库中提供的这些继承自CBaseFilter的类派生出自定义的类,然后完成它的功能,就像下面这样:
class CMyFilter : public CTransformFilter
{
private :
public :
}
创建引脚
一个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:
- 连接以下文件:
- 在连接设置中选择“忽略默认类库”
- 源代码中声明DLL入口点:
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。
连接的大体过程如下:
个别引脚支持“动态重连”,暂时不予讨论。
通常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)&&
{
}
协商分配器
当引脚建立连接,它们需要一种机制来交换数据,这种机制叫作“传输机制”。两个相连的filter可以协商使用任一种它们都支持的传输机制。
最通用的传输机制是:本地内存传输。在这种机制中,媒体数据存储在主内存中。这种传输机制又分成两种模式:推模式和拉模式。在推模式中,源filter通过调用下级filter的输入引脚上的IMemInputPin接口来向下传递数据。而在拉模式下,下级filter通过调用上级filter的输出引脚上的IAsyncReader接口来请求数据。
在本地内存传输机制下,使用分配器来负责分配缓存,分配器支持IMemAllocator接口。连接的两个引脚共享用一根分配器。任意一个引脚都可以提供一个分配器,而输出引脚最终决定使用哪个分配器。
同样由输出引脚设置分配器的相关属性,比如创建多少缓存、缓存大小、内存对齐方式。输出引脚也可以根据输入引脚的需求来设置这些属性值。
在使用IMemInputPin建立连接时,即在推模式下,分配器按照下面的步骤完成协商过程:
在使用IAsyncReader建立连接时,即在推模式下,分配器按照下面的步骤完成协商过程:
提供一个通用的分配器
这一节描述如何给一个filter提供自定义的分配器。这里只给出了推模式的描述,拉模式下,步骤类似。
首先,为自定义的分配器定义一个C++类,可以继承自一个标准分配器类,如CBaseAllocator、CMemAllocator。当然也可以完全自己实现,而且必须实现IMemAllocator接口。
剩下的步骤要取决于分配器属于filter中的输入引脚还是输出引脚。因为在分配器协商阶段,输入输出引脚扮演着不同的角色。
为输入引脚定义分配器
为输入引脚提供分配器,要重写输入引脚上的CBaseInputPin::GetAllocator函数,在这个函数内,检查m_pAllocator变量是否为空,如果不为空,说明已经选择了分配器,函数返回分配器指针。如果为空,说明还未选择分配器,要返回输入pin优先支持的分配器指针,这种情况下,创建自定义分配器的实例并返回IMemAllocator接口指针。下面代码给出了GetAllocator方法的实现:
STDMETHODIMP CMyInputPin::GetAllocator(IMemAllocator **ppAllocator)
{
}
为输出引脚定义分配器
为输出引脚提供分配器,要重写CBaseOutputPin::InitAllocator函数来创建分配器的实例:
HRESULT MyOutputPin::InitAllocator(IMemAllocator **ppAlloc)
{
}
默认的,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:
如果你的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,于是等待流线程退出。但是在流线程内部也处于等待状态,于是死锁了。
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。