文章介绍了如何在纯C#中创建虚拟视频捕获源directshow过滤器。
介绍
这个过滤器的实现是基于我的基类.NET我在上一篇文章中描述的库(C#中的纯.NET DirectShow过滤器)。当人们被询问这样的过滤器时,我决定制作它,并将它放到单独的文章中,我认为,这里需要一些实现说明和代码描述。
背景
系统中的大多数捕获设备都是以WDM驱动程序的形式出现的,驱动程序是通过WDM视频捕获过滤器在系统中处理的,WDM视频捕获过滤器是从内核流到microsoftdirectshow的代理。代理筛选器引用每个捕获设备驱动程序,并使用指定的DirectShow筛选器类别(视频捕获源)中的设备名称注册。本文介绍了如何用C#制作虚拟的非WDM视频采集源。
实现
过滤器的核心功能将是捕捉屏幕,并提供一个视频流的数据,它的工作方式和以前的文章相同。我们使用BaseSourceFilter和SourceStream作为过滤器和输出pin的基类。虚拟视频捕获源必须在“视频捕获源”类别中注册。另外,输出引脚应至少实现IKsPropertySet和IAMStreamConfig接口,而过滤器应实现IAMSFiltermiscFlags接口。
// 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
正如您所看到的,我已经为output pin提供了其他接口的支持,实际上您可以实现所有与实际WDM代理过滤器相同的接口,但是我只为应用程序提供了最有用的接口。IAMStreamControl用于指定启动和停止通知(有时相机需要0.5到3秒的时间进行校准,应用程序可以使用此界面跳过启动样本或其他需要)。IAMPushSource控制媒体采样定时偏移和延迟(主要用于配置音频视频同步)。IAMBufferNegotiation允许配置分配器设置。更多关于接口的细节可以在MSDN中找到。注册的过滤器在GraphEdit中看起来是下一个方向
IKsPropertySet 接口
使用这个接口我们可以指定pin类别guid。如果在使用ICaptureGraphBuilder2接口呈现pin或查找具有指定类别的pin时指定类别,则必须执行此操作。作为要求,这种过滤器的一个引脚应该有捕获类别。实现看起来更简单:
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结构中的索引返回所有可用的分辨率和格式。与此结构一起,此接口可以返回mediatype(实际的或可用的索引)。注意:您只能返回一种媒体类型,但请在配置结构中指定适当的设置-应用程序也应处理该设置(不是所有应用程序都这样做-但大多数专业软件工作正常)。注意VideoStreamConfigCaps结构中的值在任何索引处也可以不同,这意味着过滤器可以有不同纵横比的输出或其他参数,这些参数也属于返回的MediaType。例如,过滤器可以为RGB24和RGB32公开不同的宽度和高度粒度。注意:这个接口返回的媒体类型也可以不同于通过IEnumMediaTypes接口检索的类型(应用程序也应该处理这个问题)。例如,如果捕获源在输出时支持很少的颜色空间,则它可以通过IAMStreamConfig接口返回具有不同颜色空间的媒体类型,并通过IEnumMediaTypes仅返回具有一个“活动”颜色空间的媒体类型。在这种情况下,“活动”颜色空间将取决于SetFormat调用。在这个过滤器中,我做了一个简单的方法:我以两种方式返回所有mediatype,并且只有一种配置。让我们声明配置常量:
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;
}
检索Caps和媒体类型:
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位的代码中,如果我们将媒体类型设置为输出pin,则返回它,否则返回默认的输出宽度和高度。在其他位置值中,我们计算输出分辨率。
实现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应重新计算图像大小并重新配置位像素)或修改分辨率而不更改图像大小。我没有处理这种情况,但您可以在传入CheckMediaType之前配置这个新类型,因为在部分初始化的情况下,该方法将拒绝该类型。大多数应用程序放置正确的类型,但使用部分初始化类型的情况是可能的。
音高校正与媒体类型协商
提高性能所需的音调校正。如果我们不支持,那么我们的过滤器将连接到视频渲染器通过色彩空间转换器和内部色彩空间转换器的数据将只是从一个样本复制到另一个根据渲染器提供的音高。之所以会出现这种情况,是因为视频渲染器使用Direct3D或DirectDraw渲染示例,并且它有自己的分配器,该分配器将媒体示例作为主机分配的Direct3D曲面或纹理提供,这些曲面或纹理可能需要不同的分辨率(您可以在DirectX Caps查看器工具中找到的所有分辨率)。这方面的另一个例子是,大多数编码器使用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;
}
在中执行检查MediaType更改的代码VirtualCamStream.FillBuffer文件方法:
AMMediaType pmt;
if (S_OK == _sample.GetMediaType(out pmt))
{
if (FAILED(SetMediaType(pmt)))
{
ASSERT(false);
_sample.SetMediaType(null);
}
pmt.Free();
}
送样
我已经看到了很多directshow过滤器的例子,甚至可能在这里,其中提到他们有活源代码,只实现了简单的推源代码过滤器,这看起来像我上一篇文章中的一个例子-这是完全错误的。例如,您可以将这样的过滤器连接到AVI mux,将mux输出连接到文件编写器,按start并等待30-60秒。在检查结果文件后,您可以将其中的时间与您等待的时间进行比较-它将不相同(如果您等待30秒写入,则文件中的时间将超过30秒)。这件事发生bcs你的来源不是活的它只是设置时间戳没有等待。由于我们只有一个输入连接到AVI muxer filter-muxer不执行同步,只写入传入的样本。这不会发生在连接到视频渲染器的情况下,因为渲染器等待采样时间并根据时间戳显示它。希望你看到一个问题。如果你认为:“只需要指定睡眠”-这也是不正确的解决办法。对于初学者或那些喜欢在多线程应用程序中使用Sleep的人:避免在应用程序中使用Sleep(即使没有睡眠也最好的方法)-线程应该等待而不是睡眠-因为有好的函数,比如WaitForSingleObject和WaitMultipleObjects,所以多线程的回顾不是那篇文章的一部分,而是顺其自然作为建议。为了应付我们的情况,我们应该使用时钟。通过调用IBaseFilter接口的SetSyncSource方法,我们得到了IGraphBuilder提供给我们的过滤器的时钟。注意:可能的情况是过滤图不使用同步源,在这种情况下,我们应该创建自己的时钟(它是Microsoft本机基类中的CSystemClock类,在.NET版本中,我到目前为止还没有创建该类)。在那个例子中,没有时钟我是不会处理这种情况的,所以这取决于你。好的,让我们看看如何实现它。首先,我们应该为初始化和关闭时钟变量重写pin的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编码器中的过滤器使用示例:
笔记
如果你需要任何过滤器的例子,如网络流,视频/音频渲染器,muxers,demuxers,编码器,解码器张贴在论坛,下次也许我张贴它,因为我已经做了数百个过滤器。