采用 HTML5 屏幕共享浏览器标签页?

在过去几年中,我帮助多家不同的公司仅使用浏览器技术就实现了类似于屏幕共享的功能。根据我的经验,仅使用 Web 平台技术(即不使用插件)实现 VNC 是一项难题。需要考虑很多方面,也需要克服很多挑战。中继鼠标指针位置、转发按键操作以及以 60fps 实现完整的 24 位颜色重新绘制,这些只是其中几个问题。

截取标签页内容

如果我们移除传统屏幕共享的复杂性,并专注于共享浏览器标签页的内容,问题就会大大简化为:a.) 捕获当前状态下的可见标签页,b.) 通过网络发送该“帧”。从本质上讲,我们需要一种方法来拍摄 DOM 的快照并进行共享。

分享部分非常简单。WebSocket 非常适合发送不同格式(字符串、JSON、二进制)的数据。快照部分是一个更难的问题。html2canvas 等项目通过在 JavaScript 中重新实现浏览器的渲染引擎来截取 HTML 屏幕截图!另一个示例是 Google 反馈,但它不是开源的。这类项目非常酷,但速度也非常慢。能达到 1fps 的吞吐量就算不错了,更不用说令人垂涎的 60fps 了。

本文介绍了几个我最喜欢的用于“屏幕共享”标签页的概念验证解决方案。

方法 1:更改观察器 + WebSocket

+Rafael Weinstein 今年早些时候演示了一种镜像标签页的方法。他的方法使用了更改观察器和 WebSocket。

本质上,演示者共享的标签页会监控网页的更改,并使用 WebSocket 向观看者发送差异。当用户滚动网页或与网页互动时,观察器会捕获这些更改,并使用 Rafael 的突变摘要库将其报告回给查看器。这样可以确保系统性能良好。系统不会针对每个帧发送整个网页。

正如 Rafael 在视频中指出的那样,这只是一个概念验证。不过,我认为将较新的平台功能(例如 Mutation Observer)与较旧的平台功能(例如 WebSocket)结合使用是一种不错的方式。

方法 2:HTMLDocument 中的 Blob + 二进制 WebSocket

接下来的方法是我最近才想到的。该方法与更改观察器方法类似,但它会创建整个 HTMLDocument 的 Blob 克隆,而不是发送摘要差异,并通过二进制 WebSocket 发送该克隆。具体设置如下:

  1. 将网页上的所有网址都重写为绝对网址。这样可以防止静态图片和 CSS 资源包含损坏的链接。
  2. 克隆页面的文档元素:document.documentElement.cloneNode(true);
  3. 使用 CSS pointer-events: 'none';user-select:'none';overflow:hidden; 将克隆设置为只读、不可选择,并防止滚动
  4. 捕获网页的当前滚动位置,并将其添加为重复网页上的 data-* 属性。
  5. 基于重复项的 .outerHTML 创建 new Blob()

代码如下所示(我对完整源代码进行了简化):

function screenshotPage() {
    // 1. Rewrite current doc's imgs, css, and script URLs to be absolute before
    // we duplicate. This ensures no broken links when viewing the duplicate.
    urlsToAbsolute(document.images);
    urlsToAbsolute(document.querySelectorAll("link[rel='stylesheet']"));
    urlsToAbsolute(document.scripts);

    // 2. Duplicate entire document tree.
    var screenshot = document.documentElement.cloneNode(true);

    // 3. Screenshot should be readyonly, no scrolling, and no selections.
    screenshot.style.pointerEvents = 'none';
    screenshot.style.overflow = 'hidden';
    screenshot.style.userSelect = 'none'; // Note: need vendor prefixes

    // 4. … read on …

    // 5. Create a new .html file from the cloned content.
    var blob = new Blob([screenshot.outerHTML], {type: 'text/html'});

    // Open a popup to new file by creating a blob URL.
    window.open(window.URL.createObjectURL(blob));
}

urlsToAbsolute() 包含简单的正则表达式,用于将相对网址/不含架构的网址重写为绝对网址。这样做是为了确保图片、CSS、字体和脚本在 blob 网址(例如来自其他源)的上下文中查看时不会损坏。

我最后进行了一项调整,就是添加了滚动支持。演示者滚动页面时,观看者应跟随滚动。为此,我将当前的 scrollXscrollY 位置作为重复 HTMLDocumentdata-* 属性进行存储。在创建最终 Blob 之前,系统会注入一些在页面加载时触发的 JS:

