媒体样本长时间保存在图表中(累积效应)

Media samples held in graph for a long time (accumulative effect)

几个月前,我写了 ,关于 DirectShow 图上的缓冲区不足。

饥饿问题已通过实施自定义分配器解决,该分配器在饥饿时扩大大小。但是,这只是减轻了真正的问题。 如果有足够的时间,图表中保存的样本数量会变得过多,并且不断扩大的池会造成内存不足的情况。

以下是我收集到的一些事实:

  1. 该图基本上是将 MPEG2-TS 流转码为 MP4 文件,以及提取音频和视频数据以进行一些实时 DSP 处理。

  2. 该流以 UDP 多播流的形式出现。该流携带 14 个不同的 SD 节目。

  3. 我正在使用派生自 DsNetwork 示例的自定义过滤器读取 UDP 流。按照上述示例,围绕 UDP 接收数据块(一个 8KiB 块)创建一个媒体样本(没有时间戳),并将其传递给 Microsoft 的 MPEG2 多路分解器过滤器,该过滤器被配置为过滤感兴趣的节目。 (我应该给样本加上时间戳吗?)

  4. 需要可扩展分配器的过滤器是 MPEG2 解复用器,特别是输出视频引​​脚传送的样本需要它。输出音频 pin 使用默认分配器工作正常,音频解码器或解复用器不会保留任何样本。

  5. 视频样本正在由 LAV 视频解码器解码。将 LAV 过滤器换成 ffdshow 过滤器没有任何积极效果 - 积累仍然存在。我在 LAV 或 ffdshow(包括示例队列设置)中没有发现任何设置可以缓解累积问题。

  6. 问题完全与接收流的质量有关。在流上检测到的不连续性越多(由 MPEG 解复用器输出样本标记),累积的样本就越多。 顺便说一句,运行 并行使用相同流的 VLC 播放器记录相同的内容不连续性,所以它们似乎不是由我的错误网络代码引起的。

  7. 延迟样本并没有丢失,它们最终被图形处理。我写了一些看门狗逻辑来检测丢失样本的可能性,每个样本最终都被正确释放并返回到池中。

  8. 延迟与 CPU 饥饿无关。如果我停止向多路分解器传送样本,多路分解器就会停止向输出引脚传送样本。 我需要将新样本推送到分离器中,以便正确释放挥之不去的样本并返回到池中。

  9. 我尝试从捕获图以及复用器图中(由 GDCL 桥接滤波器桥接)移除时钟。这不能解决问题,实际上会阻塞数据流。

我不知道样本是由解复用器还是视频解码器保存的。事实是,我完全不知道如何调试并希望 修复 这种情况,非常欢迎任何指点或建议。

附录:

我有一些额外的信息:

  1. 转码后的视频相对于音频滞后。
  2. 滞后时间与延迟样本的数量成正比。

所以我认为在图处理的某个时刻,解码的音频和视频样本时间戳不同步,可能图的复用器端点阻塞了视频解码线程,等待相应的音频到达。

任何关于如何检测违规过滤器的提示,或者我如何"rebase" 同步的提示?

附录 2:

正如您在 Roman 的回答的评论中看到的那样,我实际上发现了一个错误,该错误会导致流中出现错误的不连续性。通过修复 那个 错误,我减少了问题的发生次数,但我没有修复根本原因!

事实证明,问题的根源是由 Monogram AAC 编码器过滤器引起的(至少是我设法获得的版本,因为项目似乎不是不再支持)。

编码器通过将接收到的样本量乘以输入的采样频率来递增地计算输出时间戳。 过滤器假定数据流始终是连续的,甚至不检查传入样本的不连续性!。确定问题后修复它很容易,但这确实是我作为开发人员一生中必须调试的最困难的问题,因为所有问题都指向MPEG2 解复用器(时间戳在编码输出音频和视频引脚之间漂移,正是这个过滤器 运行 首先从池化样本中出来),然而,这是由视频输出引脚的工作线程在图表的末尾被 MPEG4 多路复用器阻塞,它正在接收音频和视频之间不同步的样本,并且正在限制视频输入以试图保持同步。

确实需要谨慎对待过滤器 "black boxes" 的错觉,因为线程沿着图形流动,下游过滤器上的问题可能表现为错误问题一个上游过滤器。

首先,所描述的行为听起来像是一个错误。也就是说,意外行为会导致不良影响。但是,我同意,解决问题的尝试需要识别违规者并对已注册的问题进行详细调查。

由于视频在与挥之不去的样本相关的数量上相对滞后,并且没有其他副作用(例如丢失帧),我同意挑战在于找到谁准确地持有媒体样本。

我可以立即提出两种方法。

检查内存分配器

由于我为简洁起见而省略的原因,这种方法不太受欢迎,但是它仍然很有可能不起作用。背景是引脚连接假定内存分配器的协商。内存分配器是管脚的私有业务,因此在大多数情况下控制应用程序无法直接控制(甚至访问)数据流。更常见的是,每个引脚对都有自己的分配器定义,但有时且并不少见,多个引脚对使用相同的分配器。请注意,最终决定使用分配器的是连接上的输出引脚。

如果您碰巧熟悉我的 DirectShowSpy 工具,它所做的其中一件事就是枚举内存分配器:

它可以显示内存分配器,哪些连接共享内存分配器以及缓冲区计数和空闲缓冲区计数的快照。

为简洁起见,我省略了不准确的情况。

