私は過去数年間、ブラウザ テクノロジーのみを使用して画面共有のような機能を実現できるよう、数社の企業をサポートしてきました。私の経験上、プラグインを使用しないウェブ プラットフォーム技術のみで VNC を実装することは難しい問題です。考慮すべきことがたくさんあり、克服すべき課題もたくさんあります。マウスポインタの位置のリレー、キー入力の転送、60 fps での完全な 24 ビットカラー リペイントなどは、問題のほんの一部にすぎません。
タブのコンテンツをキャプチャする
従来の画面共有の複雑さを解消し、ブラウザタブのコンテンツの共有に重点を置けば、問題は大幅に簡素化されます。a)表示されているタブの現在の状態でキャプチャし、b)その「フレーム」を有線で送信する。基本的には、DOM のスナップショットを作成して共有する方法が必要です。
共有は簡単です。WebSocket は、さまざまな形式(文字列、JSON、バイナリ)でデータを送信できます。スナップショットの部分は、はるかに難しい問題です。html2canvas などのプロジェクトでは、ブラウザのレンダリング エンジンを JavaScript で再実装することで、HTML のスクリーン キャプチャに取り組んでいます。もう一つの例として、Google フィードバックがありますが、これはオープンソースではありません。この種のプロジェクトは非常に魅力的ですが、非常に時間のかかるものでもあります。幸運にも、1 fps のスループットが得られますが、60 fps よりもはるかに少ないスループットが得られます。
この記事では、タブの「画面共有」に関する私のお気に入りの概念実証ソリューションをいくつか紹介します。
方法 1: Mutation Observer + WebSocket
タブのミラーリングのアプローチの 1 つが、今年初めに + Rafael Weinstein によって実証されました。彼の手法では、Mutation Observer と WebSocket を使用します。
基本的には、プレゼンターが共有しているタブがページの変更を監視し、WebSocket を使用して閲覧者に差分を送信します。ユーザーがページをスクロールしたり操作したりすると、オブザーバーはこれらの変更を取得し、ラファエルのミューテーション概要ライブラリを使用して閲覧者に報告します。これによりパフォーマンスが向上します。フレームごとにページ全体が送信されるわけではありません。
ラファエルさんが動画で指摘しているように、これは単なる概念実証にすぎません。それでも、Mutation Observers のような新しいプラットフォーム機能と WebSocket のような古い機能を組み合わせるのが良い方法だと思う。
方法 2: HTMLDocument + バイナリ WebSocket からの blob
次の方法は最近私に気付きました。これは Mutation Observers のアプローチと似ていますが、サマリー差分を送信する代わりに、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 Extension API + バイナリ WebSocket
私は Google I/O 2012 で、ブラウザタブのコンテンツを画面共有する別のアプローチを紹介しました。しかし、これはチートです。そのためには 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);
今後の改善点
フレームレートはこれに対して驚くほど優れていますが、もっと良くなる可能性もあります。改善策としては、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 のタブコンテンツ キャプチャの取り組みで変化しますが、現実となるまでは、ここで説明したようなブラウザ プラグインや限定的なソリューションが必要になります。
その他の手法コメントを投稿