ここ数年、私はブラウザ技術のみを使用して画面共有のような機能を実現するお手伝いをいくつかの企業で行ってきました。私の経験から言うと、ウェブ プラットフォーム テクノロジーのみ(つまり、プラグインなし)で VNC を実装するのは困難な問題です。考慮すべきことがたくさんあり、克服すべき課題もたくさんあります。マウスポインタの位置の伝達、キーストロークの転送、60 fps での完全な 24 ビットカラーの再描画など、さまざまな問題があります。
タブのコンテンツをキャプチャしています
従来の画面共有の複雑さを取り除き、ブラウザタブのコンテンツの共有に焦点を当てると、問題は大幅に簡素化され、a.)表示されているタブを現在の状態でキャプチャすること、b.)その「フレーム」をネットワーク経由で送信すること、の 2 つになります。つまり、DOM のスナップショットを取得して共有する方法が必要です。
共有は簡単です。WebSocket は、さまざまな形式(文字列、JSON、バイナリ)でデータを送信できます。スナップショットの作成は、はるかに難しい問題です。html2canvas などのプロジェクトでは、ブラウザのレンダリング エンジンを JavaScript で再実装することで、HTML のスクリーン キャプチャに取り組んできました。もう 1 つの例は Google フィードバックです。これはオープンソースではありません。このようなプロジェクトは非常に魅力的ですが、非常に時間がかかります。1 fps のスループットが得られれば幸運で、望ましい 60 fps にはほど遠い状態です。
この記事では、タブの「画面共有」に関する、私のお気に入りの概念実証ソリューションをいくつか紹介します。
方法 1: Mutation Observer + WebSocket
タブをミラーリングする 1 つの方法は、今年初めに +Rafael Weinstein によって実証されました。この手法では、Mutation Observer と WebSocket を使用します。
基本的に、発表者が共有しているタブは、ページへの変更を監視し、WebSocket を使用して差分を閲覧者に送信します。ユーザーがページをスクロールしたり操作したりすると、オブザーバーがこれらの変更を検出し、Rafael のミューテーション サマリー ライブラリを使用してビューアに報告します。これにより、パフォーマンスが維持されます。フレームごとにページ全体が送信されるわけではありません。
動画の中で Rafael が指摘しているように、これは単なる概念実証です。それでも、Mutation Observer のような新しいプラットフォーム機能と Websockets のような古い機能を組み合わせる優れた方法だと思います。
方法 2: HTMLDocument + バイナリ WebSocket からの Blob
次の方法は、最近思いついたものです。これは Mutation Observer のアプローチに似ていますが、概要の差分を送信するのではなく、HTMLDocument 全体の Blob クローンを作成し、バイナリ WebSocket を介して送信します。設定手順は次のとおりです。
- ページ上のすべての URL を絶対 URL に書き換えます。これにより、静止画像と 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() には、相対 URL やスキームのない URL を絶対 URL に書き換えるためのシンプルな正規表現が含まれています。これは、blob URL のコンテキスト(別のオリジンなど)で表示されたときに、画像、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 サーバー、アプリ、ブックマークレットを作成しました。コードに興味がない場合は、動作の様子を短い動画でご覧ください。
今後の改善
最適化の 1 つは、フレームごとにドキュメント全体を複製しないことです。これは無駄であり、Mutation Observer の例ではうまく処理されています。もう 1 つの改善点は、urlsToAbsolute() で相対 CSS 背景画像を処理することです。これは、現在のスクリプトでは考慮されていません。
方法 3: Chrome 拡張機能 API + バイナリ WebSocket
Google I/O 2012 では、ブラウザタブのコンテンツを画面共有する別のアプローチをデモしました。ただし、これはチートです。これには Chrome 拡張機能 API が必要です。純粋な HTML5 のマジックではありません。
このソースも GitHub にありますが、要点は次のとおりです。
- 現在のタブを .png dataURL としてキャプチャします。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 または型付き配列が返された場合は、Blob への変換を自分で行うのではなく、WebSocket を介して直接送信できます。この機能の実現をご希望の場合は、crbug.com/32498 にスターを付けてください。
方法 4: WebRTC - 真の未来
最後になりましたが、
ブラウザでの画面共有の未来は、WebRTC によって実現されます。2012 年 8 月 14 日、チームはタブのコンテンツを共有するための WebRTC Tab Content Capture API を提案しました。
この方法が利用可能になるまでは、方法 1 ~ 3 を使用することになります。
まとめ
ブラウザタブの共有は、今日のウェブ技術で実現可能です。
ただし、この記述は慎重に受け止める必要があります。この記事で紹介した手法は、優れた共有 UX を実現するには不十分な点があります。これは WebRTC Tab Content Capture の取り組みによってすべて変わりますが、それが実現するまでは、ブラウザ プラグインや、ここで説明したような限定的なソリューションを使用する必要があります。
他にもテクニックをご存じですか?コメントを投稿