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

C# 中带 Direct3D 视频渲染的 VMR9 分配器演示者

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2012 年 7 月 10 日

CPOL

5分钟阅读

viewsIcon

61005

downloadIcon

3379

本文档描述了如何使用 Direct3D 在 .NET 中,通过自定义分配器演示器,在 VMR9 上实现纯 C# 视频渲染。

引言

我决定在这里分享我十年多媒体应用开发经验。我为描述不够详尽而道歉,因为有些东西对我来说非常琐碎,而且我不想描述那些可以在网上找到的简单事物。我将本教程标记为中级。让我们开始吧……

本教程的目的

这是一个在 .NET 中使用 Direct3D 渲染视频的独立应用程序。该应用程序可以作为起点,用于实现渲染您自己的场景,其中包含一个或多个视频源。

开始前您应该了解什么

我建议您在网上或 MSDN 上阅读以下主题。

  • DirectShow – 媒体播放。
  • 什么是 VMR9 – 它的作用和用法。
  • 互操作封送和 PInvoke。
  • 简要介绍 Direct3D 9 和场景呈现。
  • 多线程。
  • COM

应用程序概述

演示应用程序展示了如何使用 DirectShow 执行视频播放。它使用 VMR9 渲染过滤器,该过滤器可在 Windows XP 及更高版本的系统上使用。VMR9 允许提供您自己的引擎来分配表面和/或在屏幕上呈现这些表面。在示例中,我们使用自己的功能来实现分配和呈现这两种功能,这意味着 VMR9 将在“无渲染”模式下使用。我们将通过 Direct3D9 呈现表面,而对于我们的应用程序来说,SlimDX(Direct9D 的托管库)就足够了,但您可以使用任何其他库,甚至为所需的接口创建托管包装器。下图显示了视频数据流。

应用程序创建自定义分配器并在初始化期间将其提供给 VMR9。在播放时,VMR9 查询已分配的表面并使用视频数据更新它们,之后它将表面标记为准备好显示,我们将该表面发送给演示器,演示器在应用程序中显示它们。

我的类库

该实现不使用 DirectShow.NET 库,而是直接封送 DirectShow 接口。我有一些类可以帮助您在应用程序中使用 DirectShow,但您也可以在应用程序中使用 DirectShow.NET 库。在这里我简要描述一下我在这里使用的通用类

COMHelper – 包含常见类型定义和常见值(例如 HRESULTS 值、Windows API 帮助宏和跟踪函数)的基类。

BOOL – 表示布尔值的类。

HRESULT – COM HRESULT 值的帮助类。

FOURCC – 四字符代码实现。

DSObject<T> - COM 对象的模板类 – 在这里用于 DirectShow 中的过滤器、引脚和其他对象。

DSPin – IPin 接口的帮助类。

DSFilter – IBaseFilter 接口的帮助类。

DSFilterGraphBase – 实现 DirectShow 过滤器图的基类。

DSFilePlayback – 媒体文件播放的基类。

有关每个类的更多信息,您可以在源代码审查中找到。

实现场景呈现

正如我前面提到的,我使用 SlimDX 库进行 Direct3D 渲染。因此,我们需要将此库添加为引用,并且需要包含 using 别名

using SlimDX.Direct3D9; 
using SlimDX;

场景类的初始化,通过创建具有指定控件的 Direct3D 设备,我们将在其中呈现传入的视频,并将主 RenderTarget 存储为类变量。

public Scene(Control _control) 
: this(_control, null) 
{ 
} 

public Scene(Control _control, Device _device) 
{ 
    m_Control = _control; 
    m_Device = _device; 
    if (m_Device == null) 
    { 
        Direct3DEx _d3d = new Direct3DEx(); 
        DisplayMode _mode = _d3d.GetAdapterDisplayMode(0); 
        PresentParameters _parameters = new PresentParameters(); 
        _parameters.BackBufferFormat = _mode.Format; 
        _parameters.BackBufferCount = 1; 
        _parameters.BackBufferWidth = m_Control.Width; 
        _parameters.BackBufferHeight = m_Control.Height; 
        _parameters.Multisample = MultisampleType.None;
        _parameters.SwapEffect = SwapEffect.Discard; 
        _parameters.PresentationInterval = PresentInterval.Default; 
        _parameters.Windowed = true; 
        _parameters.DeviceWindowHandle = m_Control.Handle; 
        _parameters.PresentFlags = PresentFlags.DeviceClip | PresentFlags.Video; 
        m_Device = new DeviceEx(_d3d, 0, DeviceType.Hardware, m_Control.Handle,
            CreateFlags.Multithreaded | CreateFlags.HardwareVertexProcessing, _parameters); 
    } 
    m_RenderTarget = m_Device.GetRenderTarget(0); 
}

