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

François Beaufort
François Beaufort

ウェブ プラットフォームでは、すでに Screen Capture API を使用してタブ、ウィンドウ、画面を共有できるようになっています。ウェブアプリから getDisplayMedia() を呼び出すと、タブ、ウィンドウ、画面をウェブアプリと MediaStreamTrack 動画として共有するよう求めるメッセージが表示されます。

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

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

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

Captured Surface Control を使用する理由

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

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

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

Captured Surface Control を使用するにはどうすればよいですか?

キャプチャされたサーフェス コントロールを使用するには、いくつかの手順が必要です。たとえば、ブラウザタブを明示的にキャプチャし、キャプチャしたタブのスクロールやズームを行う前にユーザーから許可を得るなどです。

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

まず、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() 呼び出しのみです。ユーザーがウェブアプリでズームイン ボタンまたはズームアウト ボタンをクリックした場合、そのユーザー操作が発生します。ただし、アプリで先にスクロール制御を提供したい場合は、スクロールはユーザー操作ではないことに留意する必要があります。まず、次の例に示すように、「スクロール開始」ボタンをユーザーに表示するのも 1 つの方法です。

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> 要素を使用していると仮定して、次のコードは、キャプチャしたタブに send wheel イベントをリレーする方法を示しています。

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 further explained 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 は範囲外にはならないため、これを使用してユーザーに許可を求めることは安全です)。

Zoom

キャプチャしたタブのズームレベルの操作は、次の 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 を有効にする

Captured Surface Control API は、パソコンの Chrome で Captured Surface Control フラグの背後にあるときに利用可能で、chrome://flags/#captured-surface-control で有効にできます。

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

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

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

デモ

Glitch でデモを実行すると、Captured Surface Control を使ってプレイできます。必ずソースコードを確認してください。

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

Captured Surface Control に関する、注意すべき主な動作の違いは次のとおりです。

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

フィードバック

Chrome チームとウェブ標準コミュニティでは、Captured Surface Control の使用体験についてご意見を伺いたいと思います。

デザインについてお聞かせください

Captured Surface Capture に何か問題はありますか?あるいは、アイデアを実装する必要があるメソッドやプロパティがありませんか?セキュリティ モデルについてご質問やご意見がある場合は、GitHub リポジトリで仕様に関する問題を提出するか、既存の問題にご意見をお寄せください。

実装に問題がある場合

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