DirectShow 视频画面渲染

发布于 2021-11-25


DirectShow 播放视频文件 例子中,DirectShow 会自动创建一个名为 ActiveMovide Window 的窗口,在那里显示播放的视频画面。在实际程序的开发中,我们总是希望视频画面渲染到我们指定的窗口上。

render filters

DirectShow 提供了以下几个用于视频渲染画面到窗口的 filter:

  • Video Renderer filter,最基本的 filter,支持所有环境。默认使用 DirectDraw 绘制画面,否则使用 GDI(绘制效率较低)。可以用于 Windows XP 之前的系统。
  • Video Mixing Renderer Filter 7 (VMR-7),最低可用于 Windows XP 系统。使用 DirectDraw 7 渲染。
  • Video Mixing Renderer Filter 9 (VMR-9),使用 DirectDraw 9 渲染。
  • Overlay Mixer filter,用于播放 DVD 和广播视频。
  • Enhanced Video Renderer (EVR),最低可用于 Windows Vista。

渲染模式

DirectShow 渲染视频有两种模式:

  • 窗口模式。DirectShow 会自己创建一个窗口显示视频。通常我们会把这个窗口设置成我们应用程序窗口的子窗口。
  • 无窗口模式。DirectShow 会直接渲染显示画面到我们指定的窗口。

Video Renderer filter 只支持窗口模式。VMR-7 和 VMR-9 两种模式都支持。EVR 只支持无窗口模式。

使用窗口模式渲染

class DirectShowWrapper {
public:
  DirectShowWrapper() {}
  ~DirectShowWrapper() {}

  bool Init() {
    HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
                                  IID_IGraphBuilder, (void **)&graph_buiulder_);
    if (FAILED(hr)) {
      printf("ERROR:Could not create the Filter Graph Manager.\n");
      return false;
    }

    hr = graph_buiulder_->QueryInterface(IID_IMediaControl,
                                         (void **)&media_control_);
    if (FAILED(hr)) {
      printf("ERROR:Could not create the IMediaControl.\n");
      return false;
    }

    hr = graph_buiulder_->QueryInterface(IID_IMediaEvent,
                                         (void **)&media_event_);
    if (FAILED(hr)) {
      printf("ERROR:Could not create the IMediaEvent.\n");
      return false;
    }

    hr = graph_buiulder_->QueryInterface(IID_IVideoWindow,
                                         (void **)&video_window_);
    if (FAILED(hr)) {
      printf("ERROR:Could not create the IVideoWindow.\n");
      return false;
    }

    hr = graph_buiulder_->RenderFile(L"D:\\example.mp4", NULL);
    if (FAILED(hr)) {
      printf("ERROR:Could not RenderFile.\n");
      return false;
    }

    return true;
  }

  void Shutdown() {
    media_control_->Stop();
    parent_ = NULL;
    if (video_window_) {
      video_window_->put_Visible(OAFALSE);
      video_window_->put_Owner(NULL);
    }

    video_window_ = nullptr;
    media_event_ = nullptr;
    media_control_ = nullptr;
    graph_buiulder_ = nullptr;
  }

  void SetParent(HWND parent) {
    parent_ = parent;
    if (video_window_) {
      video_window_->put_Owner((OAHWND)parent_);
      video_window_->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS);

      RECT rc;
      GetClientRect(parent_, &rc);
      video_window_->SetWindowPosition(0, 0, rc.right, rc.bottom);
    }
  }

  bool Run() {
    HRESULT hr = media_control_->Run();
    if (FAILED(hr)) {
      printf("ERROR:IMediaControl run failed.\n");
      return false;
    }
    return true;
  }

  void Resize() {
    if (video_window_) {
      RECT rc;
      GetClientRect(parent_, &rc);
      video_window_->SetWindowPosition(0, 0, rc.right, rc.bottom);
    }
  }

private:
  CComPtr<IGraphBuilder> graph_buiulder_;
  CComPtr<IMediaControl> media_control_;
  CComPtr<IMediaEvent> media_event_;
  CComPtr<IVideoWindow> video_window_;
  HWND parent_ = NULL;
};

我们获取一个 IVideoWindow 接口,用于控制视频渲染的窗口。把应用程序窗口设置成它的的父窗口。调整自己的窗口位置。

在销毁 Shutdown 的时候,把的父窗口重置为空,隐藏自己。

在 OnInitDialog 中,初始化:

  m_directshow_wrapper = new DirectShowWrapper();
  m_directshow_wrapper->Init();
  m_directshow_wrapper->SetParent(m_hWnd);
  m_directshow_wrapper->Run();

在 OnSize 中,响应窗口大小的改变:

  if (m_directshow_wrapper) {
    m_directshow_wrapper->Resize();
  }

最终效果如下图:

Windowed Mode

