WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇

一、前言:

前面介绍了CaptureFilter和SinkFilter,这让我想起了抗战电视剧里面的打电话,这俩Filter就像两部电话机,两部电话机得接通,得先告诉接线员,帮我接那个"三八六旅独立团",咔一下子,接线员就把你的线怼到对端的线上了。该啥时候断,线的接头多粗多长,这就得接线员选择了。我们VideoCaptureDS当中连接俩Filter也和这个类似,这个工作交给SetCameraOutput这个函数去做了。

二、Filter和FilterGraph:

1、关系:

也就是说Filter得加入同一个FilterGraph由其管理,FilterGraph通过一系列逻辑操作,在两个Filter中间形成了一条通路(虚线好像不太妥,就这样吧 ,理解就行);两个Filter之间是通过Pin进行连接的,CaptureFilter的输出Pin,连接到SinkFilter的输入Pin;

2、ConnectDirect:

在 DirectShow 中,FilterGraphBuilder::ConnectDirect 方法是用于直接连接两个 DirectShow Filter的方法。这个方法允许在不经过其他中间Filter的情况下直接将一个Filter连接到另一个Filter,以建立媒体数据流路径。

下面是对 FilterGraphBuilder::ConnectDirect 方法的一般说明:

  • 语法

    cpp 复制代码
    HRESULT ConnectDirect(IPin *ppinOut, IPin *ppinIn, const AM_MEDIA_TYPE *pmt);
  • 参数

    • ppinOut:表示要连接的输出端口的 IPin 接口。
    • ppinIn:表示要连接的输入端口的 IPin 接口。
    • pmt:可选参数,表示连接的媒体类型(AM_MEDIA_TYPE 结构)。如果不提供此参数,则 DirectShow 将尝试在连接过程中自动匹配适当的媒体类型。
  • 返回值

    • 如果成功连接两个过滤器,则返回 S_OK
    • 如果连接失败,则返回相应的错误代码,比如 VFW_E_NOT_CONNECTED

所以,请注意不能使用Pin或者Filter的方法,要使用ConnectDirect方法。

3、AM_MEDIA_TYPE:

下面是 AM_MEDIA_TYPE 结构体的一般说明:

  • 结构体定义

    cpp 复制代码
    typedef struct _AMMediaType {
        GUID majortype;            // 主类型,如视频、音频等
        GUID subtype;              // 子类型,如 RGB、MPEG-2、PCM 等
        BOOL bFixedSizeSamples;    // 是否固定大小的样本
        BOOL bTemporalCompression; // 是否时间压缩
        ULONG lSampleSize;         // 样本大小
        GUID formattype;           // 格式类型,如 WaveFormatEx、VideoInfoHeader 等
        BYTE *pbFormat;            // 格式数据的指针
    } AM_MEDIA_TYPE;
  • 字段

    • majortype:表示媒体数据的主要类型,如视频、音频等。
    • subtype:表示媒体数据的子类型,用于更具体地描述数据的格式。
    • bFixedSizeSamples:指示数据样本是否是固定大小的。
    • bTemporalCompression:指示数据是否进行了时间压缩。
    • lSampleSize:表示数据样本的大小。
    • formattype:表示数据的格式类型,如 WaveFormatEx、VideoInfoHeader 等。
    • pbFormat:指向包含格式数据的指针。
  • 作用

    • AM_MEDIA_TYPE 结构体用于描述连接两个 DirectShow Filter时使用的媒体数据类型。通过指定主要类型、子类型和其他属性,可以确保连接的两个Filter之间传递的数据格式匹配。
  • 使用

    • 在连接 DirectShow Filter时,可以使用 AM_MEDIA_TYPE 结构体来指定连接的媒体类型,以便确保数据流的顺利传输和处理。
    • 在调用连接方法(如 ConnectDirect)时,可以将包含所需媒体类型信息的 AM_MEDIA_TYPE 结构体作为参数传递。

通过使用 AM_MEDIA_TYPE 结构体,可以在 DirectShow 中有效地描述和传递媒体数据的类型信息,帮助确保连接的Filter之间能够正确处理和渲染各种类型的音频和视频数据。

三、代码走读:

1、获得Capability:

两个Filter之间需要连接,就得协商媒体类型,就像开头说的接电话线的例子,你得根据电话的能力选合适的线,合适的插头。代码如下:

cpp 复制代码
// 入参requestedCapability就是用户请求的能力
int32_t VideoCaptureDS::SetCameraOutput(const VideoCaptureCapability& requestedCapability) {
  // Get the best matching capability
  VideoCaptureCapability capability;
  int32_t capabilityIndex;

  // Store the new requested size
  _requestedCapability = requestedCapability;
  // Match the requested capability with the supported.
  // 根据用户请求的capability,和系统的capability,综合得出最优解(最接近的)
  if ((capabilityIndex = _dsInfo.GetBestMatchedCapability(
           _deviceUniqueId, _requestedCapability, capability)) < 0) {
    return -1;
  }
  // 省略后续步骤...
  return 0;
}

虽然用户请求的能力是requestedCapability,但是,我系统硬件未必支持这个能力,所以就要协商出一个最接近的capability,看下如何协商。

