运行 IVMRWindowlessControl9.GetCurrentImage() 在后台

Run IVMRWindowlessControl9.GetCurrentImage() in background

我有一个使用 DirectShow.NET 的网络摄像头控件。我创建了一个自定义控件来显示视频并从网络摄像头捕获图像。我在另一个 WPF window 中使用该自定义控件。我在自定义控件中有一个函数 public Bitmap CaptureImage() 来抽象出一些 DirectShow 编程,只是 return 一个 Bitmap。由于图像相对较大 (1920x1080),IVMRWindowlessControl9GetCurrentImage() 函数需要相当长的时间来处理(2-3 秒)。我已经逐步检查了我的代码,可以确认此调用是唯一一个需要很长时间才能处理的调用。

因此,我的主 WPF window 中的 GUI 线程挂起,导致它有几秒钟没有响应,所以如果我想在捕获图像时显示进度微调器,它只会保持冻结状态。

这是 CaptureImage() 的代码:

public Bitmap CaptureImage()
{
  if (!IsCapturing)
    return null;

  this.mediaControl.Stop();
  IntPtr currentImage = IntPtr.Zero;
  Bitmap bmp = null;

  try
  {
    int hr = this.windowlessControl.GetCurrentImage(out currentImage);
    DsError.ThrowExceptionForHR(hr);

    if (currentImage != IntPtr.Zero)
    {
      BitmapInfoHeader bih = new BitmapInfoHeader();
      Marshal.PtrToStructure(currentImage, bih);

      ...
      // Irrelevant code removed 
      ...

      bmp = new Bitmap(bih.Width, bih.Height, stride, pixelFormat, new IntPtr(currentImage.ToInt64() + Marshal.SizeOf(bih)));
      bmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
    }
  }
  catch (Exception ex)
  {
    MessageBox.Show("Failed to capture image:" + ex.Message);
  }
  finally
  {
    Marshal.FreeCoTaskMem(currentImage);
  }

  return bmp;
}

为了解决这个问题,我尝试 运行 将其作为后台任务,如下所示:

public async void CaptureImageAsync()
{
  try
  {
    await Task.Run(() =>
    {
      CaptureImage();
    });
  }
  catch(Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
}

我尝试了多种方法来做到这一点,包括使用 BackgroundWorkers,但似乎每次我异步进行此调用时,它都会产生此错误:

Unable to cast COM object of type 'DirectShowLib.VideoMixingRenderer9' to interface type 'DirectShowLib.IVMRWindowlessControl9'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{8F537D09-F85E-4414-B23B-502E54C79927}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).

错误总是发生在这条线上:

int hr = this.windowlessControl.GetCurrentImage(out currentImage);

同步调用 CaptureImage() 会产生正常结果。图像被捕获,一切都按预期工作。但是,切换到使用任何类型的异步功能都会出现该错误。

您在这里遇到了两个问题。首先 API 最初的缓慢是设计使然。 MSDN mentions this 为:

However, frequent calls to this method will degrade video playback performance.

对于回读,视频内存可能非常慢 - 这是 2-3 秒的处理问题,而不是图像的分辨率 本身 。坏消息是即使从后台线程轮询快照也很可能会影响视觉流。

此方法过去和现在都用于拍摄零星的快照,尤其是。那些由用户交互启动的,而不是自动启动的。需要更密集和自动化的应用程序,以及那些不影响视觉提要快照的应用程序应该在将提要发送到视频内存之前拦截提要(有一些选项可以做到这一点,最流行但笨拙的是使用 Sample Grabber)。

其次,您可能遇到了 .NET 线程问题 described in this question,这会触发上述异常。通过偷偷违反 COM 线程规则并在单元之间传递接口指针,很容易在本机代码开发中使用相同的接口指针。由于 CLR 在您的代码和 COM 对象之间添加了一个中间层来执行额外的安全检查,因此您不能再从后台线程使用 COM object/interfaces,因为强制执行了 COM 线程规则。

我想您要么必须继续忍受与直接 API 调用相关的长时间冻结,要么添加有助于绕过冻结的本机代码开发(特别是,例如,在之前捕获帧的辅助过滤器发送到视频内存,同时为 .NET 调用者实现辅助功能以支持后台线程调用)。据推测,您还可以在辅助 MTA 线程池中完成所有与 DirectShow 相关的工作,从而解决后台线程调用程序问题,但在这种情况下,您很可能需要将此代码从 UI 线程中移出这是 STA - 我不认为这是人们经常做的事情,如果有的话。