无论是做音视频会议,还是做远程教育,**共享桌面**都是一个必备功能。如果说在 PC 或 Mac 端写个共享桌面程序你不会有太多感受,但通过浏览器也可以共享桌面是不是觉得就有些神奇了呢? WebRTC的愿景就是要让这些看似神奇的事情,不知不觉地发生在我们身边。 你可以想象一下,假如浏览器有了共享桌面功能,这会使得浏览器有更广阔的应用空间,一个最直接的例子就是我们可以直接通过浏览器进行远程办公、远程协助等工作,而不用再下载共享桌面的应用了,这大大提高了我们的工作效率。 ## 在WebRTC 处理过程中的位置 在正式进行主题之前,我们还是来看看本文在整个 WebRTC 处理过程中的位置,如下图所示: 没错,它仍然属于音视频采集的范畴,但是这次采集的不是音视频数据而是桌面。不过这也没什么关系,**桌面也可以当作一种特殊的视频数据来看待**。 ## 共享桌面的基本原理 共享桌面的基本原理其实非常简单,我们可以分“两头”来说明: - 对于**共享者**,每秒钟抓取多次屏幕(可以是3次、5次等),每次抓取的屏幕都与上一次抓取的屏幕做比较,取它们的差值,然后对差值进行压缩;如果是第一次抓屏或切幕的情况,即本次抓取的屏幕与上一次抓取屏幕的变化率超过80%时,就做全屏的帧内压缩,其过程与JPEG图像压缩类似(有兴趣的可以自行学习)。最后再将压缩后的数据通过传输模块传送到观看端;数据到达观看端后,再进行解码,这样即可还原出整幅图片并显示出来。 - 对于**远程控制端**,当用户通过鼠标点击共享桌面的某个位置时,会首先计算出鼠标实际点击的位置,然后将其作为参数,通过信令发送给共享端。共享端收到信令后,会模拟本地鼠标,即调用相关的 API,完成最终的操作。一般情况下,当操作完成后,共享端桌面也发生了一些变化,此时就又回到上面共享者的流程了,我就不再赘述了。 通过上面的描述,可以总结出共享桌面的处理过程为:**抓屏、压缩编码、传输、解码、显示、控制**这几步,你应该可以看出它与音视频的处理过程几乎是一模一样的。 对于共享桌面,很多人比较熟悉的可能是**RDP(Remote Desktop Protocal)协议**,它是 Windows 系统下的共享桌面协议;还有一种更通用的远程桌面控制协议——**VNC(Virtual Network Console)**,它可以实现在不同的操作系统上共享远程桌面,像TeamViewer、RealVNC都是使用的该协议。 以上的远程桌面协议一般分为桌面数据处理与信令控制两部分。 - **桌面数据**:包括了桌面的抓取(采集)、编码(压缩)、传输、解码和渲染。 - **信令控制**:包括键盘事件、鼠标事件以及接收到这些事件消息后的相关处理等。 其实在WebRTC中也可以实现共享远程桌面的功能。但由于共享桌面与音视频处理的流程是类似的,且**WebRTC 的远程桌面**又不需要远程控制,所以其**处理过程使用了视频的方式,而非传统意义上的RDP/VNC等远程桌面协议**。 下面我们就按顺序来具体分析一下,在桌面数据处理的各个环节中,WebRTC使用的方式与RDP/VNC等真正的远程桌面协议的异同点吧。 **第一个环节,共享端桌面数据的采集**。WebRTC 对于桌面的采集与RDP/VNC使用的技术是相同的,都是利用各平台所提供的相关API进行桌面的抓取。以 Windows 为例,可以使用下列 API 进行桌面的抓取。 - **BitBlt**:XP 系统下经常使用,在 vista之后,开启DWM模式后,速度极慢。 - **Hook**:一种黑客技术,实现稍复杂。 - **DirectX**:由于DirectX 9/10/11 之间差别比较大,容易出现兼容问题。最新的 WebRTC都是使用的这种方式 - **GetWindowDC**:可以通过它来抓取窗口。 **第二个环节,共享端桌面数据的编码**。WebRTC 对桌面的编码使用的是视频编码技术,即 H264/VP8等;但RDP/VNC则不一样,它们使用的是图像压缩技术。使用视频编码技术的好处是压缩率高,而坏处是在网络不好的情况下会有模糊等问题。 **第三个环节,传输**。编码后的桌面数据会通过流媒体传输协议发送到观看端。对于WebRTC来说,当网络有问题时,数据是可以丢失的。但对于 RDP/VNC 来说,桌面数据一定不能丢失。 **第四个环节,观看端解码**。WebRTC 对收到的桌面数据通过视频解码技术解码,而 RDP/VNC 使用的是图像解码技术(可对比第二个环节)。 **第五个环节,观看端渲染**。一般会通过 OpenGL/D3D等GPU进行渲染,这个 WebRTC 与 RDP/VNC 都是类似的。 通过以上的讲解,相信你应该已经对共享远程桌面有一个基本的认知了,并且也知道在浏览器下使用WebRTC 共享远程桌面,你只需要会使用浏览器提供的API即可。 **因此本文的目标就是:你只需要学会和掌握浏览器提供的抓取屏幕的API就可以了**。至于编码、传输、解码等相关知识,我会在后面的文章中陆续为你讲解。 ## 如何共享桌面 学习完共享桌面相关的理论知识,接下来,就让我们实践起来,一起来学习如何通过浏览器来抓取桌面吧! ### 1. 抓取桌面 首先我们先来了解一下在浏览器下抓取桌面的API的基本格式: ``` var promise = navigator.mediaDevices.getDisplayMedia(constraints); ``` 这个API你看着是不是似曾相识?没错,它与前面[《01 | 原来通过浏览器访问摄像头这么容易》](https://time.geekbang.org/column/article/107948)一文中介绍的采集视频的 API 基本上是一样的,我们可以再看一下采集视频的 API 的样子: ``` var promise = navigator.mediaDevices.getUserMedia(constraints); ``` 二者唯一的区别就是:一个是**getDisaplayMedia**,另一个是**getUserMedia**。 这两个API都需要一个**constraints**参数来对采集的桌面/视频做一些限制。但需要注意的是,在采集视频时,参数**constraints**也是可以对音频做限制的,而在桌面采集的参数里却不能对音频进行限制了,也就是说,不能在采集桌面的同时采集音频。**这一点要特别注意**。 下面我们就来看一下**如何通过 getDisplayMedia API 来采集桌面**: ``` ... //得到桌面数据流 function getDeskStream(stream){ localStream = stream; } //抓取桌面 function shareDesktop(){ //只有在 PC 下才能抓取桌面 if(IsPC()){ //开始捕获桌面数据 navigator.mediaDevices.getDisplayMedia({video: true}) .then(getDeskStream) .catch(handleError); return true; } return false; } ... ``` 通过上面的方法,就可以获得桌面数据了,让我们来看一下效果图吧: ### 2. 桌面的展示 桌面采集后,就可以通过 HTML 中的`<video>`标签将采集到的桌面展示出来,具体代码如下所示。 首先,在 HTML 中增加下面的代码,其中`<video>`标签用于播放抓取的桌面内容: ``` ... <video autoplay playsinline id="deskVideo"></video> ... ``` 下面的 JavaScript 则将桌面内容与`<video>`标签联接到一起: ``` ... var deskVideo = document.querySelect("video/deskVideo"); ... function getDeskStream(stream){ localStream = stream; deskVideo.srcObject = stream; } ... ``` 在 JavaScript中调用**getDisplayMedia**方法抓取桌面数据,当桌面数据被抓到之后,会触发 getDeskStream 函数。我们再在该函数中将获取到的 stream 与video 标签联系起来,这样当数据获取到时就从播放器里显示出来了。 ### 3. 录制桌面 录制本地桌面与[《04 | 可以把采集到的音视频数据录制下来吗?》](https://time.geekbang.org/column/article/109105)一文中所讲的录制本地视频的过程是一样的。首先通过**getDisplayMedia**方法获取到本地桌面数据,然后将该流当作参数传给 MediaRecorder 对象,并实现**ondataavailable**事件,最终将音视频流录制下来。 具体代码如下所示,我们先看一下 HTML 部分: ``` <html> ... <body> ... <button id="record">Start Record</button> ... </body> </html> ``` 上面的 HTML 代码片段定义了一个开启录制的**button**,当用户点击该 button 后,就触发下面的JavaScript 代码: ``` ... var buffer; ... function handleDataAvailable(e){ if(e && e.data && e.data.size > 0){ buffer.push(e.data); } } function startRecord(){ //定义一个数组,用于缓存桌面数据,最终将数据存储到文件中 buffer = []; var options = { mimeType: 'video/webm;codecs=vp8' } if(!MediaRecorder.isTypeSupported(options.mimeType)){ console.error(`${options.mimeType} is not supported!`); return; } try{ //创建录制对象,用于将桌面数据录制下来 mediaRecorder = new MediaRecorder(localStream, options); }catch(e){ console.error('Failed to create MediaRecorder:', e); return; } //当捕获到桌面数据后,该事件触发 mediaRecorder.ondataavailable = handleDataAvailable; mediaRecorder.start(10); } ... ``` 当用户点击**Record**按钮的时候,就会调用**startRecord**函数。在该函数中首先判断浏览器是否支持指定的多媒体格式,如webm。 如果支持的话,再创建**MediaRecorder**对象,将桌面流录制成指定的媒体格式文件。 当从localStream获取到数据后,会触发**ondataavailable**事件。也就是会调用 handleDataAvailable 方法,最终将数据存放到Blob中。 至于将Blob保存成文件就比较容易了,我们在前面的文章[《04 | 可以把采集到的音视频数据录制下来吗?》](https://time.geekbang.org/column/article/109105)中都有讲解过,所以这里就不再赘述了! ## 小结 本文我向你讲解了如何通过浏览器提供的 API 来抓取桌面,并将它显示出来,以及如何通过前面所讲的**MediaRecorder**对象将桌面录制下来。 其实,真正的商用客户端录制并不是录制音视频流,而是录制桌面流。这样即使是多人互动的场景,在有多路视频流的情况下,录制桌面的同时就将桌面上显示的所有视频一起录制下来了,这样问题是不是一下就简单了? 比较遗憾的是,关于我们上述录制桌面的API,目前很多浏览器支持得还不够好,只有 Chrome 浏览器相对比较完善。不过现在WebRTC 1.0规范已经出来了,相信在不久的将来,各浏览器都会实现这个API的。 ## 思考时间 为什么使用视频的编码方式就容易出现桌面模糊的现象呢?有什么办法可以解决该问题吗? 欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。 [所做Demo的GitHub链接(有需要可以点这里)](https://github.com/avdance/webrtc_web/tree/master/05_desktop/recorder)