使用无窗口模式渲染

使用窗口模式会存在一些问题:

  • 在线程之间发送窗口消息,则可能会出现死锁。
  • Filter Graph Manager 转发窗口消息给 Video Renderer。应用程序同时跟 IVideoWindow 接口交互。可能导致 Filter Graph Manager 内部维护的状态不对。
  • 应用程序要获得视频窗口上的消息,需要设置 message drain,让视频窗口将这些消息转发到应用程序。
  • 视频窗口必须设置正常的窗口 window styles,否存会出现绘制剪裁问题。
class DirectShowWrapper {
public:
  DirectShowWrapper() {}
  ~DirectShowWrapper() {}

  bool Init(HWND parent) {
    parent_ = parent;

    HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
                                  IID_IGraphBuilder, (void **)&graph_buiulder_);
    if (FAILED(hr)) {
      printf("ERROR:Could not create the Filter Graph Manager.\n");
      return false;
    }

    hr = graph_buiulder_->QueryInterface(IID_IMediaControl,
                                         (void **)&media_control_);
    if (FAILED(hr)) {
      printf("ERROR:Could not create the IMediaControl.\n");
      return false;
    }

    hr = graph_buiulder_->QueryInterface(IID_IMediaEvent,
                                         (void **)&media_event_);
    if (FAILED(hr)) {
      printf("ERROR:Could not create the IMediaEvent.\n");
      return false;
    }

    InitWindowlessVMR();

    hr = graph_buiulder_->RenderFile(L"D:\\example.mp4", NULL);
    if (FAILED(hr)) {
      printf("ERROR:Could not RenderFile.\n");
      return false;
    }

    return true;
  }

  bool InitWindowlessVMR() {
    // 创建 VMR
    CComPtr<IBaseFilter> vmr;
    HRESULT hr =
        CoCreateInstance(CLSID_VideoMixingRenderer, NULL, CLSCTX_INPROC,
                         IID_IBaseFilter, (void **)&vmr);
    if (FAILED(hr)) {
      printf("ERROR:Could not Create VideoMixingRenderer.\n");
      return false;
    }

    // 把 VMR 添加到 filter graph 中
    hr = graph_buiulder_->AddFilter(vmr, L"Video Mixing Renderer");
    if (FAILED(hr)) {
      printf("ERROR:Could not add VideoMixingRenderer to filter graph.\n");
      return false;
    }

    // 设置渲染模式
    CComPtr<IVMRFilterConfig> vmr_filter_config;
    hr = vmr->QueryInterface(IID_IVMRFilterConfig, (void **)&vmr_filter_config);
    if (FAILED(hr)) {
      printf("ERROR:Could not get IVMRFilterConfig.\n");
      return false;
    }
    hr = vmr_filter_config->SetRenderingMode(VMRMode_Windowless);

    hr = vmr->QueryInterface(IID_IVMRWindowlessControl,
                             (void **)&vmr_windowless_control_);
    if (FAILED(hr)) {
      printf("ERROR:Could not get IVMRWindowlessControl.\n");
      return false;
    }

    hr = vmr_windowless_control_->SetVideoClippingWindow(parent_);
    if (FAILED(hr)) {
      printf("ERROR:Could not SetVideoClippingWindow.\n");
      return false;
    }

    return true;
  }

  void Shutdown() {
    media_control_->Stop();

    vmr_windowless_control_ = nullptr;
    media_event_ = nullptr;
    media_control_ = nullptr;
    graph_buiulder_ = nullptr;
  }

  bool Run() {
    HRESULT hr = media_control_->Run();
    if (FAILED(hr)) {
      printf("ERROR:IMediaControl run failed.\n");
      return false;
    }
    return true;
  }

  void Resize() {
    long lWidth, lHeight;
    HRESULT hr = vmr_windowless_control_->GetNativeVideoSize(&lWidth, &lHeight,
                                                             NULL, NULL);
    if (FAILED(hr)) {
      return;
    }

    RECT rcSrc = {0};
    RECT rcDest = {0};

    SetRect(&rcSrc, 0, 0, lWidth, lHeight);
    GetClientRect(parent_, &rcDest);
    SetRect(&rcDest, 0, 0, rcDest.right, rcDest.bottom);
    hr = vmr_windowless_control_->SetVideoPosition(&rcSrc, &rcDest);
  }

private:
  CComPtr<IGraphBuilder> graph_buiulder_;
  CComPtr<IMediaControl> media_control_;
  CComPtr<IMediaEvent> media_event_;
  CComPtr<IVMRWindowlessControl> vmr_windowless_control_;
  HWND parent_ = NULL;
};

使用无窗口渲染模式,需要我们手动配置 VMR。

因 VMR 没有自己的真窗口,它需要被通知重绘或者改变视频显示大小。

参考