现在是场景呈现代码的时候了。请注意,这段代码简化了表面数据到后台缓冲表面的复制,因为我决定不完全实现场景创建。现代 GPU 允许以这种方式渲染,但对于旧 GPU,您应该进行顶点声明和纹理渲染,如何在 DirectX 文档中正确描述。

public void OnSurfaceReady(ref Surface _surface)
{
    lock (m_csLock)
    {
        m_Device.SetRenderTarget(0, m_RenderTarget);
        m_Device.Clear(ClearFlags.Target, Color.Blue, 1.0f, 0);
        m_Device.BeginScene();
        Surface _backbufer = m_Device.GetBackBuffer(0, 0);
        m_Device.StretchRectangle(_surface, _backbufer, TextureFilter.Linear);
        m_Device.EndScene();
        m_Device.Present();
    }
}

代码 lock(m_csLock) 用于多线程呈现设备访问。这就是实现场景初始化和呈现视频的全部内容,希望您还没有关闭浏览器 J

实现 VMR9 分配器

本应用程序最困难和最重要的部分就在这里。

过滤器图

我们正在应用程序内部构建的过滤器图是特定的播放图,但我们插入了 VMR9 渲染器过滤器而不是默认视频渲染器。因此图表将如下所示

事件

第一步是委托事件,该事件将在表面准备就绪时发生。

public delegate void SurfaceReadyHandler(ref Surface _surface); 

以及类中的事件变量

public event SurfaceReadyHandler OnSurfaceReady;

此处理程序将连接到前面描述的场景方法。

初始化

我不会完整描述 DirectShow 图的构建,因为这里和其它资源上有很多关于如何操作的文章,我只使用我的类来实现。VMR9 过滤器声明

[Guid("51b4abf3-748f-4e3b-a276-c828330e926a")] 
public class VMR9Renderer : DSFilter 
{ 
    public VMR9Renderer()
        : base() 
    { 
    } 
}

我们的分配器类声明将如下所示

public class DSFilePlaybackVMR9 : DSFilePlayback, IVMRSurfaceAllocator9, IVMRImagePresenter9

VMR9 类派生自基础图构建器类,该类处理通过 DirectShow 进行文件播放的所有基本事务,我们只需稍微修改初始化方法即可。

protected override HRESULT OnInitInterfaces() 
{ 
    m_Renderer = new VMR9Renderer(); 
    m_Renderer.FilterGraph = m_GraphBuilder; 
    IVMRFilterConfig9 _config = (IVMRFilterConfig9)m_Renderer.QueryInterface(typeof(IVMRFilterConfig9).GUID); 
    HRESULT hr; 
    if (_config != null) 
    { 
        hr = (HRESULT)_config.SetRenderingMode(VMR9Mode.Renderless); 
        hr.Assert(); 
        hr = (HRESULT)_config.SetNumberOfStreams(5); 
        hr.Assert(); 
    } 
    IVMRSurfaceAllocatorNotify9 _notify = (IVMRSurfaceAllocatorNotify9)m_Renderer.QueryInterface(typeof(IVMRSurfaceAllocatorNotify9).GUID); 
    if (_notify != null) 
    { 
        hr = (HRESULT)_notify.AdviseSurfaceAllocator(new IntPtr(g_ciUsedID), this); 
        hr.Assert(); 
        hr = (HRESULT)this.AdviseNotify(_notify); 
        hr.Assert(); 
    } 
    hr = base.OnInitInterfaces(); 
    return hr; 
}

这里我们将 VMR9 渲染器过滤器插入到 FilterGraph 中,将其配置为“Renderless”模式,并指定我们自己的分配器演示器(派生自 IVMRSurfaceAllocator9IVMRImagePresenter9 接口的类)。

实现 IVMRSurfaceAllocator9

我们应该实现这个接口来为 VMR9 过滤器分配表面。在媒体类型协商期间,过滤器调用这个接口来分配表面;在播放期间,它调用这个接口来查询新表面。首先看看演示器类中的变量。

protected Device m_Device = null; 
protected const int g_ciUsedID = 0x01020304; 
protected IVMRSurfaceAllocatorNotify9 m_lpIVMRSurfAllocNotify = null; 
protected object m_csLock = new object(); 
protected List<Surface> m_Surfaces = new List<Surface>(); 
protected Texture m_PrivateTexture = null;

这就是我们处理分配器所需要的一切。VMR9 过滤器调用的第一个接口方法是 AdviseNotify,它提供分配器通知对象。我们将使用它来将 Direct3D 设备句柄指定给 VMR9。

public int AdviseNotify(IVMRSurfaceAllocatorNotify9 lpIVMRSurfAllocNotify) 
{ 
    lock (m_csLock) 
    { 
        m_lpIVMRSurfAllocNotify = lpIVMRSurfAllocNotify; 
        if (m_lpIVMRSurfAllocNotify != null) 
        { 
            IntPtr hMonitor = m_Device.Direct3D.GetAdapterMonitor(
            m_Device.CreationParameters.AdapterOrdinal); 
            HRESULT hr = (HRESULT)m_lpIVMRSurfAllocNotify.SetD3DDevice(m_Device.ComPointer, hMonitor); 
            hr.Assert(); 
            return hr; 
        } 
    } 
    return NOERROR; 
} 

