要在 HTML5 中分享螢幕畫面嗎?

過去幾年,我協助幾家公司只使用瀏覽器技術,實現類似螢幕分享的功能。根據我的經驗,如果只在網路平台技術 (也就是沒有外掛程式) 中實作 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

我最近才想到下一個方法。這與突變觀察工具方法相似,但它不會傳送摘要差異,而是建立整個 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 位置儲存為重複的 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 魔法。

這個版本的原始碼也已在 GitHub 上架,但重點如下:

  1. 擷取目前的分頁,並儲存為 .png 資料網址。Chrome 擴充功能有專屬的 chrome.tabs.captureVisibleTab() API。
  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 或 Typed Array,我們可以直接透過 WebSocket 傳送,而不需要自行將 Blob 轉換為 Blob。請為 crbug.com/32498 按下星號,讓我們實現這個目標!

方法 4:WebRTC - 真正的未來

最後,

WebRTC 將實現瀏覽器中的未來螢幕分享功能。2012 年 8 月 14 日,團隊提出了 WebRTC 分頁內容擷取 API,用於分享分頁內容:

在這個方法準備就緒之前,我們只能使用方法 1 到 3。

結論

因此,我們可以利用現今的網路技術分享瀏覽器分頁!

不過,這項說法應謹慎看待。雖然這項做法很簡單,但在某種程度上,這類技術無法提供優質的分享使用者體驗。這一切都會隨著 WebRTC 分頁內容擷取作業而改變,但在實際實施前,我們只能使用瀏覽器外掛程式或本文所述的有限解決方案。

還有其他技巧嗎?發布留言!