在过去几年中,我帮助过几家不同的公司,仅使用浏览器技术就实现了类似屏幕共享的功能。根据我的经验,仅通过 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 发送。以下是具体设置步骤:
- 将网页上的所有网址重写为绝对网址。这样可以防止静态图片和 CSS 资源包含损坏的链接。
- 克隆页面的文档元素:
document.documentElement.cloneNode(true); - 使用 CSS
pointer-events: 'none';user-select:'none';overflow:hidden;将克隆设为只读、不可选择,并防止滚动 - 捕获网页的当前滚动位置,并将其作为
data-*属性添加到重复网页中。 - 基于重复项的
.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,但要点如下:
- 将当前标签页捕获为 .png dataURL。Chrome 扩展程序有一个专门的 API
chrome.tabs.captureVisibleTab()。 - 将 dataURL 转换为
Blob。请参阅convertDataURIToBlob()辅助程序。 - 通过设置
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 标签页内容捕获工作的推进,这一切都将发生改变,但在它成为现实之前,我们只能使用浏览器插件或有限的解决方案,例如此处介绍的解决方案。
还有其他技巧?发布评论!