Dshow--filter

COM编程基础

        DirectShow应用程序实际上是一种COM组件的客户程序,只是COM组件的“使用”问题,这些问题包括如何创建COM组件,如何得到组件对象的借口以及调用接口方法,如何管理组件对象(即需要熟悉COM的引用计数机制)等。

        对于filter开发人员来说,需要掌握的COM知识就要多一些。因为filter本身是一种COM组件,开发filter牵涉到了COM组件的“实现”问题。

Filter Graph Manager

Filter Graph Manager也是一个组件,他用来管理filter graph,也就是管理filter graph中参与工作的各个filter。Filter Graph Manager向下管理各个filter,向上对应用程序提供控制、交互借口。Filter Graph Manager控制一个filter graph的所有filter,其功能包括: 协调filters之间的状态变化--filters的状态变化一般都必须遵循一定的顺序,应用程序不应该直接对filters发送状态改变命令,而应该通过Filter Graph Manager分发命令。创建一个关联时钟--graph中的所有filter都是用一个时钟(reference clock),这个时钟保证所有的流是同步的。Filter Graph Manager 通常选择声卡时钟或者系统时钟作为

reference clock。把事件传递给应用程序--FGM使用事件队列来通知应用程序filter graph中有事件发生,这个机制类似windows系统的消息队列。
提供创建filter graph的方法--如添加filter、连接两个filter或者断开连接,FGM不处理数据传送,这个是filter自身完成的,这个处理通常在单独的线程中发生。

filter简介

DirectShow使用filter graph来管理filter,filter graph是filter的“容器”,而filter使filter graph中的最小功能模块。

