C# 中的 DirectShow 虚拟视频捕获源过滤器





5.00/5 (39投票s)
文章描述了如何在纯 C# 中创建虚拟视频捕获源 DirectShow 过滤器
引言
此筛选器的实现基于我的 BaseClasses.NET 库,该库在我之前的帖子(纯 .NET DirectShow 筛选器 (C#))中进行了描述。由于有人咨询此类筛选器,我决定制作它,并将其作为单独的文章发布,因为我认为有必要提供一些实现说明和代码描述。
背景
系统中的大多数捕获设备都以 WDM 驱动程序的形式存在,这些驱动程序通过 WDM 视频捕获筛选器 在系统中进行处理,该筛选器是从内核流(Kernel Streaming)到 Microsoft DirectShow 的代理。该代理筛选器引用每个捕获设备驱动程序,并在指定的 DirectShow 筛选器类别(视频捕获源)中以设备名称注册。在本文中,我将描述如何用 C# 创建一个虚拟的非 WDM 视频捕获源。
实现
筛选器的核心功能将是捕获屏幕并将该数据作为视频流提供,它的工作方式与前一篇帖子相同。我们使用 BaseSourceFilter
和 SourceStream
作为我们的筛选器和输出引脚的基类。虚拟视频捕获源需要注册到视频捕获源类别。此外,输出引脚至少应实现 IKsPropertySet
和 IAMStreamConfig
接口,筛选器应实现 IAMFilterMiscFlags
接口。
// Output pin Declaration
[ComVisible(false)]
public class VirtualCamStream: SourceStream
, IAMStreamControl // Start Stop notify
, IKsPropertySet // For expose pin category
, IAMPushSource // For push source settings and configuring
, IAMLatency // Latency
, IAMStreamConfig // Format configuring
, IAMBufferNegotiation // Allocator configuring
.........
// Filter Declaration
[ComVisible(true)]
[Guid("9100239C-30B4-4d7f-ABA8-854A575C9DFB")] // Filter Guid
// Specify Filter category for registering and filter merit
[AMovieSetup(Merit.Normal, "860BB310-5D01-11d0-BD3B-00A0C911CE86")]
[PropPageSetup(typeof(AboutForm))] // Property page
public class VirtualCamFilter : BaseSourceFilter
, IAMFilterMiscFlags // To allow getting flag that filter is live source
正如您所见,我已经为输出引脚添加了对其他接口的支持,实际上您可以实现与真实 WDM 代理筛选器相同的接口,但我只实现了对应用程序最有用的接口。IAMStreamControl
用于指定开始和停止通知(有时摄像机需要 0.5 到 3 秒的校准时间,应用程序可以使用此接口跳过启动样本或其他需求)。IAMPushSource
控制媒体样本的时间偏移和延迟(主要用于配置音视频同步)。IAMBufferNegotiation
允许配置分配器设置。有关这些接口的更多详细信息,请参阅 MSDN。在 GraphEdit 中注册的筛选器外观如下:
IKsPropertySet 接口
使用此接口,我们指定引脚类别 GUID。当使用 ICaptureGraphBuilder2
接口渲染引脚时,或者查找具有指定类别的引脚时,这是必需的。并且根据要求,此筛选器的一个引脚应具有 Capture
类别。实现如下,很简单:
public int Set(Guid guidPropSet, int dwPropID, IntPtr pInstanceData, int cbInstanceData, IntPtr pPropData, int cbPropData)
{
return E_NOTIMPL;
}
public int Get(Guid guidPropSet, int dwPropID, IntPtr pInstanceData, int cbInstanceData, IntPtr pPropData, int cbPropData, out int pcbReturned)
{
pcbReturned = Marshal.SizeOf(typeof(Guid));
if (guidPropSet != PropSetID.Pin)
{
return E_PROP_SET_UNSUPPORTED;
}
if (dwPropID != (int)AMPropertyPin.Category)
{
return E_PROP_ID_UNSUPPORTED;
}
if (pPropData == IntPtr.Zero)
{
return NOERROR;
}
if (cbPropData < Marshal.SizeOf(typeof(Guid)))
{
return E_UNEXPECTED;
}
Marshal.StructureToPtr(PinCategory.Capture, pPropData, false);
return NOERROR;
}
public int QuerySupported(Guid guidPropSet, int dwPropID, out KSPropertySupport pTypeSupport)
{
pTypeSupport = KSPropertySupport.Get;
if (guidPropSet != PropSetID.Pin)
{
return E_PROP_SET_UNSUPPORTED;
}
if (dwPropID != (int)AMPropertyPin.Category)
{
return E_PROP_ID_UNSUPPORTED;
}
return S_OK;
}
IAMStreamConfig 接口
这是允许应用程序配置输出引脚格式和分辨率的主要接口。通过此接口,筛选器在 VideoStreamConfigCaps
结构中按索引返回所有可用的分辨率和格式。除了该结构,此接口还可以返回介质类型(实际可用或按索引可用)。**注意**:您只能返回一种介质类型,但请在配置结构中正确指定设置 - 应用程序也应处理这一点(并非所有应用程序都这样做,但大多数专业软件都可以正常工作)。**注意**:VideoStreamConfigCaps
结构中的值在任何索引处也可能不同,这意味着筛选器可能具有不同的宽高比或其他参数,这些参数也属于返回的 MediaType。例如,筛选器可以为 RGB24 和 RGB32 公开不同的宽度和高度粒度。**注意**:此接口返回的 MediaTypes 也可能与通过 IEnumMediaTypes
接口检索的类型不同(应用程序也应处理这一点)。例如,如果捕获源在输出端支持多种颜色空间,它可以通过 IAMStreamConfig
接口返回具有不同颜色空间的介质类型,而通过 IEnumMediaTypes
只返回一种“活动”颜色空间。“活动”颜色空间在这种情况下将取决于 SetFormat
调用。在此筛选器中,我采用了简单的方式:我在两种方式下都返回所有 MediaTypes,并且只有一种配置。让我们声明配置常量:
private const int c_iDefaultWidth = 1024;
private const int c_iDefaultHeight = 756;
private const int c_nDefaultBitCount = 32;
private const int c_iDefaultFPS = 20;
private const int c_iFormatsCount = 8;
private const int c_nGranularityW = 160;
private const int c_nGranularityH = 120;
private const int c_nMinWidth = 320;
private const int c_nMinHeight = 240;
private const int c_nMaxWidth = c_nMinWidth + c_nGranularityW * (c_iFormatsCount - 1);
private const int c_nMaxHeight = c_nMinHeight + c_nGranularityH * (c_iFormatsCount - 1);
private const int c_nMinFPS = 1;
private const int c_nMaxFPS = 30;
获取格式数和结构大小
public int GetNumberOfCapabilities(out int iCount, out int iSize)
{
iCount = 0;
AMMediaType mt = new AMMediaType();
while (GetMediaType(iCount, ref mt) == S_OK) { mt.Free(); iCount++; };
iSize = Marshal.SizeOf(typeof(VideoStreamConfigCaps));
return NOERROR;
}
填充 VideoStreamConfigCaps
结构
public int GetDefaultCaps(int nIndex, out VideoStreamConfigCaps _caps)
{
_caps = new VideoStreamConfigCaps();
_caps.guid = FormatType.VideoInfo;
_caps.VideoStandard = AnalogVideoStandard.None;
_caps.InputSize.Width = c_iDefaultWidth;
_caps.InputSize.Height = c_iDefaultHeight;
_caps.MinCroppingSize.Width = c_nMinWidth;
_caps.MinCroppingSize.Height = c_nMinHeight;
_caps.MaxCroppingSize.Width = c_nMaxWidth;
_caps.MaxCroppingSize.Height = c_nMaxHeight;
_caps.CropGranularityX = c_nGranularityW;
_caps.CropGranularityY = c_nGranularityH;
_caps.CropAlignX = 0;
_caps.CropAlignY = 0;
_caps.MinOutputSize.Width = _caps.MinCroppingSize.Width;
_caps.MinOutputSize.Height = _caps.MinCroppingSize.Height;
_caps.MaxOutputSize.Width = _caps.MaxCroppingSize.Width;
_caps.MaxOutputSize.Height = _caps.MaxCroppingSize.Height;
_caps.OutputGranularityX = _caps.CropGranularityX;
_caps.OutputGranularityY = _caps.CropGranularityY;
_caps.StretchTapsX = 0;
_caps.StretchTapsY = 0;
_caps.ShrinkTapsX = 0;
_caps.ShrinkTapsY = 0;
_caps.MinFrameInterval = UNITS / c_nMaxFPS;
_caps.MaxFrameInterval = UNITS / c_nMinFPS;
_caps.MinBitsPerSecond = (_caps.MinOutputSize.Width * _caps.MinOutputSize.Height * c_nDefaultBitCount) * c_nMinFPS;
_caps.MaxBitsPerSecond = (_caps.MaxOutputSize.Width * _caps.MaxOutputSize.Height * c_nDefaultBitCount) * c_nMaxFPS;
return NOERROR;
}
检索功能和介质类型
public int GetStreamCaps(int iIndex,out AMMediaType ppmt, out VideoStreamConfigCaps _caps)
{
ppmt = null;
_caps = null;
if (iIndex < 0) return E_INVALIDARG;
ppmt = new AMMediaType();
HRESULT hr = (HRESULT)GetMediaType(iIndex, ref ppmt);
if (FAILED(hr)) return hr;
if (hr == VFW_S_NO_MORE_ITEMS) return S_FALSE;
hr = (HRESULT)GetDefaultCaps(iIndex, out _caps);
return hr;
}
实现 GetMediaType
方法
public int GetMediaType(int iPosition, ref AMMediaType pMediaType) { if (iPosition < 0) return E_INVALIDARG; VideoStreamConfigCaps _caps; GetDefaultCaps(0, out _caps); int nWidth = 0; int nHeight = 0; if (iPosition == 0) { if (Pins.Count > 0 && Pins[0].CurrentMediaType.majorType == MediaType.Video) { pMediaType.Set(Pins[0].CurrentMediaType); return NOERROR; } nWidth = _caps.InputSize.Width; nHeight = _caps.InputSize.Height; } else { iPosition--; nWidth = _caps.MinOutputSize.Width + _caps.OutputGranularityX * iPosition; nHeight = _caps.MinOutputSize.Height + _caps.OutputGranularityY * iPosition; if (nWidth > _caps.MaxOutputSize.Width || nHeight > _caps.MaxOutputSize.Height) { return VFW_S_NO_MORE_ITEMS; } } pMediaType.majorType = DirectShow.MediaType.Video; pMediaType.formatType = DirectShow.FormatType.VideoInfo; VideoInfoHeader vih = new VideoInfoHeader(); vih.AvgTimePerFrame = m_nAvgTimePerFrame; vih.BmiHeader.Compression = BI_RGB; vih.BmiHeader.BitCount = (short)m_nBitCount; vih.BmiHeader.Width = nWidth; vih.BmiHeader.Height = nHeight; vih.BmiHeader.Planes = 1; vih.BmiHeader.ImageSize = vih.BmiHeader.Width * Math.Abs(vih.BmiHeader.Height) * vih.BmiHeader.BitCount / 8; if (vih.BmiHeader.BitCount == 32) { pMediaType.subType = DirectShow.MediaSubType.RGB32; } if (vih.BmiHeader.BitCount == 24) { pMediaType.subType = DirectShow.MediaSubType.RGB24; } AMMediaType.SetFormat(ref pMediaType, ref vih); pMediaType.fixedSizeSamples = true; pMediaType.sampleSize = vih.BmiHeader.ImageSize; return NOERROR; }
在此代码中,如果输出引脚的介质类型已设置,我们在位置 0 返回它,否则返回默认的输出宽度和高度。在其他位置,我们计算输出分辨率。
实现 SetFormat
方法
public int SetFormat(AMMediaType pmt)
{
if (m_Filter.IsActive) return VFW_E_WRONG_STATE;
HRESULT hr;
AMMediaType _newType = new AMMediaType(pmt);
AMMediaType _oldType = new AMMediaType(m_mt);
hr = (HRESULT)CheckMediaType(_newType);
if (FAILED(hr)) return hr;
m_mt.Set(_newType);
if (IsConnected)
{
hr = (HRESULT)Connected.QueryAccept(_newType);
if (SUCCEEDED(hr))
{
hr = (HRESULT)m_Filter.ReconnectPin(this, _newType);
if (SUCCEEDED(hr))
{
hr = (HRESULT)(m_Filter as VirtualCamFilter).SetMediaType(_newType);
}
else
{
m_mt.Set(_oldType);
m_Filter.ReconnectPin(this, _oldType);
}
}
}
else
{
hr = (HRESULT)(m_Filter as VirtualCamFilter).SetMediaType(_newType);
}
return hr;
}
这里的 _newType
变量是设置的介质类型,但它可能被调用者部分配置,例如仅更改颜色空间(示例:RGB32 到 YUY2 应重新计算图像大小并重新配置 BitsPerPixel)或修改分辨率而不改变图像大小。我没有处理这些情况,但您可以在将类型传递给 CheckMediaType
之前进行配置,因为如果类型部分初始化,它将在该方法中被拒绝。大多数应用程序会放置正确的类型,但部分初始化的类型是有可能出现的。
间距校正和介质类型协商
间距校正对于提高性能是必需的。如果我们不支持,我们的筛选器将通过颜色空间转换器连接到视频渲染器,并且在颜色空间转换器内部,数据将仅根据渲染器提供的间距从一个样本复制到另一个样本。这是因为视频渲染器使用 Direct3D 或 DirectDraw 来渲染样本,它有自己的分配器,提供内存分配的 Direct3D 曲面或纹理作为媒体样本,这可能需要不同的分辨率(您可以在 DirectX Caps Viewer 工具中找到所有分辨率)。另一个例子是,大多数编码器使用 SSE、MMX 来提高性能,这要求宽度和高度在内存中按 16 或 32 对齐。为了在我们的筛选器中实现这一点,我们应该处理 CheckMediaType 方法,并检查分配器返回的 MediaSample 的 MediaType 值。通常,MediaType 在第一个请求的样本中设置,有关更多信息,请参阅 MSDN:QueryAccept (上游)。CheckMediaType 方法:
public int CheckMediaType(AMMediaType pmt) { if (pmt == null) return E_POINTER; if (pmt.formatPtr == IntPtr.Zero) return VFW_E_INVALIDMEDIATYPE; if (pmt.majorType != MediaType.Video) { return VFW_E_INVALIDMEDIATYPE; } if ( pmt.subType != MediaSubType.RGB24 && pmt.subType != MediaSubType.RGB32 && pmt.subType != MediaSubType.ARGB32 ) { return VFW_E_INVALIDMEDIATYPE; } BitmapInfoHeader _bmi = pmt; if (_bmi == null) { return E_UNEXPECTED; } if (_bmi.Compression != BI_RGB) { return VFW_E_TYPE_NOT_ACCEPTED; } if (_bmi.BitCount != 24 && _bmi.BitCount != 32) { return VFW_E_TYPE_NOT_ACCEPTED; } VideoStreamConfigCaps _caps; GetDefaultCaps(0, out _caps); if ( _bmi.Width < _caps.MinOutputSize.Width || _bmi.Width > _caps.MaxOutputSize.Width ) { return VFW_E_INVALIDMEDIATYPE; } long _rate = 0; { VideoInfoHeader _pvi = pmt; if (_pvi != null) { _rate = _pvi.AvgTimePerFrame; } } { VideoInfoHeader2 _pvi = pmt; if (_pvi != null) { _rate = _pvi.AvgTimePerFrame; } } if (_rate < _caps.MinFrameInterval || _rate > _caps.MaxFrameInterval) { return VFW_E_INVALIDMEDIATYPE; } return NOERROR; }
在 VirtualCamStream.FillBuffer
方法中执行检查 MediaType 更改的代码
AMMediaType pmt;
if (S_OK == _sample.GetMediaType(out pmt))
{
if (FAILED(SetMediaType(pmt)))
{
ASSERT(false);
_sample.SetMediaType(null);
}
pmt.Free();
}
传递样本
我见过很多 DirectShow 筛选器的示例,可能在这里也有,它们声称拥有实时源但只实现了简单的 Push Source 筛选器,这类似于我之前文章中的一个示例 - 这是完全错误的。例如,您可以将此类筛选器连接到 AVI 混合器,然后将混合器的输出写入文件编写器,按下开始并等待 30-60 秒。检查结果文件后,您可以比较其中的时间与您等待的时间 - 它们将不同步(如果您等待 30 秒将被写入,文件中的时间将超过 30 秒)。这是因为您的源不是实时的,它只是设置时间戳而不等待。由于我们只有一个连接到 AVI 混合器筛选器的输入 - 混合器不执行同步,只是写入传入的样本。如果连接到视频渲染器,则不会发生这种情况,因为渲染器会等待样本时间并根据时间戳显示它。希望您能看到问题。如果您认为:“只需指定 Sleep” - 这也不是正确的解决方案。对于初学者或喜欢在多线程应用程序中使用 Sleep 的人:请避免在应用程序中使用 Sleep(最好甚至不要有 Sleep) - 线程应该等待而不是休眠 - 为此有很好的函数,如 WaitForSingleObject
和 WaitMultipleObjects
,关于多线程的详细审查不是本文的一部分,但在这里作为建议。为了处理我们当前的情况,我们应该使用时钟。我们通过调用 IBaseFilter
接口的 SetSyncSource
方法,由 IGraphBuilder
提供给我们的筛选器一个时钟。**注意**:也可能存在筛选器图不使用同步源的情况,在这种情况下,我们应该创建自己的时钟(这是 Microsoft 本机 BaseClasses 中的 CSystemClock
类,在 .NET 版本中,我到目前为止还没有制作这个类)。在此示例中,我没有处理没有时钟的情况 - 所以这取决于您。好的,让我们看看如何实现它。首先,我们应该重写引脚的 Active
和 Inactive
方法来初始化和关闭时钟变量:
// Pin become active
public override int Active()
{
m_rtStart = 0;
m_bStartNotified = false;
m_bStopNotified = false;
{
lock (m_Filter.FilterLock)
{
m_pClock = m_Filter.Clock; // Get's the filter clock
if (m_pClock.IsValid) // Check if were SetSyncSource called
{
m_pClock._AddRef(); // handle instance
m_hSemaphore = new Semaphore(0,0x7FFFFFFF); // Create semaphore for notify
}
}
}
return base.Active();
}
// Pin become inactive (usually due Stop calls)
public override int Inactive()
{
HRESULT hr = (HRESULT)base.Inactive();
if (m_pClock != null) // we have clock
{
if (m_dwAdviseToken != 0) // do we advice
{
m_pClock.Unadvise(m_dwAdviseToken); // shutdown advice
m_dwAdviseToken = 0;
}
m_pClock._Release(); // release instance
m_pClock = null;
if (m_hSemaphore != null)
{
m_hSemaphore.Close(); // delete semaphore
m_hSemaphore = null;
}
}
return hr;
}
之后,我们应该进行样本调度。由于我们有固定的帧率,我们可以使用 AdvicePeriodic
方法。
HRESULT hr = NOERROR;
long rtLatency;
if (FAILED(GetLatency(out rtLatency)))
{
rtLatency = UNITS / 30;
}
if (m_dwAdviseToken == 0)
{
m_pClock.GetTime(out m_rtClockStart);
hr = (HRESULT)m_pClock.AdvisePeriodic(m_rtClockStart + rtLatency, rtLatency, m_hSemaphore.Handle, out m_dwAdviseToken);
hr.Assert();
}
然后等待下一个样本时间发生并设置样本时间
m_hSemaphore.WaitOne();
hr = (HRESULT)(m_Filter as VirtualCamFilter).FillBuffer(ref _sample);
if (FAILED(hr) || S_FALSE == hr) return hr;
m_pClock.GetTime(out m_rtClockStop);
_sample.GetTime(out _start, out _stop);
if (rtLatency > 0 && rtLatency * 3 < m_rtClockStop - m_rtClockStart)
{
m_rtClockStop = m_rtClockStart + rtLatency;
}
_stop = _start + (m_rtClockStop - m_rtClockStart);
m_rtStart = _stop;
lock (m_csPinLock)
{
_start -= m_rtStreamOffset;
_stop -= m_rtStreamOffset;
}
_sample.SetTime(_start, _stop);
m_rtClockStart = m_rtClockStop;
滤镜概述
最终的筛选器作为虚拟视频捕获源工作。它公开具有 VideoInfoHeader 格式的 RGB32 输出介质类型,以及以下分辨率:1024x756(默认)、320x240、480x360、640x480、800x600、960x720、1120x840、1280x960、1440x1080。分辨率仅针对最小值和最大值进行了检查,因此您可以指定范围内的任何其他分辨率。FPS 范围从 1 到 30,默认值为 20。如果某些应用程序不检查 WDM/非 WDM 设备,则可以使用该筛选器。
Skype 中筛选器使用示例
Adobe Live Flash Encoder 中筛选器使用示例
注释
如果您需要任何类型的筛选器示例,例如网络流、视频/音频渲染器、混合器、解混合器、编码器、解码器,请在论坛发帖,下次我可能会发布,因为我制作了数百个筛选器。
历史
2012-09-08 - 初始版本。
2012-10-13 - 修复了 Adobe Flash Player 中的分配器和 EnumMediaTypes 问题。