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





5.00/5 (10投票s)
本文档描述了如何使用 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”模式,并指定我们自己的分配器演示器(派生自 IVMRSurfaceAllocator9 和 IVMRImagePresenter9 接口的类)。
实现 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日 - 初始版本。