05 | 原来浏览器还能抓取桌面?

无论是做音视频会议,还是做远程教育,共享桌面都是一个必备功能。如果说在 PC 或 Mac 端写个共享桌面程序你不会有太多感受,但通过浏览器也可以共享桌面是不是觉得就有些神奇了呢?

WebRTC的愿景就是要让这些看似神奇的事情,不知不觉地发生在我们身边。

你可以想象一下,假如浏览器有了共享桌面功能,这会使得浏览器有更广阔的应用空间,一个最直接的例子就是我们可以直接通过浏览器进行远程办公、远程协助等工作,而不用再下载共享桌面的应用了,这大大提高了我们的工作效率。

在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 | 原来通过浏览器访问摄像头这么容易》一文中介绍的采集视频的 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;
         
}  

...

通过上面的方法,就可以获得桌面数据了,让我们来看一下效果图吧:

Chrome浏览器共享桌面图

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 | 可以把采集到的音视频数据录制下来吗?》一文中所讲的录制本地视频的过程是一样的。首先通过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 | 可以把采集到的音视频数据录制下来吗?》中都有讲解过,所以这里就不再赘述了!

小结

本文我向你讲解了如何通过浏览器提供的 API 来抓取桌面,并将它显示出来,以及如何通过前面所讲的MediaRecorder对象将桌面录制下来。

其实,真正的商用客户端录制并不是录制音视频流,而是录制桌面流。这样即使是多人互动的场景,在有多路视频流的情况下,录制桌面的同时就将桌面上显示的所有视频一起录制下来了,这样问题是不是一下就简单了?

比较遗憾的是,关于我们上述录制桌面的API,目前很多浏览器支持得还不够好,只有 Chrome 浏览器相对比较完善。不过现在WebRTC 1.0规范已经出来了,相信在不久的将来,各浏览器都会实现这个API的。

思考时间

为什么使用视频的编码方式就容易出现桌面模糊的现象呢?有什么办法可以解决该问题吗?

欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

所做Demo的GitHub链接(有需要可以点这里)

