DirectShow技术介绍(长篇)-5

    技术2022-07-01  74

    3.4. Filter Graph中的数据流

         这一节主要描述媒体数据是如何在filter graph中流动的。如果你只是为了编写DirectShow应用程序,你不需要知道这些细节,当然,知道这些细节对于编写directshow应用程序仍然是有帮助的。但是如果你要编写directshow filter,那么你就必须掌握这部分知识了。

    3.4.1. DirectShow数据流概述

         在这一部分先粗略地描述一下DirectShow中数据流是如何工作的。

         数据首先是被保存在缓冲区里的,在缓冲区里,它们仅仅是一个字节数组。每一个缓冲区被一个称作媒体样本(media sample)的COM对象所包容,media sample提供IMediaSample接口。media sample由另一个称作分配器(allocator)的COM对象创建,allocator提供IMemAllocator接口。每一个pin连接都指定有一个allocator,当然,两个或多个pin连接也可以共享几个allocator

     

     

     

         每一个allocator都创建一个media sample池,并为每个sample分配缓冲区。一旦一个filter需要一个缓冲区来填充数据,它就调用IMemAllocator::GetBuffer方法来请求一个sample。只要allocator有一个sample还没有被任何filter使用,GetBuffer方法就立即返回一个sample的指针。如果allocator所有的sample已经被用完,这个方法就阻塞在那里,直到有一个sample变成可用的了。GetBuffer返回一个sample后,filter就将数据写入到sample的缓冲区中去,并在sample上设置适当的标记(如时间戳),然后将它递交到下一个filter去。

         当一个renderer filter接收到了一个sample时,renderer filter检查时间戳,并将sample先保存起来,直到filter graph的参考时钟指示这个sample的数据可以被render了。当filter将数据render后,它就将sample释放掉,此时sample并不立即回到allocatorsample池中去,除非这个sample上的参考计数已经变为0,表示所有的filter都已释放这个sample

        上游的filter可能在renderer之前运行,这就意味着,上游的filter填充缓冲的速度可能快于renderer销毁它们。但是尽管如此,samples也并无必要更早地被render,因为renderer将一直保存它们直到适当的时机去render,并且,上游filter也不会意外地将这些samples的缓冲覆盖掉,因为GetSample方法只会返回那些没有被使用的sample。上游filter可以提前使用的sample的数量取决于allocator分配池中的sample的数量。

         前面的图表只显示了一个allocator,但是通常的情况下,每个流中都会有多个allocator。因此,当renderer释放了一个sample时,它会产生一个级联效应。如下图所示,一个decoder保存了一个视频压缩帧,它正在等待renderer释放一个sample,而parser filter也正在decoder去释放一个sample

       

     

     

     

     

         renderer释放了一个sample后,decoder完成尚未完成的GetBuffer调用。然后decoder便可以对压缩的视频帧进行解码并释放它保存的sample,从而使parser完成它的GetBuffer调用。

      

    3.4.2. 传输协议(Transports)

         为了使媒体数据能在filter graph中流动,Directshow filter必须能支持多个协议中的一个,这些协议被称作传输协议(transports)。当两个filter连接后,它们必须支持同一个传输协议,否则,它们将不能交换数据。通常,一个传输协议要求某个pin支持一个特定的接口,当两个filter连接时,另一个pin来调用这个pin的这个接口。

         大多数的directshow filter在主存中保存媒体数据,并且通过pin连接向另一个filter递交数据,这种类型的传输协议被称作本地内存传输协议(local memory transport)。尽管这类传输协议在directshow中应用最普遍,但并非所有的filter都使用它。例如,某些filter通过硬件途径来传递数据,使用pin仅仅是为了传递控制信息,如IOverlay接口。

         DirectShow为本地内存传输协议定义了两种机制,推(push)模式和拉(pull)模式。在推模式中,source filter产生数据,并将其递交给下游的filter,下游的filter被动地接收数据并处理它们,再将数据传递给它的下游filter。在拉模式中,source filter与一个parser filter连接,parser filtersource filter请求数据,source filter回应请求并传递数据。推模式使用IMemInputPin接口,而拉模式使用IAsyncReader接口。

         推模式比拉模式应用更广泛。

       

    3.4.3. 媒体样本(sample)和分配器(allocator)

         当一个pin向另一个pin传递媒体数据时,它并不是直接传递一个内存缓冲区的指针,而是传递一个COM对象的指针,这个COM对象管理着内存缓冲,被称为媒体样本(media sample),暴露IMediaSample接口。接收方pin通过调用IMediaSample接口的方法来访问内存缓冲,如IMediaSample::GetPointerIMediaSample::GetSizeIMediaSample::GetActualDataLength

         sample总是从输出pin到输入pin向下传输。在推模式中,输出pin通过在输入pin上调用IMemInputPin::Receive方法来传递一个sample。输入pin或者在Receive方法内部同步地处理数据,或者另开一个工作线程进行异步处理。如果输入pin需要等待资源,允许在Receive中阻塞。

         另一个用来管理媒体样本的COM对象,被称作分配器(allocator),它暴露IMemAllocator接口。一旦一个filter需要一个空闲的媒体样本,它调用IMemAllocator::GetBuffer方法来获得sample的指针。每一个pin连接都共享一个allocator,当两个pin连接时,它们协商决定哪个filter来提供allocatorpin可以设置allocator的属性,比如缓冲的数量和每个缓冲的大小。

         下图显示了allocatormedia samplefilter的关系:

       

       

     

     

     

     

        媒体样本参考计数(Media Sample Reference Counts)

         一个allocator创建的是一个拥有有限个samplesample池。在某一时刻,有些sample正在被使用,有些则可被GetBuffer方法使用。allocator使用参考计数来跟踪sampleGetBuffer方法返回的sample参考计数为1,如果参考计数变为0sample就可以返回到allocatorsample池中去了,这样它就可以再次被GetBuffer方法使用。在参考计数大于0期间,sample是不能被GetBuffer使用的。如果每个从属于allocatorsample都在被使用,则GetBuffer方法会被阻塞直至有sample可以被使用。

         举个例子,假设一个输入pin接收到一个sample。如果它同步地在Receive方法内部处理它,sample的参考计数不增加,当Receive返回时,输出pin释放这个sample,参考计数归0sample就返回到sample池中去了。另一种情况,如果输入pin异步地处理sample,它就在Receive方法返回前将sample的参考计数加1,此时参考计数变为2。当输出pin释放这个sample时,参考计数变为1sample不能返回到sample池中去,直到异步处理的工作线程完成工作,调用Release释放这个sample,参考计数变为0时,它才可以返回到sample池中去。

         当一个pin接收到一个sample,它可以将数据拷贝到另一个sample中去,或者修改原始的sample并将其传递到下一个filter中去。一个sample可能在整个graph长度内被传递,每个filter都依次调用AddRefRelease。因而,输出pin在调用Receive后一定不能重复使用同一个sample,因为下游的filter可能正在使用这个sample。输出pin只能调用GetBuffer来获得新的sample

         这个机制减少了总的内存分配过程,因为filter可以重复使用同样的缓冲。它同样防止了数据在被处理前意外地被覆盖写入。

         filter处理数据后数据量会变大(如解码数据),一个filter可以为输入pin和输出pin分配不同的allocator。如果输出数据并不比输入数据量要大,filter可以用替换的方式来处理数据而不用将其拷贝到新的sample中去,在这种情况下,两个或多个pin连接共享一个allocator

       

        提交(Commit)和反提交(Decommit)分配器

         当一个filter首次创建一个allocator时,allocator并不为其分配内存缓冲,此时如果调用GetBuffer方法的话会失败。当流开始流动时,输出pin调用IMemAllocator::Commit来提交allocator,从而为其分配内存。此时pin可以调用GetBuffer了。

         当流停止时,pin调用IMemAllocator::Decommit来反提交allocator,在allocator被再次提交前所有后来的GetBuffer调用都将失败,同样,如果有阻塞的正在等待sampleGetBuffer调用,也将立即返回失败信息。Decommit方法是否释放内存取决于实现方式,如CMemAllocator类直至析构时才释放内存。

       

    3.4.4. filter状态

         filter有三种可能的状态:停止(stopped),就绪(paused)和运行(running)。就绪状态的目的是为了让graph提前做准备以便在run命令下达时可以立即响应。Filter Graph Manager控制所有的状态转换。当一个应用程序调用IMediaControl::RunIMediaControl::PauseIMediaControl::Stop时,Filter Graph Manager在所有filter上调用相应的IMediaFilter方法。在停止状态和运行状态之间转换时总是要经过就绪状态,即如果应用程序在一个处于停止状态的graph上调用Run时,Filter Graph Manager在运行它之前先将其转为pause状态。

         对于大多数filter来说,运行状态和就绪状态是等同的。看下面的这个graph:

         Source > Transform > Renderer

         假设这个source filter不是一个实时采集源,当source filter就绪时,它创建一个线程来尽可能快地产生新数据并写入到media sample中去。线程通过在transform filter的输入pin上调用IMemInputPin方法将sample“推”到下游filtertransform filtersource filter的线程中接收数据,它可能也使用一个工作线程赤将sample传递给renderer,但是在通常情况下,它在同一个线程中传递它们。如renderer处理就绪状态下,它等待接收sample,当它接收到一个时,它或阻塞或保存那个sample,如果这是一个Video renderer,则它将sample显示为一个静态的图片,只在必要的时候刷新它。

         此时,流已经准备充分去被render,如果graph仍然处理就绪状态下,sample会在每一个sample后堆积,直至每个filter都被阻塞在ReceiveGetBuffer下。没有数据会被丢失。一旦source线程的阻塞被解除时,它只是简单地从阻塞点那里进行恢复。

         source filtertransform filter忽略从就绪状态转到运行状态——它们仅仅是尽可能快地继续处理数据。但是当renderer运行时,它就要开始render sample了。首先,它render在就绪状态下保存的那个sample,接着,每接收到一个新的sample,它计算这个sample的呈现时间,renderer保存每个sample直至到了它们的呈现时间再render它们。在等待合适的呈现时间时,它或者阻塞在Receive方法上,或者在一个工作线程中接收数据并将其放入队列中去。renderer的上一个filter不关心这些问题。

         实时源(live source),如采集设备,是通常情况中的一个例外。在实时源中,不适合提前准备数据。应用程序可能将graph置于就绪状态下,然后等很长时间才再运行它。graph不应该再render就绪期间的sample,因此,一个实时源在就绪状态时不产生新的sample。要将这种情况通知给filter graph managersource filterIMediaFilter::GetState方法返回VFW_S_CANT_CUE。这个返回值表示filter已切换到就绪状态下,即使renderer还没有收到任何数据。

         当一个filter停止时,它不再接收任何传递给它的数据。source filter关闭它们的流线程,别的filter关闭所有它们创建的工作线程。pin反提交(decommit)它们的allocator

       

        状态转换

         filter graph manager按从下游filter到上游filter的次序来完成所有的状态转换,从renderer开始逐个向上直至source filter,这个次序是必要的,可以防止数据丢失或graph死锁。最重要状态转换是就绪状态和停止状态间的转换:

         *停止状态到就绪状态:当每一个filter被置为就绪态时,它便准备好从上一个filter接收samplesource filter是最后一个被置为就绪态的filter,它创建数据流线程并开始传递sample。因为所有下游filter都处于就绪状态,所以没有一个filter会拒绝接收sample。当graph中所有的renderer都接收到一个sample后,filter graph manager才彻底完成状态转换工作(实时源除外)。

         *就绪状态到停止状态:当一个filter停止时,它释放了所有它保存的sample,就将解除所有上游filter调用GetBuffer时的阻塞。如果filter正在Receive方法中等待数据,则它停止等待并从Receive中返回,从而解除阻塞。因而,此时当filter graph manager再去将上游filter转换为停止状态时,它已经不再阻塞于GetBufferReceive,从而可以响应停止命令。上游filter在得到停止命令前可能会传递下一些过时的sample,但下游filter不再接收它们,因为此时下游filter已处于停止状态了。

       

    3.4.5. 拉模式

         IMemInputPin接口中,上游filter决定哪些数据要被发送,然后将数据推到下游filter中去。但是在某些情况下,拉模式会更加合适。在拉模式中,只有当下游filter从上游filter中请求数据时,数据才被传递下去,数据流动由下游filter发起。这种类型的连接使用IAsyncReader接口。

         典型的拉模式应用是文件回放。比如,在一个AVI回放graph中,Async File Source filter完成一般的文件读操作并将数据作为字节流传递下去,没有什么格式信息。AVI Splitter filter读取AVI头并将数据流分解成视频和音频sampleAVI SplitterAsync File Source filter更能决定它们需要哪些数据,因此需用IAsyncReader接口来代替IMemInputPin接口。

         要从输出pin请求数据,输入pin调用下面方法中的一个:

         *IAsyncReader::Request

         *IAsyncReader::SyncRead

         *IAsyncReader::SyncReadAligned

         第一个方法是异步的,支持多重读操作。其余的是同步的。

         理论上,任一个filter都能支持IAsyncReader,但是实际上,它仅仅在连接有一个parser filtersource filter上使用。分析器(parser)非常象一个推模式的source filter,当它就绪时,它创建一个数据流线程,从IAsyncReader连接中拉数据并将其推到下一游filter中去。它的输出pin使用IMemInputPingraph余下的部分使用标准的推模式。


    最新回复(0)