Captured Surface Control APIを使ってWebRTC経由で画面共有の遠隔操作をする

はじめに

Captured Surface Control APIを利用すると、共有中の画面に対するスクロールやズームの操作をJavaScriptで行うことができます。
この機能の何が嬉しいのかと言うと、オンラインでプレゼンをしているときに発表者以外の参加者がスライド送りを担当したり、miroを使ったミーティング中に参加者全員が共有中の画面をズームをしたりできるようになります。
今回は、Captured Surface Control APIとWebRTCのDataChannelを組み合わせて、画面共有中にリモートからスクロールの操作をできるサンプルを作ってみたので、つまづいたポイントなどをブログに残しておこうと思います。

Captured Surface Control APIの現状

記事作成時点でCaptured Surface Control APIは開発中のステータスとなっており、Chrome 122以降でのOrigin Trial、または chrome://flags/#captured-surface-control フラグの有効化によって利用できます。
各ブラウザの対応状況は、Chromeのみの対応となっており、FirefoxSafariは未対応です。
Captured Surface Control APIの正式リリースはまだですが、Google Meetはすでにこの機能を利用しているようです。

実装

実際にサンプルアプリケーションを作って動作を確認してみます。
今回は以下の環境でサンプルを作りました。

  • macOS Sonoma 14.6.1
  • Google Chrome 128.0.6613.138
    • あらかじめ chrome://flags/#captured-surface-control フラグを有効化しておきます
  • @skyway-sdk/room 1.9.2

サンプルコードはこのリポジトリに置いてあります。

github.com

Captured Surface Control APIの権限を取得する

Captured Surface Control APIは画面共有の権限に加えて、操作用の権限を追加で取得する必要があります。
まず、以下のコードで画面共有のMediaStreamを取得します。

const controller = new CaptureController();

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

// video要素に表示する
const screenStream = new LocalVideoStream(stream.getVideoTracks()[0]);
screenStream.attach(localVideoElement);

getDisplayMedia() の引数として CaptureControllerインスタンスを渡すのがポイントです。
これによって、操作を行うcontrollerと画面共有中のセッションとの紐付けが行われます。

画面共有を開始した後で、以下のコードでCaptured Surface Control APIを利用するための追加の権限を取得します。

await controller.sendWheel({});

つまづいたポイントは、権限を取得するタイミングです。
最初は、画面共有の開始とCaptured Surface Control APIの権限取得を同時に行おうとしましたが、 UnknownError: Unknown error. というエラーが出てCaptured Surface Control APIの権限の取得ができませんでした。
Captured Surface Control APIの権限取得は独立したユーザー操作で行う必要があるようです。
サンプルでは、最初にStart Screen Shareボタンを押して画面共有を開始した後、Allow Remote Controlボタンを押してCaptured Surface Control APIの権限取得を行うようにしています。
Captured Surface Control APIの権限は、 sendWheel()setZoomLevel() を最初に呼び出したときにプロンプトが表示され、そこで許可した場合に取得できます。
従って、 sendWheel()setZoomLevel() の最初の呼び出しは操作としては反映されません。
そのため、サンプルでは権限取得ボタンをクリックした際に空オブジェクトを指定して sendWheel() を実行し、Captured Surface Control APIの権限の取得のみを行うようにしています。

スクロールの情報をWebRTCのDataChannelで送受信する

スクロールの位置や移動量の情報は、HTMLVideoElementのwheelイベントで取得できます。
取得した値は、SkyWayのDataStreamを利用して送信します。
DataStreamの実態はWebRTCでP2PのDataChannelなので、オプションを指定して動作を柔軟に変更できます。
今回は以下のオプションを指定しました。

const dataStream = await SkyWayStreamFactory.createDataStream({
  maxRetransmits: 0,
  ordered: true,
});

今回送るデータは画面のスクロールに関する値なので、一部が欠損していたとしても後続のデータが送られてくるならばあまり問題にはなりません。
一方で、値の順序が逆転してしまうと、下にスクロールしたはずなのに一瞬上にスクロールされてしまうという問題が起きる可能性があります。
送るデータの特性を踏まえ、今回は maxRetransmits を0にして再送は無し、orderedtrue にして順序保証は有りという設定にしました。

データを受信する側では、受け取った値を自分の画面の座標に合わせた値に変換してから、sendWheel() に渡してスクロールの操作として反映させます。

const { offsetX, offsetY, deltaX, deltaY } = message;
const [x, y] = translateCoordinates(offsetX, offsetY); // 略
const [wheelDeltaX, wheelDeltaY] = [-deltaX, -deltaY];

await controller.sendWheel({ x, y, wheelDeltaX, wheelDeltaY });

ここでつまづいたポイントとしては、共有中の画面の映像を表示しているウィンドウを選択してアクティブ状態にしておかないと UnknownError: Unknown error. というエラーが出てスクロールの操作が反映されないことです。
アクティブ状態にするウィンドウは、共有中の画面そのものではなく共有中の画面の映像を表示しているタブである点に注意が必要です。
これは、操作される側が気づかずに勝手に操作されることを防ぐための仕様のようです。

However, user agents need to ensure that while a user is actively interacting with a captured tab, the capturing tab would not be able to concurrently zoom and scroll the captured tab, which would be confusing and frustrating for the user even in non-malicious settings. Due to the complexity of specifying and implementing this, Chrome's initial implementation of this API will only allow the capturing application to scroll/zoom while the capturing application is focused. The first draft of the spec will pose this as a requirement, but this may be changed at a later time.

github.com

なぜWebRTCのDataChannelを使うのか

スクロールの情報を他のユーザーに送る方法は、WebRTCのDataChannelの他にWebSocketなども考えられます。
もちろんWebSocketを使っても問題はないのですが、「スクロールの情報をWebRTCのDataChannelで送受信する」で説明したように、WebRTCのDataChannelには送受信するデータの特性に合わせた柔軟な設定ができるというメリットがあります。
再送処理や順序保証が必要な信頼性が求められる通信はWebSocketでもWebRTCのDataChannelでも実現できますが、スクロールの情報のような「順序保証は欲しいが欠損は許容できる」というユースケースにおいては、WebRTCのDataChannelを用いることで、再送の処理を省くことができます。
ネットワーク環境などによるため一概には言えませんが、サーバーを経由するWebSocketよりもP2Pで繋いだWebRTCのDataChannelの方が、低遅延で使い勝手の良いアプリケーションを実現できる可能性があります。
データの送受信の機能を実装する際は、WebRTCのDataChannelについても検討してみてください。

おわりに

記事作成時点では開発中のステータスであるCaptured Surface Control APIを実際に触ってみて、つまづいたポイントなどをまとめました。
共有中の画面に対する操作を遠隔で行うというのはセキュリティーやプライバシーに配慮する必要があるため、安全に実現できる仕様が出てきたのは良いことだなと思いました。
また、複雑なコードを書くことなく容易に使えるのもメリットだと感じました。
開発中のステータスということもあって、エラーメッセージが UnknownError: Unknown error. のみで分かりづらいというデメリットもありましたが、正式リリースまでにはより分かりやすいメッセージに改善されているように思います。
今回実装したサンプルは、Google Mapやmiroのような操作にスクロールが多用されるWebアプリケーションを画面共有した際に、特に動作が分かりやすいです。
リモートからGoogle Mapの操作ができるのは、実際にやってみて結構面白かったです。
Captured Surface Control APIを実際に使ってみて、正式リリース後に活用できる場面を考えてみると楽しいかもしれません。