過去幾年中,我曾協助幾間不同的公司只使用瀏覽器技術,就能夠實現類似螢幕分享的功能。根據我的經驗,只用網頁平台技術 (也就是沒有外掛程式) 實作 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 傳送。設定方式如下:
- 將網頁上所有的網址改寫為絕對網址。這樣一來,靜態圖片和 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、字型及指令碼時 (例如來自不同的來源) 時,圖片、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 Extension API + 二進位 WebSocket
在 2012 年 Google I/O 大會上,我示範了另一種分享螢幕畫面的方式,說明如何分享螢幕畫面的瀏覽器分頁內容。不過,這只是作弊這需要 Chrome Extension API:不是純粹的 HTML5 魔法。
這個項目的來源也在 GitHub 上,但要點如下:
- 將目前分頁擷取為 .png dataURL 格式。Chrome 擴充功能有適用於該
chrome.tabs.captureVisibleTab()
的 API。 - 將 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);
日後改進
對這個畫面來說,顯示影格速率很不錯,但也可以更佳。改善其中一個功能是移除將資料網址轉換為 Blob 的負擔。很遺憾,chrome.tabs.captureVisibleTab()
僅提供資料網址。如果傳回 Blob 或 Typed Array,我們可以直接透過 websocket 傳送該訊息,而不是將其轉換為 Blob。為達成此目標,請加上星號 crbug.com/32498!
方法 4:WebRTC - 真實未來
最後要說的!
未來使用瀏覽器分享螢幕畫面時,WebRTC 將能瞭解到。2012 年 8 月 14 日,該團隊提議使用 WebRTC 分頁內容擷取 API 來分享分頁內容:
在此之前,我們先結束方法 1-3。
結論
因此,今日的網路技術可以讓您分享瀏覽器分頁!
但是...這個說法應該以鹽度顆粒為根據。雖說精簡,但本文提到的技巧並非只有一種做法能提升分享使用者體驗。隨著 WebRTC 分頁內容擷取的幫助,一切都會有所改變,但在此之前,我們只保留瀏覽器外掛程式或有限的解決方案,例如本文介紹的解決方案。
還有其他技巧嗎?發布留言!