キャプチャしたタブのスクロールとズーム

François Beaufort
François Beaufort

タブ、ウィンドウ、画面の共有は、ウェブ プラットフォームで Screen Capture API を使用してすでに可能です。ウェブアプリが getDisplayMedia() を呼び出すと、Chrome はタブ、ウィンドウ、または画面を MediaStreamTrack 動画としてウェブアプリと共有するようユーザーに求めるメッセージを表示します。

getDisplayMedia() を使用する多くのウェブアプリでは、キャプチャされたサーフェスの動画プレビューがユーザーに表示されます。たとえば、ビデオ会議アプリでは、この動画をリモート ユーザーにストリーミングするとともに、ローカルの HTMLVideoElement にレンダリングして、ローカル ユーザーが共有している内容のプレビューを常に表示します。

このドキュメントでは、Chrome の新しい Captured Surface Control API について説明します。この API を使用すると、ウェブアプリでキャプチャされたタブをスクロールしたり、キャプチャされたタブのズームレベルを読み書きしたりできます。

ユーザーがキャプチャしたタブをスクロールしてズームする(デモ)。

キャプチャされたサーフェス コントロールを使用する理由

すべてのビデオ会議アプリには同じ欠点があります。キャプチャされたタブまたはウィンドウを操作するには、そのサーフェスに切り替える必要があり、ビデオ会議アプリから離れてしまいます。これにはいくつかの課題があります。

  • ピクチャー イン ピクチャーを使用するか、ビデオ会議タブと共有タブに別々の並列ウィンドウを使用する場合を除き、キャプチャしたアプリとリモート ユーザーの動画は同時に表示できません。小さな画面では、この操作が難しい場合があります。
  • ビデオ会議アプリとキャプチャされたサーフェスを切り替える必要があるため、ユーザーの負担が増大します。
  • ユーザーがビデオ会議アプリから離れている間、ビデオ会議アプリによって公開されているコントロール(埋め込みチャットアプリ、絵文字のリアクション、通話への参加をリクエストするユーザーに関する通知、マルチメディアとレイアウトのコントロール、その他の便利なビデオ会議機能など)にアクセスできなくなります。
  • プレゼンターは、リモート参加者に操作を委任できません。そのため、リモート ユーザーがプレゼンターにスライドの変更、上下のスクロール、ズームレベルの調整を依頼する、というよくあるシナリオが発生します。

Captured Surface Control API は、これらの問題に対処します。

キャプチャされたサーフェス コントロールを使用するにはどうすればよいですか?

キャプチャされたサーフェス コントロールを正常に使用するには、ブラウザタブを明示的にキャプチャし、キャプチャされたタブをスクロールおよびズームする前にユーザーから権限を取得するなど、いくつかの手順が必要です。

ブラウザタブをキャプチャする

まず、getDisplayMedia() を使用して共有するサーフェスを選択するようユーザーにプロンプトを表示します。このプロセスで、キャプチャ セッションに CaptureController オブジェクトを関連付けます。まもなく、このオブジェクトを使用してキャプチャされたサーフェスを制御する予定です。

const controller = new CaptureController();
const stream = await navigator.mediaDevices.getDisplayMedia({ controller });

次に、キャプチャしたサーフェスのローカル プレビューを <video> 要素の形式で生成します。

const previewTile = document.querySelector('video');
previewTile.srcObject = stream;

ユーザーがウィンドウまたは画面の共有を選択した場合は、現時点ではサポート範囲外ですが、タブの共有を選択した場合は、対応できます。

const [track] = stream.getVideoTracks();

if (track.getSettings().displaySurface !== 'browser') {
  // Bail out early if the user didn't pick a tab.
  return;
}

権限プロンプト

特定の CaptureController オブジェクトで sendWheel() または setZoomLevel() を初めて呼び出すと、権限プロンプトが表示されます。ユーザーが権限を付与すると、その CaptureController オブジェクトに対するこれらのメソッドの呼び出しが許可されます。ユーザーが権限を拒否すると、返された Promise は拒否されます。

CaptureController オブジェクトは特定のキャプチャ セッションに一意に関連付けられ、別のキャプチャ セッションに関連付けることはできません。また、定義されているページのナビゲーションを行った後も保持されません。ただし、キャプチャ セッションは、キャプチャされたページのナビゲーション後も存続します。