filter一般由一个或者多个filter组成,filter之间通过pin相互连接,构成一个顺序链路。filter根据功能的不同大致可分为3类:source filter(仅含有输出pin,没有输入pin的filter为source filter,既有输入pin又有输出pin的filter为transfer filter,仅有输入pin,没有输出pin的filter为rendering filter。

filter媒体类型

媒体类型主要由3部分来描述: majortype(主类型,定性地描述媒体类型,如视频还是音频),subtype(辅助说明类型,指明具体是哪种格式,比如,如果是视频,指明是yv12还是yuv420)。formattype(格式细节类型,比如大小,帧率等)。

filter的连接

filter的连接实际上也就是filter上pin的连接,连接的方向一般总是由上一级filter的输出pin指向下一级filter的输入pin。pin的连接实际上是连接双方使用的媒体类型的一个“协商”过程。pin也是一个COM组件,而且每个pin都实现了IPin接口。

整个连接过程的步骤大致如下:
1 filter graph manager在输出上调用IPin::Connect(带输入Pin的指针作为参数);
2 如果输出Pin接收连接,则调用输入Pin上的IPin::ReceiveConnection;
3 如果输入Pin也接收这次连接,则双方连接成功。

连接过程有个AgreeMediaType的函数很重要,HRESULT CBasePin:AgreeMediaType(IPin* pReceivePin,const CMediaoType*pmt)。如果pmt是一个完全指定的媒体类型,那么就以这种媒体类型调用内部函数AttemptConnection直接进行试连接。无论这个试连接成功与失败,连接过程都会停止。但是,如果pmt是一个空指针,或者pmt包含一个不完全指定的媒体类型,那么真正的协商就开始了。连接进程进行到这里,会首先得到输入pin上的媒体类型枚举器的试连接;如果不成功,再得到输出Pin山的媒体类型枚举器的试连接。
函数调用顺序为:

CBasePin::Connect(  IPin * pReceivePin,   const AM_MEDIA_TYPE *pmt   // optional media type)

-》HRESULT CBasePin::AgreeMediaType( IPin *pReceivePin,const CMediaType *pmt)

-》HRESULT CBasePin::TryMediaTypes( IPin *pReceivePin,const CMediaType *pmt,IEnumMediaTypes *pEnum)

-》CBaseOutputPin::CompleteConnect(IPin *pReceivePin)

-》HRESULT CBaseOutputPin::DecideAllocator(IMemInputPin *pPin, IMemAllocator **ppAlloc)

-》HRESULT CVideoOutPin::DecideBufferSize(IMemAllocator *pIMemAlloc,ALLOCATOR_PROPERTIES *pProperties)

当pin的Connect or ReceiveConnection方法被调用的时候,pin必须检查一下自己是否支持这个连接。通常要进行下列检查:
1 检查媒体类型是否匹配。
2 就内存的分配达成一致。
3请求其他pin的其他接口。
媒体类型匹配
当一个filter 图表管理器调用IPin::Connect方法时,可能有下面的几种媒体类型。
1 完整类型
如果媒体类型每一个部分都定义的很完成,那么pin就严格按照定义的类型类型进行连接。如果不匹配,连接失败。
2 部分媒体类型
如果媒体类型的机构中,major type, subtype, or format type的值为GUID_NULL,这个值是一个通配符号。任何类型都可以匹配。
3没有媒体类型
如果filter图表管理器传递过来一个NULL的指针,这个pin就可以和任意的类型的媒体类型匹配。
一般在连接过程中,都有一个完整的媒体类型。图表管理器传递媒体类型的目的是为了限制连接类型。
一般来说,都是输出pin通过调用输入pin IPin::ReceiveConnection提供一个媒体类型。输入pin可以拒绝也可以接受这个媒体类型。这个过程一直重复,直到输入pin接受了一个类型,或者输出pin枚举完了它支持的所有的媒体类型,连接失败。
输出pin通过调用输入pin上的IPin::EnumMediaTypes枚举输入pin所支持的媒体类型。
看看如何匹配媒体类型的吧。
if ((pmt->formattype == FORMAT_VideoInfo) &&
(pmt->cbFormat > sizeof(VIDEOINFOHEADER) &&
(pbFormat != NULL))
{
VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;
// Now you can dereference pVIH.
}
Pin连接中的内存分配
当两个pin连接起来后,他们需要一种机制来交换媒体数据。大多数数据交换采用的局部内存交换机制。所有的媒体数据都在主内存中。DirectShow为局部存储器传输定义了两种机制:推模式(push model)和拉模式(pull model)。在推模式中,源过滤器生成数据并提交给下一级过滤器。下一级过滤器被动的接收数据,完成处理后再传送给再下一级过滤器。在拉模式中,源过滤器与一个分析过滤器相连。分析过滤器向源过滤器请求数据后,源过滤器才传送数据以响应请求。推模式使用的是IMemInputPin接口,拉模式使用IAsyncReader接口,推模式比拉模式要更常用。
在局部存储器传输中,负责分配内存的对象称为allocator。每个allocator都支持一个IMemAllocator接口。所有的pin都共享一个allocator。
每个pin都提供一个allocator,但是输出pin选择使用哪个allocator。
输出pin可以设置allocator的属性。比如,分配内存的大小,
在IMemInputPin连接中,allocator工作过程如下
1 首先,输出pin调用IMemInputPin::GetAllocatorRequirements,这个方法检查输入pin对内存的要求,比如内存的队列,一般来说,输出pin要满足输入pin对内存的要求。
2 输出pin然后调用IMemInputPin::GetAllocator.,这个方法从输入pin请求一个allocator,
3 输出pin选择一个allocator,可以是输入pin提供,也可以是自己生产的。
4输出pin调用IMemAllocator::SetProperties来设置allocator的属性。
5然后输出pin通过IMemInputPin::NotifyAllocator来通知输入pin,选择的allocator。
6输入pin通过IMemAllocator::GetProperties来检查是否能够接受allocator的属性。
7当数据流开始和停止的时候,输出pin负责提交allocator。
在IAsyncReader连接过程如下:
1 输入pin调用输出pin上的IAsyncReader::RequestAllocator,输入pin确定内存的属性,并提供一个allocator。
2 输出pin选择一个allocator,
3 输入pin检查

如何提供一个自定义的allocator
这里只讲一下IMemInputPin连接,IAsyncReader类似。
首先,定义一个C++类,你的allocator应该从一个标准的allocator类中派生,比如CBaseAllocator or CMemAllocator,你也可以自己创建一个新的allocator类,如果你是新建的类,你必须支持IMemAllocator接口。
下面看看在输入pin和输出pin中如何使用你定义的allocator。
在输入pin中提供allocator
在输入pin中提供allcator,必须重载CBaseInputPin::GetAllocator方法。在这个方法里,首先检查m_pAllocator是否可用,如果为非空,就表明allocator已经被选中,所以直接返回这个allocator指针即可,如果m_pAllocator为空,表明allocator还没有被选中,所以,就要返回输入pin的allocator,因此,创建一个allcaotor的实例,返回IMemAllocator接口。
看下面的代码把
STDMETHODIMP CMyInputPin::GetAllocator(IMemAllocator **ppAllocator)
{
CheckPointer(ppAllocator, E_POINTER);
if (m_pAllocator)
{
// We already have an allocator, so return that one.
*ppAllocator = m_pAllocator;
(*ppAllocator)->AddRef();
return S_OK;
}
// No allocator yet, so propose our custom allocator. The exact code
// here will depend on your custom allocator class definition.
HRESULT hr = S_OK;
CMyAllocator *pAlloc = new CMyAllocator(&hr);
if (!pAlloc)
{
return E_OUTOFMEMORY;
}
if (FAILED(hr))
{
delete pAlloc;
return hr;
}
// Return the IMemAllocator interface to the caller.
return pAlloc->QueryInterface(IID_IMemAllocator, (void**)ppAllocator);
}
当输出pin选择一个allocator,它就调用输入pin的IMemInputPin::NotifyAllocator,因此,要重载CBaseInputPin::NotifyAllocator方法来检查allocator的属性。
在输出pin中如何提供一个定制的Allocator
在输出pin中提供一个allcator,要重载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;
}
// Return the IMemAllocator interface.
return pAlloc->QueryInterface(IID_IMemAllocator, void**)ppAllocator);}
}
缺省情况下CBaseOutputPin首先从输入pin中申请一个allocator,

