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

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;

ユーザーがウィンドウまたは画面の共有を選択した場合、それは当面対象外です。ただし、タブを共有することを選択した場合は、Google が手続きを進めることがあります。

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 がキャプチャされたサーフェス コントロールにアクセスする方法を管理できます。セキュリティのトレードオフについて詳しくは、キャプチャされたサーフェス制御の説明のプライバシーとセキュリティに関する考慮事項をご覧ください。

デモ

Glitch でデモを実行すると、Captured Surface Control を試してみることができます。ソースコードを確認してください。

以前のバージョンの 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 は、再現可能なバグを共有するのに適しています。