DirectShow 滤镜开发第一部分:使用 Direct2D 进行视频渲染






4.85/5 (43投票s)
本文主要介绍 DirectShow 开发,特别是滤镜开发。
引言
本文主要介绍 DirectShow 开发,特别是滤镜开发。我决定分享这方面的知识和经验,因此将为希望编写自己的滤镜但找不到足够参考资料的开发人员提供一个简单的教程。我假设您对 DirectShow 图形管理和 C++/COM 编程有基本了解。所有源代码示例均使用 Windows 7 上的 Visual Studio 2010,您必须安装 Windows 7 的 Windows SDK 和 .NET Framework 4。
API 的强大之处不仅体现在其功能上,还体现在其可扩展性模型上。谈到可扩展性,DirectShow 确实表现出色:您可以通过构建其称为滤镜的基本结构块来扩展该框架。每个滤镜实际上是一个 C++ 对象,带有一个或多个称为引脚的嵌套对象,这些引脚负责与其他滤镜的连接以及它们之间的数据传输。
大多数情况下,您会使用现有滤镜,因为您的操作系统上已经安装了大量滤镜,并且您可以从这里下载许多免费滤镜。然而,有时您需要做一些不寻常的事情,当在预算有限的初创公司工作时,不可能购买您需要的东西。所以有一天我发现自己正在为滤镜开发而苦恼……
DirectShow 滤镜有三种类型
- 源滤镜 – 负责生成媒体样本并将其推送到图中的下游滤镜。源滤镜本身分为三组
- 文件源滤镜 – 负责解析媒体文件、读取媒体样本,并将其推送到处理视频、音频、文本或数据的相应输出引脚的滤镜。
- 捕获源滤镜 – 通常绑定到网络摄像头或视频采集卡等外部设备,并负责以恒定速率生成媒体样本并将其推送到输出引脚的滤镜。
- 实时源滤镜 - 以未指定的速度从网络流或外部函数调用获取视频样本并将其推送到下游的滤镜。
- 转换滤镜 – 可能是绝大多数 DirectShow 滤镜,负责媒体流的编码/解码、多路复用/解复用、解析和拆分。转换滤镜有两种
- 原地转换 – 对媒体样本执行某些操作并将其传递到输出引脚而无需任何缓冲区复制的滤镜。
- 接收媒体样本、执行某些操作,并将其输出保存到另一个媒体样本中并推送到下游的转换滤镜。
- 渲染器滤镜 – 作为媒体样本“最终站点”的滤镜,负责将样本发送到网络、保存到文件或在屏幕上显示。
渲染器滤镜
渲染器滤镜最容易实现,只需继承 DirectShow 基类并覆盖一些方法调用,因此我决定从渲染滤镜开始本系列文章。
滤镜开发先决条件
安装 Windows SDK 后,您必须在 Debug 和 Release 配置中构建位于 C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses 的 baseclasses 解决方案。成功构建后,您将在 Debug 文件夹中获得 strmbasd.lib 库,在 Release 文件夹中获得 strmbase.lib 库。
如下所示创建一个新的 Visual C++ -> Win32 项目,然后按 OK。
之后,选择 DLL 应用程序类型并勾选 Empty project 复选框,这样向导就不会为我们创建任何主函数。
创建项目后,右键单击它并选择属性。现在您必须将头文件的包含路径设置为 C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses,并将 *.lib 文件的链接器路径设置为 C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses\Debug。
您的项目将静态链接以下库:strmbasd.lib、winmm.lib、d2d1.lib。
现在配置已完成,您需要创建实际的渲染器类并从 CBaseVideoRenderer
继承它
CBaseVideoRenderer
类为您实现了大部分滤镜功能。您所需要做的就是实现两个纯虚方法
CheckMediaType
– 负责在连接阶段引脚之间的媒体类型协商。DoRenderSample
– 每次应渲染媒体样本时调用的实际渲染方法。
此外,我还重写了两个虚方法
SetMediaType
– 当引脚就特定媒体达成一致且连接完成时调用。StartStreaming
- 当图表开始媒体流式传输时调用。
除了上述方法,我还添加了一个设置视频窗口句柄的方法,该句柄将用于视频呈现,以及一个用于黑边填充的 setter/getter:保持宽高比或拉伸到显示窗口。这些方法在 IVideoRenderer
接口中声明,以及 GUID 和其他声明。
// {269BA141-1FDE-494B-9024-453A17838B9F}
static const GUID CLSID_Direct2DVideoRenderer =
{ 0x269ba141, 0x1fde, 0x494b, { 0x90, 0x24, 0x45, 0x3a, 0x17, 0x83, 0x8b, 0x9f } };
// {34E5B77C-CCBA-4EC0-88B5-BABF6CF3A1D2}
static const GUID IID_IVideoRenderer =
{ 0x34e5b77c, 0xccba, 0x4ec0, { 0x88, 0xb5, 0xba, 0xbf, 0x6c, 0xf3, 0xa1, 0xd2 } };
#define FILTER_NAME L"Direct2D Video Renderer"
enum DisplayMode
{
KeepAspectRatio = 0,
Fill = 1
};
DECLARE_INTERFACE_(IVideoRenderer, IUnknown)
{
STDMETHOD(SetVideoWindow)(HWND hWnd) PURE;
STDMETHOD_(void, SetDisplayMode)(DisplayMode) PURE;
STDMETHOD_(DisplayMode, GetDisplayMode)(void) PURE;
};
class CD2DVideoRender : public CBaseVideoRenderer, public IVideoRenderer
{
public:
DECLARE_IUNKNOWN;
CD2DVideoRender(LPUNKNOWN pUnk, HRESULT* phr);
virtual ~CD2DVideoRender(void);
virtual HRESULT DoRenderSample(IMediaSample *pMediaSample);
virtual HRESULT CheckMediaType(const CMediaType *pmt);
virtual HRESULT SetMediaType(const CMediaType *pmt);
virtual HRESULT StartStreaming();
static CUnknown * WINAPI CreateInstance(LPUNKNOWN lpunk, HRESULT *phr);
STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void **ppv);
STDMETHODIMP SetVideoWindow(HWND hWnd);
STDMETHOD_(void, SetDisplayMode)(DisplayMode);
STDMETHOD_(DisplayMode, GetDisplayMode)(void);
void CreateDefaultWindow();
private:
HWND m_hWnd;
CD2DRenderer* m_renderer;
HANDLE m_event;
BITMAPINFOHEADER m_bmpInfo;
CMediaType m_mediaType;
CColorSpaceConverter* m_converter;
};
使用 Direct2D 进行视频渲染
Windows 提供了各种适用于实时视频渲染的 API:Video For Windows、GDI/GDI+、Direct3D、DXVA——都是不错的选择;然而,我想研究一个相对较新且很有前途的 API,称为 Direct2D。Direct2D 是一个全新的 API,专为硬件加速 2D 图形设计。由于它在某种程度上是旧 GDI/GDI+ 的替代品,我们也可以用它进行图像渲染。Direct2D 是一个基于 Direct3D10 的轻量级 COM API;然而,它比 Direct3D API 简单得多。
创建 Direct2D 应用程序的步骤
- 创建一个工厂对象
- 创建一个渲染目标(这里我使用的是
HWND
渲染目标) - 创建一个 Direct2D 位图对象。
步骤 1 在渲染器类构造函数中执行,因为它应该只调用一次。步骤 2 和 3 应该在滤镜就连接类型和媒体类型达成一致并已知帧边界时执行。当样本准备好呈现时,其数据缓冲区被复制到 Direct2D 位图并在屏幕上显示。
Direct2D HWND 渲染目标
渲染目标是进行渲染的表面。它是一种设备特定资源,可能会在某些更改(如分辨率更改)时丢失。因此需要在程序运行时重新创建。由于 HWND
渲染目标绑定到某个窗口,其初始大小将与窗口大小相同;然而,在程序运行时,窗口的大小可能会改变,因此渲染目标也需要调整大小。由于我们正在处理视频渲染,我决定在 DrawSample
方法中检查窗口大小,该方法针对视频序列中的每一帧执行。下面显示的方法创建渲染目标,设置变换矩阵,并创建将用于显示的位图
HRESULT CD2DRenderer::CreateResources()
{
D2D1_PIXEL_FORMAT pixelFormat =
{
DXGI_FORMAT_B8G8R8A8_UNORM,
D2D1_ALPHA_MODE_IGNORE
};
D2D1_RENDER_TARGET_PROPERTIES renderTargetProps =
{
D2D1_RENDER_TARGET_TYPE_DEFAULT,
pixelFormat,
0,
0,
D2D1_RENDER_TARGET_USAGE_NONE,
D2D1_FEATURE_LEVEL_DEFAULT
};
RECT rect;
::GetClientRect(m_hWnd, &rect);
D2D1_SIZE_U windowSize =
{
rect.right - rect.left,
rect.bottom - rect.top
};
D2D1_HWND_RENDER_TARGET_PROPERTIES hWndRenderTargetProps =
{
m_hWnd,
windowSize,
D2D1_PRESENT_OPTIONS_IMMEDIATELY
};
HR(m_d2dFactory->CreateHwndRenderTarget(renderTargetProps,
hWndRenderTargetProps, &m_hWndTarget));
// (0,0) + --------> X
// |
// |
// |
// Y
if(m_bFlipHorizontally)
{
// Flip the image around the X axis
D2D1::Matrix3x2F scale = D2D1::Matrix3x2F(1, 0,
0, -1,
0, 0);
// Move it back into place
D2D1::Matrix3x2F translate =
D2D1::Matrix3x2F::Translation(0, windowSize.height);
m_hWndTarget->SetTransform(scale * translate);
}
FLOAT dpiX, dpiY;
m_d2dFactory->GetDesktopDpi(&dpiX, &dpiY);
D2D1_BITMAP_PROPERTIES bitmapProps =
{
pixelFormat,
dpiX,
dpiY
};
D2D1_SIZE_U bitmapSize =
{
m_pBitmapInfo.biWidth,
m_pBitmapInfo.biHeight
};
return m_hWndTarget->CreateBitmap(bitmapSize, bitmapProps, &m_bitmap);
}
一些图像格式是自底向上的图像,因此我们需要水平翻转它们。我们可以在代码中手动翻转,也可以通过在 X 轴上翻转图像并将其平移回原位来改变变换矩阵。渲染目标上的所有后续操作都将受到变换矩阵的影响。
之后,我们创建一个具有与渲染目标相同像素格式,但具有实际视频帧大小的 Direct2D 位图对象。由于 Direct2D 是 GPU 加速的,为了获得最佳性能,最好渲染 RGB32 位图,因此渲染目标和位图对象都必须创建为 RGB32 或 ARGB 像素格式。
呈现帧
设置完所有必要的资源后,视频流开始,并且 DrawSample
方法根据视频帧率每秒执行 25 或 30 次。
HRESULT CD2DRenderer::DrawSample(const BYTE* pRgb32Buffer)
{
CheckPointer(pRgb32Buffer, E_POINTER);
if(!m_bitmap || !m_hWndTarget)
{
HR(CreateResources());
}
HR(m_bitmap->CopyFromMemory(NULL, pRgb32Buffer, m_pitch));
if (!(m_hWndTarget->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED))
{
RECT rect;
::GetClientRect(m_hWnd, &rect);
D2D1_SIZE_U newSize = { rect.right, rect.bottom };
D2D1_SIZE_U size = m_hWndTarget->GetPixelSize();
if(newSize.height != size.height || newSize.width != size.width)
{
m_hWndTarget->Resize(newSize);
}
D2D1_RECT_F rectangle = D2D1::RectF(0, 0, newSize.width, newSize.height);
if(m_displayMode == KeepAspectRatio)
{
ApplyLetterBoxing(rectangle, m_bitmap->GetSize());
}
m_hWndTarget->BeginDraw();
m_hWndTarget->Clear(D2D1::ColorF(D2D1::ColorF::Black));
m_hWndTarget->DrawBitmap(m_bitmap, rectangle);
HRESULT hr = m_hWndTarget->EndDraw();
if(hr == D2DERR_RECREATE_TARGET)
{
DiscardResources();
}
}
return S_OK;
}
首先,我需要确保渲染目标和位图有效,因为它们可能在上次调用时被丢弃。然后我将像素数据复制到位图并检查窗口是否未被遮挡。然后,我检查窗口大小是否改变,并根据需要改变渲染目标大小并应用信箱(下面讨论)。正如我之前提到的,Direct2D 比它的大哥 Direct3D 简单得多,因此没有交换链、后备缓冲区和其他繁重的东西:)。您所要做的就是调用 BeginDraw
,清除渲染目标,呈现位图,然后调用 EndDraw
以检查渲染目标是否需要重新创建。
视频信箱
视频帧具有通过将帧宽度除以其高度计算的宽高比。常见的宽高比是 4:3 和 16:9(宽屏)。由于显示窗口可能会调整大小,视频帧可能会失去其宽高比并被拉伸或缩小。为了保持原始宽高比,我编写了一个名为 ApplyLetterBoxing
的简单函数,该函数计算每个视频帧的目标矩形
static inline void ApplyLetterBoxing(D2D1_RECT_F& rendertTargetArea, D2D1_SIZE_F& frameArea)
{
const float aspectRatio = frameArea.width / frameArea.height;
const float targetW = fabs(rendertTargetArea.right - rendertTargetArea.left);
const float targetH = fabs(rendertTargetArea.bottom - rendertTargetArea.top);
float tempH = targetW / aspectRatio;
if(tempH <= targetH)
// desired frame height is smaller than display
// height so fill black on top and bottom of display
{
float deltaH = fabs(tempH - targetH) / 2;
rendertTargetArea.top += deltaH;
rendertTargetArea.bottom -= deltaH;
}
else
//desired frame height is bigger than display
// height so fill black on left and right of display
{
float tempW = targetH * aspectRatio;
float deltaW = fabs(tempW - targetW) / 2;
rendertTargetArea.left += deltaW;
rendertTargetArea.right -= deltaW;
}
}
因此,如果启用信箱(默认情况下是启用的),视频的宽高比将始终保持不变,显示窗口的其余区域将是黑色
色彩空间转换
大多数视频解码器以 YUV420 像素格式之一输出视频帧。它是一种平面格式,具有亚采样的色度值。有关此主题的更多信息,请访问 FOURCC 网站。此代码示例中的滤镜支持大多数常见的 DirectShow 像素格式,包括平面和打包格式
- YV12 – 12 位每像素平面格式,Y 平面后跟 V 和 U 平面
- I420(IYUV) – 与 YV12 相同,但 V 和 U 互换
- NV12 – 12 位每像素平面格式,Y 平面和交错的 UV 平面
- YUY2 – 16 位每像素打包 YUYV 数组
- RGB555 – 16 位每像素,1 位未使用,每个 RGB 通道 5 位
- RGB565 – 16 位每像素,红色 5 位,绿色 6 位,蓝色 5 位
- RGB24 – 24 位每像素,每个 RGB 通道 8 位
- RGB32 – 32 位每像素,Alpha 8 位,每个 RGB 通道 8 位
所有这些格式,除了最后一种,都需要转换才能由 Direct2D 渲染,因为目前它不支持 YUV 图像。由于这是一篇入门级文章,我为此目的使用了简单的 C 方法——不幸的是,它们速度慢且占用 CPU,因此在实际应用程序中,您应该考虑使用更合适的 API 和工具,例如 IPP 或 FFMPEG 库中的 swscale。
滤镜注册
由于每个 DirectShow 滤镜都是一个 COM 容器,因此它应该在系统注册表中注册,以便 CoCreateInstance
方法调用将通过其 GUID 找到它,以定位注册路径处的 *.ax 文件,将其加载到进程内存空间中,并创建滤镜类的实例。这些注册代码对于所有滤镜类型都是重复的,并且取自 DirectShow SDK 示例。Setup.cpp 包含注册代码
#include <olectl.h>
#include <initguid.h>
#include "D2DVideoRender.h"
#pragma warning(disable:4710)
const AMOVIESETUP_MEDIATYPE sudOpPinTypes =
{
&MEDIATYPE_Video, // Major type
&MEDIASUBTYPE_NULL // Minor type
};
const AMOVIESETUP_PIN sudOpPin =
{
L"Input", // Pin string name
TRUE, // Is it rendered
FALSE, // Is it an output
FALSE, // Can we have none
FALSE, // Can we have many
&CLSID_NULL, // Connects to filter
NULL, // Connects to pin
1, // Number of types
&sudOpPinTypes }; // Pin details
const AMOVIESETUP_FILTER sudBallax =
{
&CLSID_Direct2DVideoRenderer, // Filter CLSID
FILTER_NAME, // String name
MERIT_DO_NOT_USE, // Filter merit
1, // Number pins
&sudOpPin // Pin details
};
CFactoryTemplate g_Templates[] =
{
{
FILTER_NAME,
&CLSID_Direct2DVideoRenderer,
CD2DVideoRender::CreateInstance,
NULL,
&sudBallax
}
};
int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);
STDAPI DllRegisterServer()
{
return AMovieDllRegisterServer2(TRUE);
}
STDAPI DllUnregisterServer()
{
return AMovieDllRegisterServer2(FALSE);
}
滤镜调试
成功注册滤镜后,您可以使用位于 C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\GraphEdt.exe 的 GraphEdit 进行调试。您可以通过将此路径添加到项目属性的调试部分来从 Visual Studio 启动它
这样,当您的 DLL 项目是启动项目并且您按下 F5 时,GraphEdit 将启动,您可以添加滤镜并在必要时进行调试。
使用代码
首先,您必须通过调用带有 ax 文件的完整路径的 regsvr32 实用程序来注册滤镜。之后,您可以尝试在 GraphEdit 中使用滤镜。在 GraphEdit 中使用时,未设置视频句柄,因此将有一个默认窗口呈现视频。它将具有与视频帧相同的尺寸,并在屏幕中心创建。
要在您自己的代码中使用滤镜,请执行以下操作
- 在您的项目中包含 IVideoRenderer.h。
- 创建一个滤镜图管理器。
- 创建一个 Direct2D 视频渲染器滤镜并将其添加到图中。
- 添加一些源文件或其他源滤镜并渲染它——它将使用图中已存在的渲染器。
- 设置视频窗口句柄。
- 运行图。
注意:为了清晰起见,省略了错误处理。
CComPtr<IGraphBuilder> m_graph;
CComPtr<IFilterGraph2> m_filterGraph2;
CComPtr<IMediaControl> m_mediaCtrl;
CComPtr<IBaseFilter> m_renderFilter;
CComPtr<IVideoRenderer> m_render;
CComPtr<IQualProp> m_quality;
CoInitialize(NULL);
m_graph.CoCreateInstance(CLSID_FilterGraph);
m_graph->QueryInterface(IID_IFilterGraph2, (void**)&m_filterGraph2);
m_graph->QueryInterface(IID_IMediaControl, (void**)&m_mediaCtrl);
m_renderFilter.CoCreateInstance(CLSID_Direct2DVideoRenderer);
m_renderFilter->QueryInterface(IID_IVideoRenderer, (void**)&m_render);
m_renderFilter->QueryInterface(IID_IQualProp, (void**)&m_quality);
m_render->SetVideoWindow(m_hWnd);
m_filterGraph2->AddFilter(m_renderFilter, FILTER_NAME);
m_filterGraph2->RenderFile(fileName, NULL);
m_mediaCtrl->Run();
希望本文对您有所帮助。欢迎任何建议和意见。