主要分为几大步:

  • 获取系统所有的capabilities;
  • 遍历capabilities,挑出和目标capability最接近的capability(接近的判断优先级:高度 > 宽度 > 最大帧率)
  • 最佳capability保存到resulting(出参);
  • 返回最佳capability在数组中的index;

注意,这个类和平台无关,linux和windows都这么干的!

cpp 复制代码
// 根据用户请求的能力requested,结合系统支持的能力,协商出一个最接近的能力,用resulting返回
int32_t DeviceInfoImpl::GetBestMatchedCapability(
    const char* deviceUniqueIdUTF8,
    const VideoCaptureCapability& requested,
    VideoCaptureCapability& resulting) {
  if (!deviceUniqueIdUTF8)
    return -1;

  MutexLock lock(&_apiLock);
  if (!absl::EqualsIgnoreCase(deviceUniqueIdUTF8, absl::string_view(_lastUsedDeviceName, _lastUsedDeviceNameLength))) {
    // 获取系统所有的capabilities
    if (-1 == CreateCapabilityMap(deviceUniqueIdUTF8)) {
      return -1;
    }
  }

  int32_t bestformatIndex = -1;
  // 获取到capability的数量
  const int32_t numberOfCapabilies = static_cast<int32_t>(_captureCapabilities.size());
  // 遍历capabilities,挑出和目标capability最接近的capability(优先级:高度 > 宽度 > 最大帧率)
  for (int32_t tmp = 0; tmp < numberOfCapabilies; ++tmp)  // Loop through all capabilities
  {
     // 比较高度、宽度、最大帧率的部分省略...
  }

  // Copy the capability
  if (bestformatIndex < 0)
    return -1;
    
  resulting = _captureCapabilities[bestformatIndex]; // 最佳capability保存到resulting(出参)
  return bestformatIndex; // 返回最佳capability的index
}

我把最繁琐的选择最接近能力的部分删除了,线理解下主流程,就是前面我们说的那四步,接下来看看如何删掉的这部分代码;

cpp 复制代码
// 根据用户请求的能力requested,结合系统支持的能力,协商出一个最接近的能力,用resulting返回
int32_t DeviceInfoImpl::GetBestMatchedCapability(
    const char* deviceUniqueIdUTF8,
    const VideoCaptureCapability& requested,
    VideoCaptureCapability& resulting) {
    
  // 前面的代码删除....
  int32_t bestformatIndex = -1;
  int32_t bestWidth = 0;
  int32_t bestHeight = 0;
  int32_t bestFrameRate = 0;
  VideoType bestVideoType = VideoType::kUnknown;
  // 遍历capabilities,挑出和目标capability最接近的capability(优先级:高度 > 宽度 > 最大帧率)
  for (int32_t tmp = 0; tmp < numberOfCapabilies; ++tmp) {
    // 获取当前的capability
    VideoCaptureCapability& capability = _captureCapabilities[tmp];
    // 计算当前capability和用户请求的capability之间的宽度、高度、帧率的差值
    const int32_t diffWidth = capability.width - requested.width;
    const int32_t diffHeight = capability.height - requested.height;
    const int32_t diffFrameRate = capability.maxFPS - requested.maxFPS;
    // 计算之前循环中选择的最优值的宽度、高度、最大帧率和用户请求的capability之间的差值
    const int32_t currentbestDiffWith = bestWidth - requested.width;
    const int32_t currentbestDiffHeight = bestHeight - requested.height;
    const int32_t currentbestDiffFrameRate = bestFrameRate - requested.maxFPS;
    // 线判断当前capability的高度是否比前面选的bestHeight更接近用于请求的高度,有两种情况:
    // 1、当前高度大于目标高度,但是更接近
    // diffHeight >= 0表示我们当前capability的height不小于用户请求的
    // diffHeight <= abs(currentbestDiffHeight)表示更接近目标能力的高度
    // 2、当前告诉小于目标高度,但是更接近
    // currentbestDiffHeight < 0表示之前选择的最优高度小于目标高度
    // diffHeight >= currentbestDiffHeight表示当前更接近目标高度
    if ((diffHeight >= 0 && diffHeight <= abs(currentbestDiffHeight))
        || (currentbestDiffHeight < 0 && diffHeight >= currentbestDiffHeight)) {
      // 如果当前高度差等于之前的最优高度差,那就继续比较宽度和最大帧率
      if (diffHeight == currentbestDiffHeight)  // Found best height. Care about the width)
      {
        // 如果高度差和之前相同,看当前宽度是否最优,和高度类似,都是当前宽度大于目标宽度,或者小于目标宽的,但是更接近
        if ((diffWidth >= 0 && diffWidth <= abs(currentbestDiffWith))
            || (currentbestDiffWith < 0 && diffWidth >= currentbestDiffWith)) {
          // 如果高度和宽度都和当前最优值一样
          if (diffWidth == currentbestDiffWith && diffHeight == currentbestDiffHeight)
          {
            // 如果高度差和宽度差都和之前相同,看当前帧率是否最优,
            if (((diffFrameRate >= 0 && diffFrameRate <= currentbestDiffFrameRate) 
                 || (currentbestDiffFrameRate < 0 && diffFrameRate >= currentbestDiffFrameRate))
            ) {
              // 如果帧率差值相等 或者 之前最优帧率 不小于 用户请求的帧率
              // (当前帧率和之前最优解的帧率相等,或者之前的帧率已经足够好了)
              if ((currentbestDiffFrameRate ==
                   diffFrameRate)  // Same frame rate as previous  or frame rate
                                   // allready good enough
                  || (currentbestDiffFrameRate >= 0)) {
                // 如果videoType和用户请求的不一致,并且不是第一次设置videoType,
                // 同时本次capability的videoType也是webrtc支持的videoType
                if (bestVideoType != requested.videoType &&
                    requested.videoType != VideoType::kUnknown &&
                    (capability.videoType == requested.videoType ||
                     capability.videoType == VideoType::kI420 ||
                     capability.videoType == VideoType::kYUY2 ||
                     capability.videoType == VideoType::kYV12)) {
                  // 设置当前capability的videoType 和 capability 索引为最佳(也就是当前capability是最佳的)
                  bestVideoType = capability.videoType;
                  bestformatIndex = tmp;
                }
                // If width height and frame rate is full filled we can use the
                // camera for encoding if it is supported.
                // 如果高度、宽度和用户请求的一样,并且当前帧率不低于请求最大帧率,那么当前就是最佳capability
                if (capability.height == requested.height &&
                    capability.width == requested.width &&
                    capability.maxFPS >= requested.maxFPS) {
                  bestformatIndex = tmp;
                }
              } else  // Better frame rate
              {
                // 本次帧率差 和 之前最优解的帧率差 不相等,并且之前最优解的 最大帧率比用户请求的小。
                // 那么我们当前的capability就是最优的
                bestWidth = capability.width;
                bestHeight = capability.height;
                bestFrameRate = capability.maxFPS;
                bestVideoType = capability.videoType;
                bestformatIndex = tmp;
              }
            }
          } else  // Better width than previously
          {
            // 如果宽度差更小,那么当前就是最capability
            bestWidth = capability.width;
            bestHeight = capability.height;
            bestFrameRate = capability.maxFPS;
            bestVideoType = capability.videoType;
            bestformatIndex = tmp;
          }
        }     // else width no good
      } else  // Better height
      { 
        // 如果高度差更小,那么当前就是最优capability
        bestWidth = capability.width;
        bestHeight = capability.height;
        bestFrameRate = capability.maxFPS;
        bestVideoType = capability.videoType;
        bestformatIndex = tmp;
      }
    }  // else height not good
  }    // end for

  // Copy the capability
  if (bestformatIndex < 0)
    return -1;
  resulting = _captureCapabilities[bestformatIndex]; // 最佳capability保存到resulting(出参)
  return bestformatIndex; // 返回最佳capability的index
}

