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

過去幾年中,我曾協助幾間不同的公司只使用瀏覽器技術,就能夠實現類似螢幕分享的功能。根據我的經驗,只用網頁平台技術 (也就是沒有外掛程式) 實作 VNC 是相當困難的問題。有很多需要考量的事項,還有許多挑戰需要克服。轉送滑鼠指標位置、轉發按鍵動作,以及以 60fps 重新繪製 24 位元的全彩圖像,只是其中一小部分的問題。

擷取分頁內容

如果我們移除複雜的傳統螢幕畫面分享功能,並將重點放在分享瀏覽器分頁的內容,問題就可大幅簡化:) 擷取目前狀態的可見分頁,以及 b.) 傳輸該「頁框」。基本上,我們需要能對 DOM 進行快照並與他人分享。

要分享檔案很簡單。WebSocket 非常能夠以不同的格式 (字串、JSON、二進位) 傳送資料。建立快照的部分比較困難。html2canvas 這類專案已在 JavaScript 中重新導入瀏覽器的算繪引擎,進而成功擷取 HTML 螢幕畫面!另一個範例是 Google 意見回饋,但並非開放原始碼。這類專案「非常」很酷,但速度也非常慢。您很幸運能夠獲得 1fps 的處理量,遠低於所選的 60 FPS。

本文會介紹幾個我最愛的「螢幕分享」分頁概念驗證解決方案。

方法 1:變動觀察器 + WebSocket

今年稍早 +Rafael Weinstein 示範了鏡像分頁的其中一種方法。他的技術使用 Mutation Observers 和 WebSocket。

基本上,簡報者分享監控畫面的分頁,會透過 websocket 傳送差異給檢視者。當使用者捲動頁面或與網頁互動時,觀察器會擷取這些變更,並使用 Rafael 的異動摘要程式庫向檢視者回報。藉此維持高效能。系統不會為每個頁框傳送整個網頁。

Rafael 在影片中提到,這只是一個概念驗證。然而,我認為這是整合 Mutation Observers 這類新平台功能與 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() 包含簡單的規則運算式,會將相對/無法配置的網址重新寫入絕對網址。此為必要步驟,確保在 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 Extension API:不是純粹的 HTML5 魔法。

這個項目的來源也在 GitHub 上,但要點如下:

  1. 將目前分頁擷取為 .png dataURL 格式。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);

日後改進

對這個畫面來說,顯示影格速率很不錯,但也可以更佳。改善其中一個功能是移除將資料網址轉換為 Blob 的負擔。很遺憾,chrome.tabs.captureVisibleTab() 僅提供資料網址。如果傳回 Blob 或 Typed Array,我們可以直接透過 websocket 傳送該訊息,而不是將其轉換為 Blob。為達成此目標,請加上星號 crbug.com/32498

方法 4:WebRTC - 真實未來

最後要說的!

未來使用瀏覽器分享螢幕畫面時,WebRTC 將能瞭解到。2012 年 8 月 14 日,該團隊提議使用 WebRTC 分頁內容擷取 API 來分享分頁內容:

在此之前,我們先結束方法 1-3。

結論

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

但是...這個說法應該以鹽度顆粒為根據。雖說精簡,但本文提到的技巧並非只有一種做法能提升分享使用者體驗。隨著 WebRTC 分頁內容擷取的幫助,一切都會有所改變,但在此之前,我們只保留瀏覽器外掛程式或有限的解決方案,例如本文介紹的解決方案。

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