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

François Beaufort
François Beaufort

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

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

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

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">をご覧ください。
ユーザーがキャプチャしたタブをスクロールしてズームする(デモ)。

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;

ユーザーがウィンドウまたは画面の共有を選択した場合、それは当面対象外です。ただし、タブを共有することを選択した場合は、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 オブジェクトは特定の capture-sessionに一意に関連付けられますが、別のキャプチャ セッションに関連付けることはできず、また、そのオブジェクトが定義されているページを移動しても存続しません。ただし、キャプチャ セッションは、キャプチャしたページのナビゲーション後も維持されます。

ユーザーに権限プロンプトを表示するには、ユーザー操作が必要です。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: 水平スクロールと垂直スクロールの、スクロールの大きさ(ピクセル単位)。これらの値は、元の wheel イベントと比較して反転されています。

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() の呼び出しは、権限により制限されます。他の読み取り専用ズームメソッドの呼び出しは、イベントのリッスンと同様に「free」です。

<ph type="x-smartling-placeholder">

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

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

フィードバック

Chrome チームとウェブ標準コミュニティは、Captured Surface Control の使用体験についてお聞かせください。

デザインについて教えてください

キャプチャした表面のキャプチャに関して、想定どおりに機能していないものはありますか?あるいは、アイデアを実装するために不足しているメソッドやプロパティがないでしょうか。セキュリティ モデルについて質問や意見がある場合は、GitHub リポジトリで仕様に関する問題を報告するか、既存の問題にご意見をお寄せください。

実装に問題がある場合

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