65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (39投票s)

2012年8月9日

CPOL

7分钟阅读

viewsIcon

593632

downloadIcon

28888

文章描述了如何在纯 C# 中创建虚拟视频捕获源 DirectShow 过滤器

引言

此筛选器的实现基于我的 BaseClasses.NET 库,该库在我之前的帖子(纯 .NET DirectShow 筛选器 (C#))中进行了描述。由于有人咨询此类筛选器,我决定制作它,并将其作为单独的文章发布,因为我认为有必要提供一些实现说明和代码描述。

背景

系统中的大多数捕获设备都以 WDM 驱动程序的形式存在,这些驱动程序通过 WDM 视频捕获筛选器 在系统中进行处理,该筛选器是从内核流(Kernel Streaming)到 Microsoft DirectShow 的代理。该代理筛选器引用每个捕获设备驱动程序,并在指定的 DirectShow 筛选器类别(视频捕获源)中以设备名称注册。在本文中,我将描述如何用 C# 创建一个虚拟的非 WDM 视频捕获源。

实现

筛选器的核心功能将是捕获屏幕并将该数据作为视频流提供,它的工作方式与前一篇帖子相同。我们使用 BaseSourceFilterSourceStream 作为我们的筛选器和输出引脚的基类。虚拟视频捕获源需要注册到视频捕获源类别。此外,输出引脚至少应实现 IKsPropertySetIAMStreamConfig 接口,筛选器应实现 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) - 线程应该等待而不是休眠 - 为此有很好的函数,如 WaitForSingleObjectWaitMultipleObjects,关于多线程的详细审查不是本文的一部分,但在这里作为建议。为了处理我们当前的情况,我们应该使用时钟。我们通过调用 IBaseFilter 接口的 SetSyncSource 方法,由 IGraphBuilder 提供给我们的筛选器一个时钟。**注意**:也可能存在筛选器图不使用同步源的情况,在这种情况下,我们应该创建自己的时钟(这是 Microsoft 本机 BaseClasses 中的 CSystemClock 类,在 .NET 版本中,我到目前为止还没有制作这个类)。在此示例中,我没有处理没有时钟的情况 - 所以这取决于您。好的,让我们看看如何实现它。首先,我们应该重写引脚的 ActiveInactive 方法来初始化和关闭时钟变量:

// 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 问题。

© . All rights reserved.