下一个调用是在过滤器连接期间为分配表面而进行的 InitializeDevice。在这个方法中,我们分配表面并将它们放入列表中。

lpAllocInfo.dwFlags |= VMR9SurfaceAllocationFlags.TextureSurface; 
m_Surfaces.Clear(); 
IntPtr[] lplpSurfaces = new IntPtr[lpNumBuffers]; 
hr = (HRESULT)m_lpIVMRSurfAllocNotify.AllocateSurfaceHelper(ref lpAllocInfo, ref lpNumBuffers, lplpSurfaces); 
hr.Assert(); 
if (hr.Succeeded) 
{ 
    for (int i = 0; i < lplpSurfaces.Length; i++) 
    { 
        Marshal.AddRef(lplpSurfaces[i]); 
        Surface _surface = Surface.FromPointer(lplpSurfaces[i]); 
        m_Surfaces.Add(_surface); 
    } 
} 

在播放期间,VMR9 请求更新表面

public int GetSurface(IntPtr dwUserID, int SurfaceIndex, int SurfaceFlags, out IntPtr lplpSurface) 
{ 
    ASSERT(dwUserID.ToInt32() == g_ciUsedID); 
    lplpSurface = IntPtr.Zero; 
    if (SurfaceIndex > m_Surfaces.Count) return E_INVALIDARG; 
    lock (m_csLock) 
    { 
        lplpSurface = m_Surfaces[SurfaceIndex].ComPointer; 
        Marshal.AddRef(lplpSurface); 
    } 
    return NOERROR; 
}

在 TerminateDevice 中,我们只是从列表中删除所有表面并释放它们。

实现 IVMRImagePresenter9

VMR9 的呈现部分应该实现此接口。这里的主要方法是 PresentImage,当帧准备好显示时,VMR9 会调用它。在这个方法中,我们只需调用上面描述的事件处理程序,并指定该表面。

public int PresentImage(IntPtr dwUserID, ref VMR9PresentationInfo lpPresInfo) 
{ 
    if ((object)lpPresInfo == null) 
    { 
        return E_POINTER; 
    } 
    else 
    if (lpPresInfo.lpSurf == IntPtr.Zero) 
    { 
        return E_POINTER; 
    } 
    lock (m_csLock) 
    { 
        Surface _source = Surface.FromPointer(lpPresInfo.lpSurf); 
        if (_source != null && OnSurfaceReady != null) 
        { 
            if (m_PrivateTexture != null) 
            { 
                Surface _target = m_PrivateTexture.GetSurfaceLevel(0); 
                m_Device.StretchRectangle(_source, _target, TextureFilter.None); 
                OnSurfaceReady(ref _target); 
            } 
            else 
            { 
                OnSurfaceReady(ref _source); 
            } 
        } 
    } 
    return NOERROR; 
}

关机

我们需要释放我们在 FilterGraph 初始化期间创建的对象

protected override HRESULT OnCloseInterfaces() 
{ 
    if (m_Renderer) 
    { 
        m_Renderer.Dispose(); 
        m_Renderer = null; 
    } 
    if (m_lpIVMRSurfAllocNotify != null) 
    { 
        Marshal.ReleaseComObject(m_lpIVMRSurfAllocNotify); 
        m_lpIVMRSurfAllocNotify = null; 
    } 
    return base.OnCloseInterfaces(); 
}

主应用程序

主窗体的实现非常简单,我在这里描述最有趣的方法。场景和播放的变量声明

private Scene m_Scene = null;
private DSFilePlayback m_Playback = null;

创建场景对象

m_Scene = new Scene(this.pbView); 

这里 pbView 控制我们将在此控件上呈现视频。开始播放代码

m_Playback = new DSFilePlaybackVMR9(m_Scene.Direct3DDevice); 
m_Playback.OnPlaybackStop += new EventHandler(btnStart_Click); 
((DSFilePlaybackVMR9)m_Playback).OnSurfaceReady += new VMR9.SurfaceReadyHandler(m_Scene.OnSurfaceReady); 
m_Playback.FileName = this.tbFileName.Text; 
if (m_Playback.Start().Succeeded) 
{ 
    btnStart.Text = "Stop"; 
    btnBrowse.Enabled = false; 
}

在这段代码中,我们创建了一个带有指定场景设备的 VMR9 渲染图。之后,我们提供了一个用于表面就绪通知的事件处理程序,并开始播放。停止代码相当简单——只需处置播放。

m_Playback.Dispose(); 
m_Playback = null;

评论和注意事项

欢迎任何评论,希望我能抽出时间发布一些其他有趣的代码。

历史

2012年7月10日 - 初始版本。

© . All rights reserved.