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

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

正在截取标签页内容

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

分享部分很简单。Websocket 非常擅长以不同格式(字符串、JSON、二进制)发送数据。快照部分是一个更难的问题。html2canvas 等项目通过在 JavaScript 中重新实现浏览器的渲染引擎,解决了 HTML 屏幕截图问题!另一个例子是 Google 反馈,不过它不是开源的。这类项目非常酷,但速度也慢得可怕。如果能达到 1fps 的吞吐量,您就已经很幸运了,更不用说梦寐以求的 60fps 了。

本文将讨论一些我最喜欢的“屏幕共享”标签页概念验证解决方案。

方法 1:Mutation Observer + WebSocket

今年早些时候,+Rafael Weinstein 演示了一种镜像标签页的方法。他的技术使用了 Mutation Observer 和 WebSocket。

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

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

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

接下来这个方法是我最近才想到的。它与 Mutation Observer 方法类似,但它不会发送摘要差异,而是创建整个 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() 包含简单的正则表达式,用于将相对/无架构网址重写为绝对网址。这是必要的,这样在 blob 网址(例如来自其他来源)的上下文中查看图片、CSS、字体和脚本时,它们就不会损坏。

我做的最后一项调整是添加滚动支持。当演示者滚动页面时,观看者应跟随滚动。为此,我将当前的 scrollX 和 scrollY 位置作为 data-* 属性存储在重复的 HTMLDocument 上。在创建最终 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 dataURL。Chrome 扩展程序有一个专门的 API chrome.tabs.captureVisibleTab()。
  2. 将 dataURL 转换为 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);

未来的改进

这款游戏的帧率出奇地好,但还可以更上一层楼。一项改进是消除将 dataURL 转换为 Blob 的开销。遗憾的是,chrome.tabs.captureVisibleTab() 仅向我们提供 dataURL。如果它返回了 Blob 或类型化数组,我们可以直接通过 WebSocket 发送,而无需自行转换为 Blob。请为 crbug.com/32498 加星标,以便实现此目标!

方法 4:WebRTC - 真正的未来

最后但同样重要的是!

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

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

总结

因此,利用当今的 Web 技术,可以实现浏览器标签页共享!

不过,这句话不能全信。虽然本文中的技术很巧妙,但它们在某些方面无法提供出色的分享用户体验。随着 WebRTC 标签页内容捕获工作的推进,这一切都将发生改变,但在它成为现实之前,我们只能使用浏览器插件或有限的解决方案,例如此处介绍的解决方案。

还有其他技巧?发布评论!