// 4. Preserve current x,y scroll position of this page. See addOnPageLoad().
screenshot.dataset.scrollX = window.scrollX;
screenshot.dataset.scrollY = window.scrollY;

// 4.5. When screenshot loads (e.g. in blob URL), scroll it to the same location
// of this page. Do this by appending a window.onDOMContentLoaded listener
// which pulls out the screenshot (dupe's) saved scrollX/Y state on the DOM.
var script = document.createElement('script');
script.textContent = '(' + addOnPageLoad_.toString() + ')();'; // self calling.
screenshot.querySelector('body').appendChild(script);

// NOTE: Not to be invoked directly. When the screenshot loads, scroll it
// to the same x,y location of original page.
function addOnPageLoad() {
    window.addEventListener('DOMContentLoaded', function(e) {
    var scrollX = document.documentElement.dataset.scrollX || 0;
    var scrollY = document.documentElement.dataset.scrollY || 0;
    window.scrollTo(scrollX, scrollY);
    });

通过伪造滚动,会给人一种错觉,以为我们截取了原始网页的一部分,但实际上,我们复制了整个网页,只是重新调整了其位置。#clever

演示

但对于标签页共享,我们需要持续截取标签页并将其发送给观看者。为此,我编写了一个小型 Node WebSocket 服务器、应用和书签页,以演示该流程。如果您对代码不感兴趣,可以观看下面这个演示短视频:

未来的改进

一种优化方法是,不要在每个帧上复制整个文档。这很浪费,而 Mutation Observer 示例可以很好地解决这个问题。另一项改进是在 urlsToAbsolute() 中处理相对 CSS 背景图片。这是当前脚本没有考虑到的。

方法 3:Chrome 扩展程序 API + 二进制 WebSocket

2012 年 Google I/O 大会上,我演示了另一种用于共享浏览器标签页内容的方法。不过,这是一个例外情况。它需要使用 Chrome 扩展程序 API,而不是纯粹的 HTML5 魔法。

此示例的源代码也在 GitHub 上,但要点是:

  1. 将当前标签页截取为 .png data网址。Chrome 扩展程序提供了一个用于此目的 API chrome.tabs.captureVisibleTab()
  2. 将 data网址 转换为 Blob。请参阅 convertDataURIToBlob() 帮助程序。
  3. 通过设置 socket.responseType='blob',使用二进制 WebSocket 将每个 Blob(帧)发送给观看者。

示例

以下代码会将当前标签页截取为 png 图片,并通过 WebSocket 发送该画面:

var IMG_MIMETYPE = 'images/jpeg'; // Update to image/webp when crbug.com/112957 is fixed.
var IMG_QUALITY = 80; // [0-100]
var SEND_INTERVAL = 250; // ms

var ws = new WebSocket('ws://…', 'dumby-protocol');
ws.binaryType = 'blob';

function captureAndSendTab() {
    var opts = {format: IMG_MIMETYPE, quality: IMG_QUALITY};
    chrome.tabs.captureVisibleTab(null, opts, function(dataUrl) {
    // captureVisibleTab returns a dataURL. Decode it -> convert to blob -> send.
    ws.send(convertDataURIToBlob(dataUrl, IMG_MIMETYPE));
    });
}

var intervalId = setInterval(function() {
    if (ws.bufferedAmount == 0) {
    captureAndSendTab();
    }
}, SEND_INTERVAL);

未来的改进

此视频的帧速率出乎意料地出色,但还可以进一步提升。一个改进方法是移除将 data网址 转换为 Blob 的开销。很抱歉,chrome.tabs.captureVisibleTab() 只会向我们提供 data网址。如果它返回 Blob 或类型化数组,我们可以直接通过 WebSocket 发送,而不是自行转换为 Blob。请为 crbug.com/32498 加星标,以便我们实现这一目标!

方法 4:WebRTC - 真正的未来

最后但同样重要的是!

WebRTC 将实现浏览器中屏幕共享的未来。2012 年 8 月 14 日,该团队提出了用于共享标签页内容的 WebRTC 标签页内容截取 API:

在该功能准备就绪之前,我们只能使用方法 1-3。

总结

因此,借助当今的 Web 技术,浏览器标签页共享功能已成为可能!

不过,对这句话应持保留态度。虽然本文介绍的技术很实用,但在某种程度上,它们无法提供出色的分享体验。随着 WebRTC 标签页内容截取功能的推出,这一切都将改变,但在该功能真正实现之前,我们只能使用浏览器插件或本文介绍的有限解决方案。

还有其他技巧吗?发表评论!