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
结构体作为参数传递。
- 在连接 DirectShow Filter时,可以使用
通过使用 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。里面两个重要成员变量:
requested_capability_
这个用户请求的capability会被更新(使用前面我们获取的capability);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的协商;
- 媒体类型协商:主要由
ReceiveConnection
函数完成,让输入pin决定我们是否要接受这个连接;(里面会判断媒体类型我们是否可以接受); - Allocator的协商;
- 第一步:看是否需要什么特殊条件,webrtc直接返回了,表示没特殊条件;
- 第二步:调用输入Pin的
GetAllocator
来获取一个分配器;最终由输出Pin决定是否要使用这个Allocator; - 第三步: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进行连接,连接之后就可以进行数据采集了。