Mike Borozdin
适用于:
Microsoft DirectShow
Microsoft Direct3D
Microsoft DirectX 9.0
摘要: 从 DirectShow 或 Direct3D 应用程序开发人员的角度了解如何在 Direct 3D 环境中播放视频。
本页内容
简介
VMR-9 的体系结构
实现分配演示器
配置 VMR 和筛选器图形
获得更多信息
简介
Microsoft7® DirectX® 9.0 中的一个最重要的新功能是人们一直翘首以盼的视频和图形呈现管道的合并。之前,Microsoft® DirectShow® 主要通过 Microsoft DirectDraw® 或者系统内存中的 GDI 在硬件覆盖表面上呈现视频。
在 Microsoft Windows® XP 中引入了 Video Mixing Renderer 7 (VMR-7)。该程序能够呈现到屏幕外 DirectDraw 7 表面上,但是它与 Microsoft Direct3D® 8.0 接口不兼容。最近,DirectX 9.0 则引入了 VMR-9,VMR-9 在受 DirectX 支持的所有平台上均可用,它使用 Direct3D 表面来呈现视频帧。
VMR-9 具有几个优点。首先,您可以使用 Direct3D 提供的所有处理转换。例如,您可以通过像素着色引擎轻松运行视频流,从而获得自定义的视频效果。实际上,VMR-9 是您熟悉的一个功能非常强大的实时数字信号处理器 (DSP)。视频还可以呈现到纹理上 — 作为一个最简单的情形,您可以设想在一个旋转立方体的一个表面上播放视频。如果您是一位游戏开发人员,视频元素将不再局限于沉闷的剪辑片断。现在,您可以将视频与 3D 图形进行组合,以便制作像图形一样动态和交互的视频剪辑。
第二,VMR-9 使得视频能够更加容易地与用户界面 (UI) 进行集成。现在,UI 可以是动态的 — 您将不再局限于使用颜色控制方法来将静态 UI 元素覆盖到视频中。
除了与 Direct3D 的集成之外,VMR 还提供了其他一些重要的新功能:
-
同时呈现多个视频流的功能。
-
支持最新的取消隔行扫描硬件。当隔行扫描源(例如,电视或 DV 视频)显示在一个逐行扫描显示器上时,隔行扫描场必须取消隔行扫描才能形成一个逐行图像。复杂的取消隔行扫描技术可由图形处理单元 (GPU) 实时提供。该 VMR 使得应用程序能够从一些可用技术中进行选择。
-
支持能够控制色调、饱和度、亮度和对比度的新硬件,这种硬件称为过程放大 或 ProcAmp。ProcAmp 不再是用户必须在显示器上进行调整的全局设置。在 3D 环境中,通常不需要全局对比度和亮度功能。通过与 VMR-9 的混合、alpha 混合以及颜色控制功能相结合,对 ProcAmp 的支持可以作出各种可能的电影淡入淡出效果。
本文包括以下主题:
-
VMR-9 的体系结构描述了 VMR-9 筛选器的插件组件。
-
实现分配演示器描述了如何为 VMR-9 创建自定义分配演示器对象。
-
创建 Direct3D 设备描述了 3-D 呈现过程的第一步:创建 Direct3D 设备对象。
-
分配表面说明了 3-D 呈现过程的第二步:分配一个或多个用来承载视频图像的 Direct3D 表面。
-
演示图像说明了 3-D 呈现过程的最后一步:演示图像。
-
配置 VMR-9 和筛选器图形描述了应用程序如何配置 VMR-9 筛选器和构建筛选器图形,从而呈现视频。
VMR-9 的体系结构
如果您对 VMR-7 非常熟悉,则会发现 VMR-9 也不是非常生疏,这是因为 VMR-9 体系结构实际上与 VMR-7 的体系结构完全相同。因为该体系结构在 SDK 文档中进行了详细说明,所以在此只是简单提一下。
VMR-9 是一个输出程序筛选器;它位于视频流最后的筛选器图形中。它包含了两个插件组件,这两个组件可以替换为您应用程序中的自定义组件:
-
分配演示器。分配 VMR-9 使用的 Direct3D 表面,并在 VMR-9 使用视频帧填充这些表面时,演示这些表面。
-
图像合成器。控制视频输入流的混合方式。(本文不讨论图像排列器。)
本文会说明下面的过程如何简单易行:将默认分配演示器替换为一个向 D3D 纹理表面绘制视频流的自定义分配演示器,然后该表面呈现到一个旋转的平面上。
实现分配演示器
要使用自定义分配演示器,应用程序需要完成下列操作:
-
创建该自定义分配演示器对象的实例。
-
配置 VMR-9。
-
构建筛选器图形。
但是在描述这些步骤之前,我们要先来看一下分配演示器对象的实现。
在 VMR9Allocator SDK 示例中,该自定义分配演示器实现为一个名为 CAllocator 的 C++ 类。该类会执行三项基本任务:
-
创建 Direct3D 设备
-
分配表面
-
演示图像
创建 Direct3D 设备
自定义分配演示器的第一个任务是设置 Direct3D 环境。如果您对 Direct3D 编程非常熟悉,这个过程则相对简单一些。首先,创建 Direct3D 对象,然后使用此对象创建 Direct3D 设备。CAllocator 类在其类构造函数中执行上述步骤,该构造函数会调用 CreateDevice 成员函数,如下面的代码所示:
CAllocator::CAllocator(HRESULT& hr, HWND wnd, IDirect3D9* d3d, IDirect3DDevice9* d3dd) : m_refCount(1), m_D3D(d3d), m_D3DDev(d3dd), m_window( wnd ) { CAutoLock Lock(&m_ObjectLock); hr = E_FAIL; if ( IsWindow( wnd ) == FALSE ) { hr = E_INVALIDARG; return; } if ( m_D3D == NULL ) { ASSERT( d3dd == NULL ); m_D3D.Attach( Direct3DCreate9(D3D_SDK_VERSION) ); if (m_D3D == NULL) { hr = E_FAIL; return; } } if ( m_D3DDev == NULL ) { hr = CreateDevice(); } } HRESULT CAllocator::CreateDevice() { m_D3DDev = NULL; D3DPRESENT_PARAMETERS pp; ZeroMemory(&pp, sizeof(pp)); pp.Windowed = TRUE; pp.hDeviceWindow = m_window; pp.SwapEffect = D3DSWAPEFFECT_COPY; return m_D3D->CreateDevice(0, D3DDEVTYPE_HAL, m_window, D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, &pp, &m_D3DDev.p); }
在本例中,分配给 IDirect3D9::CreateDevice 的标志指定了有窗口模式和 D3DSWAPEFFECT_COPY。如果适用,您还可以使用其他标志。但是,因为 VMR-9 在一个单独的线程上运行,所以 D3DCREATE_MULTITHREADED 标志是必需的。
分配表面
分配演示器的下一个主要的任务是为 VMR-9 分配表面。表面分配可能比较麻烦,这是因为解码器会输出各种视频格式,而且图形卡对于这些格式会提供各种不同的支持。您分配表面的目的是为了创建一个可以绘制到 3-D 基元上的视频图像。为此,您必须考虑上游筛选器、硬件以及 Direct3D 的格式转换功能。
表面是在分配演示器对象的 InitializeDevice 方法中创建的,当 VMR-9 与上游解码器协商针连接时会调用该方法。此方法具有下列特征:
STDMETHODIMP InitializeDevice( DWORD_PTR dwUserID, VMR9AllocationInfo *lpAllocInfo, DWORD *lpNumBuffers );
dwUserID 参数标识该 VMR-9 实例。以上述代码为例,其中只有一个实例,因此可以忽略 dwUserID。VMR9AllocationInfo 结构包含了几个信息;VMR 根据上游筛选器提出的格式填充这些信息,该上游筛选器通常是一个视频解码器。dwWidth 和 dwHeight 成员包含视频维度,Format 成员是一种 Direct3D 格式类型,dwFlags 成员则描述该表面。
例如,VMR9AllocFlag_TextureSurface 标志指示纹理表面,VMR9AllocFlag_3DRenderTarget 标志则指示呈现目标。lpNumBuffers 参数指定所要创建的表面的数量。
VMR-9 需要一个上游解码器能够接受的表面。要创建能够与视频解码器和 Direct3D 设备兼容的 Direct3D 表面,建议您执行下列步骤:
-
通过调用 IVMRSurfaceAllocatorNotify::SetD3DDevice 向 VMR-9 通知关于该 Direct3D 设备和显示器的信息。
-
向 VMR9AllocationInfo 结构添加 VMR9AllocFlag_TextureSurface 标志,以便请求纹理表面。
-
分配表面。如果此操作成功,则返回 S_OK。
-
如果上面的步骤失败,而且上游筛选器未能请求要成为呈现目标的表面,则请尝试创建屏幕外表面。
下面的代码说明了 CAllocator 类如何实现上述步骤:
HRESULT CAllocator::InitializeDevice(DWORD_PTR dwUserID, VMR9AllocationInfo *lpAllocInfo, DWORD *lpNumBuffers) { if (lpNumBuffers == NULL) { return E_POINTER; } if ( m_lpIVMRSurfAllocNotify == NULL ) { return E_FAIL; } HRESULT hr = m_lpIVMRSurfAllocNotify->SetD3DDevice( m_D3DDev, MonitorFromWindow( m_window, MONITOR_DEFAULTTOPRIMARY ) ); if (FAILED(hr)) { return hr; } // Make sure to create textures. Otherwise, the surface cannot be // textured onto our primitive. Therefore, add the "texture" flag. lpAllocInfo->dwFlags |= VMR9AllocFlag_TextureSurface; DeleteSurfaces(); // Release any surfaces that we allocated previously. // Resize the array of surface pointers. m_surfaces.resize(*lpNumBuffers); // Ask the VMR-9 to allocate the surfaces for us. hr = m_lpIVMRSurfAllocNotify->AllocateSurfaceHelper( lpAllocInfo, lpNumBuffers, & m_surfaces.at(0) ); // If this call failed, create a private texture. We will copy the // decoded video frames into the private texture. if (FAILED(hr) && !(lpAllocInfo->dwFlags & VMR9AllocFlag_3DRenderTarget)) { DeleteSurfaces(); // Is the format YUV? if (lpAllocInfo->Format > '0000') { D3DDISPLAYMODE dm; hr = m_D3DDev->GetDisplayMode(NULL, & dm ); if (SUCCEEDED(hr)) { // Create the private texture. hr = m_D3DDev->CreateTexture( lpAllocInfo->dwWidth, lpAllocInfo->dwHeight, 1, D3DUSAGE_RENDERTARGET, dm.Format, D3DPOOL_DEFAULT, & m_privateTexture.p, NULL ); } if (FAILED(hr)) { return hr; } } lpAllocInfo->dwFlags &= ~ VMR9AllocFlag_TextureSurface; lpAllocInfo->dwFlags |= VMR9AllocFlag_OffscreenSurface; hr = m_lpIVMRSurfAllocNotify->AllocateSurfaceHelper( lpAllocInfo, lpNumBuffers, & m_surfaces.at(0) ); if (FAILED(hr)) { return hr; } } return m_scene.Init(m_D3DDev); }
将 VMR9AllocationInfo 结构中的信息转换为一系列 Direct3D 调用以分配正确的表面,这个过程很重要。还好,VMR-9 提供了一个 Helper 函数 IVMRSurfaceAllocatorNotify9::AllocateSurfaceHelper,该函数能够为您完成大部分工作。由于可用硬件的多样性,最好使用一个或多个后备算法,以确保至少会创建部分表面。如果表面的格式为 YUV 格式,该分配演示器则必须创建一个 RGB 表面,该表面将纹理化到 Direct3D 基元上。
演示图像
对于每个输入流,视频解码器都会将该视频帧写入 VMR-9 输入缓冲区中。VMR-9 会将 ProcAmp 值应用到每个流,合成这些流,应用颜色控制和 alpha 混合(如果需要的话),最后将合成好的图像传递到分配演示器。然后 VMR-9 会调用分配演示器对象的 GetSurface 方法,以便为解码器获得表面,并且会在应该演示图像的时候调用 PresentImage 方法。
演示图像的正确方式很大程度上取决于已分配表面的特征。因此最好将分配演示器实现为一个单独的对象,使其同时执行分配和演示任务。除了整合 3-D 场景这样的明显任务之外,您的应用程序还必须处理其他一些情况,例如,设备已丢失,或者窗口已经移到了另一个显示器。
为了将图像呈现到 3-D 场景,建议您执行下列步骤:
-
检查是否正在进行显示更改。如果设备已经发生了变化,则需要在新设备上重新创建这些表面。
-
如果设备不支持本地视频格式的纹理,并且您创建了一个私有纹理,则请执行下列操作:
-
通过调用 IDirect3DTexture9::GetSurfaceLevel 获取该表面。
-
通过调用 IDirect3DDevice9::StretchRect 将该表面从 VMR-9 拉伸到您的私有纹理,并进行颜色转换。
-
-
如果您没有创建私有纹理,请从该表面获取纹理。
-
绘制该场景,同时将视频纹理化到该场景的某个顶点集上。
-
演示图像。
-
如果设备已经丢失,则退出场景的呈现,恢复该设备,然后重新创建表面。
下面的代码显示了 CAllocator 类如何实现 PresentImage 方法:
HRESULT CAllocator::PresentImage( DWORD_PTR dwUserID, VMR9PresentationInfo *lpPresInfo) { CAutoLock Lock(&m_ObjectLock); // Check to see if we are in the middle of a display change. if ( NeedToHandleDisplayChange() ) { // Switch the Direct3D device. (Code not shown.) } HRESULT hr = PresentHelper( lpPresInfo ); // Code shown below. if ( hr == D3DERR_DEVICELOST) { // Try to restore the device. if (m_D3DDev->TestCooperativeLevel() == D3DERR_DEVICENOTRESET) { DeleteSurfaces(); hr = CreateDevice(); if (FAILED(hr)) { return hr; } hr = m_lpIVMRSurfAllocNotify->ChangeD3DDevice( m_D3DDev, MonitorFromWindow( m_window,MONITOR_DEFAULTTOPRIMARY ) ); if (FAILED(hr)) { return hr; } } hr = S_OK; } return hr; } HRESULT CAllocator::PresentHelper(VMR9PresentationInfo *lpPresInfo) { // Parameter validation. if ( ( lpPresInfo == NULL ) || ( lpPresInfo->lpSurf == NULL ) ) { return E_POINTER; } CAutoLock Lock(&m_ObjectLock); // Get the device from the surface. CComPtr <IDirect3DDevice9> device; HRESULT hr = lpPresInfo->lpSurf->GetDevice(& device.p ); if (FAILED(hr)) { return hr; } // If we created a private texture, blit the decoded image onto it. if ( m_privateTexture != NULL ) { CComPtr <IDirect3DSurface9> surface; hr = m_privateTexture->GetSurfaceLevel( 0 , & surface.p ); if (FAILED(hr)) { return hr; } // Copy the full surface onto the texture's surface. hr = device->StretchRect( lpPresInfo->lpSurf, NULL, surface, NULL, D3DTEXF_NONE ); if (FAILED(hr)) { return hr; } hr = m_scene.DrawScene( device, m_privateTexture ) ; if (FAILED(hr)) { return hr; } } else // The texture was allocated by the VMR-9. { // Get the texture from the surface. CComPtr <IDirect3DTexture9> texture; hr = lpPresInfo->lpSurf->GetContainer( IID_IDirect3DTexture9, (LPVOID*) & texture.p ) ); if (FAILED(hr)) { return hr; } hr = m_scene.DrawScene (device, texture ); if (FAILED(hr)) { return hr; } } hr = device->Present( NULL, NULL, NULL, NULL ); return hr; }
配置 VMR 和筛选器图形
VMR-9 支持三种不同的操作模式:
-
有窗口
在此默认模式中,VMR-9 会创建一个单独的窗口,并将视频呈现到该窗口上。
-
无窗口
此模式通过将视频直接呈现到应用程序窗口上(实际上是使得视频进入窗口上的控件中),为应用程序提供了更强的控制能力。
-
无呈现
此模式使用自定义分配演示器对象。
要针对 VMR-9 使用 CAllocator 类,您必须配置 VMR-9 以获得无呈现模式,通过执行下列步骤可以完成此任务:
-
创建 VMR-9 筛选器。
-
查询 VMR-9 是否具有 IVMRFilterConfig9 接口。
-
设置无呈现模式。
-
通知 VMR-9 筛选器有关自定义分配演示器对象的信息,还要通知自定义分配演示器对象关于该 VMR-9 筛选器的信息。
下面的代码显示了上面的步骤 1 到步骤 3:
CComPtr<IBaseFilter> g_filter; g_filter.CoCreateInstance(CLSID_VideoMixingRenderer9); CComQIPtr<IVMRFilterConfig9> filterConfig(g_filter); if (filterConfig) { filterConfig->SetRenderingMode( VMR9Mode_Renderless ) }
步骤 4 如下面的代码所示:
HRESULT SetAllocatorPresenter(IBaseFilter *filter, HWND window) { HRESULT hr; CComQIPtr <IVMRSurfaceAllocatorNotify9> lpIVMRSurfAllocNotify(filter); if (!lpIVMRSurfAllocNotify) { return E_FAIL; { // Create the allocator-presenter object. g_allocator.Attach(new CAllocator( hr, window )); if ( FAILED( hr ) ) { g_allocator = NULL; return hr; } // Notify the VMR-9. hr = lpIVMRSurfAllocNotify->AdviseSurfaceAllocator(g_userId, g_allocator ); if (FAILED(hr)) { return hr; } // Notify the allocator-presenter. hr = g_allocator->AdviseNotify(lpIVMRSurfAllocNotify); return hr; }
DirectShow 设计为支持各种格式。因此,DirectShow 图形构建机制非常灵活。应用程序可以通过逐个连接筛选器来构建图形,也可以使用 Intelligent Connect,并让 DirectShow 构建出尽可能最好的图形。以上面的代码为例,最好的方法是手动向图形添加 VMR-9 筛选器,而让筛选器图形管理器 (Filter Graph Manager) 自动呈现该图形的其他部分。这种方法与 DirectShow 支持的大多数格式都兼容。
按照前面的代码创建并配置了 VMR-9 之后,请调用 IFilterGraph::AddFilter 以便向图形添加该筛选器。然后调用 IGraphBuilder::RenderFile 以呈现该文件:
hr = g_graph->AddFilter(g_filter, L"Video Mixing Renderer 9"); if (SUCCEEEDED(hr)) { hr = g_graph->RenderFile( wszPath, NULL ) }
RenderFile 方法支持图形中已有的输出程序。只有图形中的输出程序无法呈现该文件时,它才会添加新的输出程序。有关本例中的图形构建的详细过程,请参阅 VMR9Allocator 示例中的代码。
获得更多信息
要了解有关 Microsoft DirectShow 的更多信息,请参阅 DirectShow SDK 文档。