Windows直播软件开发——DXGI

6,350 阅读9分钟

简单使用:

  对于Desktop Duplication API,MSDN中并没有给出详细的一些教学步骤。对于新手来说,看了还是不懂如何获取一张桌面图片。不过,这里先给出对应的官方文档介绍,说不定有你感兴趣的部分:

Desktop Duplication API:https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api

除此之外,官方还给出一个例子。但是这个例子对新手来说非常不友好,非常非常复杂。里面涉及到了多线程间的同步,桌面采集,脏矩形处理,D3D绘制等等,而且代码是以面向对象的形式去设计的,用几个类对功能进行了封装,想找到关于桌面采集部分的核心代码比较困难。不过这里还是将例子的链接给出,毕竟它是官方的例子:

https://github.com/microsoft/Windows-classic-samples/tree/master/Samples/DXGIDesktopDuplication

  其实采集一张图片没有想象中的那么困难,我把最简要的步骤介绍一下。

DXGI与D3D的关系

  在开始写代码之前,他们两关系是必须要介绍的。DXGI全称是Microsoft DirectX Graphics Infrastructure,即微软图形设备基础架构,它的工作,就是为图形库提供底层的硬件设备的接口支持,如枚举显卡,设置交换链,设置前后缓冲等等。不止D3D,像gdi,opengl,d2d(这货是d3d的封装)等都要去访问它,以获得底层硬件访问的支持。正是由于它的存在,也使得不同图形库之间能进行交互,因为在DXGI中,资源的接口是IDXGIResource,它可以转换成上层的图形库的接口,如D3D的纹理。了解了这个之后,我们才能理解后面的内容。它的框架图如下:

初始化与获取图像

获取相应的DXGI设备

  在Windows中,像D3D,DXGI这样的架构,都是基于COM组件的,因此我们需要使用COM的API来获取。这里我先获得D3D的设备,然后再通过它获得DXGI的设备。当然,也不止这一种办法,官方文档里说还可以直接去访问DXGI设备(但没具体说要怎么做)。
  首先,我们要获取的是DXGI设备,它代表底层的图形设备。获取代码如下:

