本篇主要介绍 Windows 端和 macOS 端上屏幕分享的实现方式与注意事项。这两套系统都是闭源的,主要信息来源于官方文档,以及加上各位技术前辈和个人的一些摸索,如有不当或者错误的地方,还请诸位不吝指正。
作者:刘国元 网易资深开发工程师
一、前言
实时音视频通信的整个流程,可以大致分为数据采集、编码、传输、解码、渲染几个环节。各类平板、手机、电脑等终端是数据采集和渲染的重要环节。鉴于目前市面上介绍桌面端的屏幕数据采集资料总结的比较少,本文抛砖引玉讨论一下,在开发桌面端屏幕数据采集过程中的一些实践经验。
在 Windows 和 macOS 两大桌面操作系统的应用层上实现屏幕采集,主要工作是针对这类基于窗口的系统实现窗口图像的抓取。不同桌面端的图形系统提供了不同的系统级实现,以及不同的系统 API,我们无法通过一套标准的接口来实现该功能,只能针对性地给出不同的封装,下面分条细述。
二、Windows
Windows 上实现屏幕分享算是几个平台最复杂的,单单是 Windows 提供的抓取屏幕图像的技术就有好多种,简单列举概述如下:
- Windows GDI:这是 Windows 系统上兼容性较好的一套方案,从 Windows 2000 开始就有,现在还在继续提供服务,效率也还不错,部分接口底层支持了硬件加速功能,至于缺点我们后面叙述。
- Microsoft DirectX Graphics Infrastructure(DXGI):这套 API 是为了封装不同版本 DirectX 部分接口而出现的,首次出现是在 Direct 3D 10,现在还在更新。本文关注的是 DirectX 提供的显示器数据抓取技术。
- DWM:这项技术是微软在 Vista 上,为了提高桌面窗口显示效果而提供的一套 API,其基本思路是:改变过去直接绘制窗口到屏幕的做法,在视频内存中开辟一块离屏渲染区域,用来做各种像素处理,之后再交给屏幕显示,以此实现 Vista 有的毛玻璃效果,以及按下窗口键 +Tab 键来做桌面窗口切换时的 3D 转换效果等。通过这套 API 我们可以实现窗口缩略图的渲染,在 Windows 7 上,DWM 可以通过切换系统主题来控制其打开和关闭,即 Aero 主题。从 Windows 8 开始,系统默认都是打开 DWM 。这套 API 一般可以用来获取目标窗口的缩略图(thumbnail,支持动态更新),用来分享画面的话,分辨率偏低,不太能满足需求。
- Magnification API:我们姑且称之为“放大镜 API”,这个是 Windows 上预装的放大镜程序底层使用的技术,我们正好可以利用这个技术,同时还可以实现窗口过滤功能。这套 API 也是目前兼容范围较广,各类 App 使用非常多的窗口显示数据抓取技术,我们后面会详细分析。
- Windows Grapics Capture(WGC API):微软最新提供的一套正式用来抓取屏幕数据的接口,遗憾的是最初是给 UWP 提供的,而非 Win32 App,且要求必须是 Windows 10, Update(1809)及之后的系统才支持,但是官方提供的,我们也要予以足够的重视。
- Windows Media API,以及各种 hook 技术 …… 可用范围较窄,因此这里不讨论。
虽然可以列举的方案有很多种,但是没有一种方案可以覆盖从 Windows 7 到 Windows 10 上运行的所有 GUI 程序窗口(且不论 Windows XP 系统),从这一点可以看到微软最近这二十年在窗口系统上“激进”的“创新”。
屏幕分享功能常见场景可以大致分为两种:
- 分享某个应用的单个窗口;
- 分享指定显示器的内容。如果有多块外接显示器,可能还要考虑是否要分享整个虚拟桌面(把几块显示器图像拼接起来),不过目前几乎没看到这类应用场景。
我们要做的就是组合前面提到的可用的技术,来覆盖这两种场景。接下来我们分别讨论。
(一) 应用窗口采集
在详细介绍应用窗口采集实现方案之前,我们先看一下要这个场景下还有哪些细节需要考虑进来。
我们开始屏幕分享之后,为了让分享者能够做一些常用的操作,比如开始/停止/暂停/恢复/分享、邀请联系人、打开一个 IM 窗口进行交流等。一般情况下,我们在应用层会做一个操作的浮窗,在上面暴露多个操作入口,但观看端是不想看到这些画面的,且这些窗口还可能挡住分享者桌面上一些重要的信息。这就引出我们需要考虑的最重要,也是最难实现的一个点:有一些桌面上的窗口需要在分享过程中被过滤掉。
针对屏幕分享场景,上述支持窗口过滤的方案基本上只有两种:Magnification API 和 WGC API。
1. Magnification API
Magnification API 方案,因为其覆盖范围较广,所支持的功能又正好满足我们需要,所以我们将其作为优选方案,其他方案作为补充。
在 Magnification API 官方资料Magnification API Overview中有这么一段话:
The Magnification API is not supported under WOW64; that is, a 32-bit magnifier application will not run correctly on 64-bit Windows.
意思是说目前的 32-bit 的应用程序运行在 64-bit 系统上可能出问题。但是实践下来,32-bit 与 64-bit 的应用程序没有什么差异,个人认为微软应该可以重新整理一下这部分文档了。
主要的 API 中,MagSetWindowFilterList 支持设置一组待过滤的窗口句柄,在上面的场景中,表现为前文提到的浮窗,MagSetWindowSource 用来启动一次窗口数据抓取,具体数据通过 MagSetImageScalingCallback 设置的回调函数 Magimagescalingcallback 同步返回,我们可以在自定义的 Magimagescalingcallback 函数中拿到抓取的数据做进一步处理,理想情况下,这样就足够了。
当然,实际情况没有这么理想。还有好多“杂事儿”需要处理:
- 比如被分享的窗口被不小心关闭了,最小化了,大小改变了,隐藏了,甚至整个被分享的应用程序直接 crash 了。
- 每次返回的数据都需要申请一大块内存来保存,然后转发给其他处理环节,那么就需要维护内存申请的问题。对于 32-bit 的应用程序,用户内存空间仅有 2G(非常多 3G)大小,而一般涉及到音视频的应用程序中难免会有大量内存操作,这会进一步导致内存碎片的产生,如果碰到分享者在分享一个 4K 屏上全屏窗口的场景,那么很可能刚开始分享的一段时间内运行正常,在未来某个时间点,突然发生大块内存申请不到的情况。这就需要根据实际情况做具体的处理。
- 在某些 Nvidia 显卡(尤其是一些笔记本)上该接口会认真完成全套流程调用,但问题是返回的数据是纯黑屏数据,有时候是一个4×4大小的数据,于是我们需要检测返回的数据。
- 支持过滤的窗口有一个最大数值,测试下来大概是 24 个窗口左右,所以应该提示应用层的开发同学,使用过滤窗口的时候保持克制。
- Magnification API 接口对非主屏支持的比较差,还有主屏显示分辨率低于非主屏的场景,这时候不出意外是会出现 crash。
对于情况 1、2 我们可以写一些逻辑代码处理;对于情况 3、4、5 我们就不得不另外找保底方案来处理;尤其是情况5往往会直接 crash,这时候需要提前判断,采用保底方案。市面上大部分会议 App 都是创建一个独立的屏幕分享进程,这样即使这个进程崩溃也不会影响主进程的功能,大不了让用户重启一下分享功能。
2. Windwos GDI
GDI 方案的核心函数有两个: PrintWindow 和 BitBlt。
PrintWindow 内部实现是向目标窗口发送了一个消息(WM_PRINT 或者 WM_PRINTCLIENT)。PrintWindow 有一个优点是即使目标窗口被遮挡,也会把目标窗口的内容绘制出来,但缺点是这个函数在Windows8.1以下操作系统上无法获取到硬件加速区域的渲染数据,而且可能会造成目标窗口闪烁。Windows 8.1 及以上版本的系统,支持把 nFlags 设置为 PW_RENDERFULLCONTENT。这样即便是使用硬件加速区域的渲染数据也能获取到: PrintWindow(window_, mem_dc, PW_RENDERFULLCONTENT)。
相比 PrintWindow 而言, BitBlt 函数的优点是速度较快,从 Windows 7 开始支持了硬件加速,但其缺点是无法绘制被其他窗口遮挡的区域。
测试下来发现,Win8.1以上的系统,使用PrintWindow要比Magnification API性能高,可以获得较高得采集帧率。
针对无法使用 Magnification API 的情况,回退到 GDI 方案之后,又无法采集使用硬件加速渲染的应用,我们还有最后一个办法:通过采集目标窗口所在显示器的画面,计算目标窗口区域,再从截取到的显示器画面中把目标区域裁剪出来,从而得到最后的画面,但这个方案无法处理目标窗口被遮挡的场景。
3. Windows Grapics Capture(WGC API)
这种方案放在最后说,是因为微软推出的时间很晚,参考 New Ways to do Screen Capture。这个是千呼万唤始出来的技术,仅支持 Windows 10, Update(1809)(时间是2018-11-13)以及更高版本的操作系统。如果要使用 WGC,开发环境需要安装 Windows 10 SDK, version 1809(10.0.17763.0)及以上版本,这套 API 原本是给 UWP 程序使用的,如果想在 Win32 程序上使用,可以通过 C++/WinRT 来做。关于 C++/WinRT 的内容读者需要单独了解,这里就不做介绍了。
这套方案诞生之初,会给目标应用的窗口带一个的高亮的黄框,麻烦的是没有提供 API 控制这个黄框的开关,也不能换颜色,总之是无法控制,在这个问题被人们诟病了近3年之后,终于在 Windows 10,Update(21H1)(时间是2021-05-25)中加了一个控制开关 GraphicsCaptureSession.IsBorderRequired Property,开发环境需要更新 Windows SDK 到 10.0.20348.0 及以上版本。
同样地,WGC 也需要像放大镜 API 一样,手动处理目标窗口移动、大小变化、最小化、恢复、隐藏、关闭的场景。
WGC 可以根据产品的策略,决定 Windows 10, Update(1809)以上的系统是否都使用这种方案,或者是 Windows 10, Update(21H1)以上的系统才使用。
最后提一下,WGC 方案除了是官方针对屏幕分享场景而出的一套解决方案外,采集速度也是其他方案不能比拟的,这对于采集一些画面变化较快的场景(比如游戏,视频)来说,是一个很好的选择,可以比较完美地解决屏幕分享画面卡顿的问题。当然,天下没有免费的午餐,伴随高采集帧率而来的是较高的性能消耗。
(二)应用窗口采集策略
针对上面介绍的技术,网易云信 SDK 在实现的时候做了如下策略:
- 如果系统高于 Windows 10, Update(1809),我们使用 WGC 方案;
- 其次是GDI 方案,如果采集对象是UWP窗口,或者当前运行在Win7系统上、没有开启Aero、并且采集窗口所在进程是黑名单项,那么执行回退策略;否则,继续使用 GDI 方案;
- Magnication API方案,在检测到采集的是黑屏数据,或者发现返回的数据格式不正确的时候,执行回退策略;
- 最后是通过 GDI 先采集指定显示器,然后裁剪目标区域的方式模拟应用采集。
(三) 显示器的采集
这里,我们只讨论分享某一块显示器的场景,虚拟桌面场景方案类似。
1. Magnification API
同应用分享一样,我们优先考虑 Magnification API,因为这种方案支持过滤窗口,并且使用方法也是完全一样的,只不过是前面设置的是目标应用窗口的位置坐标,在这里换成要抓取的显示器坐标值。
2. DXGI
DXGI 用来管理独立于 Direct3D 图形运行时的低级任务。DXGI 为多个版本的 Direct3D 提供了一个通用框架。DXGI 的 Desk较好 Duplication 方案只能抓取显示器画面,正好可以使用在这种场景。DXGI 的架构图如下:
Figure 1:DXGI架构
相比应用分享,有一些特殊的情况,比如正在分享的显示器(有意或者无意地)突然被移除了,这时候是直接退出分享?还是先暂停,然后等待显示器重新接上并继续分享呢?
3. GDI
GDI 也可以用在在采集屏幕上,而且兼容性很好,只是同样也不支持过滤窗口。
4. WGC
WGC 方案在使用上,相比与前面的应用窗口画面抓取,只需要注意把为窗口创建的 IGraphicsCaptureItem 换为显示器目标的即可,其余流程一致。这种方案只能通过SetWindowDisplayAffinity过滤本进程内的窗口。
(四) 显示器采集策略
显示器采集的策略按照下面的原则执行:
如果系统高于 Windows 10, Update(20H1),我们使用 WGC 方案;
然后尝试使用 Magnification API,如果失败,执行回退策略;
尝试使用 DXGI 的方式采集显示器,如果不支持,那么执行回退策略;
最后一步尝试使用 GDI 方案。
三、macOS
Mac 上的取屏由于系统原本就提供了支持,比 Windows 上的简单很多,也不需要回退策略,故在此简单介绍,取屏的几个核心的方法是 Mac 上 CG 库提供的。
(一)应用窗口采集
核心方法是 CGWindowListCreateImage,为指定的一组窗口生成图片,同 Windows 系统上的应用窗口采集一样,需要处理窗口的变化。
(二)显示器的采集
显示器采集的核心方法是:
- CGWindowListCreateImageFromArray 支持从一组 Windows id 中生成一张图片;
- CGDisplayCreateImage 支持获取指定显示器的图片。
显示器分享时,对于过滤窗口的实现,大致思路是:先枚举屏幕上所有窗口,并把过滤窗口排除在外,得到一个窗口 id 数组;接着通过
CGWindowListCreateImageFromArray 得到这组窗口的一个截图 img_orig,然后通过计算过滤窗口的位置 rect,在 img_orig 中扣出 rect 处的图像 img_exclude。接着通过 CGDisplayCreateImage 得到目标显示器的截图 img_monitor,然后按照相对位置,把 img_exclude 盖在 img_monitor 上,这样就得到了一个看起来过滤掉某些窗口的显示器截图。
四、还有一些场景
在屏幕分享场景中,还有一些地方没有提及,比如 Windows 和 macOS 系统都支持一些视觉辅助功能,比如整屏图像放大,以及高清DPI。
macOS 和 Windows 10 上还有一个多桌面的概念,允许用户把一些程序拖动到这些桌面上运行。
Windows 上的 Office 和 WPS 的幻灯片应用,以及 macOS 中的 Keynote,开始播放幻灯片时,一般是新创建一个窗口,这时候如果不做特殊的处理,抓取到的就还是老的窗口显示数据,或者老的窗口可能已经被销毁了。
随着云信 G2 SDK 跟客户产品不断迭代更新,我们会为客户持续提供更加稳定易用的技术和产品,帮助各行各业的组织,连接和服务10亿人。
作者介绍
刘国元,网易云信资深开发工程师,长期从事桌面端开发,现专注于 RTC 方面的开发工作。
关于网易云信
网易云信:网易智企旗下融合通信云服务专家、通信与视频 PaaS 平台。集网易 24 年 IM 以及音视频技术打造的融合通信云服务专家,稳定易用的通信与视频 PaaS 平台。提供融合通信与视频的核心能力与组件,包含 IM 即时通讯、5G 消息平台、一键登录、信令、短信与号码隐私保护等通信服务,音视频通话、直播、点播、互动直播与互动白板等音视频服务,视频会议等组件服务,并联合网易易盾推出一站式安全通信方案「安全通」。目前,网易云信已经成功发送 1.6 万亿条消息,覆盖智能终端 SDK 数累计超过 186 亿,我们期待每个智能终端都有云信的融合通信能力。
文章标题:桌面端屏幕分享实践,发布者:网易智企,转载请注明出处:https://worktile.com/kb/p/6009
评论列表(1条)
这个方案如下向dxgi一样满足如下需求
1)如何拿单独鼠标(而不是将鼠标嵌入图里面)
2)如何拿到赃区域(即变化区域,而不是全图)
谢谢