過去幾年,我協助幾家公司只使用瀏覽器技術,實現類似螢幕分享的功能。根據我的經驗,如果只在網路平台技術 (也就是沒有外掛程式) 中實作 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) 與較舊的平台功能 (例如 Websockets) 結合,是個不錯的做法。
方法 2:從 HTMLDocument 取得 Blob + 二進位 WebSocket
我最近才想到下一個方法。這與突變觀察工具方法相似,但它不會傳送摘要差異,而是建立整個 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 位置儲存為重複的 HTMLDocument 上的 data-* 屬性。在建立最終 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 魔法。
這個版本的source也已在 GitHub 上架,但重點如下:
- 擷取目前的分頁,並儲存為 .png 資料網址。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 或 Typed Array,我們可以直接透過 WebSocket 傳送,而不需要自行將 Blob 轉換為 Blob。請為 crbug.com/32498 按下星號,讓我們實現這個目標!
方法 4:WebRTC - 真正的未來
最後,
WebRTC 將實現瀏覽器中的螢幕分享功能。2012 年 8 月 14 日,該團隊提出了 WebRTC 分頁內容擷取 API,用於分享分頁內容:
在這個方法準備就緒之前,我們只能使用方法 1 到 3。
結論
因此,我們可以利用現今的網路技術分享瀏覽器分頁!
不過,這項說法應謹慎看待。雖然這麼做很方便,但本文的技術在某種程度上無法提供良好的分享使用者體驗。這一切都將隨著 WebRTC 分頁內容擷取功能的推出而改變,但在那之前,我們只能使用瀏覽器外掛程式或本文所述的有限解決方案。
還有其他技巧嗎?發布留言!