DirectShow 采集视频

发布于 2021-11-27


DirectShow 的视频采集源是多样的,不仅仅可以从摄像头这样的设备采集,还支持电视模拟信号卡,视频磁带(VTR)。采集到的数据既可以实时预览,也可以保存在磁盘文件里。

capture graph

capture graph 相比之前的 filter graph 复杂一点。DirectShow 提供了 ICaptureGraphBuilder2 对象来简化这个工作。

  // 创建 Capture Graph Builder
  CComPtr<ICaptureGraphBuilder2> capture_graph_builder2;
  HRESULT hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL,
                                CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2,
                                (void **)&capture_graph_builder2);
  if (FAILED(hr)) {
    return 0;
  }

  // 创建 Filter Graph Manager
  CComPtr<IGraphBuilder> graph_builder;
  hr = CoCreateInstance(CLSID_FilterGraph, 0, CLSCTX_INPROC_SERVER,
                        IID_IGraphBuilder, (void **)&graph_builder);
  if (FAILED(hr)) {
    return 0;
  }
  // 初始化 Capture Graph Builder
  capture_graph_builder2->SetFiltergraph(graph_builder);

查找硬件设备

之前我们提到 DirectShow 枚举系统硬件设备和 filter,可以枚举系统的硬件设备。硬件设备的 IMoniker 可以获取到以下几个属性:

  • FriendlyName,设备的名字,易于人理解
  • Description,设备的描述信息
  • DevicePath,视频设备唯一的 ID 信息
  • WaveInID,音频设备唯一的 ID 信息

我们就根据 FriendlyName 来查找一个摄像头设备。

  // 创建 System Device Enumerator
  CComPtr<ICreateDevEnum> system_device_enum;
  hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,
                        IID_ICreateDevEnum, (void **)&system_device_enum);
  if (FAILED(hr)) {
    return 0;
  }

  // 创建视频输入设备枚举器
  CComPtr<IEnumMoniker> video_input_device_category;
  hr = system_device_enum->CreateClassEnumerator(
      CLSID_VideoInputDeviceCategory, &video_input_device_category, 0);
  if (FAILED(hr)) {
    return 0;
  }

  // 查找 FriendlyName 为 KS2MR01 的视频输入设备
  std::wstring camera_name(L"KS2MR01");
  CComPtr<IBaseFilter> camera_filter;
  CComPtr<IMoniker> moniker;
  ULONG cFetched;
  while (video_input_device_category->Next(1, &moniker, &cFetched) == S_OK) {
    CComPtr<IPropertyBag> property_bag;
    hr = moniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&property_bag);
    if (FAILED(hr)) {
      continue;
    }

    // 获得 FriendlyName
    CComVariant friendly_name;
    hr = property_bag->Read(L"FriendlyName", &friendly_name, 0);
    if (FAILED(hr)) {
      continue;
    }
    wprintf(L"FriendlyName:%s\n", friendly_name.bstrVal);

    // 获得 DevicePath
    CComVariant device_path;
    hr = property_bag->Read(L"DevicePath", &device_path, 0);
    if (FAILED(hr)) {
      continue;
    }
    wprintf(L"DevicePath:%s\n", device_path.bstrVal);

    if (camera_name == std::wstring(friendly_name.bstrVal)) {
      // 把 IMoniker 转换成对应的 Filter
      hr = moniker->BindToObject(NULL, NULL, IID_IBaseFilter, (void **)&camera_filter);
      if (SUCCEEDED(hr)) {
        if (graph_builder) {
          // 将 Filter 添加到 filter graph 中工作
          hr = graph_builder->AddFilter(camera_filter, L"Video Capture");
          if (SUCCEEDED(hr)) {
            break;
          }
        }
      }
    }

    moniker = nullptr;
  }

输出结果:
FriendlyName:KS2MR01
DevicePath:\\?\usb#vid_1bcf&pid_3399&mi_00#6&2e536f83&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global

采集预览视频

  hr = capture_graph_builder2->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video,
    camera_filter, NULL, NULL);

  media_control->Run();

有了前面的工作,采集预览视频就非常简单了。运行后还是会自动创建一个名为 ActiveMovide Window 的窗口,在那里显示播放的从摄像头采集到视频画面。效果如下:

DirectShow video preview

采集保存视频

  CComPtr<IBaseFilter> mux_filter;
  hr = capture_graph_builder2->SetOutputFileName(
      &MEDIASUBTYPE_Avi,  // Specifies AVI for the target file.
      L"D:\\example.avi", // File name.
      &mux_filter,        // Receives a pointer to the mux.
      NULL);              // (Optional) Receives a pointer to the file sink.

  hr = capture_graph_builder2->RenderStream(
    &PIN_CATEGORY_CAPTURE, // Pin category.
    &MEDIATYPE_Video,      // Media type.
    camera_filter,                  // Capture filter.
    NULL,                  // Intermediate filter (optional).
    mux_filter);                 // Mux or file sink 

采集保存视频到文件,首选需要通过设置文件路径获取一个 mux filter。

另外 RenderStream 的时候,选择的 pin 是 PIN_CATEGORY_CAPTURE,而不是 PIN_CATEGORY_PREVIEW。因为 capture filter 通常会有多个 pin 输出不同的数据,一般不能混用。

  • Preview Pins,输出的数据用于预览目的。Preview Pin 为了保证数据吞吐量可能会丢弃视频帧。
  • Capture Pins,输出的数据用于保存目的。Capture Pin 数据视频帧是带有时间戳数据的,而 Preview Pin 则没有。

参考