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

时间:2019-04-06 02:28:06

标签: c# c++-cli directshow

几个月前,我写了this question,涉及DirectShow图上的缓冲区不足。

饥饿问题是通过实现自定义分配器解决的,该分配器在饥饿时会扩展大小。但是,这仅减轻了实际问题。 如果有足够的时间,图表中保存的样本数量将变得过多,并且不断扩大的池会导致内存不足。

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

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

  2. 该流作为UDP多播流出现。该视频流包含14种不同的SD节目。

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

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

  5. 视频样本正在由LAV视频解码器解码。将LAV过滤器交换为ffdshow过滤器没有积极作用-仍然存在累积。我发现LAV或ffdshow中都没有设置可减轻累积问题的设置(包括示例队列设置)。

  6. 问题完全与接收到的流的质量有关。在流上检测到的不连续性越多(由MPEG多路分配器输出样本标记),倾向于累积更多的样本。顺便说一下,并行运行消耗相同流的VLC播放器会记录相同的不连续性,因此它们不会看来并不是我的网络代码有问题。

  7. 挥之不去的样本不会丢失,它们最终将由图形处理。我编写了一些看门狗逻辑来检测丢失样本的可能性,并且最终每个样本都被正确释放并返回到池中。

  8. 滞后与CPU饥饿无关。如果我停止将样品输送到多路分配器,则多路分配器将停止将样品输送到输出引脚。 我需要将新样品推入解复用器中,以便将残留的样品正确释放并返回池中。

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

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

附录:

我还有一些其他信息:

  1. 已转码的视频相对于音频滞后。
  2. 滞后时间与残留样品的数量成正比。

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

关于如何检测有问题的过滤器的任何提示,或者如何使同步“变基”?

附录2:

正如您在对Roman的答案的评论中所看到的那样,我实际上发现了一个错误,该错误会导致流中出现错误的中断。通过修复该那个错误,我减少了该问题的发生率,但并未解决根本原因!

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

编码器通过将接收到的样本量乘以输入的采样频率来增量计算输出时间戳。 过滤器假定数据流始终是连续的,甚至不检查输入的样本是否存在间断!。一旦发现问题,修复起来就很容易了,但是这确实是我一生中要调试的最困难的问题,因为所有问题都指向MPEG2多路分配器 em>(时间戳在编码的输出音频和视频引脚之间漂移,并且此过滤器首先耗尽了合并的样本),但这是由于视频输出引脚的工作线程被间接阻塞而导致的通过MPEG4多路复用器,该图的末尾正在接收音频和视频之间不同步的采样,并限制了视频输入以尝试保持同步。

实际上,当线程沿着图形流动时,需要谨慎对待过滤器为“黑匣子”的错觉,并且下游过滤器上的问题可能表现为上游过滤器中的错误问题。 / strong>

2 个答案:

答案 0 :(得分:2)

首先,所描述的行为听起来像是一个错误。也就是说,意外行为会导致不良后果。但是,我同意,要解决此问题,就必须确定罪犯并就已注册的问题进行详细调查。

由于视频的音频量相对滞后于相对滞后的样本,并且没有其他副作用(例如丢失帧),因此我同意挑战在于找到谁能准确地保存媒体样本。

我可以建议两种方法。

检查内存分配器

出于简洁的原因,此方法并不那么受欢迎,但是这种方法仍然很有可能无法使用。背景是引脚连接假定协商内存分配器。内存分配器是引脚的私有业务,因此在大多数情况下,控制应用程序无法直接控制(和前夕访问)数据流。通常每个引脚对都有自己的分配器定义,但是有时(并非罕见)多个引脚对使用相同的分配器。请注意,它是连接上的输出引脚,由谁来决定要使用的分配器。

如果您碰巧熟悉我的DirectShowSpy工具,它的作用之一就是枚举内存分配器:

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

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

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

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

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

您可以通过AlaxInfoDirectShowSpy::ISpy的间谍类型库获得#import。如果间谍没有通过COM注册,并且没有挂接OS Filter Graph Manager对象,则您在上述#3中的QueryInterface将会失败。

通过C#代码(分别标记问题),可以将DirectShowSpy.dll导入为COM引用。

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

添加临时诊断过滤器以跟踪引脚连接通信

另一种可能有更多机会进行总体计算,但需要大量编写代码的方法,是开发和过滤,以将数据从输入透明地转发到输出引脚,例如CTransInPlaceFilter,并将媒体样本数据记录到某处。共享输出。您可能要为此特别使用GraphStudioNext的分析器过滤器。

这个想法是尽早将此滤波器安装在多路分解器输出引脚上,并在其从滤波器下游传输时监视/记录数据。在流传输数据时比较各个分支上的时间戳,您应该能够检测到违规者。如果看到滞后监控多路分解器输出引脚的连接,则说明多路分解器是违法者。如果那里进展顺利,您将向下游移动跟踪,尤其是。在解码器上移动,并在您移动跟踪过滤器时隔离他的冒犯者。

可能的解决方法

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

答案 1 :(得分:0)

最后,我找到了问题的根源。

在重写UDP读取代码以使用高性能I / O(RIO)之后,我想获得有关丢弃的数据包数量的指标。我实现了一个非常非常简单的MPEG-TS连续性检查器,发现有些奇怪的地方。我没有丢失任何数据包,但是,编码器仍在标记不连续性。根本没有道理!

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

因此,从本质上讲,存在竞争的情况,网络代码可以获得解复用器仍在使用的“空闲缓冲区”,并且数据正在被破坏。我的猜测是,解复用器发现了导致同步丢失的严重,无法解释的错误。

这里是连续性检查器代码,以防对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);
}