精选留言

  • 恋着歌

    2019-07-25 17:09:58

    如果是解决网络引起的模糊,那么可能就要牺牲实时性,提高延迟,就像我们看视频时卡顿要缓冲一下。

    具体的解决方法是:
    1,解决网络问题😂
    2,关闭 Web RTC 的自适应码率,frameRate,width,height 设置固定值或高范围值。
    作者回复

    赞!

    2019-07-25 21:56:56

  • 月光伴奏

    2019-11-22 10:33:32

    老师!文中(不能在采集桌面的同时采集音频),好像是能同时采集的,弹出了一个分享音频的选择框,勾上好像就能采集音频了
    作者回复

    不能用同一个 stream进行采集

    2019-11-23 17:23:49

  • 春风吹又生

    2019-10-27 09:49:39

    老师,webrtc能否录制系统内声音呢?
    作者回复

    你是指的播放了的多媒体文件的声音吧?是可以的!

    2019-10-29 12:00:22

  • 魏仁勋

    2020-11-13 16:15:06

    老师,我现在在用electron-vue写客户端,但是我想分享我的屏幕,结果报这个错误navigator.mediaDevices.getDisplayMedia is not a function,但是我是能看到视频和听到声音的,我之前在web是可以调的,请问这是为什么呢
    作者回复

    感觉你在调用 vue时,没有真正调用到系统的 API,而是被封装了,你要自己查一下。还有一个种可能是你的electron用的chomre核版本太低了

    2020-11-19 14:40:16

  • 宇宙之王

    2020-08-23 21:06:07

    是不是这一讲的代码只能在Chrome下面运行,别的浏览器不支持,我试了一下,edge和火狐获取不到桌面,另,看到所有代码里面都有<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
    为什么要引入这两个js,删掉好像一样可以运行
    作者回复

    chrome对webrtc兼容的最好,共享桌面是一个试验性的功能,所以其它浏览器支持的不好;
    adapter-latest.js 主要为了兼容老版本chrome浏览器;socket.io.js是 socket.io的js 客户端;

    2020-09-19 23:32:13

  • 月光伴奏

    2019-11-15 18:26:24

    老师,也就是说webrtc这个分享时没办法实现分享系统声音的嘛?分享游戏听不到声音的嘛?
    作者回复

    可以分别采集,然后入到同一个stream中。

    2019-11-23 17:41:44

  • | ~浑蛋~

    2022-09-16 17:43:18

    为啥我的会出现一个一个图片,层层套娃地呈现出来呢
  • Embrace

    2020-10-19 20:20:58

    老师,按照demo写的,录下来的桌面视频好模糊,调整了分辨率和帧率也没啥用,分辨率太大了,录下来的视频会嵌套变成很多层;另外getDisplayMedia接口的具体参数在哪里能查到? MDN的参数很简单,跟实际要用到的相比少了很多
    作者回复

    webrtc1.0规范手册,网上查一下

    2020-11-10 18:24:55

  • 初音韶歌

    2019-08-26 10:06:53

    老师,我的截取桌面播放的视频内容是嵌套的是怎么回事呢,就是一个桌面内容里又有n层小的内容
    作者回复

    你切换一下窗口就好了,你可以想一下为什么会这样哈

    2019-08-27 12:03:32

  • ZeroIce

    2019-08-04 08:36:09

    老师,录制的像素太低怎么办?
    作者回复

    getUserMedia API 中调整一下分辩率就好了!

    2019-08-04 19:46:23

  • ZeroIce

    2019-08-04 07:59:44

    交作业:https://codepen.io/vicksiyi/pen/oKGJzL

    // 判断是否为PC端的Chrome
    let ifMachine = (() => {
    var ua = navigator.userAgent,
    isWindowsPhone = /(?:Windows Phone)/.test(ua),
    isSymbian = /(?:SymbianOS)/.test(ua) || isWindowsPhone,
    isAndroid = /(?:Android)/.test(ua),
    isFireFox = /(?:Firefox)/.test(ua),
    isChrome = /(?:Chrome|CriOS)/.test(ua),
    isTablet = /(?:iPad|PlayBook)/.test(ua) || (isAndroid && !/(?:Mobile)/.test(ua)) || (isFireFox && /(?:Tablet)/.test(ua)),
    isPhone = /(?:iPhone)/.test(ua) && !isTablet,
    isPc = !isPhone && !isAndroid && !isSymbian;
    return {
    isChrome: isChrome,
    isPc: isPc
    };
    })();


    作者回复

    赞!

    2019-08-04 19:46:38

  • K

    2019-07-25 20:57:14

    //第一部分代码

    let videoTypes = "video/webm\;codecs=vp8";
    let userMediaSetting = { video: true };
    let playVideo = document.querySelector('video#play');
    let rePlayVideo = document.querySelector('video#replay');
    let recordingButton = document.querySelector('button#recording');
    let playbackButton = document.querySelector('button#playback');
    let downloadButton = document.querySelector('button#download');
    let buffer;
    let mediaRecorder;
    let blob;

    function init() {
    if (!(mediaSupport() && mediaRecorderSupport()))
    return;
    getVideo();
    }

    function mediaSupport() {
    if (!navigator.mediaDevices) {
    console.log('不支持 mediaDevices');
    return false;
    }
    console.log('支持 mediaDevices');
    return true;
    }

    function mediaRecorderSupport() {
    if (!MediaRecorder.isTypeSupported(videoTypes)) {
    console.log(`不支持 ${videoTypes}`);
    return false;
    }
    console.log(`支持 ${videoTypes}`);
    return true;
    }

    function getVideo() {
    navigator.mediaDevices.getDisplayMedia(userMediaSetting)
    .then(handleGetVideo)
    .catch(handleGetVideoError)
    }

    function handleGetVideo(mediaStream) {
    playVideo.srcObject = mediaStream;
    }

    function handleGetVideoError(err) {
    console.log(`获取视频输入出错: ${err.name} : ${err.message}`)
    }

    function recordingVideo() {
    if ( typeof(mediaRecorder) != "undefined" && mediaRecorder.state === 'recording') {
    console.log('已开始录制,请勿重复录制');
    return;
    }
    console.log('开始录制');
    buffer = [];
    let options = {
    mimeType : videoTypes
    };

    mediaRecorder = new MediaRecorder(playVideo.srcObject, options);
    mediaRecorder.ondataavailable = handleDataAvailable;
    mediaRecorder.start(10);
    }

    function handleDataAvailable(d) {
    if (d && d.data && d.data.size > 0) {
    buffer.push(d.data);
    }
    }
    作者回复

    赞!

    2019-07-25 21:45:45

  • Jason

    2019-07-25 15:37:43

    思考题:我猜一下,wenrtc底层主要使用udp传输,网络不好的情况下,会有丢包。所以会有模糊的现象发生。
    作者回复

    如果是自适应码率,当发生丢包时,编码器会降低码率,当分辨率不变,码率变低时,就是模糊哈!

    2019-07-25 21:48:37

  • ifelse

    2025-07-10 12:18:17

    学习打卡
  • keepgoing

    2025-04-15 20:52:51

    老师想请教两个问题:
    1. 请问有相关的学习交流群吗,想对音视频以及 rtc 技术长期学习和交流
    2. 如果我想了解 webrtc 源码,另外想在移动端平台上集成 webrtc 来实现音视频直播能力,应该有什么渠道可以学习,希望老师指点,万分感谢!
  • Geek_e46272

    2022-05-04 20:32:29

    图片编码每一帧都可以独立还原,视频编码丢失关键帧的话就只能马赛克了。怎么解决呢?
  • LST😘 CYX

    2022-01-11 10:34:37

    采集桌面时,通过设置width,height,frameRate,参数上很高,也没丢包,但是清晰度依然不够,很模糊,怎么办
  • npersonal

    2021-12-02 18:31:14

    老师,想问下webrtc有提供api可以获取到屏幕、窗口这些实时流吗,想自己实现共享框
  • caidy

    2021-04-23 00:20:19

    录制的视频清晰度很差,这个要怎么解决呢?还有显示的时候清晰度也非常差
    作者回复

    这与你直播时的视频分辨率有关,你可以确定一下你直播时的分辨率是否与录制的分辨率一致

    2021-05-01 20:07:22

  • CaptainJack

    2021-04-01 14:45:21

    请问,我这边做了一对一的视频连接,这个时候要共享桌面,拿到流之后,要怎么让对端看到这个流呢,我 stream.getTracks().forEach((track) => this.pc.addTrack(track, stream)) 之后,对端没有收到共享的流
    作者回复

    要重新媒体协商一下试试

    2021-04-02 16:34:55