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

過去幾年,我協助幾家公司使用瀏覽器技術,實現類似螢幕分享的功能。根據我的經驗,單純使用網路平台技術 (即不使用外掛程式) 實作 VNC 是一項艱鉅的任務。有許多事項需要考量,也有許多挑戰需要克服。例如轉送滑鼠游標位置、轉送按鍵動作,以及以 60 FPS 的速度重新繪製完整的 24 位元色彩。

擷取分頁內容

如果我們移除傳統分享螢幕畫面的複雜性,並專注於分享瀏覽器分頁的內容,問題就會大幅簡化為:a.) 擷取目前狀態的可見分頁,以及 b.) 透過網路傳送該「影格」。基本上,我們需要一種方法來擷取 DOM 快照並分享。

分享內容很簡單。Websocket 能夠以不同格式 (字串、JSON、二進位) 傳送資料。快照部分是更棘手的問題。html2canvas 等專案已透過在 JavaScript 中重新實作瀏覽器的轉譯引擎,解決 HTML 螢幕截圖的問題!另一個例子是 Google 意見回饋,但這不是開放原始碼。這類專案非常酷,但速度也慢得驚人。能達到 1 fps 的處理量就很幸運了,更別說達到 60 fps。

本文將介紹幾個我最喜歡的概念驗證解決方案,用於「分享」分頁畫面。

方法 1:Mutation Observer + WebSocket

今年稍早,+Rafael Weinstein 示範了其中一種鏡像分頁的方法。他的技術使用變動觀察器和 WebSocket。

基本上,分享者分享的分頁會監控網頁變更,並使用 WebSocket 將差異傳送給檢視者。當使用者捲動網頁或與網頁互動時,觀察器會偵測到這些變更,並使用 Rafael 的變動摘要程式庫回報給檢視器。這可確保效能。系統不會為每個影格傳送整個頁面。

正如 Rafael 在影片中指出,這只是概念驗證。不過,我認為這是一種巧妙的方法,可以將較新的平台功能 (例如 Mutation Observer) 與較舊的功能 (例如 Websocket) 結合。

方法 2:HTMLDocument + 二進位 WebSocket 的 Blob

接下來這個方法是我最近才想到的。這與 Mutation Observers 方法類似,但不會傳送摘要差異,而是建立整個 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 網址 (例如來自不同來源) 的內容中查看時不會中斷。

最後一項調整是新增捲動支援。簡報者捲動頁面時,觀眾應跟著捲動。為此,我將目前的 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。

這個來源也位於 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 或 Typed Array,我們可以直接透過 WebSocket 傳送,而不必自行轉換為 Blob。請為 crbug.com/32498 加上星號,協助我們實現這個目標!

方法 4:WebRTC - 真正的未來

最後但同樣重要的是!

WebRTC 將實現瀏覽器螢幕分享的未來。2012 年 8 月 14 日,團隊提出 WebRTC Tab Content Capture API,用於分享分頁內容:

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

結論

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

不過,這句話不一定完全正確。雖然本文介紹的技術很實用,但仍無法提供完美的分享體驗。WebRTC 分頁內容擷取功能將改變這一切,但在這項功能推出前,我們只能使用瀏覽器外掛程式或有限的解決方案,例如本文介紹的方案。

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