另一个重要的注意事项是,仅当您从 DirectShow 图为 运行 的进程调用间谍 UI 时,此数据才可用,而不是通过 运行 远程访问过滤器图对象 Table.

这意味着您应该执行以下操作:

  1. 注册间谍
  2. 有你的应用程序运行(带过滤图)
  3. 来自控制线程(通常)IUnknown::QueryInterface for AlaxInfoDirectShowSpy::ISpy 来自您的 IGraphBuilder 接口指针
  4. 执行 ISpy::DoPropertyFrameModal 以显示有问题的 UI

你可以通过间谍类型库的#import获得AlaxInfoDirectShowSpy::ISpy。如果间谍没有通过 COM 注册并且它没有挂钩 OS Filter Graph Manager 对象,那么上面#3 中的 QueryInterface 将会失败。

从 C# 代码(如您分别标记问题),您可以导入 DirectShowSpy.dll 作为 COM 引用。

尽管不能保证此方法有效,但它很有可能通过内存分配器状态的可视化向您展示违规者,并且需要在您的应用程序中插入大约 10 行代码。

为跟踪 pin 连接通信添加临时诊断过滤器

另一种总体上有更多机会解决但需要相当多编写代码的方法是开发和过滤透明地将数据从输入引脚转发到输出引脚,例如 CTransInPlaceFilter 记录媒体示例数据某处共享输出。为此,您可能希望重新使用 GraphStudioNext 的分析器过滤器。

想法是尽早将此过滤器连接到多路分解器输出引脚和 monitor/log 数据,因为它从过滤器下游传输。在数据流式传输时比较不同腿上的时间戳,您应该能够检测到违规者。如果您看到滞后监控解复用器输出引脚连接,那么解复用器就是罪魁祸首。如果事情进展顺利,您可以将跟踪移动到下游,尤其是。在解码器上并在移动跟踪过滤器时隔离违规者。

可能的解决方法

一旦确定违规者,您将不得不考虑诱使它释放其持有的媒体样本,这本身可能是一个挑战。此时没有其他有用的信息,我准备通过发送流结束通知或刷新或使用动态媒体类型协商以某种方式随时耗尽它,以最终迫使它耗尽其内部队列。

终于找到问题的根源了

在重写 UDP 读取代码以使用高性能 I/O (RIO) 后,我想获得关于有多少数据包被丢弃的指标。我实现了一个非常非常简单的 MPEG-TS 连续性检查器,我发现了一些非常奇怪的东西。我没有丢失任何数据包,但编码器仍在标记不连续性。完全没有意义!

经过全面审查后,我发现网络缓冲区中存在引用计数问题。我显然是提前将 TS 数据包返回到池中,而它们仍在被多路分解器使用。 (网络数据包在许多图表上共享,我使用引用计数来控制共享生命周期)。

因此,从本质上讲,存在一种竞争条件,其中网络代码可以获得一个 "free buffer" 仍在被多路分解器使用并且数据被破坏。我的猜测是解复用器发现了导致同步丢失的严重的、无法解释的错误。

这是连续性检查代码,以防遇到 UDP 多播流问题的人有用。

void MulticastMediaSample::Initialize(MulticastSourceFilter* pFilter, MulticastSourceFilter::UDPBuffer* pBuffer) {
   _props.pbBuffer = pBuffer->Data;
   _props.lActual = pBuffer->payloadSizeInBytes;
   _pBuffer = pBuffer;

   // Network packet should be a multiple of a TS packet length (188 bytes)
   int tsPacketCount = pBuffer->payloadSizeInBytes / 188;
   if( pBuffer->payloadSizeInBytes % 188 != 0 ) {
      printf("Invalid TCP packet, length is not multiple of 188\r\n");
      exit(-8828);
   }
   BYTE* pPacket = pBuffer->Data;
   UINT header;
   for( int i = 0; i < tsPacketCount; i++ ) {
      if( pPacket[0] != 0x47 ) {
         printf("Lost Sync!\r\n");
         exit(-12423);
      }
      UINT pId = (pPacket[1] & 0x1f) << 8 | pPacket[2];
      if( pId != 0x1fff ) {  // ignore "filler" packets
         UINT afc = (pPacket[3] & 0x30) >> 4;
         BYTE cc = pPacket[3] & 0xf;
         auto it = pFilter->_ccMap.lower_bound(pId);
         if( it != pFilter->_ccMap.end() && !(pFilter->_ccMap.key_comp()(pId, it->first)) ) {
            // PID key exists in map, check continuity
            if( afc != 2 ) {  // don't check for packets carrying no payload
               BYTE expected = (it->second + 1) & 0xf;
               if( cc != expected ) {
                  printf("Continuity check error for pId %d: expected %d, got %d\r\n", pId, expected, cc);
                  SetDiscontinuity(TRUE);
               }
            }
            // update key
            it->second = cc;
         } else {
            // key does not exist, insert first time 
            pFilter->_ccMap.insert(it, std::map<UINT16, BYTE>::value_type(pId, cc));
         }
      }
      pPacket += 188;
   }
#ifdef DEBUG
   ASSERT(pBuffer->payloadSizeInBytes <= sizeof pBuffer->DataCopy);
   memcpy(pBuffer->DataCopy, pBuffer->Data, pBuffer->payloadSizeInBytes);
#endif
   _pBuffer->AddRef();
   ASSERT(_refCnt == 1);
}