概述
数据保存在缓冲区中,它们只是字节数组。 每个缓冲区都被称为媒体样本的COM对象封装,该样本实现了IMediaSample接口。 样本由另一种类型的对象创建,称为分配器,它实现IMemAllocator接口。 为每个引脚连接分配一个分配器,尽管两个或多个引脚连接可能共享相同的分配器。 下图说明了这个过程:
每个分配器创建一个媒体采样池并为每个采样分配缓冲区。只要过滤器需要用数据填充缓冲区,它就会通过调用IMemAllocator :: GetBuffer从分配器请求一个样本。如果分配器有任何当前未被其他过滤器使用的样本,则GetBuffer方法立即返回一个指向样本的指针。如果所有分配器的样本都在使用中,则方法会阻塞,直到样本变为可用。当方法返回样本时,过滤器将数据放入缓冲区,在样本上设置适当的标志(通常包括时间戳),并向下游传递样本。
当渲染器过滤器接收到一个样本时,它会检查时间戳记并保存到样本上,直到过滤器图形的参考时钟指示应该呈现数据。过滤器呈现数据后,它将释放样本。样本不会回到分配器的样本池中,直到样本的参考计数为零,这意味着每个过滤器都释放了样本。下图说明了这个过程。
上游过滤器可能在渲染器之前运行 - 也就是说,它可能比渲染器消耗它们更快地填充缓冲区。 即便如此,样本也不会提前渲染,因为渲染器会保留每个样本直到其呈现时间。 而且,上游过滤器不会意外覆盖缓冲区,因为GetSample只返回未被使用的样本。 上游过滤器可以提前运行的量由分配器池中的样本数决定。
前面的图只显示一个分配器,但通常每个流有几个分配器。 因此,渲染器发布样本时,可能会产生级联效应。 下图显示了解码器在等待渲染器释放样本时保存压缩视频帧的情况。 解析器过滤器也在等待解码器释放样本。
当渲染器释放其样本时,解码器对GetBuffer的挂起的调用返回。 然后解码器可以解码压缩的视频帧并释放它所持有的样本,从而解除对解析器未决的GetBuffer调用的阻止。
传输
为了通过过滤器图形移动媒体数据,DirectShow过滤器必须支持几种可能的协议之一。这些协议被称为传输。当两个过滤器连接时,它们必须支持相同的传输;否则他们不能交换媒体数据。通常,传输需要其中一个引脚支持特定的接口。当过滤器连接时,一个引脚向另一个查询接口。
大多数DirectShow过滤器将媒体数据保存在主内存中,并通过引脚连接将其传送到其他过滤器。这种类型的传输被称为本地内存传输。虽然本地内存传输是DirectShow中最常见的传输方式,但并非所有过滤器都使用它。例如,一些过滤器沿着硬件路径发送媒体数据,并仅使用引脚来传送控制信息。
DirectShow为本地内存传输定义了两种机制,即推式模型和拉式模型。在推送模型中,源过滤器生成数据并将其传递到下游的下一个过滤器。该过滤器被动接收数据,处理数据,并将数据发送到下游。在拉模型中,源过滤器连接到解析器过滤器。解析器过滤器从源过滤器请求数据。源过滤器通过传递数据来响应请求。推式模型使用IMemInputPin接口,拉式模型使用IAsyncReader接口。推模式比拉模式更普遍
样本和分配器
当一个引脚将媒体数据传送到另一个引脚时,它不会将直接指针传递到内存缓冲区。相反,它传递一个指向管理内存的COM对象的指针。这个称为媒体样本的对象公开了IMediaSample接口。接收引脚通过调用IMediaSample方法(如IMediaSample :: GetPointer,IMediaSample :: GetSize和IMediaSample :: GetActualDataLength)来访问内存缓冲区
样品总是从下游输出,从输出引脚到输入引脚。在推模式中,输出引脚通过在输入引脚上调用IMemInputPin :: Receive来传送样本。输入引脚将同步处理数据(即完全在Receive方法内),或者在工作线程上异步处理。如果需要等待资源,则允许输入引脚在Receive方法内进行阻塞。
另一个称为分配器的COM对象负责创建和管理媒体样本。分配器公开IMemAllocator接口。每当过滤器需要带有空缓冲区的媒体样本时,它将调用IMemAllocator :: GetBuffer方法,该方法返回一个指向样本的指针。每个引脚连接共享一个分配器。当两个引脚连接时,他们决定哪个过滤器将提供分配器。这些引脚还在分配器上设置属性,例如缓冲区的数量和每个缓冲区的大小。
下图显示了分配器,媒体样本和过滤器之间的关系:
Media Sample Reference Counts
分配器创建一个有限的样本池。在任何时候,一些样本可能正在使用,而其他样本可用于GetBuffer调用。分配器使用引用计数来跟踪样本。 GetBuffer方法返回一个引用计数为1的样本。如果引用计数变为零,则样本将返回到分配器的池中,在该池中可用于下一次GetBuffer调用。只要引用计数保持在零以上,该示例就不可用于GetBuffer。如果属于分配器的每个样本都在使用中,则GetBuffer方法会阻塞,直到样本变为可用。
例如,假设一个输入引脚接收一个采样。如果它在Receive方法内同步处理样本,它不增加引用计数。接收返回后,输出引脚释放样本,引用计数变为零,并且样本返回到分配器的池。另一方面,如果输入引脚在工作线程上处理样本,则会在离开接收方法之前增加引用计数。现在引用计数为2.当输出引脚释放样本时,计数变为1;样本尚未返回到池中。工作线程完成样本后,它会调用Release来释放样本。现在样本返回到池中。
当一个引脚接收到一个样本时,它可以将数据复制到另一个样本,或者它可以修改原始样本并将其传送到下一个过滤器。潜在地,样本可以遍历图的整个长度,每个过滤器依次调用AddRef和Release。因此,输出引脚在调用接收后不能重复使用样本,因为下游过滤器可能正在使用样本。输出引脚必须始终调用GetBuffer来获取新的样本。这种机制减少了内存分配量,因为过滤器重用了相同的缓冲区。它还防止过滤器意外地写入尚未处理的数据,因为分配器维护可用样本的列表。
过滤器可以使用单独的分配器来输入和输出。如果它扩展输入数据(例如,解压缩它),它可能会这样做。如果输出不大于输入,过滤器可能会处理数据,而不会将其复制到新样本。在这种情况下,两个或多个引脚连接可以共享一个分配器。
Committing and Decommitting Allocators
当一个过滤器首先创建一个分配器时,分配器没有保留任何内存缓冲区。 此时,对GetBuffer方法的任何调用都将失败。 当流式传输开始时,输出引脚调用IMemAllocator :: Commit,它提交分配器,导致它分配内存。 引脚现在可以调用GetBuffer。
流式传输停止时,该引脚会调用IMemAllocator :: Decommit,这将解除分配器。 所有后续调用GetBuffer失败,直到再次提交分配器。 另外,如果任何对GetBuffer的调用当前都被阻塞等待一个样本,它们会立即返回一个失败代码。 取消提交方法可能会或可能不会释放内存,具体取决于实现。 例如,CMemAllocator类一直等到它的析构函数方法释放内存。
Filter States
过滤器有三种可能的状态:停止,暂停和运行。 暂停状态表示数据就绪,以便运行命令立即响应。 过滤器图形管理器控制所有状态转换。 当应用程序调用IMediaControl :: Run,IMediaControl :: Pause或IMediaControl :: Stop时,筛选器图形管理器将在所有筛选器上调用相应的IMediaFilter方法。 停止和运行之间的转换总是会经过暂停状态,因此如果应用程序调用停止图上的运行,过滤器图形管理器会在运行之前暂停图形。
过滤器图形管理器以上游顺序执行所有状态转换,从渲染器开始并向后转换到源过滤器。 此顺序对于防止样品掉落以及防止图形死锁是必要的。 最重要的状态转换在暂停和停止之间:
·Stopped to paused:当每个过滤器暂停时,它就准备好接收来自下一个过滤器的样本。 源过滤器是最后暂停。 它创建流线程并开始提供样本。 由于所有下游过滤器都已暂停,因此没有过滤器会拒绝任何样本。 过滤器图形管理器不会完成转换,直到图表中的每个呈现器都接收到一个样本;
·Paused to stopped:当一个过滤器停止时,它释放它持有的任何样本,这会阻止在GetBuffer中等待的任何上游过滤器。 如果过滤器正在等待Receive方法中的资源,它将停止等待,并从Receive返回,这会释放调用过滤器。 因此,当过滤器图形管理器停止下一个上游过滤器时,该过滤器不会在GetBuffer或Receive中被阻塞,并且可以响应停止命令。 上游过滤器在获得停止命令之前可能会传递一些额外的样本,但下游过滤器会拒绝它们,因为它已停止。
Pull Model
在IMemInputPin接口中,上游过滤器确定要发送的数据,并将数据推送到下游过滤器。 对于某些过滤器,拉式模型更合适。 这里,下游过滤器请求来自上游过滤器的数据。 样本仍然从下游输出,从输出引脚到输入引脚,但下游滤波器启动数据流。 这种类型的连接使用IAsyncReader接口。
拉动模式的典型用途是在文件播放中。 例如,在AVI回放图中,异步文件源过滤器执行通用文件读取操作,并将数据作为字节流传送,但不包含格式信息。 AVI Splitter过滤器读取AVI头文件并将流解析为视频和音频样本。 AVI Splitter可以确定哪些数据比Async File Source过滤器更好,因此它使用IAsyncReader而不是IMemInputPin。
要从输出引脚请求数据,输入引脚会调用以下方法之一:
·IAsyncReader::Request
·IAsyncReader::SyncRead
·IAsyncReader::SyncReadAligned.
第一种方法是异步的,以支持多个重叠读取, 其他是同步的。从理论上讲,任何过滤器都可以支持IAsyncReader,但实际上它是为连接解析器过滤器的源过滤器而设计的。 解析器的行为非常像推模式中的源过滤器。 当它暂停时,它会创建一个流式线程,从IAsyncReader连接中提取数据并将其下推。 输出引脚使用IMemInputPin,图的其余部分使用标准推式模型。