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

在过去的几年里,我帮助几家不同的公司只使用浏览器技术就实现了类似屏幕共享的功能。根据我的经验,仅在网络平台技术中(即不使用插件)实现 VNC 并非易事。需要考虑的事情有很多,要克服的挑战也很多。中继鼠标指针位置、转发按键,以及以 60fps 的速率实现完整的 24 位颜色重绘等只是众多问题。

捕获标签页内容

如果我们消除传统屏幕共享的复杂性,而专注于共享浏览器标签页的内容,问题就会大大简化为 a.) 捕获当前状态下的可见标签页,以及 b.) 跨线发送该“框架”。从本质上讲,我们需要一种对 DOM 截取快照并将其共享出来的方法。

分享部分非常简单。Websocket 能够非常有效地以不同格式(字符串、JSON、二进制)发送数据。拍摄快照是一个更困难的问题。html2canvas 等项目通过在 JavaScript 中重新实现浏览器的呈现引擎,解决了 HTML 截屏问题!另一个示例是 Google 反馈,尽管它不是开源的。这些类型的项目非常,但速度也非常慢。你是幸运的,能够实现 1fps 的吞吐量,远低于令人梦寐以求的 60fps。

本文介绍了我最喜欢的一些与标签页“共享屏幕”相关的概念验证解决方案。

方法 1:变更观察器 + WebSocket

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

从本质上讲,演示者分享的标签页会监控页面更改,并使用 websocket 向查看器发送 diff。当用户滚动页面或与页面互动时,观察者会获取这些更改,并使用 Rafael 的突变摘要库将这些更改报告给观看者。这样可以确保一切保持高效。但并不会针对每一帧发送整个网页,

正如 Rafael 在视频中指出的,这只是概念验证。尽管如此,我认为将较新的平台功能(例如 Mutation Observer)与旧版平台功能(例如 Websockets)相结合还是一种很好的方式。

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

我最近发现的下一个方法是。它与变更观察者方法类似,但并非发送摘要差异,而是创建整个 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、字体和脚本时,图片、CSS、字体和脚本都不会中断。

我最后进行的调整是添加滚动支持。当演示者滚动页面时,观看者应跟上。为此,我将当前的 scrollXscrollY 位置作为 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 Extension API + 二进制 WebSocket

2012 年 Google I/O 大会上,我展示了另一种通过屏幕共享浏览器标签页内容的方法。不过,这是骗子。它需要 Chrome 扩展程序 API,而不是纯粹的 HTML5 功能。

GitHub 上的源代码也在 GitHub 上找到,但要点是:

  1. 将当前标签页截取为 .png data网址 格式。Chrome 扩展程序有一个适用于该 chrome.tabs.captureVisibleTab() 的 API。
  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 Tab Content Capture API 来分享标签页内容:

这个人还没准备好,我们只剩下 1-3 种方法了。

总结

因此,通过当今的网络技术可以实现浏览器标签页共享!

但是...这种说法应该持怀疑态度。虽然本文中的技巧简洁明了,但无论从哪种方式来看,都无法提供出色的共享用户体验。这一切都会因 WebRTC 标签页内容捕获方面的工作而改变,但在实现这一目标之前,我们还剩下浏览器插件或诸如本文介绍的此类有限解决方案。

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