Windows桌面采集技术
在进入具体的方式讨论前,我们先看看 Windows 桌面图形界面的简化架构,如下图:
在 Windows Vista 之前,Windows 界面的复合画面经由 Graphics Device Interface(以下简称 GDI)技术直接渲染到桌面上。
在 Windows Vista 以及之后的版本,Desktop Composition 的工作就交由一个新的模块Desktop Window Manager(以下简称 DWM) 来完成了。
如上图所示,应用程序画完自己的界面后,提交给 DWM 把它合成到桌面上,而 DWM 经过一系列演进后,为了提效,基于微软自己的 Direct3D(以下简称 D3D) 实现了整套技术,在 D3D 这层的下面是 Windows Display Driver Model(以下简称 WDDM,Windows 图形驱动程序模型)。
所以在 Windows 下实现录屏采集,基本上可以从最基本的 GDI 技术和 D3D 技术两方面考虑。
GDI:第一代桌面采集
Windows 图形设备接口(GDI)是为与设备无关的图形设计的。基于 Windows 的应用程序不能直接访问图形硬件,应用程序通过 GDI 来与设备驱动程序进行交互。GDI 截图就是通过屏幕的DC获取到当前屏幕的位图数据。
调用过程
GetDC(GetDesktokWindow() )获取桌面的DC
然后使用CreateDIBSection创建一个设备无关位图以及内存DC
使用BitBlt把桌面DC的复制到内存DC,这样通过内存DC就能直接获取到原始RGB数据。
基本采样流程
在 Windows 平台上有过图形开发经验的开发者,应该都知道 BitBlt 这个 API, 它为我们实现了 Windows DC 间的内容拷贝,假如将 Source DC 指定为 Window DC 或是 Destop DC,这就实现了对屏幕指定源的画面截取。下图说明了基于 GDI 技术录屏采集的大致调用过程:
以整个桌面作为采集源举例,通用做法是调用 GetDC(GetDesktokWindow()) 获取桌面 DC,通过 CreateDIBSection 创建一个设备无关的位图对象以及内存 DC,最后调用 BitBlt 把桌面 DC 的原始数据翻转到内存 DC 上,这样从内存 DC 上就能直接获取到桌面的原始 RGB 数据。
需要注意的是,创建一个设备无关的位图时,CreateDIBSection 的第4个参数 ppvBits 是提前分配好的位图数据缓冲区。而在以实时视频流的方式共享屏幕的场景下,需要以每秒十几次甚至几十次的频率进行采样,从效率的要求考虑,这里自然不可能每次都重新分配缓冲区,所以可以根据源的分辨率,在采集前就分配一个足够大的空间。
CreateDIBSection的函数原型如下图所示:
HBITMAP CreateDIBSection(
HDC hdc,
const BITMAPINFO *pbmi,
UINT usage,
VOID **ppvBits,
HANDLE hSection,
DWORD offset
);
经过以上基本采样流程,得到的画面内容是不含鼠标的,如果需要将鼠标还原到画面中
CURSORINFO ci = { 0 };
ci.cbSize = sizeof(ci);
ZeroMemory(&ci, sizeof(CURSORINFO));
ci.cbSize = sizeof(CURSORINFO);
if (::GetCursorInfo(&ci))
{
POINT ptCursorPos = { 0, 0 };
ptCursorPos = ci.ptScreenPos;
::DrawIconEx(m_hCompDC,
ptCursorPos.x , ptCursorPos.y,
ci.hCursor,
0, 0, 0, 0,
DI_NORMAL | DI_DEFAULTSIZE | DI_COMPAT);
}
优缺点
优点:GDI函数实现的通用做法,能在所有windows平台实现
缺点:通用归通用,截取的效率则是有点低,尤其是要达到每秒20帧以上的截取,占用CPU有点高,GDI不能获取鼠标,需要在截取的图像中把鼠标画上去。
由于整体的运算、拷贝过程都在 CPU 中完成,导致采样效率偏低,尤其在高频采样(> 20fps)时,对 CPU资源的消耗过高,而且后续还要处理为实时视频流,经过同样高频的编码、网络包发送,多方面因素叠加,自然对机器性能有更高的要求。 GDI 则可以作为一种保底方案。
在具体实现中,还有两点需要特别提醒:
1)在 Windows XP 下,可以通过 BitBlt 函数最后的参数,来控制是否拷贝 Layered Window。只有 SRCCPY 标识,表示拷贝内容不包含 Layered Window;如果是SRCCPY | CAPTUREBLT,则表示拷贝包括 Layered Window 在内的所有窗口。而这个标识,在 Windows Vista 之后的系统版本开启 DWM 的情况下,已经无效,因为这种情况下所有的窗口都是 Layered Window;
2)在 Windows Vista 之后的系统版本开启 DWM 的情况下,单次抓取速度变得非常慢(作者机器实测 30ms +)
且无法采集到使用gpu渲染的窗口,显示黑屏:
DXGI:高性能桌面采集技术
DXGI(Microsoft DirectX Graphics Infrastructure)是微软提供的一种可以在win8及以上系统使用的图形设备接口。它负责枚举图形适配器、枚举显示模式、选择缓冲区格式、在进程之间(例如,在应用程序和桌面窗口管理器(DWM)之间)共享资源,以及将呈现的帧传给窗口或监视器以供显示。其直接和硬件设备进行交互,具有很高的效率和性能。
基本采样流程
除了用 GDI 技术实现录屏,实际上在 Windows 平台上,微软提供了多种录屏方案,相对 GDI 技术来说,其大多数接口的处理性能并不理想,或存在诸多限制,通用性不足。
从 Windows 8 开始,微软引入了一套新技术叫 Desktop Duplication API,应用程序可以通过这套 API 请求桌面的图形数据。由于 Desktop Duplication API 是通过 DirectX Graphics Infrastructure(以下简称 DXGI)来提供桌面图像的,竞争的是 GPU 流水线资源,所以 CPU 占用率很低,采集性能非常高。
由于这套能力整合在 DirextX 中提供,所以与大部分 DirectX 接口的使用方式基本一致,其流程概括如下图。
如图所示,使用 DXGI 需要一些简单的 DirectX 基础知识,通过各种 DirectX COM 接口的查询,最终获取 IDXGIOutputDuplication 接口指针,截屏时使用其中核心的AcquireNextFrame API 获取当前桌面图像,此外,它还提供 GetFrameDirtyRects 等 API,可以获取经过 GPU 计算后发生了变化的脏矩形区域。
绘制鼠标
和 GDI 面临相同的问题,直接通过 AcquireNextFrame API 获取到的画面中,也是不含鼠标图像的。想要将鼠标绘制到画面中,我们需要和GDI相关的API配合使用。
在采集之前创建数据缓冲区时,将缓冲区关联到设备无关的位图上,并将位图选入临时的内存DC(一般由桌面DC生成的临时内存DC),再将通过 AcquireNextFrame获取的画面拷贝到缓冲区后,这时可以使用GDI绘制鼠标的方法,将鼠标绘制到位图上,这样数据缓冲区中的图像数据就包含了鼠标了。
优劣势分析
在 Windows 平台上,从现有的录屏采集方式(包括GDI采集和放大镜采集)来看,DXGI 是性能最好的。
其劣势是只在 Windows 8 系统版本及以上才支持,所以在整体方案中,一般要与 GDI 共同组合提供。此外,它无法指定某个程序窗口进行采集
Ø 特点:Win Vista 以后支持,使用GPU直接处理纹理,效率最高;
Ø 缺点:根据Direct3D 版本不同,存在硬件的支持以及调用特性 的区别,且因为采集需要获取设备的adapter,所以无法采集桌面窗口。、
如果用户机器有A卡和N卡,出现了跨卡问题导致采集失效,禁用AMD显卡后问题解决
Magnification:弯道超车的采集技术
Magnification API 使用于放大屏幕某个区域的 辅助应用技术,初衷是用于协助视力存在问题或者色弱的用户能跟方便的看到桌面内容的api
Ø 特点:能实现放大缩小颜色转换等操作,能过滤窗口
基本采样流程
前面所述的两种方式都可以实现录屏采集,也是最常用的两种方式。
但有时,我们要指定源来采集,并且希望采集到的画面不被其他内容干扰。
在 Windows XP 时代,用 GDI 的 BitBlt API 进行采集时,指定一个 Window DC,并且对最后一个参数去掉 CAPTUREBLT 标识,即可排除掉其他 Layered Window 的干扰。但如今 Windows XP 已成过去式,这项措施无法解决现有系统的过滤问题,必须找到另一替代方案。
从 Windows Vista 开始,微软新引入了一个新的 Magnification API(放大镜效果),当我们将放大倍率设置成1(默认倍率就是1)亦可以用它来截取屏幕图像。MSDN上提供了该库的完整文档。根据文档,可以通过以下步骤简单地完成录屏采集过程:
在初始化相关模块后,首先创建放大镜控件的主窗口,并且将其设置为全屏不可见,因为我们要使用它来捕获图像,它只是一个工具,所以不能也不需要在用户侧显示它。因此,设置窗口扩展属性 WS_EX_LAYERED,调用 SetLayeredWindowAttributes 设置全透明。
::SetLayeredWindowAttributes(hwnd, 0 ,255, LWA_ALPHA);
接着创建放大镜窗口作为主窗口的子窗口,窗口类名必须为“Magnifier”。如果要捕获鼠标光标,还要设置窗口属性为 MS_SHOWMAGNIFIEDCURSOR。
hwndMag = ::CreateWindow(WC_MAGNIFIER, TEXT("Magnifier"),
WS_CHILD /*| MS_SHOWMAGNIFIEDCURSOR */| WS_VISIBLE,
0, 0, m_ScreenX, m_ScreenY,
hostDlg->GetSafeHwnd(), NULL, hInstance, NULL );
最关键的部分,利用 MagSetWindowFilterList 这个神奇的 API,它能够指定一些窗口,在我们截取指定源目标时,从采集到的图像中将 FilterList 中的窗口过滤掉,好像这些窗口根本没有显示一样。这就是我们使用这放大镜方案的主要原因。
那么如何获得录屏图像呢?每当我们调用 MagSetWindowSource 时,都会触发回调MagSetImageScalingCallback数据回调。原型如下:
BOOL MagImageScaling(HWND hwnd, void *srcdata, MAGIMAGEHEADER srcheader, void *destdata, MAGIMAGEHEADER destheader, RECT unclipped, RECT clipped, HRGN dirty)
其中第二个参数 srcdata 就是指向录屏结果图像的原始数据,srcheader 则包含数据的长度,以及图像长宽等元信息,至此,我们可以使用这两个参数来构建位图了。
Magnification
Window Graphics Capturer:新世代采集技术
WGC 全称为 Windows Graphics Capture 是微软目前主推的一个桌面/窗口采集技术,使用 D3D11 库实现。该采集技术最早在Windows 10 18年3月份的更新中提供。WGC 对比放大镜采集(Magnification Capture) 具有更高的性能、更低CPU及GPU消耗。但是在 使用方面比起其他采集方式会更复杂。
- 系统版本不低于10.0.17134.0 (Windows 10, version 1803)
- 相关接口均基于微软的新一代运行时库接口C++/WinRT,而且是最低要求 C++17
鼠标支持,自Windows 10, version 2004 (introduced in 10.0.19041.0)才开始支持捕获鼠标。
GraphicsCaptureSession.IsCursorCaptureEnabled Property
黄色边框去除,自Windows 10, version 2104 (introduced in 10.0.20348.0)才开始去除采集目标的黄色边框。
GraphicsCaptureSession.IsBorderRequired
FFMPEG录屏(15)---- WGC 捕获桌面(三) WGC(Windows Graphics Capture)采集_ffmpeg采集桌面-CSDN博客
Ø 特点:效率高,拓展屏采集支持高,1080p采集消耗gpu达到个位数;
Ø 缺点:
1. 当Capture Session开始采集后,在刚开始采集的时候可能存在 HRESULT 为S_OK ,但是画面数据为空,因为这个时候采集Engine可能处于启动中状态;
2. 当开始采集是,Windows会在采集源(窗口或桌面)区域增加一个黄色边框去标识正在采集的区域,目前无法设置该边框的样式或者去除该边框;
3. WGC 使用SetWindowDisplayAffinity 实现窗口过滤,但是设置的窗口必须为当前进程创建的子窗口才能设置成功,否则无法实现过滤。