配合注释其实最容易理解了,就是找出高度最接近的,如果高度和目标高度接近,我就是最优capability,如果高度和别人一样接近,那么就再判断宽度和帧率。

上面提到的获取系统所有能力的接口CreateCapabilityMap,我们windows平台就是使用DirectShow那些接口获取的,可以看看:

cpp 复制代码
int32_t DeviceInfoDS::CreateCapabilityMap(const char* deviceUniqueIdUTF8)
{
  // Reset old capability list
  _captureCapabilities.clear();

  const int32_t deviceUniqueIdUTF8Length =
      (int32_t)strlen((char*)deviceUniqueIdUTF8);
  if (deviceUniqueIdUTF8Length > kVideoCaptureUniqueNameLength) {
    RTC_LOG(LS_INFO) << "Device name too long";
    return -1;
  }
  RTC_LOG(LS_INFO) << "CreateCapabilityMap called for device "
                   << deviceUniqueIdUTF8;

  char productId[kVideoCaptureProductIdLength];
  // 这儿个captureDevice就是CapatureFilter
  IBaseFilter* captureDevice = DeviceInfoDS::GetDeviceFilter(
      deviceUniqueIdUTF8, productId, kVideoCaptureProductIdLength);
  if (!captureDevice)
    return -1;
  // 获取输出引脚
  IPin* outputCapturePin = GetOutputPin(captureDevice, GUID_NULL);
  if (!outputCapturePin) {
    RTC_LOG(LS_INFO) << "Failed to get capture device output pin";
    RELEASE_AND_CLEAR(captureDevice);
    return -1;
  }
  IAMExtDevice* extDevice = NULL;
  // 获取IID_IAMExtDevice接口到extDevice
  // IAMExtDevice 接口是用来操作外部设备的扩展接口之一,它提供了一些方法和属性,比如,
  // 控制设备的特定功能、设置参数或获取设备状态信息等
  HRESULT hr =
      captureDevice->QueryInterface(IID_IAMExtDevice, (void**)&extDevice);
  // 判断我们现在使用的是不是一个外设
  if (SUCCEEDED(hr) && extDevice) {
    RTC_LOG(LS_INFO) << "This is an external device";
    extDevice->Release();
  }
  // 使用输出Pin生成一个新的接口IID_IAMStreamConfig到streamConfig
  // 这个接口获取/设置设备的能力
  IAMStreamConfig* streamConfig = NULL;
  hr = outputCapturePin->QueryInterface(IID_IAMStreamConfig,
                                        (void**)&streamConfig);
  if (FAILED(hr)) {
    RTC_LOG(LS_INFO) << "Failed to get IID_IAMStreamConfig interface "
                        "from capture device";
    return -1;
  }

  // this  gets the FPS
  IAMVideoControl* videoControlConfig = NULL;
  // 再获取CaptureFilter的IID_IAMVideoControl接口到videoControlConfig
  /**
   * 视频格式控制:允许应用程序控制视频设备的格式,如帧率、分辨率、像素格式等。
   * 摄像头控制:提供了对摄像头属性的访问,比如调整亮度、对比度、色调、饱和度等。
   * 相机参数设置:可以通过该接口设置摄像头的参数,如曝光时间、白平衡、对焦等。
   * 视频流控制:允许控制视频流的开始、停止、暂停等操作。
   * 镜头控制:对于某些视频设备,可能还包括对镜头控制的支持,如变焦、焦距等。
   */
  HRESULT hrVC = captureDevice->QueryInterface(IID_IAMVideoControl,
                                               (void**)&videoControlConfig);
  if (FAILED(hrVC)) {
    RTC_LOG(LS_INFO) << "IID_IAMVideoControl Interface NOT SUPPORTED";
  }

  AM_MEDIA_TYPE* pmt = NULL;
  VIDEO_STREAM_CONFIG_CAPS caps;
  int count, size;
  // 获取所有的Capabilities
  hr = streamConfig->GetNumberOfCapabilities(&count, &size);
  if (FAILED(hr)) {
    RTC_LOG(LS_INFO) << "Failed to GetNumberOfCapabilities";
    RELEASE_AND_CLEAR(videoControlConfig);
    RELEASE_AND_CLEAR(streamConfig);
    RELEASE_AND_CLEAR(outputCapturePin);
    RELEASE_AND_CLEAR(captureDevice);
    return -1;
  }

  // Check if the device support formattype == FORMAT_VideoInfo2 and
  // FORMAT_VideoInfo. Prefer FORMAT_VideoInfo since some cameras (ZureCam) has
  // been seen having problem with MJPEG and FORMAT_VideoInfo2 Interlace flag is
  // only supported in FORMAT_VideoInfo2
  // 遍历所有的capabilities,看每一种capability的格式类型是什么,
  // FORMAT_VideoInfo2是比较新的,老设备基本都还是supportFORMAT_VideoInfo
  bool supportFORMAT_VideoInfo2 = false;
  bool supportFORMAT_VideoInfo = false;
  bool foundInterlacedFormat = false;
  GUID preferedVideoFormat = FORMAT_VideoInfo;
  for (int32_t tmp = 0; tmp < count; ++tmp) {
    hr = streamConfig->GetStreamCaps(tmp, &pmt, reinterpret_cast<BYTE*>(&caps));
    if (hr == S_OK) {
      if (pmt->majortype == MEDIATYPE_Video &&
          pmt->formattype == FORMAT_VideoInfo2) {
        RTC_LOG(LS_INFO) << "Device support FORMAT_VideoInfo2";
        supportFORMAT_VideoInfo2 = true;
        VIDEOINFOHEADER2* h =
            reinterpret_cast<VIDEOINFOHEADER2*>(pmt->pbFormat);
        assert(h);
        foundInterlacedFormat |=
            h->dwInterlaceFlags &
            (AMINTERLACE_IsInterlaced | AMINTERLACE_DisplayModeBobOnly);
      }
      if (pmt->majortype == MEDIATYPE_Video &&
          pmt->formattype == FORMAT_VideoInfo) {
        RTC_LOG(LS_INFO) << "Device support FORMAT_VideoInfo2";
        supportFORMAT_VideoInfo = true;
      }
    }
  }
  // 如果支持videoInfo2
  if (supportFORMAT_VideoInfo2) {
    // 如果也支持videoInfo格式,并且没有交错格式(foundInterlacedFormat一种老的格式,现在都是逐行扫描)
    // 那么就使用videoInfo
    if (supportFORMAT_VideoInfo && !foundInterlacedFormat) {
      preferedVideoFormat = FORMAT_VideoInfo;
    } else {
      // 否则,使用VideoInfo2
      preferedVideoFormat = FORMAT_VideoInfo2;
    }
  }
  // 遍历所有的capabilities
  for (int32_t tmp = 0; tmp < count; ++tmp) {
    // 获取当前的capability,同时获取媒体类型(pointer media type)
    hr = streamConfig->GetStreamCaps(tmp, &pmt, reinterpret_cast<BYTE*>(&caps));
    if (hr != S_OK) {
      RTC_LOG(LS_INFO) << "Failed to GetStreamCaps";
      RELEASE_AND_CLEAR(videoControlConfig);
      RELEASE_AND_CLEAR(streamConfig);
      RELEASE_AND_CLEAR(outputCapturePin);
      RELEASE_AND_CLEAR(captureDevice);
      return -1;
    }
    // 如果我们获取的媒体类型是Video,并且格式和我们之前首选的格式一致
    if (pmt->majortype == MEDIATYPE_Video &&
        pmt->formattype == preferedVideoFormat) {
      VideoCaptureCapabilityWindows capability;
      int64_t avgTimePerFrame = 0;
      // 如果是videoInfo格式,将能力赋值给capability变量
      if (pmt->formattype == FORMAT_VideoInfo) {
        VIDEOINFOHEADER* h = reinterpret_cast<VIDEOINFOHEADER*>(pmt->pbFormat);
        assert(h);
        capability.directShowCapabilityIndex = tmp; // 当前capability在capabilities中的index
        capability.width = h->bmiHeader.biWidth; // 宽
        capability.height = h->bmiHeader.biHeight; // 高
        avgTimePerFrame = h->AvgTimePerFrame; // 每帧的平均时间
      }
      // 如果是videoInfo2格式,同理
      if (pmt->formattype == FORMAT_VideoInfo2) {
        VIDEOINFOHEADER2* h =
            reinterpret_cast<VIDEOINFOHEADER2*>(pmt->pbFormat);
        assert(h);
        capability.directShowCapabilityIndex = tmp;
        capability.width = h->bmiHeader.biWidth;
        capability.height = h->bmiHeader.biHeight;
        capability.interlaced =
            h->dwInterlaceFlags &
            (AMINTERLACE_IsInterlaced | AMINTERLACE_DisplayModeBobOnly);
        avgTimePerFrame = h->AvgTimePerFrame;
      }
      // 如果之前IMediaControl接口获取成功了
      if (hrVC == S_OK) {
        LONGLONG* frameDurationList; // 帧率
        LONGLONG maxFPS; // 最大帧率
        long listSize; // 帧率列表大小
        SIZE size; // 帧图像大小(宽和高)
        size.cx = capability.width;
        size.cy = capability.height;

        // GetMaxAvailableFrameRate doesn't return max frame rate always
        // eg: Logitech Notebook. This may be due to a bug in that API
        // because GetFrameRateList array is reversed in the above camera. So
        // a util method written. Can't assume the first value will return
        // the max fps.
        // 获取设备所支持的所有帧率(引脚,capability index,帧图像大小(宽和高),帧率列表大小,帧率列表)
        hrVC = videoControlConfig->GetFrameRateList(
            outputCapturePin, tmp, size, &listSize, &frameDurationList);

        // On some odd cameras, you may get a 0 for duration.
        // GetMaxOfFrameArray returns the lowest duration (highest FPS)
        if (hrVC == S_OK && listSize > 0 &&
            0 != (maxFPS = GetMaxOfFrameArray(frameDurationList, listSize))) { // 获取最大值帧率(微妙)
          capability.maxFPS = static_cast<int>(10000000 / maxFPS); // 转换帧率单位,微妙->秒
          capability.supportFrameRateControl = true;
        } else  // use existing method
        {
          RTC_LOG(LS_INFO) << "GetMaxAvailableFrameRate NOT SUPPORTED";
          if (avgTimePerFrame > 0)
            capability.maxFPS = static_cast<int>(10000000 / avgTimePerFrame);
          else
            capability.maxFPS = 0;
        }
      } else  // use existing method in case IAMVideoControl is not supported
      {
        if (avgTimePerFrame > 0)
          capability.maxFPS = static_cast<int>(10000000 / avgTimePerFrame);
        else
          capability.maxFPS = 0;
      }

      // can't switch MEDIATYPE :~(
      // 转换下mediaType,从用户的枚举到设置被枚举
      if (pmt->subtype == MEDIASUBTYPE_I420) {
        capability.videoType = VideoType::kI420;
      } else if (pmt->subtype == MEDIASUBTYPE_IYUV) {
        capability.videoType = VideoType::kIYUV;
      } else if (pmt->subtype == MEDIASUBTYPE_RGB24) {
        capability.videoType = VideoType::kRGB24;
      } else if (pmt->subtype == MEDIASUBTYPE_YUY2) {
        capability.videoType = VideoType::kYUY2;
      } else if (pmt->subtype == MEDIASUBTYPE_RGB565) {
        capability.videoType = VideoType::kRGB565;
      } else if (pmt->subtype == MEDIASUBTYPE_MJPG) {
        capability.videoType = VideoType::kMJPEG;
      } else if (pmt->subtype == MEDIASUBTYPE_dvsl ||
                 pmt->subtype == MEDIASUBTYPE_dvsd ||
                 pmt->subtype ==
                     MEDIASUBTYPE_dvhd)  // If this is an external DV camera
      {
        capability.videoType =
            VideoType::kYUY2;  // MS DV filter seems to create this type
      } else if (pmt->subtype ==
                 MEDIASUBTYPE_UYVY)  // Seen used by Declink capture cards
      {
        capability.videoType = VideoType::kUYVY;
      } else if (pmt->subtype ==
                 MEDIASUBTYPE_HDYC)  // Seen used by Declink capture cards. Uses
                                     // BT. 709 color. Not entiry correct to use
                                     // UYVY. http://en.wikipedia.org/wiki/YCbCr
      {
        RTC_LOG(LS_INFO) << "Device support HDYC.";
        capability.videoType = VideoType::kUYVY;
      } else {
        WCHAR strGuid[39];
        StringFromGUID2(pmt->subtype, strGuid, 39);
        RTC_LOG(LS_WARNING)
            << "Device support unknown media type " << strGuid << ", width "
            << capability.width << ", height " << capability.height;
        continue;
      }
      // 已经设置好了一个capability,存到下面这俩变量当中
      _captureCapabilities.push_back(capability);
      _captureCapabilitiesWindows.push_back(capability);
      RTC_LOG(LS_INFO) << "Camera capability, width:" << capability.width
                       << " height:" << capability.height
                       << " type:" << static_cast<int>(capability.videoType)
                       << " fps:" << capability.maxFPS;
    }
    FreeMediaType(pmt);
    pmt = NULL;
  }
  RELEASE_AND_CLEAR(streamConfig);
  RELEASE_AND_CLEAR(videoControlConfig);
  RELEASE_AND_CLEAR(outputCapturePin);
  RELEASE_AND_CLEAR(captureDevice);  // Release the capture device

  // Store the new used device name
  _lastUsedDeviceNameLength = deviceUniqueIdUTF8Length;
  _lastUsedDeviceName =
      (char*)realloc(_lastUsedDeviceName, _lastUsedDeviceNameLength + 1);
  memcpy(_lastUsedDeviceName, deviceUniqueIdUTF8,
         _lastUsedDeviceNameLength + 1);
  RTC_LOG(LS_INFO) << "CreateCapabilityMap " << _captureCapabilities.size();
  // 返回capabilities中capability个数
  return static_cast<int32_t>(_captureCapabilities.size());
}

