HTML5 でブラウザタブを画面共有しますか?

ここ数年、私は複数の企業がブラウザ テクノロジーのみを使用して画面共有のような機能を実現できるよう支援してきました。私の経験では、ウェブ プラットフォーム技術のみ(プラグインなし)で VNC を実装するのは難しい問題です。考慮すべき点や克服すべき課題はたくさんあります。マウス ポインタの位置の転送、キー入力の転送、60 fps で 24 ビットカラーの完全な再描画の実現など、問題は数多くあります。

タブのコンテンツのキャプチャ

従来の画面共有の複雑さを排除し、ブラウザタブのコンテンツの共有に焦点を当てると、問題は大幅に簡素化され、a.)表示されているタブを現在の状態でキャプチャし、b.)その「フレーム」をワイヤーを介して送信するだけになります。基本的には、DOM をスナップショットして共有する方法が必要です。

共有は簡単です。WebSocket は、さまざまな形式(文字列、JSON、バイナリ)でデータを送信できます。スナップショット作成部分は、はるかに難しい問題です。html2canvas などのプロジェクトでは、JavaScript でブラウザのレンダリング エンジンを再実装することで、HTML のスクリーンショットのキャプチャに取り組んでいます。別の例として、Google フィードバックがありますが、これはオープンソースではありません。このようなプロジェクトは非常にクールですが、非常に遅くもあります。1 fps のスループットが得られれば幸運で、60 fps は夢のまた夢です。

この記事では、タブを「画面共有」するための概念実証ソリューションについて説明します。

方法 1: Mutation Observers + WebSocket

タブをミラーリングする 1 つの方法は、今年初めに +Rafael Weinstein がデモしました。この手法では、Mutation Observer と WebSocket を使用します。

基本的に、プレゼンターが共有しているタブはページの変更を監視し、WebSocket を使用して差分を視聴者に送信します。ユーザーがページをスクロールしたり操作したりすると、オブザーバーがこれらの変更を検出し、Rafael のミューテーション サマリー ライブラリを使用してビューアに報告します。これにより、パフォーマンスが維持されます。フレームごとにページ全体が送信されるわけではありません。

ラファエルが動画で指摘しているように、これは単なる概念実証です。それでも、Mutation Observer などの新しいプラットフォーム機能と、Websocket などの古い機能を組み合わせるのは良い方法だと思います。

方法 2: HTMLDocument の Blob + バイナリ WebSocket

次に紹介する方法は、最近思いついたものです。これは Mutation Observer のアプローチに似ていますが、サマリー diff を送信する代わりに、HTMLDocument 全体の Blob クローンを作成し、バイナリ WebSocket 経由で送信します。設定は次のとおりです。

  1. ページ上のすべての URL を絶対 URL に書き換えます。これにより、静的画像アセットと 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() には、相対 URL やスキームなしの URL を絶対 URL に書き換える単純な正規表現が含まれています。これは、画像、CSS、フォント、スクリプトが、(別のオリジンからなど)Blob URL のコンテキストで表示されたときに破損しないようにするために必要です。

最後に、スクロール サポートを追加しました。プレゼンターがページをスクロールすると、視聴者もそれに沿ってスクロールする必要があります。そのためには、現在の scrollXscrollY の位置を、重複する HTMLDocumentdata-* 属性として保存します。最終的な 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

Google I/O 2012 では、ブラウザタブの内容を画面共有する別の方法をデモしました。ただし、これはチートです。純粋な HTML5 マジックではなく、Chrome Extension API が必要です。

このツールのソースも GitHub に公開されていますが、要点は次のとおりです。

  1. 現在のタブを .png dataURL としてキャプチャします。Chrome 拡張機能には、その chrome.tabs.captureVisibleTab() 用の API があります。
  2. dataURL を Blob に変換します。convertDataURIToBlob() ヘルパーをご覧ください。
  3. socket.responseType='blob' を設定してバイナリ ウェブソケットを使用して、各 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 が返された場合は、Blob に変換するのではなく、WebSocket 経由で直接送信できます。実現に向けて、crbug.com/32498 にスターを付けてください。

方法 4: WebRTC - 真の未来

最後に

ブラウザでの画面共有の未来は WebRTC によって実現されます。2012 年 8 月 14 日、タブの内容を共有するための WebRTC Tab Content Capture API が提案されました。

この機能が準備されるまでは、方法 1 ~ 3 しかありません。

まとめ

つまり、現在のウェブ技術ではブラウザのタブを共有できるのです。

ただし、この記述は鵜呑みにしないでください。この記事で紹介する手法は便利ですが、共有の UX としては不十分です。これは、WebRTC タブ コンテンツ キャプチャの取り組みによってすべて変わるでしょうが、それが現実になるまでは、ブラウザ プラグインや、ここで説明するような限定的なソリューションに頼るしかありません。

他にもテクニックはありますか?コメントを投稿する