ユーザーに権限プロンプトを表示するには、ユーザーの操作が必要です。ユーザー操作が必要な呼び出しは、sendWheel()setZoomLevel() のみで、プロンプトを表示する必要がある場合に限られます。ユーザーがウェブアプリでズームインまたはズームアウト ボタンをクリックした場合、そのユーザー操作は当然のことですが、アプリで最初にスクロール コントロールを提供する場合、スクロールはユーザー操作に該当しないことをデベロッパーは念頭に置く必要があります。たとえば、まずユーザーに「スクロールを開始」ボタンを提示します。

const startScrollingButton = document.querySelector('button');

startScrollingButton.addEventListener('click', async () => {
  try {
    const noOpWheelAction = {};

    await controller.sendWheel(noOpWheelAction);
    // The user approved the permission prompt.
    // You can now scroll and zoom the captured tab as shown later in the article.
  } catch (error) {
    return; // Permission denied. Bail.
  }
});

スクロール

sendWheel() を使用すると、キャプチャ アプリは、タブのビューポート内の任意の座標で、任意の値のホイール イベントを送信できます。キャプチャされたアプリから、イベントがユーザーの直接操作と区別できない。

キャプチャ アプリが "previewTile" という <video> 要素を使用していると仮定すると、次のコードは、キャプチャされたタブにホイール イベントをリレー送信する方法を示しています。

const previewTile = document.querySelector('video');