其实核心函数就是调用了GetNumberOfCapabilities(),之所以这么又臭又长,是因为存在版本的兼容,以及一些格式的转换,都是低级垒砖代码,毫无技术含量,甚至是反面教材,兼容性太差。还想吐槽,算了。

但是,你要注意看刚才的代码,所有能力都存到两个数组中了,要记住,后面会用。

cpp 复制代码
  // 已经设置好了一个capability,存到下面这俩变量当中
  _captureCapabilities.push_back(capability);
  _captureCapabilitiesWindows.push_back(capability);

2、微调Capability:

继续回到VideoCaptureDS::SetCameraOutput,看获得了最接近的能力之后,后面怎么处理。

cpp 复制代码
int32_t VideoCaptureDS::SetCameraOutput(
    const VideoCaptureCapability& requestedCapability) {
  // Reduce the frame rate if possible.
  // 如果我们获得的最大帧率比用户请求的大,那么减小一点,使用用户请求的最大帧率即可
  // 否则,就使用30帧(比如用户设置了一个十万八千帧和你闹着玩)
  if (capability.maxFPS > requestedCapability.maxFPS) {
    capability.maxFPS = requestedCapability.maxFPS;
  } else if (capability.maxFPS <= 0) {
    capability.maxFPS = 30;
  }

  // Convert it to the windows capability index since they are not nexessary
  // the same
  VideoCaptureCapabilityWindows windowsCapability;
  // 上面对capability的帧率做了修改,这儿保存一份我们之前获得的最优capablity到windowsCapability
  if (_dsInfo.GetWindowsCapability(capabilityIndex, windowsCapability) != 0) {
    return -1;
  }

  IAMStreamConfig* streamConfig = NULL;
  AM_MEDIA_TYPE* pmt = NULL;
  VIDEO_STREAM_CONFIG_CAPS caps;
  // 获取IID_IAMStreamConfig接口
  HRESULT hr = _outputCapturePin->QueryInterface(IID_IAMStreamConfig,
                                                 (void**)&streamConfig);
  if (hr) {
    RTC_LOG(LS_INFO) << "Can't get the Capture format settings.";
    return -1;
  }

  // Get the windows capability from the capture device
  bool isDVCamera = false;
  // 从硬件设备获取capabilities和pmt(媒体类型),然后,微调_dsInfo.GetWindowsCapability的值
  hr = streamConfig->GetStreamCaps(windowsCapability.directShowCapabilityIndex,
                                   &pmt, reinterpret_cast<BYTE*>(&caps));
  if (hr == S_OK) {
    if (pmt->formattype == FORMAT_VideoInfo2) {
      VIDEOINFOHEADER2* h = reinterpret_cast<VIDEOINFOHEADER2*>(pmt->pbFormat);
      if (capability.maxFPS > 0 && windowsCapability.supportFrameRateControl) {
        h->AvgTimePerFrame = REFERENCE_TIME(10000000.0 / capability.maxFPS);
      }
    } else {
      VIDEOINFOHEADER* h = reinterpret_cast<VIDEOINFOHEADER*>(pmt->pbFormat);
      if (capability.maxFPS > 0 && windowsCapability.supportFrameRateControl) {
        // 设置硬件每帧平均时间
        h->AvgTimePerFrame = REFERENCE_TIME(10000000.0 / capability.maxFPS);
      }
    }
}
  • 根据用户请求的帧率,丢获取的capability进行帧率微调;
  • 保存了一份原始的,没有经过微调的capability;
  • 从硬件设备获取capabilities和pmt(媒体类型),然后,微调_dsInfo.GetWindowsCapability的值;
  • 我们目前还都没有使用FORMAT_VideoInfo2,还用的是FORMAT_VideoInfo,因此,设置每帧平均时间;

3、设置SinkFilter输入Pin:

将GetWindowsCapability获取的capability设置给SinkFilter的inputPin。里面两个重要成员变量:

  1. requested_capability_这个用户请求的capability会被更新(使用前面我们获取的capability);
  2. resulting_capability_就是存储的最终结果;(只是构造对象,还没设置);
cpp 复制代码
int32_t VideoCaptureDS::SetCameraOutput(
    const VideoCaptureCapability& requestedCapability) {
	// Set the sink filter to request this capability
    // 将前面获取到的capabilities,设置给sinkFilter的input pin,
    // 看sinkFilter是否支持captureFilter设置的capability
    sink_filter_->SetRequestedCapability(capability);
}
cpp 复制代码
// 之前存储的是用户期望的capability(也就是requested_capability_)
// 现在根据系统的capability来调整capability,得到结果resulting_capability_
// 其实只是构造了对象resulting_capability_,具体的值还要交给下一轮去处理
HRESULT CaptureInputPin::SetRequestedCapability(
    const VideoCaptureCapability& capability) {
  RTC_DCHECK_RUN_ON(&main_checker_);
  RTC_DCHECK(Filter()->IsStopped());
  requested_capability_ = capability;
  resulting_capability_ = VideoCaptureCapability();
  return S_OK;
}

4、设置硬件capability:

就是告诉硬件我们最终选择的capability;

cpp 复制代码
int32_t VideoCaptureDS::SetCameraOutput(
    const VideoCaptureCapability& requestedCapability) {  
	// Order the capture device to use this capability
    // 根据用户设置的和硬件支持的,校正出最终的capability
    hr += streamConfig->SetFormat(pmt);
}

5、连接两个Filter:

入口:

cpp 复制代码
int32_t VideoCaptureDS::SetCameraOutput(
    const VideoCaptureCapability& requestedCapability) {    
	// 调用FilterGraph的方法进行两个Filter的连接
    // 最后一个参数NULL说明我们不校验媒体类型,直接将两个pin连接起来即可
    hr = _graphBuilder->ConnectDirect(_outputCapturePin, _inputSendPin, NULL);
}

里面会进行媒体类型协商,Allocator的协商;

  1. 媒体类型协商:主要由ReceiveConnection函数完成,让输入pin决定我们是否要接受这个连接;(里面会判断媒体类型我们是否可以接受);
  2. Allocator的协商;
    1. 第一步:看是否需要什么特殊条件,webrtc直接返回了,表示没特殊条件;
    2. 第二步:调用输入Pin的GetAllocator来获取一个分配器;最终由输出Pin决定是否要使用这个Allocator;
    3. 第三步:pin选择好之后通过NotifyAllocator通知输入Pin到底我们选择了哪个Allocator;

上面执行完 _graphBuilder->ConnectDirect 之后,里面就会自动调用CaptureInputPin::ReceiveConnection,看输入pin是否接受这个连接。

看代码:

cpp 复制代码
/**
 * 检查这个接收pin(也就是input pin)是否可以接收(比如媒体格式什么的是否符合这个filter pin的预期)
 * @param connector: 上一个filter的输出pin
 * @param media_type: 上一个filter所支持的媒体类型
 */
STDMETHODIMP CaptureInputPin::ReceiveConnection(
    IPin* connector,
    const AM_MEDIA_TYPE* media_type) {
  RTC_DCHECK_RUN_ON(&main_checker_);
  RTC_DCHECK(Filter()->IsStopped());
  // 不为空说明之前和其他pin已经连接了
  if (receive_pin_) {
    RTC_DCHECK(false);
    return VFW_E_ALREADY_CONNECTED;
  }
  // 检查上一个filter的pin是不是输出pin
  HRESULT hr = CheckDirection(connector);
  if (FAILED(hr))
    return hr;
  // 将media_type转换为capability
  if (!TranslateMediaTypeToVideoCaptureCapability(media_type,
                                                  &resulting_capability_))
    return VFW_E_TYPE_NOT_ACCEPTED;

  // Complete the connection
  // 因为之前枚举类型传入的null,表示我们sink来什么类型就接收什么类型,不用校验
  receive_pin_ = connector;
  ResetMediaType(&media_type_);
  CopyMediaType(&media_type_, media_type);

  return S_OK;
}
  • 确保这个pin没有和别的pin相连,不允许被抢占;
  • 因为我们自己是输入pin,因此,和我们相连的必须是输出pin;
  • 同时,发现竟然对sink类型没有做什么校验;

上面medi_type转换为capability的方法如下:

cpp 复制代码
// Returns true if the media type is supported, false otherwise.
// For supported types, the |capability| will be populated accordingly.
// 将media_type转换为capability
bool TranslateMediaTypeToVideoCaptureCapability(
    const AM_MEDIA_TYPE* media_type,
    VideoCaptureCapability* capability) {
  RTC_DCHECK(capability);
  if (!media_type || media_type->majortype != MEDIATYPE_Video ||
      !media_type->pbFormat) {
    return false;
  }

  const BITMAPINFOHEADER* bih = nullptr;
  if (media_type->formattype == FORMAT_VideoInfo) {
    bih = &reinterpret_cast<VIDEOINFOHEADER*>(media_type->pbFormat)->bmiHeader;
  } else if (media_type->formattype != FORMAT_VideoInfo2) {
    bih = &reinterpret_cast<VIDEOINFOHEADER2*>(media_type->pbFormat)->bmiHeader;
  } else {
    return false;
  }

  RTC_LOG(LS_INFO) << "TranslateMediaTypeToVideoCaptureCapability width:"
                   << bih->biWidth << " height:" << bih->biHeight
                   << " Compression:0x" << rtc::ToHex(bih->biCompression);
  // 转换媒体子类型,比如用户请求的是MEDIASUBTYPE_RGB24,那么我们摄像头就按照VideoType::kRGB24采集
  const GUID& sub_type = media_type->subtype;
  if (sub_type == MEDIASUBTYPE_MJPG &&
      bih->biCompression == MAKEFOURCC('M', 'J', 'P', 'G')) {
    capability->videoType = VideoType::kMJPEG;
  } else if (sub_type == MEDIASUBTYPE_I420 &&
             bih->biCompression == MAKEFOURCC('I', '4', '2', '0')) {
    capability->videoType = VideoType::kI420;
  } else if (sub_type == MEDIASUBTYPE_YUY2 &&
             bih->biCompression == MAKEFOURCC('Y', 'U', 'Y', '2')) {
    capability->videoType = VideoType::kYUY2;
  } else if (sub_type == MEDIASUBTYPE_UYVY &&
             bih->biCompression == MAKEFOURCC('U', 'Y', 'V', 'Y')) {
    capability->videoType = VideoType::kUYVY;
  } else if (sub_type == MEDIASUBTYPE_HDYC) {
    capability->videoType = VideoType::kUYVY;
  } else if (sub_type == MEDIASUBTYPE_RGB24 && bih->biCompression == BI_RGB) {
    capability->videoType = VideoType::kRGB24;
  } else {
    return false;
  }

  // Store the incoming width and height
  // 存储传进来的宽和高
  capability->width = bih->biWidth;

  // Store the incoming height,
  // for RGB24 we assume the frame to be upside down
  // 如果存储RGB24,由于图像是YUV,需要将高度转换成负值
  if (sub_type == MEDIASUBTYPE_RGB24 && bih->biHeight > 0) {
    capability->height = -(bih->biHeight);
  } else {
    capability->height = abs(bih->biHeight);
  }

  return true;
}

此时,已经连接好了Filter,接下来就是协商分配器了。

6、协商Allocator:

步骤如下:

  • 首先获取分配器属性:

    cpp 复制代码
    /**
     * 获取分配器的属性(由于我们对分配器没有特殊要求,这儿是null)
     */
    STDMETHODIMP CaptureInputPin::GetAllocatorRequirements(
        ALLOCATOR_PROPERTIES* props) {
      return E_NOTIMPL;
    }
  • 获取分配器:

    cpp 复制代码
    /**
     * 获取分配器
     */
    STDMETHODIMP CaptureInputPin::GetAllocator(IMemAllocator** allocator) {
      RTC_DCHECK_RUN_ON(&main_checker_);
      if (allocator_ == nullptr) {
        // 调用CLSID_MemoryAllocator类的IID_IMemAllocator接口,最终分配好allocator
        HRESULT hr = CoCreateInstance(CLSID_MemoryAllocator, 0,
                                      CLSCTX_INPROC_SERVER, IID_IMemAllocator,
                                      reinterpret_cast<void**>(allocator));
        if (FAILED(hr))
          return hr;
        allocator_.swap(allocator); // allocator设置给成员变量
      }
      *allocator = allocator_; // 设置出参
      allocator_->AddRef(); // 增加引用计数
      return S_OK;
    }
  • 至此,输入pin就已经创建好了allocator;

    cpp 复制代码
    /**
     * 通知输入pin,选择了哪个Allocator
     */
    STDMETHODIMP CaptureInputPin::NotifyAllocator(IMemAllocator* allocator,
                                                  BOOL read_only) {
      RTC_DCHECK_RUN_ON(&main_checker_);
      allocator_.swap(&allocator);
      if (allocator_)
        allocator_->AddRef();
      if (allocator)
        allocator->Release();
      return S_OK;
    }

四、总结:

本节主要讲了怎么将两个Filter进行连接,连接之后就可以进行数据采集了。

相关推荐
小一亿2 小时前
【0基础PS】PS工具详解--仿制图章工具
学习·平面·adobe·信息可视化·媒体·photoshop
朱古力(音视频开发)4 小时前
NDI开发指南
fpga开发·音视频·实时音视频·视频编解码·流媒体
科技资讯快报10 小时前
法国声学智慧 ,音响品牌SK (SINGKING AUDIO) 重构专业音频边界
重构·音视频
云霄IT13 小时前
python之使用ffmpeg下载直播推流视频rtmp、m3u8协议实时获取时间进度
python·ffmpeg·音视频
WSSWWWSSW15 小时前
Jupyter Notebook 中显示图片、音频、视频的方法汇总
ide·人工智能·jupyter·音视频·python notebook
sukalot20 小时前
window显示驱动开发—Direct3D 11 视频播放改进
驱动开发·音视频
ls_qq_267081347021 小时前
cocos打包web - ios设备息屏及前后台切换音频播放问题
前端·ios·音视频·cocos-creator
科技资讯快报1 天前
法式基因音响品牌SK(SINGKING AUDIO)如何以硬核科技重塑专业音频版图
科技·音视频
天天向上10241 天前
vue2 使用liveplayer加载视频
音视频