如何提供一个自定义的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,
2filter间的数据流动
1 传递Samples
本文讲述了如何传递一个sample,包括两种模式下,推模式下采用IMemInputPin的方法,在拉模式下调用IAsyncReader的方法。
推模式
输出pin通过调用IMemInputPin::Receive或者IMemInputPin::ReceiveMultiple方法来传递一个sample。在Receive和ReceiveMultiple方法里,输入pin可以阻塞数据流。如果输入pin阻塞,那么IMemInputPin::ReceiveCanBlock必须返回S_OK。如果pin保证不会阻塞,那么ReceiveCanBlock方法要返回S_FALSE,返回S_OK并不表明Receive方法阻塞,只是表明可能阻塞。
尽管Receive可以阻塞一直等待某种资源变的可用,但是它不能通过阻塞来等待数据流的到来。因为如果上游的filter正在等待下游的filter正在等待下游的filter释放资源,就会造成死锁。如果一个filter拥有多个输入pin,那么其中的一个pin可以等待另外的一个pin接收数据。例如AVI Mux filter就是通过这种方法来同步音频和视频流的。
如果有以下原因,pin可能拒绝sample。
1pin正在flushing
2pin没有连接
3filter停止
4发生了其他错误
如果输入pin拒绝了sample,那么Receive方法就要返回S_FALSE,或者其它的错误码,如果Receive没有返回S_OK,那么上游的fitler就会停止发送sample。
前面三种错误都是可以预见的错误,第四种是不可预见的错误,即使pin正处于接收数据流德状态,当这种错误发生时,接受pin就拒绝接受sample,发送pin就给下游的连接pin发送一个结束发送数据流的通知,并且给Filter 图表管理器发送一个EC_ERRORABORT事件通知。
在directshow的基类中CBaseInputPin::CheckStreaming方法用来检查通常的数据流错误,比如flushing, stopped, and so forth。派生类要检查所发生的错误。在发生错误的时候,CBaseInputPin::Receive方法将发送一个结束数据流的通知和一个EC_ERRORABORT事件通知。
在拉模式下,IAsyncReader接口中,输入pin将通过以下方法从输出pin中请求samples。
? IAsyncReader::Request
? IAsyncReader::SyncRead
? IAsyncReader::SyncReadAligned
Reques方法是异步的,输入pin调用IAsyncReader::WaitForNext来等待请求数据传递结束。另外两个方法是同步。
2数据处理
//略
3数据流结束的通知
当一个源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响应的更及时一些。
推模式和拉模式在处理flushing的时候有点不同。我们先讨论一下推模式,然后再讨论拉模式。
下面两种情况下发生flushing
1 源filter调用下游filter输入pin的IPin::BeginFlush方法,然后下游的filter就开始拒绝从上游filter接收数据流。然后它开始抛弃它正在处理的samples,继续调用下游filter的IPin::BeginFlush方法
2 当源filter准备好新的数据时,它调用输入pin的IPin::EndFlush方法,这就告诉下游的filter可以接收新的samples,然后继续调用下游的filter的IPin::EndFlush。
在BeginFlush方法中,输入pin进行了下列工作
1 首先调用下游filter的输入pin上的Calls BeginFlush方法
2拒绝处理数据流,包括Receive和endofstream方法
3 取消那些正在阻塞等待filter释放allocator的等待,
4 如果filter正处于阻塞数据状态,那么filter就退出阻塞。例如当停止的时候Renderer filter
就阻塞,此时,filter就要取消阻塞。
在EndFlush方法中,输入pin做了下列工作
1等待所有正在队列中的samples被抛弃
2 释放存放数据的buffer,这一步也可能在BeginFlush方法里,但是,beginflush方法streaming 线程是不同步的。Filter在BeginFlush和EndFlush方法之间不能够处理如何数据
3清除所有的EC_COMPLETE通知
4调用下游filter的EndFlush方法
此时,filter可以再次接收sample。
在拉模式中,parser Filter初始化flushing,而不是由source filter,它不仅调用了下游filterIPin::BeginFlush and IPin::EndFlush方法,它又调用了源filter输出pin上的IAsyncReader::BeginFlush and IAsyncReader::EndFlush,如果此时,源filter有未决的read请求,它就抛弃
6Seeking
Filter通过IMediaSeeking接口支持seeking。应用程序从filter 图表管理器请求IMediaSeeking接口,然后通过这个接口,执行seek命令。Filter图表管理器传递到graph里所有的renderer filter。每个renderer 通过上游filter的输出pin来传递seek命令,直到某一个filter可以执行这个seek命令。一般来说,源filter,或者parser filter可以执行seek 命令。
当一个filter执行seek命令的时候,它就flushes所有的未决的数据,这是为了减少seek命令的迟延。当一个seek命令后,stream time设置为零。
下面的图表演示了seek过程
图1
如果一个parser filter 的输出pin不止一个的话,它就指定一个pin来接收seek commands,其他的pin当接收到seek 命令时,就拒绝或者忽略seek 命令。这样,parser就保持了所有的数据流同步。
IMediaPosition接口(略)
3pin连接时数据格式的动态改变
当两个filter连接的时候,他们会就某种媒体类型达成协议。这种数据类型用来描述上游filter将传递什么格式的数据。大多数情况下,在连接的持续过程中,这个媒体类型是不变的,但是,directshow也支持动态改变媒体类型。
Directshow定义了一些机制来支持动态改变媒体类型。关键在于filter graph的状态和将要改变的类型。
如果graph处于停止状态,pin可以重新连接,重新就某个媒体类型达成协议。
一些filter还支持动态的连接,
当graph处于活动状态,并且不支持动态的重新连接的时候,有三种机制支持媒体类型的改变。
QueryAccept (Downstream) 适用于输出pin向下游的filter提出改变媒体类型,并且新的媒体格式的大小不超过原来的buffer。
QueryAccept (Upstream) 适用于输入pin首先向上游的filter提出媒体类型的改变,新的媒体类型格式可以和原来的大小一致,也可以大于。
ReceiveConnection适用于输出pin首先提出改变媒体类型,但是新的媒体类型的格式大于原来的buffer。
4 Threads and Critical Sections
本章讲述一下dshow filters的线程和临界区,这样,你在开发filter的过程中就可以避免死锁和系统崩溃。
1 The Streaming and Application Threads
任何一个Directshow的应用程序中都至少包括两个重要的线程,一个是应用程序线程,一个或者多个的Streaming线程。在streaming线程中,samples不断的传递,然后在应用程序中,**的状态不断的变化。Streaming的主线程一般都是由源filter或者parser Filter来创建,其他的filter可以创建用来传递samples的工作线程,所有的这些线程都称为streming线程。
下面我们看看应用程序线程和streaming线程经常用到的
Streaming thread(s):IMemInputPin::Receive, IMemInputPin::ReceiveMultiple, IPin::EndOfStream, IMemAllocator::GetBuffer.
Application thread: IMediaFilter::Pause, IMediaFilter::Run, IMediaFilter::Stop, IMediaSeeking::SetPositions, IPin::BeginFlush, IPin::EndFlush.
Either IPin::NewSegment.
当我们的用户应用程序在等待用户输入的同时,数据流可以通过streaming线程在graph图中流动。在多线程中,filter在暂停的时候,创建了资源,然后在Streaming方法中使用这些资源,当这个filter停止的时候,就销毁这些资源,这样就很危险,如果你不小心,streaming线程有可能使用了已经被销毁的资源。解决的方法就是通过临界区来保护这些资源,来同步这些线程。
一个filter需要一个临界区来保护filter的状态。CBaseFilter类有一个成员变量来保护这个filter的状态,CBaseFilter::m_pLock。这个临界区称为filter 锁。同时,每一个输入pin都需要一个临界区来保护streaming线程使用的资源,这些临界区成为streaming锁。你必须在你派生的pin类中(输入pin)中来设置这些变量。使用一个CCritSec类很容易实现临界区。这个CCritSec类中包含一个CRITICAL_SECTION窗口对象。可以使用CAutoLock类来锁住这个临界区。
当一个filter停止或者flushes的时候,就必须同步应用线程和streaming线程。为了避免死锁,你必须要解锁streaming线程。
Streaming线程加锁的原因主要有以下;
1 当streaming线程通过IMemAllocator::GetBuffer方法来请求samples的时候,如果此时所有的allocators 分配的samples都在使用,那么此时线程就要等到
2 等到其他的filter从streaming线程方法返回,比如,Receive
3 在streaming方法中等待其他的资源的释放。
4 renderer Filter正在Receive方法等待,如果filter停止,就处于死锁状态了
因此,当filter停止或者flushes时候,必须做下列事情
1 一定要释放它正在占用的任何资源
2尽可能快地从streaming方法中返回,如果这个方法正在等到某种资源,就放弃等待,立即返回
3在Receive方法开始停止接受samples,这样就不会再请求新的资源了
4 Stop方法一定要decommit所有filter的内存分配器。(当然了,CBaseInputPin会自动地处理这个事情)
Flushing和stoping方法在应用程序中都会发生。IMediaControl::Stop方法可以使filter停止运行。Filter管理器一般让stop命令从render filter开始,逆流向上到源filter逐渐停止。通过CBaseFilter::Stop方法,在这个方法返回的时候,filter的状态就转变为停止状态。
Filter在seek命令时一般都会Flushing。Flush命令一般都从源filter或者parser filter开始,然后向下游传递执行。Flushing有两个状态,IPin::BeginFlush方法通知一个filter开始抛弃所有的正在接受到的数据IPin::EndFlush方法通知filterflush结束,可以开始再次的接收数据。Flushing之所以有两个阶段,因为 Beginflush方法是由应用程序调用的,同时有可能还在传递数据。这样,在调用beginflush方法后,还有数据在filter中流动,此时filter就要抛弃这些数据。在调用endflush后,就能保证filter接受的数据都是新的,可以处理了。
下面的一些临界区的例子,演示了如何保护filter的重要的方法,比如Pause,receive等
2 Pausing
在filter的状态发生变化的时候,必须要加锁,在Pause方法中,要创建filter需要的资源
HRESULT CMyFilter::Pause()
{
CAutoLock lock_it(m_pLock);
/* Create filter resources. */
return CBaseFilter::Pause();
}
CBaseFilter::Pause方法设置filter处于State_Paused)状态,然后调用和filter相连接的pin上的CBasePin::Active方法,Active方法通知pin:filter开始处于激活状态了。如果pin需要创建资源,那么就要派生Active方法了
HRESULT CMyInputPin::Active()
{
// You do not need to hold the filter lock here. It is already held in Pause.
/* Create pin resources. */
return CBaseInputPin::Active()
}
3 Receiving and Delivering Samples
下面的伪代码演示了如何派生输入pin的Receive方法
HRESULT CMyInputPin::Receive(IMediaSample *pSample)
{
CAutoLock cObjectLock(&m_csReceive);
// Perhaps the filter needs to wait on something.
WaitForSingleObject(m_hSomeEventThatReceiveNeedsToWaitOn, INFINITE);
// Before using resources, make sure it is safe to proceed. Do not
// continue if the base-class method returns anything besides S_OK.
hr = CBaseInputPin::Receive(pSample);
if (hr != S_OK)
{
return hr;
}
/* It is safe to use resources allocated in Active and Pause. */
// Deliver sample(s), via your output pin(s).
for (each output pin)
pOutputPin->Deliver(pSample);
return hr;
}
Receive方法设置了一个streaming锁,不是filter锁。Filter在开始处理数据之前可能需要等待其他事件发生,这里就调用了WaitForSingleObject。当然并不是所有的filter都需要这样做。CBaseInputPin::Receive方法验证一些基本的streaming条件,如果filter停止旧返回VFW_E_WRONG_STATE,如果filter flushing该方法返回s_FALSE;如果没有返回S_OK就表示Receive方法应该立即返回,不能处理sample。
在sample被处理后,就调用CBaseOutputPin::Deliver.方法向下传递。依次类推。数据开始传递下去。
4 Delivering the End of Stream
当一个输入pin接收到一个数据流停止的通知后,它就会向下传播下去。下游的可以从这个输入pin接收数据的filter也应该接收到数据流停止的通知。如果filter还有未决的数据没有处理,filter应该在它传递停止通知之前一定要将数据传递下去,在end tream消息发布以后,不能再向下游filter发送数据了
HRESULT CMyInputPin::EndOfStream()
{
CAutoLock lock_it(&m_csReceive);
/* If the pin has not delivered all of the data in the stream
(based on what it received previously), do so now. */
// Propagate EndOfStream call downstream, via your output pin(s).
for (each output pin)
{
hr = pOutputPin->DeliverEndOfStream();
}
return S_OK;
}
CBaseOutputPin::DeliverEndOfStream调用了与输出pin连接的输入pin的IPin::EndOfStream方法来通知下游的filter数据流即将停止了。
5 Flushing Data
下面的伪代码演示IPin::BeginFlush方法
HRESULT CMyInputPin::BeginFlush()
{
CAutoLock lock_it(m_pLock);
// First, make sure the Receive method will fail from now on.
HRESULT hr = CBaseInputPin::BeginFlush();
// Force downstream filters to release samples. If our Receive method
// is blocked in GetBuffer or Deliver, this will unblock it.
for (each output pin)
{
hr = pOutputPin->DeliverBeginFlush();
}
// Unblock our Receive method if it is waiting on an event.
SetEvent(m_hSomeEventThatReceiveNeedsToWaitOn);
// At this point, the Receive method can't be blocked. Make sure
// it finishes, by taking the streaming lock. (Not necessary if this
// is the last step.)
{
CAutoLock lock_2(&m_csReceive);
/* Now it's safe to do anything that would crash or hang
if Receive were executing. */
}
return hr;
}
当开始Flushing的时候,BeginFlush方法使用了filter锁。如果使用streaming锁不是很安全,因为flush是发生在应用程序中,有可能此时streaming 线程正处于Receive方法中。Pin要保证Receive方法没有被阻塞,否则,后续调用Receive方法都会失败。
CBaseInputPin::BeginFlush设置了一个标志位CBaseInputPin::m_bFlushing.,如果这个标志为TRUE,Receive方法就会失败。
在下游filter传递Beginflush方法的时候,pin一定要保证下游的filter释放他们的samples并且从Receive方法中返回。这就保证了输入pin不会阻塞在GetBuffer和Receive方法中。如果你的pin的Receive方法正在等待某种资源,那么GeginFlush方法就通知设置某些特定事件来结束这种等待,这就保证Receive方法能够立即返回,m_bFlushing标志就阻止Receive方法调用。
对于一些filter来说,这些都是必须要做的,EndFlush方法通知filter可以重新接收数据了。Endflush方法使用的是filter锁。然后向下传播。
HRESULT CMyInputPin::EndFlush()
{
CAutoLock lock_it(m_pLock);
for (each output pin)
hr = pOutputPin->DeliverEndFlush();
return CBaseInputPin::EndFlush();
}
CBaseInputPin::EndFlush重新设置m_bFlushing标志为FALSE。这样Receive方法就可以开始接收数据了,必须在最后调用这个方法。
6 Stopping
Stop方法必须unblock Receive方法并且decommit filter的内存分配器。这样就使GetBuffer返回。Stop方法使用了filter锁,然后调用了CBaseFilter::Stop,这个stop方法调用了filter pins上的CBasePin::Inactive方法。
HRESULT CMyFilter::Stop()
{
CAutoLock lock_it(m_pLock);
// Inactivate all the pins, to protect the filter resources.
hr = CBaseFilter::Stop();
/* Safe to destroy filter resources used by the streaming thread. */
return hr;
}
Override the input pin's Inactive method as follows:
HRESULT CMyInputPin::Inactive()
{
// You do not need to hold the filter lock here.
// It is already locked in Stop.
// Unblock Receive.
SetEvent(m_hSomeEventThatReceiveNeedsToWaitOn);
// Make sure Receive will fail.
// This also decommits the allocator.
HRESULT hr = CBaseInputPin::Inactive();
// Make sure Receive has completed, and is not using resources.
{
CAutoLock c(&m_csReceive);
/* It is now safe to destroy filter resources used by the
streaming thread. */
}
return hr;
}
7 Getting Buffers
如果你有一个内存分配器来分配资源,那么GetBuffer方法采用streaming锁。
HRESULT CMyInputAllocator::GetBuffer(
IMediaSample **ppBuffer,
REFERENCE_TIME *pStartTime,
REFERENCE_TIME *pEndTime,
DWORD dwFlags)
{
CAutoLock cObjectLock(&m_csReceive);
/* Use resources. */
return CMemAllocator::GetBuffer(ppBuffer, pStartTime, pEndTime, dwFlags);
}
8 Streaming Threads and the Filter Graph Manager
当filter图表管理器停止graph,它要等待所有的streaming线程停止,
9 Summary of Filter Threading
Streaming线程调用的函数
IMemInputPin::Receive
IMemInputPin::ReceiveMultiple
IPin::EndOfStream
IPin::NewSegment
IMemAllocator::GetBuffer
看看应用程序调用的函数把
状态改变
IBaseFilter::JoinFilterGraph, IMediaFilter::Pause, IMediaFilter::Run, IMediaFilter::Stop, IQualityControl::SetSink.
参考时钟
IMediaFilter::GetSyncSource, IMediaFilter::SetSyncSource.
Pin的操作
IBaseFilter::FindPin, IPin::Connect, IPin::ConnectedTo, IPin::ConnectionMediaType, IPin::Disconnect, IPin::ReceiveConnection.
内存的分配
IMemInputPin::GetAllocator, IMemInputPin::NotifyAllocator.
Flushing
IPin::BeginFlush, IPin::EndFlush.
5质量控制管理
DirectShow Base Classes提供了一些缺省的方法来控制视频的质量。质量消息从renderer开始,这个renderer基类为CBaseVideoRenderer,这个基类有下列一些行为
1 当视频render接收到sample的时候,它就将sample上的时间戳和当前的参考时间比较
2 视频render产生一个质量消息,在基类中,质量消息的比例值被限制在500(50%)~~2000(200%),超出这个范围,质量就变得不好
3缺省的时候,render就给上游的filter输出pin发送一个质量消息,应用程序可以同过setsink方法来override这种行为。
当消息被逆流传递到上一个filter时会发生什么呢?一般来说,上游的filter都是一个transform 过滤器。这种transformfilter都有一个CTransformInputPin and CTransformOutputPin。
1 CTransformOutputPin::Notify方法调用CTransformFilter::AlterQuality
2transformfilte可以通过重载AlterQuality方法来处理质量消息,缺省的情况下,AlterQuality忽略质量消息
3如果AlterQuality不处理质量消息,输出pin就调用输入pin上的CBaseInputPin::PassNotify,
4PassNotify方法就将质量消息传递给上游的其他的filter。
假定没有传输filter来处理质量消息,质量消息就最后到达了源filter。在基类中,CBasePin::Notify 返回 E_NOTIMPL。源filter如何处理质量消息取决于源filter的nature。一些filter可以调整他们发送sample的速率。
图2
6Directshow和com
微软的Directshow是基于COM的,如果你开发自己的filter,你一定实现一个com对象。Directshow的基类提供了一个框架可以实现com接口,你不一定适用基类,但是使用基类可以减轻你的开发任务。下面我们讲讲dshow和com关系
我们假设你了解如何开发com的客户端,也就是说你了解IUnknown方法,但是并不详细了解如何开发一个com对象,Directshow将如何开发com的一切细节都替你处理好了。如果你有开发com的经验,你应该读一下Using CUnknown一章。
Com是一个协议,它规定了所有组件必须遵循的规则,如何应用这些规则就留给开发者了。在Directshow中,所有的对象都是从一系列的基类中继承而来,这些基类的函数和构造器将com对象的构造工作都基本做完了,例如引用计数,接口等功能。你将你的filter从基类中继承,你就继承了基类的函数,同时你的filter就是一个com对象了。为了好好继承基类,你必须要了解基类是如何实现com对象的。
本章要求你了解一下三个内容
How IUnknown Works
How to Create a DLL.
How to Register DirectShow Filters.
1 How IUnknown Works
通过IUnknown接口的方法应用程序可以请求对象的接口,管理组件的引用计数。
引用计数
引用是个内部变量,通过addref方法可以是变量增加,通过release方法可以使该变量值减少。基类管理者这个引用计数,并且是多线程同步。
接口请求
接口请求也是简单易懂的。调用者传递两个参数,一个是接口的ID,和接口的指针的地址。如果组件支持该接口,就将该指针指向接口,引用计数增加1,返回S_OK,否则,将该指针设置为NULL,返回E_NOINTERFACE。下面的伪代码演示了QueryInterface函数
if (IID == IID_IUnknown)
set pointer to (IUnknown *)this
AddRef
return S_OK
else if (IID == IID_ISomeInterface)
set pointer to (ISomeInterface *)this
AddRef
return S_OK
else if ...
else
set pointer to NULL
return E_NOINTERFACE
两个组件的querinterface函数的区别就在于IIDS的列表不同,对于组件支持的每一个接口,都要检查IID是否正确。
聚合和委托
组件的聚合对于调用者必须是透明的,因此,聚合体必须只能暴露一个IUnknown接口,被聚合组件的接口听从外部的组件的调用,否则客户端就看到两个不同的IUnknown接口,如果组件不被聚合,他们都有自己的实现。
为了支持聚合,组件必须增加一个额外的间接的指针,用来分别指向外部接口和内部接口,nondelegating IUnknown接口就可以完成这项工作。
外部的接口是公开的,还叫做IUnknown,内部的接口叫做INonDelegatingUnknown.,这个名字不是com定义的,因为它不是一个公开的接口
当客户端创建了一个组件的实例,它调用IClassFactory::CreateInstance方法,一个参数就指向组件的IUnknown接口,组件就利用这个参数来保存一个变量用来标示需要采用哪个IUnknown接口,看看下面的代码
CMyComponent::CMyComponent(IUnknown *pOuterUnkown)
{
if (pOuterUnknown == NULL)
m_pUnknown = (IUnknown *)(INonDelegatingUnknown *)this;
else
m_pUnknown = pOuterUnknown;
[ ... more constructor code ... ]
}
委托的IUnknown接口调用的它的副本,如下
HRESULT QueryInterface(REFIID iid, void **ppv)
{
return m_pUnknown->QueryInterface(iid, ppv);
}
Using CUnknown
Directshow在CUnknown.基类中实现了IUnkown接口,directshow的其他基类也多是从CUnknown类中继承过来的,所以,你也可以从这个类派生一个新类或者从其他基类派生你自己的类。
INonDelegatingUnknown
CUnknown类还实现了一个INonDelegatingUnknown接口,这个接口是用来管理引用计数的。大多数情况下,你的派生类可以直接这个接口的两个管理引用计数的函数而不用做任何的修改。但是你要记住,当引用计数等于0的时候,CUnknown就要销毁自己。有时候你必须要派生CUnknown::NonDelegatingQueryInterface函数,因为在基类中,如果它发现请求接口的ID不是IID_IUnknown的时候就返回E_NOINTERFACE,在你的派生类中,你就要自己来测试你所支持的接口ID,看下面的例子
STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_ISomeInterface)
{
return GetInterface((ISomeInterface*)this, ppv);
}
// default
return CUnknown::NonDelegatingQueryInterface(riid, ppv);
}
GetInterface将接口指针设置为所请求的接口,返回s_ok,增加引用计数,缺省的情况下,要调用你继承类的NonDelegatingQueryInterface
IUnknown
日前所述,任何一个组件的IUnknown接口的处理流程都是一样的。因为它仅仅是根据客户端的请求返回正确的接口即可。因此,为了方便,在Combase.h头文件中声明了一个宏DECLARE_IUNKNOWN,这种宏中定义了三个内联函数,如下
STDMETHODIMP QueryInterface(REFIID riid, void **ppv) {
return GetOwner()->QueryInterface(riid,ppv);
};
STDMETHODIMP_(ULONG) AddRef() {
return GetOwner()->AddRef();
};
STDMETHODIMP_(ULONG) Release() {
return GetOwner()->Release();
};
CUnknown::GetOwner函数返回拥有这个IUnknown接口的组件,对于聚合组件,ower指的外部的组件。否则,指的就是自己。在你自己的类中的公共部分,包含DECLARE_IUNKNOWN宏声明。
类的构造
你自己的类的构造函数一定要从基类的构造函数继承过来,如下
CMyComponent(TCHAR *tszName, LPUNKNOWN pUnk, HRESULT *phr)
: CUnknown(tszName, pUnk, phr)
{
/* Other initializations */
};
构造函数一般都有三个参数,这三个参数传递到CUnknown类的构造函数中,其实你的构造函数基本不用作任何工作,一切都在CUnknown函数的构造函数给你做好了。
TszName指定组件的名字
pUnk指向一个IUnknown指针
pHr指向一个HRESULT值,表示成功还是失败。
下面的例子演示了,如何派生你自己的类,这个类支持Iunknown接口还有一个ISomeInterface接口
class CMyComponent : public CUnknown, public ISomeInterface
{
public:
DECLARE_IUNKNOWN;
STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void **ppv)
{
if( riid == IID_ISomeInterface )
{
return GetInterface((ISomeInterface*)this, ppv);
}
return CUnknown::NonDelegatingQueryInterface(riid, ppv);
}
CMyComponent(TCHAR *tszName, LPUNKNOWN pUnk, HRESULT *phr)
: CUnknown(tszName, pUnk, phr)
{
/* Other initializations */
};
// More declarations will be added later.
};
这个例子假定了一下情况
1, CUnknown类实现了IUnknown接口,如何新的组件从CUnknown基类继承过来,那么同时也就继承了基类所支持的接口。你可以从其他继承于CUnknown的类派生你自己的类,而不一定要从CUnknown派生
2, DECLARE_IUNKNOWN宏将IUnknown接口的三个函数声明为内联函数
3, CUnknown类提供了INonDelegatingUnknown.的实现
4, 如果新的组件支持的接口不仅仅是IUnknown,那么就一定要重新继承INonDelegatingUnknown.函数,测试新接口的ID,如上代码
5, 派生类的构造函数触发了CUnknown的构造函数
下一步就是使得应用程序创建组件的一个实例,这就要求了解DLLs和类厂,以及构造函数的关系,请看How to Create a DLL.
2How to Create a DLL.
在客户端创建一个com对象的实例以前,它首先通过CoGetClassObject方法创建一个对象类厂的实例。然后客户端调用类厂的IClassFactory::CreateInstance方法。类厂自动创建一个组件然后返回一个指向组件对象接口的指针。CoCreateInstance方法是上面两步的综合。
下面的图演示了对象创建
图3
CoGetClassObject方法调用了DLL中的DllGetClassObject方法,这个方法创建了一个类厂对象,并且返回一个类厂对象的接口。Directshow已经替你完成了DllGetClassObject方法 为了了解他们是如何工作,你必须了解directshow是如何实现类厂的。
类厂也是一个com对象,它可以用来创建其他的com组件。每个类厂只能用来创建特定的com组件对象。在directshow中,每一个类厂都是C++类CClassFactory的一个实例,类厂是通过一个叫做类厂模板CFactoryTemplate,来实现的。每个类厂类都有一个指向类厂模板的指针,类厂模板包含了要创建的组件的信息,比如CLSID,和一个指向创建对象函数的指针。
DLL中定义了一个全局的类厂模板的数组,每一个动态的DLL中都有这么一个全局的模版数组,当DllGetClassObject函数创建组件对象的时候,它就搜索全局数组里的匹配的CLSID,如果它找到匹配的数组,它就创建一个包含一个指向匹配模板指针的类厂对象,当客户端调用IClassFactory::CreateInstance 方法的时候,类厂就调用模版中的函数来创建组件对象,下面的DllGetClassObject函数是我从dshow的基类中找到的,我们仔细看看吧
STDAPI
DllGetClassObject(
REFCLSID rClsID,
REFIID riid,
void **pv)
{
if (!(riid == IID_IUnknown) && !(riid == IID_IClassFactory)) {
return E_NOINTERFACE;
}
// 首先在类厂模板数组中查找相应的CLSID
// class id
for (int i = 0; i < g_cTemplates; i++) {
const CFactoryTemplate * pT = &g_Templates[i];
if (pT->IsClassID(rClsID)) {
//如果找到,就用这个类厂模板作参数生成一个类厂对象
// found a template - make a class factory based on this
// template
*pv = (LPVOID) (LPUNKNOWN) new CClassFactory(pT);
if (*pv == NULL) {
return E_OUTOFMEMORY;
}
((LPUNKNOWN)*pv)->AddRef();
return NOERROR;
}
}
return CLASS_E_CLASSNOTAVAILABLE;
}
图4
下面我们看看类厂模板数组
类厂模板数组包含以下变量
const WCHAR * m_Name; // Name
const CLSID * m_ClsID; // CLSID
LPFNNewCOMObject m_lpfnNew; // Function to create an instance of the component
LPFNInitRoutine m_lpfnInit; // Initialization function (optional)
const AMOVIESETUP_FILTER * m_pAMovieSetup_Filter; // Set-up information (for filters)
两个函数指针m_lpfnNew and m_lpfnInit使用如下的定义
typedef CUnknown *(CALLBACK *LPFNNewCOMObject)
(LPUNKNOWN pUnkOuter, HRESULT *phr);
Typedef void (CALLBACK *LPFNInitRoutine)
(BOOL bLoading, const CLSID *rclsid);
第一个用来创建对象的实例函数,第二个函数是初始化函数,如果你定义了这个函数,在动态库DLL的进入点函数就会调用这个初始化函数。
假设你定义了一个DLL,这个DLL包含了一个叫做CMYComponent组件,它继承与CUnknown,你必须在你的DLL定义下面的几个东西
1 一个创建你的组件的实例的公有函数。
2一个全局的类厂模板数组,名字一定要命名为g_Templates,这个数组包含创建组件的类厂模板
3一个叫做全局变量g_cTemplates,这个变量用来表示类厂模板数组的大小。
下面是示例代码
// Public method that returns a new instance.
CUnknown * WINAPI CMyComponent::CreateInstance(LPUNKNOWN pUnk, HRESULT *pHr)
{
CMyComponent *pNewObject = new CMyComponent(NAME("My Component"), pUnk, pHr );
if (pNewObject == NULL) {
*pHr = E_OUTOFMEMORY;
}
return pNewObject;
}
CFactoryTemplate g_Templates[1] =
{
{
L"My Component", // Name
&CLSID_MyComponent, // CLSID
CMyComponent::CreateInstance, // Method to create an instance of MyComponent
NULL, // Initialization function
NULL // Set-up information (for filters)
}
};
int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);
类厂的CreateInstance方法调用组件的构造函数,并返回一个指向新类实例的一个指针。参数punk指向IUnknown接口指针,看看我从基类里找到的Createinstance函数的代码
STDMETHODIMP
CClassFactory::CreateInstance(
LPUNKNOWN pUnkOuter,
REFIID riid,
void **pv)
{
CheckPointer(pv,E_POINTER)
ValidateReadWritePtr(pv,sizeof(void *));
/* Enforce the normal OLE rules regarding interfaces and delegation */
if (pUnkOuter != NULL) {
if (IsEqualIID(riid,IID_IUnknown) == FALSE) {
return ResultFromScode(E_NOINTERFACE);
}
}
/* Create the new object through the derived class's create function */
HRESULT hr = NOERROR;
CUnknown *pObj = m_pTemplate->CreateInstance(pUnkOuter, &hr);
if (pObj == NULL) {
if (SUCCEEDED(hr)) {
hr = E_OUTOFMEMORY;
}
return hr;
}
/* Delete the object if we got a construction error */
if (FAILED(hr)) {
delete pObj;
return hr;
}
/* Get a reference counted interface on the object */
/* We wrap the non-delegating QI with NDAddRef & NDRelease. */
/* This protects any outer object from being prematurely */
/* released by an inner object that may have to be created */
/* in order to supply the requested interface. */
pObj->NonDelegatingAddRef();
hr = pObj->NonDelegatingQueryInterface(riid, pv);
pObj->NonDelegatingRelease();
/* Note that if NonDelegatingQueryInterface fails, it will */
/* not increment the ref count, so the NonDelegatingRelease */
/* will drop the ref back to zero and the object will "self-*/
/* destruct". Hence we don't need additional tidy-up code */
/* to cope with NonDelegatingQueryInterface failing. */
if (SUCCEEDED(hr)) {
ASSERT(*pv);
}
return hr;
}
我们发现,类厂就是通过全局变量g_Templates and g_cTemplates来创建组件的,所以,g_Templates and g_cTemplates的名字不能改变
下面看看dll的导出函数
DLL Functions
一个动态库必须导出如下的函数,才能够注册,注销,加载。
? DllMain: The DLL entry point. The name DllMain is a placeholder for the library-defined function name. The DirectShow implementation uses the name DllEntryPoint. For more information, see the Platform SDK. Dshow没有DllMain,改用了DllEntryPoint。进入点函数
? DllGetClassObject: Creates a class factory instance. Described in the previous sections.
? DllCanUnloadNow: Queries whether the DLL can safely be unloaded.
? DllRegisterServer: Creates registry entries for the DLL.
? DllUnregisterServer: Removes registry entries for the DLL.
当然了,前面的三个函数,directshow已经替我们完成了,如果你的类厂模板数组中的m_lpfnInit指针有一个初始化函数,那么在DLL的入口函数就会被调用。
你自己必须要提供DllRegisterServer and DllUnregisterServer函数来实现组件的注册和反注册,但是Directshow提供了提供了一个函数叫做AMovieDllRegisterServer2已经替你做完了必要的工作,所以你要做的就很简单了,只需如下就可以了
STDAPI DllRegisterServer()
{
return AMovieDllRegisterServer2( TRUE );
}
STDAPI DllUnregisterServer()
{
return AMovieDllRegisterServer2( FALSE );
}
当然,在DllRegisterServer and DllUnregisterServer 函数中,你可以根据需要定制自己的注册信息,如果你的组件包含一个过滤器,你就要自己来做一下额外的工作了,具体的你可以下节How to Register DirectShow Filters.
在你的module-definition (.def) file文件中,除了进入点函数,你要导出下面的函数,实例如
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
你可以用Regsvr32.exe来注册你的组件
3How to Register DirectShow Filters.
Directshow的filter一般都注册在两个地方
1 包含filter的DLL一般都注册为filter的COM 服务器,当用户调用CoCreateInstance来创建一个filter的时候,微软的COM库就从这个注册表的入口加载DLL。
2 另外,filter可以注册到filter 种类里,这样,System Device Enumerator and the Filter Mapper就可以找到filter了。
第二种注册不是必须的,只要filter注册成为com服务器,一个应用程序就可以创建一个filter 并将它加入到 filter Graph中,但是,如果你想要你的filter 可以被System Device Enumerator and the Filter Mapper发现,你必须注册到filter 种类里。
Com服务器的入口注册有下列以些键值
HKEY_CLASSES_ROOT
CLSID
Filter CLSID
REG_SZ: (Default) = Friendly name
InprocServer32
REG_SZ: (Default) = File name of the DLL
REG_SZ: ThreadingModel = Both
注册成filter 种类里需要下面的键值
HKEY_CLASSES_ROOT
CLSID
Category
Instance
Filter CLSID
REG_SZ: CLSID = Filter CLSID
REG_BINARY: FilterData = Filter information
REG_SZ: FriendlyName = Friendly name
Category is the GUID of a filter category.
所有的filter信息在注册表的filter种类都如下所示
HKEY_CLASSES_ROOT\CLSID\{DA4E3DA0-D07D-11d0-BD50-00A0C911CE86}\Instance
1 声明filter信息Declaring Filter Information
第一步要声明filter的信息,directshow定义了如下的结构来声明filter ,pin和media Types
Structure Description
AMOVIESETUP_FILTER Describes a filter.
AMOVIESETUP_PIN Describes a pin.
AMOVIESETUP_MEDIATYPE Describes a media type.
这些结构是必须的。
AMOVEIESETUP_FILTER结构包含一个指针指向AMOVIESETUP_PIN结构数组,这两个结构中都有一个指针指向AMOVEIESETUP_MEDIATYPE。这些结构提供了足够的信息可以让IFilterMapper2指针找到filter 的位置。但是,这并不能完全描述一个filter,例如,如果一个filter创建了一个pin的多个实例,你只需要声明一个AMOVIESETUP_PIN结构即可。同样,一个filter 没有必须支持注册的所有的媒体类型,也没有必要注册所有的媒体类型。
在你的DLL中声明一些全局的Set_up结构变量,如下
static const WCHAR g_wszName[] = L"Some Filter";
AMOVIESETUP_MEDIATYPE sudMediaTypes[] = {
{ &MEDIATYPE_Video, &MEDIASUBTYPE_RGB24 },
{ &MEDIATYPE_Video, &MEDIASUBTYPE_RGB32 },
};
AMOVIESETUP_PIN sudOutputPin = {
L"", // Obsolete, not used.
FALSE, // Is this pin rendered?
TRUE, // Is it an output pin?
FALSE, // Can the filter create zero instances?
FALSE, // Does the filter create multiple instances?
&GUID_NULL, // Obsolete.
NULL, // Obsolete.
2, // Number of media types.
sudMediaTypes // Pointer to media types.
};
AMOVIESETUP_FILTER sudFilterReg = {
&CLSID_SomeFilter, // Filter CLSID.
g_wszName, // Filter name.
MERIT_NORMAL, // Merit.
1, // Number of pin types.
&sudOutputPin // Pointer to pin information.
};
Filter的名字被声明成静态全局变量,因为它有可能在其它地方用到。
2 声明类厂模板数组Declaring the Factory Template
CFactoryTemplate g_Templates[] = {
{
g_wszName, // Name.上面定义的全局变量
&CLSID_SomeFilter, // CLSID.
CSomeFilter::CreateInstance, // Creation function.
NULL,
&sudFilterReg // Pointer to filter information.
}
};
int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);
3生成DllRegisterServer
最后一步是生成DllRegisterServer函数,包含组件的DLL必须导出这个函数,这个函数在安装的时候被调用,或者当用户运行Regsvr32.exe时调用到。
简单的实现如下
STDAPI DllRegisterServer(void)
{
return AMovieDllRegisterServer2(TRUE);
}
AMovieDllRegisterServer2对于g_Templates数组中的所有组件都创建注册表入口,但是这个函数有一些限制,第一,它将所有的filter都注册到"DirectShow Filters"类下(CLSID_LegacyAmFilterCategory),其实并非所有的filter都属于这个种类。例如,捕捉filter和压缩filter就有他们自己的种类,第二,如果你的filte支持一个硬件设备,你必须要注册额外的两个信息the medium and the pin category.,但是AMovieDLLRegisterServer2并不支持,pin 的种类定义了一个pin的函数方法。Mediums和硬件的驱动有关。
如果你要注册filter的种类,medium或者pin的种类,你可以在DllRegisterServer()中调用IFilterMapper2::RegisterFilter,这个方法有个REGFILTER2结构,包含了filter的信息。
为了支持复杂的情况,REGFILTER2结构支持两种不同格式pin的注册,dwVersion表示两种格式
如果dwVersion为1,pin的类型就是AMOVIESETUP_PIN
如果dwVersion为2,拼得类型就是REGFILTERPINS2.
REGFILTERPINS2.结构中包含mediums和pin的categories
下面的例子演示了,如何在DllRegistServer中调用IFilterMapper2::RegisterFilter
REGFILTER2 rf2FilterReg = {
1, // Version 1 (no pin mediums or pin category).
MERIT_NORMAL, // Merit.
1, // Number of pins.
&sudPins // Pointer to pin information.
};
STDAPI DllRegisterServer(void)
{
HRESULT hr;
IFilterMapper2 *pFM2 = NULL;
hr = AMovieDllRegisterServer2(TRUE);
if (FAILED(hr))
return hr;
hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER,
IID_IFilterMapper2, (void **)&pFM2);
if (FAILED(hr))
return hr;
hr = pFM2->RegisterFilter(
CLSID_SomeFilter, // Filter CLSID.
g_wszName, // Filter name.
NULL, // Device moniker.
&CLSID_VideoCompressorCategory, // Video compressor category.
g_wszName, // Instance data.
&rf2FilterReg // Pointer to filter information.
);
pFM2->Release();
return hr;
}
4filter注册指南
Filter的注册信息决定了,在filter Graph管理器中如何Intelligent Connect.。因此,你必须要遵从下列的规则,使得你的filter能够正常运行。
1 你是否需要在注册表中保存你的filter数据,对于许多filter来说,没有必要让filter Mapper和System Device Enumerator来发现你的filter,只要你注册了你的filter,你的应用程序通过
ConCreateInstance方法来创建你的filter,此时,忽略了类厂模板中的AMOVIESETUP_FILTER结构,缺点是,在GraphEdit中看不到你的filter。
2选择正确的filter 种类,缺省的Directshow Filters可能适用于大多数的filter,但是如果你的filter有特殊的用处,你要选择一个恰当的种类
3避免在pin的AMOVIESETUP_MEDIATYPE结构中使用MEDIATYPE_None, MEDIASUBTYPE_None, or GUID_NULL,IFilterMapper2会将这些视做通配符。
4下面是一些建议的最小******不明白
Type of filter Recommended merit
Default renderer MERIT_PREFERRED. For standard media types, however, a custom renderer should never be the default.
Non-default renderer MERIT_DO_NOT_USE or MERIT_UNLIKELY
Mux MERIT_DO_NOT_USE
Decoder MERIT_NORMAL
Spitter, parser MERIT_NORMAL or lower
Special purpose filter; any filter that is created directly by the application MERIT_DO_NOT_USE
Capture MERIT_DO_NOT_USE
"Fallback" filter; for example, the Color Space Converter Filter
MERIT_UNLIKELY
5不要将接受24位RGB的filter注册到Directshow filter,你的filter将会干扰Color Space Converter filter.工作
5 反注册
为了反注册filter,
要提供一个DllUnregisterServer方法,在这个方法中,调用AMovieDllRegisterServer2,注意传递参数,FASLE,如果你是使用IFilterMapper2::RegisterFilter注册的你的filter,那么你必须要用IFilterMapper2::UnregisterFilter方法来反注册你的filter。如下
STDAPI DllUnregisterServer()
{
HRESULT hr;
IFilterMapper2 *pFM2 = NULL;
hr = AMovieDllRegisterServer2(FALSE);
if (FAILED(hr))
return hr;
hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER,
IID_IFilterMapper2, (void **)&pFM2);
if (FAILED(hr))
return hr;
hr = pFM2->UnregisterFilter(&CLSID_VideoCompressorCategory,
g_wszName, CLSID_SomeFilter);
pFM2->Release();
return hr;
}
作者简介:李强,目前暂时供职于山大联润信息科技有限公司,从事网络视频会议软件的开发,目前的感兴趣的方向,移动设备上多媒体的开发。[email protected] 欢迎转载本文档 。
本文地址:http://com.8s8s.com/it/it21831.htm