previewTile.addEventListener('wheel', async (event) => {
  // Translate the offsets into coordinates which sendWheel() can understand.
  // The implementation of this translation is explained further below.
  const [x, y] = translateCoordinates(event.offsetX, event.offsetY);
  const [wheelDeltaX, wheelDeltaY] = [-event.deltaX, -event.deltaY];

  try {
    // Relay the user's action to the captured tab.
    await controller.sendWheel({ x, y, wheelDeltaX, wheelDeltaY });
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

メソッド sendWheel() は、次の 2 つの値セットを含むディクショナリを受け取ります。

  • xy: ホイール イベントを配信する座標。
  • wheelDeltaXwheelDeltaY: 水平方向と垂直方向のスクロール量(ピクセル単位)。これらの値は、元のホイール イベントと比較して反転されています。

translateCoordinates() の可能な実装は次のとおりです。

function translateCoordinates(offsetX, offsetY) {
  const previewDimensions = previewTile.getBoundingClientRect();
  const trackSettings = previewTile.srcObject.getVideoTracks()[0].getSettings();

  const x = trackSettings.width * offsetX / previewDimensions.width;
  const y = trackSettings.height * offsetY / previewDimensions.height;

  return [Math.floor(x), Math.floor(y)];
}

上記のコードには、3 種類のサイズが使用されています。

  • <video> 要素のサイズ。
  • キャプチャされたフレームのサイズ(ここでは trackSettings.widthtrackSettings.height で表されます)。
  • タブのサイズ。

<video> 要素のサイズはキャプチャ アプリのドメイン内にあり、ブラウザには不明です。タブのサイズはブラウザのドメイン内にあり、ウェブアプリには認識されません。

ウェブアプリは translateCoordinates() を使用して、<video> 要素を基準としたオフセットを、動画トラック独自の座標空間内の座標に変換します。ブラウザは同様に、キャプチャされたフレームのサイズとタブのサイズを変換し、ウェブアプリの想定に応じたオフセットでスクロール イベントを配信します。

sendWheel() によって返される Promise は、次の場合に拒否される可能性があります。

  • キャプチャ セッションがまだ開始されていない場合、またはすでに停止している場合(sendWheel() アクションがブラウザによって処理されている間に非同期で停止した場合を含む)。
  • ユーザーがアプリに sendWheel() の使用権限を付与していない場合。
  • キャプチャ アプリが [trackSettings.width, trackSettings.height] の外側の座標でスクロール イベントを配信しようとした場合。これらの値は非同期で変更される可能性があるため、エラーをキャッチして無視することをおすすめします。(通常、0, 0 は範囲外にならないため、権限をリクエストするメッセージに使用しても安全です)。

ズーム

キャプチャされたタブのズームレベルを操作するには、次の CaptureController サーフェスを使用します。

  • getSupportedZoomLevels() は、ブラウザでサポートされているズームレベルのリストを返します。このリストは、「デフォルトのズームレベル」の割合(100% と定義)で表されます。このリストは単調に増加し、値 100 が含まれています。
  • getZoomLevel() は、タブの現在のズームレベルを返します。
  • setZoomLevel() は、タブのズームレベルを getSupportedZoomLevels() にある整数値に設定し、成功すると Promise を返します。キャプチャ セッションの終了時にズームレベルがリセットされないことに注意してください。
  • oncapturedzoomlevelchange を使用すると、キャプチャされたタブのズームレベルの変更をリッスンできます。ユーザーは、キャプチャ アプリまたはキャプチャされたタブの直接操作でズームレベルを変更する可能性があります。

setZoomLevel() の呼び出しは権限によって制限されます。他の読み取り専用ズーム メソッドの呼び出しは、イベントのリッスンと同様に「自由」です。

次の例は、既存のキャプチャ セッションでキャプチャされたタブのズームレベルを上げる方法を示しています。

const zoomIncreaseButton = document.getElementById('zoomInButton');

zoomIncreaseButton.addEventListener('click', async (event) => {
  const levels = CaptureController.getSupportedZoomLevels();
  const index = levels.indexOf(controller.getZoomLevel());
  const newZoomLevel = levels[Math.min(index + 1, levels.length - 1)];

  try {
    await controller.setZoomLevel(newZoomLevel);
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

次の例は、キャプチャしたタブのズームレベルの変更に応答する方法を示しています。

controller.addEventListener('capturedzoomlevelchange', (event) => {
  const zoomLevel = controller.getZoomLevel();
  document.querySelector('#zoomLevelLabel').textContent = `${zoomLevel}%`;
});

特徴検出

ホイール イベントの送信がサポートされているかどうかを確認するには、次のコマンドを使用します。

if (!!window.CaptureController?.prototype.sendWheel) {
  // CaptureController sendWheel() is supported.
}

ズームの制御がサポートされているかどうかを確認するには、次のコマンドを使用します。

if (!!window.CaptureController?.prototype.setZoomLevel) {
  // CaptureController setZoomLevel() is supported.
}

キャプチャされたサーフェス コントロールを有効にする

Captured Surface Control API は、デスクトップ版 Chrome の Captured Surface Control フラグで利用できます。このフラグは chrome://flags/#captured-surface-control で有効にできます。

この機能は、パソコン版 Chrome 122 以降のオリジン トライアルにも導入されます。この機能により、デベロッパーはサイト訪問者から実際のユーザーデータを収集できます。オリジン トライアルとその仕組みの詳細については、オリジン トライアルの開始をご覧ください。

セキュリティとプライバシー

"captured-surface-control" 権限ポリシーを使用すると、キャプチャ アプリと埋め込まれたサードパーティの iframe がキャプチャされたサーフェス コントロールにアクセスする方法を管理できます。セキュリティのトレードオフについて詳しくは、キャプチャされたサーフェス制御の説明のプライバシーとセキュリティに関する考慮事項をご覧ください。

デモ

Captured Surface Control を試すには、Glitch でデモを実行します。ソースコードを確認してください。

以前のバージョンの Chrome からの変更点

キャプチャされたサーフェス コントロールの動作には、次の主な違いがあります。

  • Chrome 124 以前:
    • 権限が付与されている場合、その権限のスコープは、キャプチャ元ではなく、その CaptureController に関連付けられたキャプチャ セッションに限定されます。
  • Chrome 122 の場合:
    • getZoomLevel() は、タブの現在のズームレベルを含む Promise を返します。
    • ユーザーがアプリの使用許可を付与しなかった場合、sendWheel() はエラー メッセージ "No permission." とともに拒否された Promise を返します。Chrome 123 以降では、エラータイプは "NotAllowedError" です。
    • oncapturedzoomlevelchange は使用できません。この機能は setInterval() を使用してポリフィルできます。

フィードバック

Chrome チームとウェブ標準コミュニティは、Captured Surface Control に関するご意見をお待ちしています。

デザインについて

キャプチャされたサーフェス キャプチャが想定どおりに動作しない点はありますか?または、アイデアを実装するために必要なメソッドやプロパティが不足している場合は、セキュリティ モデルについてご質問やご意見がございましたら、GitHub リポジトリで仕様に関する問題を報告するか、既存の問題にコメントを追加してください。

実装に関する問題

Chrome の実装にバグが見つかりましたか?それとも、実装が仕様と異なるのでしょうか?https://new.crbug.com でバグを報告してください。できるだけ詳細な情報と、再現手順を記載してください。Glitch は、再現可能なバグを共有するのに適しています。