filter的数据传输

       filter之间的成功连接为数据传输做好了准备。filter之间是以Sample的形式来传送数据的。Sample是一个封装了一定大小数据内存的COM组件。那么,数据到底是怎么从上一级filter传送到下一级filter的。

       大家知道,每个pin上都实现了IPin接口。但这个接口主要是用于Pin连接的,而不是用于数据传输的。真正用于数据传送的一半是输入Pin上实现的IMemInputPin接口(调用其接口方法IMemInputPin::Receive)。

       连接着的双方Pin拥有同一个Allocator(即Sample分配器):Allocator创建,管理一个或多个Sample。数据传送时,上一级filter从输出Pin的Allocator中(调用IMemAllocator::GetBuffer)得到一个空闲Sample,然后得到Sample的数据内存地址,将数据放入其中,最后,再将这个Sample传送给下一级filter的输入Pin。

filter数据传输分为推模式和拉模式,推模式为render filter从source filter要数据,推模式为source filter主动把数据发给render filter。

filter有三种状态:停止(stopped),暂停(paused),运行(running),一般使用Filter Graph Manager上的IMdediaControl接口来控制Filter Graph的状态转换。实际上Graph也是调用Filter上的IMediaFilter::Run等接口。

DirectShow SDK提供了一套开发filter的基类源代码,我们强烈推荐使用,以为基于这些基类开发filter将大大简化开发过程。

CSource

CSource继承自CBaseFilter,一般作为推模式Source Filter的父类。CSource类的使用方法如下:

1 从CSource类中派生一个新的filter类;2 在新的filter类的构造函数中创建各个CSourceStream类实例(CSourceStream类的构造函数会自动将该Pin加入Filter中,并在析构函数中自动删除);3 使用Csource::pStateLock函数返回的同步对象进行filter对象上的多线程同步。

CSourceStream

CSource实际上继承自CBaseFilter,提供一个”推“数据的能力。这种filter至少有一个输出Pin采用CSourceStream类(或CSourceStream的子类)。CSourceStream上实现了一个线程(CSourceStream从CAMThread类继承而来),Sample数据就是靠这个线程向下一级filter发送的。CSourceStream类的使用方法如下:

1 从CSourceStream派生一个输出Pin类;2 重写CSourceStream::GetMediaType,提供输出Pin上的首选媒体类型;3 重写CSourceStream::CheckMediaType,进行连接时媒体累心的检查;4 实现CSourceStream::DecideBufferSize,决定内存Sample的大小;5 实现CSourceStream::FilterBuffer,为即将传送出去的Sample填充数据;

