4.3 Filter States
Filter有三种状态,停止,暂停,运行。暂停状态是为了在Graph中Cue Data, 使得运行命令可以立即响应。Filter Graph Manager控制着所有状态的转换。当应用程序调用IMediaControl的Run, Pause, Stop方法时, Filter Graph Manager就调用所有Filter的相应IMediaFilter方法。停止,运行状态的切换总是要经过暂停,因此,当应用程序对停止的Graph 调用RUN命令时,Filter图表管理器在Run之前首先要暂停。对于大多数的Filter来说,Running和Paused状态是一样的。考虑如下的Filter Graph:
Source -> Transform -> Renderer
假定Source Filter不是实时源。当Source Filter暂停,它创建一个线程生成新的数据并尽可能快的写入到Media Samples. 线程通过调用Transform Filter的输入PIN上的IMemInputPin的 Receive方法把数据向下推。Transform Filter接收到在Source Filter的线程中的Sample.它可能用一个工作线程把Sample递送给Renderer,但是通常是在同一个线程完成。此时Renderer暂停,它等待接收Sample. 在接收到一个Sample后,它就阻塞线程并不定的保存数据。如果是视频Renderer, 把Sample作为张贴图像显示,如果必要就重画图像。
现在,数据流完全准备好进行提交。如果Graph保持暂停,在第一帧Sample后Sample就堆积在Graph, 直到每个Filter都阻塞在Receive或GetBuffer. 但是不会有数据丢失。一旦Source线程非阻塞,它简单从阻塞的点恢复。
Source Filter和Transform Filter忽略从暂停到运行的变换,它们简单的尽可能快的继续处理数据。当Render开始运行,它就开始提交Sample. 首先提交的就是当它阻塞时保存的Sample. 然后,每次都接收到新Sample. 计算Sample的提交时间。(细节可参考Time and Clocks in DirectShow),Render会保存每个Sample直到提交时间,到点后再提交Sample. 当它等待提交时间时,线程也阻塞在Receive方法,或者在工作线程用队列接收Sample. 从Renderer向上的所有Filter都不会陷于时间安排。
实时源(比如捕捉设备)与普通结构有些不同。在实时源中,不合适提前准备任何数据。应用程序可能暂停Graph,在运行前等待比较长的时间。Graph不应该提交过时Sample. 因此,暂停时实时源不会产生数据,只有运行时才有。为了把事件通知给Filter Graph Manager, Source Filter的IMediaFilter方法GetState返回VFW_S_CANT_CUE。此返回值表示Filter已经转换到暂停状态,即使Renderer没有接收到任何数据。
当一个Filter停止时,它拒绝发送给它的任何Samples,Source Filter关闭他们的Stream线程,其他Filter也关闭他们创建的工作线程,Pin再Decommit他们的内存分配器。
4.3.1 State Transitions
Filter Graph Manager按照逆流的方向来切换Filter的状态,从Renderer Filter到Source Filter,这种方式可以防止死锁和Sample的丢失。最关键的状态切换是暂停和停止之间。
·从停止到暂停,当Filter暂停时就准备好了从连接Filter接收Sample。Source Filter是最后一个暂停的。它创建Streaming线程发送Sample,因为下游的Filter的状态都已经切换到暂停了,所以,所有的Filter都可以接收Sample。只有当Graph所有的Renderer都接收到Sample,Filter Graph Manager才算完成了状态的切换(除了前面讲的实时源例外)。
·从暂停到停止。当Filter停止时它要释放它拥有的所有的Sample,以使得上一级Filter中的IMemAllocator::GetBuffer调用脱离阻塞。如果Filter在Receive函数中等待资源,它也会停止等待,并从Receive返回,以使调用Filter解锁。因此,当Filter Graph Manager停止连接的上一级Filter时,Filter就不会在GetBuffer和Receive函数中阻塞,也就能响应停止命令。上级Filter在得到停止命令前可以会递送一些额外Sample。但是下级Filter可能拒绝他们,因为他们已经停止。
4.4 Pull Model
在IMemInputPin接口中,上级Filter决定发送什么样数据,然后将数据推给下级Filter。但对某些Filter,拉模式更适合。现在,下级Filter向上级Filter请求数据。数据依然是从上级到下级,从输出Pin到输入Pin,但是由下级Filter发动数据流动。这种类型连接采用的是IAsyncReader接口。
拉模式的典型应用是文件回放。例如在AVI文件的回放Graph中,Async File Source Filter就执行通用文件读写操作,然后将数据以字节流的方式(不带格式信息)发送给下级Filter。AVI Splitter读取AVI头并把数据流分析为视频、音频流。AVI Splitter能比Async File Source Filter更好的决定它需要什么类型的数据,因此它用IMemInputPin代替IAsyncReader接口。
为了从输出PIN请求数据,输入PIN调用如下一种方法:
·IAsyncReader::Request
·IAsyncReader::SyncRead
·IAsyncReader::SyncReadAligned
第一个方法是异步的,支持多个重叠读写。其他是同步的。
理论上,任何Filter可以支持IAsyncReader,但是实际上它被设计来连接Source Filter和Parser Filter。Parser的功能与推模式中的Source Filter非常相同。当它暂停时,创建一个数据流线程从IAsyncReader连接拉数据,并推向下一级。输出PIN使用IMemInputPin,Graph的余下部分使用标准的推模式。
5. Event Notification in DirectShow
5.1 Overview of Event Notification
当某个事件发生时,比如数据流结束,产生一个错误,提交流失败等,Filter就给Filter Graph Manager发送一个事件通知。Filter Graph Manager自己处理部分事件,另一部分交给应用程序处理。如果Filter Graph Manager没有处理事件,它就把事件通知放入到一个队列中。图表管理器也可以将自己的事件通知放进队列中。
应用程序以事件队列获取事件并根据事件类型进行处理。DirectShow中的事件通知和Windows的消息机制差不多。应用程序也可以取消Filter Graph Manager特定事件的默认行为。Filter Graph Manager就直接把事件放入队列,留给应用程序来处理。
5.2 Retrieving Events
Filter Graph Manager暴露了三个接口处理事件通知:
·IMediaEventSink Filter用这个接口来Post事件。
·IMediaEvent 应用程序利用这个接口来从队列中查询消息。
·IMediaEventEx 是IMediaEvent的扩展。
Filter通过IMediaEventSink::Notify方法向Filter Graph Manager提交事件。事件通知包括一个事件Code,这个Code不仅仅代表了事件的类型,还包含两个DWORD类型的参数用来传递一些其他的信息。根据不同的事件Code,附加信息可能是指针、返回码、参考时间或其他信息。获取事件Code和参数的全部列表可参考Event Notification Codes.
应用程序通过调用IMediaEvent::GetEvent从事件队列中获取事件。如果有事件发生,该函数就返回一个事件码和两个参数,如果没有事件,则一直阻塞直到有事件发生和超过某个时间。调用GetEvent函数后,应用程序必须调用IMediaEvent::FreeEventParams来释放事件码所关联的资源。例如,参数可能是Filter Graph分配的BSTR内存。下面的代码演示了如何从事件队列中提取事件:
long evCode, param1, param2;
HRESULT hr;
while (hr = pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0), SUCCEEDED(hr))
{
switch(evCode)
{
// Call application-defined functions for each
// type of event that you want to handle.
}
hr = pEvent->FreeEventParams(evCode, param1, param2);
}
为了重载Filter Graph Manager对事件的缺省处理,可以用某个事件码做参数调用IMediaEvent的CancelDefaultHandling方法。这样就可以屏蔽Filter Graph Manager对某个事件码的处理了。如果要恢复图表管理器对该事件码的缺省处理,可以调用RestoreDefaultHandling方法。如果Filter Graph对某个事件没有缺省的处理,调用这两个函数是不起作用的。
5.3 Learning When an Event Occurs
为了处理DirectShow事件,应用程序需要一种机制来知道什么时候队列中有等待的事件。Filter Graph Manager提供了两种方法:
·窗口通知:Filter Graph Manager发送开发者自己定义的窗口消息
·事件信号:如果队列中有DirectShow事件,就用事件信号通知应用程序,如果队列为空就重新设置事件信号。
应用程序可以使用这两种技术。窗口通知通常更简单。
5.3.1 Window Notification
建立窗口通知可调用IMediaEventEx::SetNotifyWindow方法,并指定一个私有消息。应用程序可使用从WM_APP到0XBFFF之间的消息值作为私有消息。只要Filter Graph Manager把一个新事件放入到队列时,就向目标窗口发送消息。应用程序从消息队列响应消息。
下面的代码演示了如何利用消息通知:
#define WM_GRAPHNOTIFY WM_APP+1 // Private message.
pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);
消息是一个普通的窗口消息,是从DirectShow的事件通知队列单独提交的。这种方法的好处多数程序已经实现了消息循环。因此,我们可以合并DirectShow的事件通知而不需要过多额外工作。下面的代码说明了如何响应消息通知的框架。完整的例子可参考Responding to Events.
LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam)
{
switch (msg)
{
case WM_GRAPHNOTIFY:
HandleEvent(); // Application-defined function.
break;
// Handle other Windows messages here too.
}
return (DefWindowProc(hwnd, msg, wParam, lParam));
}
由于事件通知和窗口的消息循环都是异步的,因此,当你的应用程序处理消息的时候,队列中或许有N个事件等待处理。同样,如果事件失效,它也可能从事件队列清除掉。因此,在你调用GetEvent的时候,一定要循环调用,直到返回一个错误码,这表明队列是空的。
当你释放IMediaEventEx指针时,你可以调用SetNotifyWindow来取消事件通知,记住此时要给这个函数传递一个NULL指针。在你的事件处理程序中,在调用GetEvent之前一定要检查IMediaEventEx指针是否为空,这样就可以避免错误。因为有时在释放IMediaEventEx指针后可能会得到通知。
5.3.2 Event Signaling
在Filter Graph Manager里有一个手动设置的Event内核对象,用来反映事件队列的状态。如果队列中有等待处理的事件,Event就处于通知状态,如果队列是空的,IMediaEvent::GetEvent函数调用就会重置该Event对象。应用程序可利用此事件来判断队列的状态。
应用程序可以调用IMediaEvent::GetEventHandle获得Event内核对象的句柄,然后就可以调用WaitForMultipleObjects来等待事件的发生,如果Event被通知了,就可以调用GetEvent来获得DirectShow的事件。下面的代码演示了事件信号。返回事件句柄,再等待1000毫秒。如果事件变为信号状态,调用GetEvent返回事件Code和参数。获取到EC_COMPLETE事件Code时表示回放结束中止循环:
HANDLE hEvent;
long evCode, param1, param2;
BOOLEAN bDone = FALSE;
HRESULT hr = S_OK;
hr = pEvent->GetEventHandle((OAEVENT*)&hEvent);
if (FAILED(hr)
{
/* Insert failure-handling code here. */
}
while(!bDone)
{
if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent, 100))
{
while (hr = pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0), SUCCEEDED(hr))
{
printf("Event code: %#04x\n Params: %d, %d\n", evCode, param1, param2);
pEvent->FreeEventParams(evCode, param1, param2);
switch (evCode)
{
case EC_COMPLETE: // Fall through.
case EC_USERABORT: // Fall through.
case EC_ERRORABORT:
CleanUp();
PostQuitMessage(0);
return;
}
}
}
}