ID3D11Device* p_d3dDevice = NULL;
IDXGIDevice* p_dxgiDevice = NULL;
D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, NULL, NULL, D3D11_SDK_VERSION, &p_d3dDevice, NULL, &p_d3dDeviceContext);
p_d3dDevice->QueryInterface(IID_PPV_ARGS(&p_dxgiDevice);

D3D11CreateDevice可以帮我们获得一个D3D11设备对象,由于COM组件技术,我们可以通过这个设备对象来访问到DXGI设备对象。

获取DXGI Adapter

  Adapter可以理解为我们显卡的抽象层。当我们创建D3D设备时,其实参数中已经指定好了哪块显卡了,我填的NULL,而且指定是硬件类型的,所以它会为我们指定一块默认的(当然,我就一块显卡)。除此之外,还可以通过获取IDXGIFactory对象,然后调用EnumAdapters方法来获取某个Adapter。代码如下:

IDXGIAdapter* p_dxgiAdapter = NULL;
p_dxgiDevice->GetParent(IID_PPV_ARGS(&p_dxgiAdapter);

获取DXGI Output

  指定好显卡之后,我们就要指定它的输出了。我们知道,有时候我们可能不止使用一块显示器,显然,显卡是支持多显示设备进行输出的,因此我们要指定好哪一个输出设备,我们需要调用IDXGIAdapter里的EnumOutputs方法,代码如下:

IDXGIOutput* p_dxgiOutput = NULL;
p_dxgiAdapter->EnumOutputs(0, &p_dxgiOutput);

我就一块显示器,第一个参数直接填0就完事了。

获取DXGI Output1

  这时看到这个标题的读者可能又要问了,output1是什么鬼,刚才不是已经获取了output了么。当然,output1也并不是代表第一个显示器,它可以说是output的扩展,它是在DXGI 1.2之后推出的,里面包含了我们Desktop Duplication API接口。可能微软为了与之前的接口兼容,让我们先获取output,然后再通过output来获取到output1。获取DXGI output1的代码如下:

IDXGIOutput1* p_dxgiOutput1 = NULL;
p_dxgiOutput->QueryInterface(IID_PPV_ARGS(&p_dxgiOutput1);

获取Output Duplication

  在我们刚才获取到的output1中,就可以调用DuplicateOutput方法来获取Output Duplication,然后我们就可以通过它来采集桌面图像了,代码如下:

IDXGIOutputDuplication* p_dxgiOutputDup = NULL;
p_dxgiOutput1->DuplicateOutput(p_d3dDevice, &this->p_dxgiOutputDup);

获取Output Desc

  在采集桌面前,我们需要获得显示设备相关的信息,这样才方便我们后续去做编码和渲染等工作,如显示器的宽高,刷新率,像素格式,扫描方式,缩放格式等等。调用GetDesc方法,我们可以获得与这些信息有关的一个结构体对象,便可以通过它来读取,代码如下:

DXGI_OUTDUPL_DESC m_dxgiOutDupDesc;
p_dxgiOutputDup->GetDesc(&this->m_dxgiOutDupDesc);

获取桌面图像

  到了这步,我们就可以获取桌面图像了,直接调用AcquireNextFrame即可,代码如下:

IDXGIResource* p_DesktopResource = NULL;
p_dxgiOutputDup->AcquireNextFrame(0, &frame_info, &p_DesktopResource)

IDXGIResource便是我们的图像了。我们并不能直接访问里面的数据,这些数据是存在于显存中的。对于英伟达的硬编码,肯定是在显卡中的,所以理论上我们可以直接将他扔给它来帮我们做编码了。然而,目前英伟达并不支持IDXGIResource形式的图像输入,但支持DirectX 的纹理输入(dx版本最低是9,最高是11,12在官方文档中是不支持的),因此,我们需要将其转成dx纹理,才能扔进去进行编码。转换也很方便,直接调用QueryInterface即可:

ID3D11Texture2D* p_texture2d = NULL;
p_DesktopResource->QueryInterface(IID_PPV_ARGS(&p_texture2d));

到这一步,我们前期所做的准备就完成了,可以将图像进行编码了。

需要注意的点:

  事情当然没有那么简单,这套API还是相对比较底层的,因此有一些情况我们需要注意,或者采取一些措施来处理,这些也是在我开发中所遇到的。

1.系统要求

  这个至少要Win8以上,没得商量啦。如果非要兼顾Win7或XP的话就考虑使用其它技术吧。

2.获取图像可能会失败

  官方文档提到,在以下情形,会发生DXGI_ERROR_ACCESS_LOST的错误:

  • 桌面发生改变
  • 显示模式的变化
  • DWM(桌面窗口管理器)的开关与全屏应用的切换

我们可能一下子并不能理解,但这里我们需要知道的是,当我们从普通模式进入到全屏模式的时候,AcquireNextFrame会失效。如我们英雄联盟选完英雄了,配完符文了,倒计时也完成了的时候,就会进入全屏的游戏载入画面(默认是全屏的,当然也有窗口和无边框模式的),这时我们的output duplication就会失效。当碰到这种情况时,我们要将IDXGIOutputDuplication释放,然后重新进行获取,再重新调用AcquireNextFrame才能解决。

3.Desktop Duplication API很懒

  为什么说他懒呢?因为在显示的内容没发生变化时,它会懒得理你。在你AcquireNextFrame设置超时时间期间,它可能鸟都不鸟你,就让你干等着,等到超时了之后,它就会返回DXGI_ERROR_WAIT_TIMEOUT错误给你(实在太气人了!)。但是对于我们直播应用来说,帧率都是固定的。也就是说在规定时间内,得不到固定数量的图片会让我们播放器来卡顿、播放速度增快等等问题。
  因此,我们需要采取补帧策略。补帧的方法也很简单,既然这段时间没发生变化是吧,那我就把上一次得到的图像再重新扔进去进行编码,这样我们就可以得到稳定数量的图像了。当然也有其它的补帧策略,但需要注意的是,万万不可把已经编码后的数据再复制一份发去传输。对于编码后的图像,分为I帧、P帧和B帧三种类型。其中I帧是帧内编码,数据量也是最大的。P帧和B帧都是参考帧,P帧会向前参考,B帧则是双向参考,由于只包含有少量的图像数据,图像之间的差别,和物体的运动矢量等内容,他们大小相对于仅进行帧内编码的I帧来说要小得多。但也正因为如此,他们需要进行正确的参考,如果把编码后的数据再复制一份的话,那新出来的图像数据的参考对象就可能是错误的,将导致解码出来的图像不准确,可能会出现类似于物体运动中突然退回去一段的情况。

4.获取的图像没有光标

  Desktop Duplication API并不会帮你绘制光标的形状。也就是说,光标是和桌面图像相互分开的,但是它会给出光标的位置,你还可以获取光标的形状数据,这样,你就可以据此来获取光标的形状了。由于光标数据是存在于系统内存中的,为了提高绘制效率,你可以将其转成显存中的纹理(CreateTexture2D),让绘制速度更快,CPU占用更少。具体的做法如下:

更新光标图像 https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api#updating-the-desktop-pointer

补充的内容

API文档

  我在上面并没有讲到这些API的具体使用,更没有去讲解它的参数。我承认偷懒是一个原因,但也限于篇幅的关系。下面给出有关于DXGI和D3D的API Reference,大家可以挑选择感兴趣的来看:

DXGI:https://docs.microsoft.com/en-us/windows/win32/api/_direct3ddxgi/
D3D11:https://docs.microsoft.com/en-us/windows/win32/api/_direct3d11/

当然,更简单的办法,你直接在VS中敲入相应的API,按下F1即可跳转到相应的文档页面了。

一个可以运行的例子

  有人可能会说,我怎么知道我采集出来的图像对不对,别说它样子,就算连数据内容是什么我都没看到。确定出来的图像无误后,再去进行编码确实是一个很好的做法。对于我们之前得到的d3d纹理对象,我们可以用d3d进行绘制。但这对于不熟悉d3d图形库的朋友来说,会很苦恼,我这里给出了一个例子,作者是一位澳大利亚的工程师,Evgeny Pereguda。在这个例子中,它将得到的显存中的纹理拷贝成系统内存中的纹理(期间还转换成GDI兼容形式来进行光标绘制),最后将他写成rgb文件,并用Windows自带的相册应用来打开它。通过运行这个例子,大家就可以看到我们所截取的图片内容了,链接如下:

desktop duplication API 例子: https://www.codeproject.com/Tips/1116253/Desktop-Screen-Capture-on-Windows-via-Windows-Desk

为方便大家下载,我直接把这个文件上传到百度云了(但是出处还是要标注的),链接如下:

desktop duplication API 例子下资链接:https://pan.baidu.com/s/11RNUPtL0PHL-tzUtqtZNuw 
提取码:37yv 

最后

  作者水平有限,有讲的不好,措辞不当的地方,望大家多多包涵(抱拳了!)