Media sample引用计数
  Allocator创建了一个sample池。因此 ,当某个Filter调用GetBuffer函数时,一些sample被使用,其他空闲的sample可以响应。Allocator通过引用计数来跟踪samples。Filter调用Getbuffer返回的sample的引用计数是1。当sample的引用计数为0时,sample就返回内存池,成为空闲的sample,可以再次响应Getbuffer的调用。如果所有的sample都处于繁忙状态,Getbuffer就会阻塞,直到有一个sample空闲。
  例如,假设一个输入pin接到一个sample,如果它在Receive方法里同步的处理这个sample,没有增加该sample的引用计数,等到Receive返回后,输出pin就释放这个sample,引用计数为0,sample就返回到内存池中。如果输入pin的线程还要处理该sample,引用计数增加1,成为2,输出pin返回,释放,计数成1。
  当一个输入pin接收一个sample时,它可以将数据复制到另一个sample中,也可以将这个sample传递到下一个Filter。一个sample可以流遍整个filter graph。不过引用计数要保持大于0。 当一个输出pin调用了Release以后,就不应该再次使用该sample,因为也许下游还有filter正在使用该sample。输出pin必须调用GetBuffer获取新的sample。
  这种机制减少 了内存分配的,因为buffer可以重用。也防止了数据没有被处理的sample被重新写入。
  当一个Filter创建一个allocator的时候,allocator还没有保留任何的内存,如果这个时候有人Getbuffer,就会失败。只有当数据流开始的时候,输出pin调用IMemAllocator::Commit,提交allocator,现在才能分配内存。
  当数据流停止的时候,pin就调用IMemAllocator::Decommit,来销毁allocator。在allocator再次committ之前,所有调用GetBuffer方法都会失败。当然,如果有一个GetBuffer阻塞调用在等待sample的时候,遇到Decommit方法,会立即返回一个错误码。

filter数据传输的结束

数据流结束的通知
        当一个源filter结束发送数据流时,它调用和它连接的filter的输入pin的IPin::EndOfStream,然后下游的filter再依次通知与之相连的filter。当EndOfStream方法一直调用到renderer filter的时候,最后的一个filter就给filter图表管理器发送一个EC_COMPLETE事件通知。如果renderer有多个输入pin,当所有的输入pin都接收到end of stream通知的时候,它才会给filter图表管理器发送一个EC_COMPLETE事件通知。

Filter必须在其他函数调用之后调用EndOfStream函数,比如IMemInputPin::Receive.。
        在一些情况下,下游的filter可能比源filter更早的发现数据流的结束。在这种情况下,下游filter发送 结束stream的通知,同时, IMemInputPin::Receive函数返回S_FALSE直到图表管理器停止。这个返回值提示源filter停止发送数据。
对EC_COMPLETE事件的缺省处理
        缺省的情况下,filter图表管理器并不将EC_COMPLETE事件通知发送给应用程序,当所有的数据流都发送了EC_COMPLETE事件通知后,它才给应用程序发送一个EC_COMPLETE事件通知。所以,应用程序只有在所有的数据流停止的时候才能接收到这个通知。

filter图表管理器通过计算支持seeking接口的filter,并且具有一个renderer pin,没有相应的输出pin,就可以确定数据流的数目。Filter图表管理器通过下面的方法来决定一个pin是否是个renderer 。
1 pin的IPin::QueryInternalConnections方法通过nPin参数返回0;
2 filter保露一个IAMFilterMiscFlags接口,并且返回一个AM_FILTER_MISC_FLAGS_IS_RENDERER标志。
在拉模式下的数据流结束通知
在IAsyncReader连接中,源filter并不发送数据流结束的通知,相应的发送数据流结束的通知是有renderer filter发出的。
4New Segments(本节翻译的不好,我自己都不理解,乱七八糟)
一个段就是一组media samples,这些sample具有共同的开始时间,结束时间,播放速率。
The IPin::NewSegment 方法用来通知一个new segments的开始。源filter通过这种方法来通知下游的filter segment的开始时间和播放速率。例如,如果源filter在数据流中改变了新的开始点,它就用新的时间做参数来通知下游的filter。
下游的filter在处理sample的时候需要segment。例如,在桢间压缩的时候,if the stop time falls on a delta frame, the source filter may need to send additional samples after the stop time. This enables the decoder to decode the final delta frame.为了确定正确的结束桢,解码器指向色gement的停止时间。另外一个例子,在音频播放的过程中,播放filter利用segment的速度和音频sample速度来产生正确的输出。
在推模式中,源filter 产生一个新的segment,并初始化。在拉模式,这个工作是由剖析器(parser)来完成的。两种情况下,filter都调用下游filter的输入pin上的NewSegment,一直到达renderfilter。当filters调用数据流时候,必须序列化NewSegment。
当每一个新的segment,数据流的时间都被重新设置为零,当segment从零开始的时候,samples 重新贴上了time标签。
5 Flushing
当graph运行的时候,在整个graph中会有大量的数据流动。同时也有一些数据排在队列里等到传递。当graph移动这些未决的数据,并在该内存块中写入新的数据是需要一定的时间的。例如,在seek命令后,源filter在生成新的sample,这些是需要一定时间的。为了减小延迟,下游的filter在seek命令必须丢掉以前的sample。这个抛弃sample的过程就叫flushing。
当事件改变了数据的流向时,这可以使garph响应的更及时一些。

你可能感兴趣的:(filter)