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

WebRTC视频 01 - 视频采集整体架构
WebRTC视频 02 - 视频采集类 VideoCaptureModule
WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇
WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇(本文)
WebRTC视频 05 - 视频采集类 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 方法的一般说明:

  • 语法

    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 结构体的一般说明:

  • 结构体定义

    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之间需要连接,就得协商媒体类型,就像开头说的接电话线的例子,你得根据电话的能力选合适的线,合适的插头。代码如下:

// 入参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都这么干的!

// 根据用户请求的能力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
}

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

// 根据用户请求的能力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那些接口获取的,可以看看:

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(),之所以这么又臭又长,是因为存在版本的兼容,以及一些格式的转换,都是低级垒砖代码,毫无技术含量,甚至是反面教材,兼容性太差。还想吐槽,算了。

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

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

2、微调Capability:

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

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_就是存储的最终结果;(只是构造对象,还没设置);
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);
}
// 之前存储的是用户期望的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;

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

5、连接两个Filter:

入口:

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是否接受这个连接。

看代码:

/**
 * 检查这个接收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的方法如下:

// 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:

步骤如下:

  • 首先获取分配器属性:

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

    /**
     * 获取分配器
     */
    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;

    /**
     * 通知输入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进行连接,连接之后就可以进行数据采集了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/917498.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI在电商平台中的创新应用:提升销售效率与用户体验的数字化转型

1. 引言 AI技术在电商平台的应用已不仅仅停留在基础的数据分析和自动化推荐上。随着人工智能的迅速发展&#xff0c;越来越多的电商平台开始将AI技术深度融合到用户体验、定价策略、供应链优化、客户服务等核心业务中&#xff0c;从而显著提升运营效率和用户满意度。在这篇文章…

Blossom:开源私有部署的markdown笔记软件

在信息化、数字化时代&#xff0c;我们每个人的生活和工作都离不开笔记和知识管理。从简单的待办事项&#xff0c;到复杂的项目计划&#xff0c;再到存储大量个人知识的工具&#xff0c;如何选择一个高效、便捷且符合个人需求的笔记软件&#xff0c;成了许多人的难题。最近在逛…

Linux debian系统安装ClamTk开源图形用户界面(GUI)杀毒软件

一、ClamTk简介 ClamTk 是一个基于 ClamAV 的开源图形用户界面&#xff08;GUI&#xff09;杀毒软件。它使用 GTK2-Perl 脚本构建而成&#xff0c;支持32位与64位操作系统。ClamTk 提供了一个直观的用户界面&#xff0c;使得用户无需深入了解命令行即可完成大部分操作。它具备…

Linux 进程信号的产生

目录 0.前言 1. 通过终端按键产生信号 1.1 CtrlC&#xff1a;发送 SIGINT 信号 1.2 Ctrl\&#xff1a;发送 SIGQUIT 信号 1.3 CtrlZ&#xff1a;发送 SIGTSTP 信号 2.调用系统命令向进程发信号 3.使用函数产生信号 3.1 kill 函数 3.2 raise 函数 3.3 abort 函数 4.由软件条件产…

【大数据学习 | HBASE高级】hive操作hbase

一般在查询hbase的数据的时候我们可以直接使用hbase的命令行或者是api进行查询就行了&#xff0c;但是在日常的计算过程中我们一般都不是为了查询&#xff0c;都是在查询的基础上进行二次计算&#xff0c;所以使用hbase的命令是没有办法进行数据计算的&#xff0c;并且对于hbas…

微信小程序 https://thirdwx.qlogo.cn 不在以下 downloadFile 合法域名列表中

授权登录后&#xff0c;拿到用户头像进行加载&#xff0c;但报错提示&#xff1a; https://thirdwx.qlogo.cn 不在以下 downloadFile 合法域名列表中 解决方法一&#xff08;未完全解决&#xff0c;临时处理&#xff09;&#xff1a;在微信开发者工具将不校验...勾上就可以访问…

rk3399开发环境使用Android 10初体验蓝牙功能

版本 日期 作者 变更表述 1.0 2024/11/10 于忠军 文档创建 零. 前言 由于Bluedroid的介绍文档有限&#xff0c;以及对Android的一些基本的知识需要了(Android 四大组件/AIDL/Framework/Binder机制/JNI/HIDL等)&#xff0c;加上需要掌握的语言包括Java/C/C等&#xff0…

1. Django中的URL调度器 (项目创建与简单测试)

1. 创建 Django 项目 运行以下命令创建一个名为 blog_project 的 Django 项目&#xff1a; django-admin startproject blog_project2. 创建博客应用 Django 中&#xff0c;项目可以包含多个应用。创建一个名为 blog 的应用&#xff1a; cd blog_project python manage.py …

数据结构(初阶4)---循环队列详解

循环队列 1.循环队列的结构  1).逻辑模式 2.实现接口  1).初始化  2).判断空和满  3).增加  4).删除  5).找头  6).找尾 3.循环队列的特点 1.循环队列的结构 1).逻辑模式 与队列是大同小异的&#xff0c; 其中还是有一个指向队列头的head指针&#xff0c; 也有一个指向尾…

【蓝桥杯算法】Java的基础API

1. BigInteger 的使用 1.1. 判素数 package 模板;import java.math.BigInteger; import java.util.Scanner;public class 判素数 {static Scanner in new Scanner(System.in);public static void main(String[] args) {int q in.nextInt();while (q-- > 0) {BigInteger …

STM32设计井下瓦斯检测联网WIFI加Zigbee多路节点协调器传输

目录 目录 前言 一、本设计主要实现哪些很“开门”功能&#xff1f; 二、电路设计原理图 1.电路图采用Altium Designer进行设计&#xff1a; 2.实物展示图片 三、程序源代码设计 四、获取资料内容 前言 本系统基于STM32微控制器和Zigbee无线通信技术&#xff0c;设计了…

320页PDF | 集团IT蓝图总体规划报告-德勤(限免下载)

一、前言 这份报告是集团IT蓝图总体规划报告-德勤。在报告中详细阐述了德勤为某集团制定的全面IT蓝图总体规划&#xff0c;包括了集团信息化目标蓝图、IT应用规划、数据规划、IT集成架构、IT基础设施规划以及IT治理体系规划等关键领域&#xff0c;旨在为集团未来的信息化发展提…

Python毕业设计选题:基于django+vue的二手物品交易系统

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 用户管理 店铺管理 二手物品管理 广告管理 留言反馈 订单…

Android CoordinatorLayout:打造高效交互界面的利器

目录 一、CoordinatorLayout 介绍及特点 二、使用方法 2.1 创建 CoordinatorLayout 布局 2.2 添加需要协调的子视图 2.3 自定义 Behavior 三、结语 相关推荐 在Android开发中&#xff0c;面对复杂多变的用户界面需求&#xff0c;CoordinatorLayout以其强大的交互管理能力…

docker-hub 无法访问,使用windows魔法拉取docker images再上传到linux docker环境中

云机的服务器是可以docker拉取镜像的&#xff0c;但是本地的虚拟机、物理服务器等网络环境不好的情况&#xff0c;是无法访问docker-hub的&#xff0c;即使更换了docker镜像源国内源也无法使用。 本文章使用 在魔法网络环境下的windows&#xff0c;下载docker images后&#xf…

在Ubuntu22.04上源码构建ROS noetic环境

Ubuntu22.04上源码构建ROS noetic 起因准备环境创建工作目录并下载源码安装编译依赖包安装ros_comm和rosconsole包的两个补丁并修改pluginlib包的CMakeLists的编译器版本编译安装ROS noetic和ros_test验证 起因 最近在研究VINS-Mono从ROS移植到ROS2&#xff0c;发现在编写feat…

【linux学习指南】VSCode部署Ubantu云服务器,与Xshell进行本地通信文件编写

文章目录 &#x1f4dd;前言&#x1f320; 步骤&#x1f309;测试同步 &#x1f6a9;总结 &#x1f4dd;前言 本文目的是讲使用Vscode连接Ubantu,与本地Xshell建立通信同步文件编写。 查看本机系统相关信息&#xff1a; cat /etc/lsb*DISTRIB_IDUbuntu: 表示这是 Ubuntu 发行…

【JavaSE线程知识总结】

多线程 一.创建线程1.多线程创建方式一(Thread)2.多线程创键方式二(Runnable)3.线程创建方式三 二.线程安全问题解决办法1.使用同步代码块synchornized 2 .使用Lock解决线程安全问题 三.总结 线程就是程序内部的一条执行流程 一.创建线程 常用的方法 Thread.currentThread()…

用OMS进行 OceanBase 租户间数据迁移的测评

基本概念 OceanBase迁移服务&#xff08;&#xff0c;简称OMS&#xff09;&#xff0c;可以让用户在同构或异构 RDBMS 与OceanBase 数据库之间进行数据交互&#xff0c;支持数据的在线迁移&#xff0c;以及实时增量同步的复制功能。 OMS 提供了可视化的集中管控平台&#xff…

第一个 Flutter 项目(1)共46节

前端开发工具vs code&#xff0c;安装Flutter sdk&#xff0c;如果你的下载速度比较慢&#xff0c;可以选择这个&#x1f604; flutter sdk 解压码&#xff1a;stwq 配置可以看这Flutter 新建工程一直等待 解决办法-CSDN博客 如果你是新的 Flutter 开发者&#xff0c;我们建…