🤔

WebRTCのウィンドウ共有は複雑怪奇なり

2021/09/23に公開

背景

2021年において多くのブラウザはWebRTCをサポートしており、ZoomやGoogle Meetのようなリモート会議の実現が可能になっています。
このWebRTCには画面を共有するScreen Capture APIが存在しており、画面共有を簡単に実現できます。

今回は、Screen Capture APIのデモを用いて実際に画面共有した場合の挙動について検証してみます。

各ブラウザにおける挙動

Screen Capture APIのデモのStart Capture ボタンを押した場合の挙動について確認してみます。

OSやブラウザによっては画面共有の許可を求められるので、それに従って処理を続行します。

Chrome 94

以下のようなダイアログが表示され、共有する範囲を選択できます。

画面全体か、ウィンドウか、ブラウザのタブかを選択できます。

Microsoft Edge 93

以下のようなダイアログが表示され、共有する範囲を選択できます。

機能としてはChromeと同じです。

Safari 15

画面全体を共有します。選択肢はありません。

なお、マルチディスプレイの場合は画面共有を開始した時点でSafariが存在したスクリーンを対象としています。

FireFox 92

以下のような画面が表示され、共有する範囲を選択できます。


画面全体か、ウィンドウかを選択できます。

その他ブラウザ

ブラウザの画面共有はMediaDevices.getDisplayMediaを使用しており、このサポート状況は以下のページに記述してあります。

https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia

概ねデスクトップのブラウザは画面共有が可能ですが、モバイル端末では不可能になっています。

キャプチャ対象を判定方法

ChromiumベースのブラウザとFireFoxは画面共有をウィンドウにするか全画面にするか選択できます。これをJavaScript側で区別するには以下のプロパティを参照します。

videoElem.srcObject.getVideoTracks()[0].label

Chrome:

選択 label
画面全体 'screen:0:0'
ウィンドウ 'window:983474:0'
タブ 'web-contents-media-stream://54:4'

Firefox

選択 label
全画面 "Primary Monitor" モニターが1枚の場合選択可能
画面 1 "Screen 1" モニターが複数枚の場合選択可能
ウィンドウ "index.html - 未設定 (ワークスペース) - Visual Studio Code" ウィンドウタイトル

ウィンドウを共有した場合の挙動

Chrome,EdgeとFirefoxはウィンドウを選択して共有することができます。
ここではmacOSとWindowsのChromeとFirefoxでウィンドウ共有の検証をおこないます。

macOS 11.6

macOSでテキストエディタを共有した場合の画面共有を確認してみます。

Chrome94

・共有対象のウィンドウ上のマウスカーソルは表示される。
・ダイアログは共有されない

・ポップアップメニューは共有されない

FireFox 92

・共有対象のウィンドウ上のマウスカーソルは表示される。
・ダイアログは共有されない
・ポップアップメニューは共有されない

Windows 10 Pro 21H1

WindowsでVOICEROID2を画面共有してみます。

Chrome94

・共有対象のウィンドウ上のマウスカーソルは表示される。
・オプション画面などのダイアログは共有されない

・ファイルダイアログなどは共有される

・ポップアップメニューは共有される

FireFox 92

・共有対象のウィンドウ上のマウスカーソルは表示される。
・ダイアログは共有されない。
・ポップアップメニューは共有されない

Chrome(windows)のウィンドウ共有時の対象について

前述の実験結果よりWindows+Chromeの場合、共有対象のウィンドウをオーナーとするウィンドウは共有の対象にされる場合があります。

共有されるダイアログとされないダイアログの違い

Windowsの場合、spy++というツールを使用することでウィンドウの詳細情報が確認できます。

共有されないウィンドウ

共有されるウィンドウ

共有されたウィンドウにはWS_POPUPがスタイルとして設定されていることが確認できます。

Chromiumのコードの確認

ここまででWS_POPUPの有無が共有対象となるか、ならないかの条件であると推測できます。
これを確定するためにChromiumのコードを読んでみましょう。

まず、Chromiumのコードは以下のリポジトリで確認できます。
https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main

ただし、third_partyの一部モジュールは外部のリポジトリから取得されています。
この情報はchromiume直下のDEPSファイルに記述されています。

今回重要なのは以下のwebrtcのリポジトリです。
https://webrtc.googlesource.com

上記のリポジトリを観察すると以下のような実装が確認できます。
https://webrtc.googlesource.com/src/+/refs/heads/main/modules/desktop_capture/win/window_capturer_win_gdi.cc#54

このコードではEnumWindowsを用いてすべてのポップアップウィンドウをキャプチャ対象に含めているように見えます。
この実装中、WS_POPUPがついていない場合はキャプチャの対象外となるので前述の実験結果どおりの動作となります。

// Called via EnumWindows for each root window; adds owned/pop-up windows that
// should be captured to a vector it's passed.
BOOL CALLBACK OwnedWindowCollector(HWND hwnd, LPARAM param) {
  OwnedWindowCollectorContext* context =
      reinterpret_cast<OwnedWindowCollectorContext*>(param);
  if (hwnd == context->selected_window()) {
    // Windows are enumerated in top-down z-order, so we can stop enumerating
    // upon reaching the selected window.
    return FALSE;
  }
  // Skip windows that aren't visible pop-up windows.
  if (!(GetWindowLong(hwnd, GWL_STYLE) & WS_POPUP) ||
      !context->window_capture_helper()->IsWindowVisibleOnCurrentDesktop(
          hwnd)) {
    return TRUE;
  }
  // Owned windows that intersect the selected window should be captured.
  if (context->IsWindowOwnedBySelectedWindow(hwnd) &&
      context->IsWindowOverlappingSelectedWindow(hwnd)) {
    // Skip windows that draw shadows around menus. These "SysShadow" windows
    // would otherwise be captured as solid black bars with no transparency
    // gradient (since this capturer doesn't detect / respect variations in the
    // window alpha channel). Any other semi-transparent owned windows will be
    // captured fully-opaque. This seems preferable to excluding them (at least
    // when they have content aside from a solid fill color / visual adornment;
    // e.g. some tooltips have the transparent style set).
    if (GetWindowLong(hwnd, GWL_EXSTYLE) & WS_EX_TRANSPARENT) {
      const WCHAR kSysShadow[] = L"SysShadow";
      const size_t kClassLength = arraysize(kSysShadow);
      WCHAR class_name[kClassLength];
      const int class_name_length =
          GetClassNameW(hwnd, class_name, kClassLength);
      if (class_name_length == kClassLength - 1 &&
          wcscmp(class_name, kSysShadow) == 0) {
        return TRUE;
      }
    }
    context->owned_windows->push_back(hwnd);
  }
  return TRUE;
}

PowerPointをウィンドウ共有した場合

リモート会議を行う際に、PowerPointは最も頻繁に画面共有されることになるでしょう。
PowerPointをウィンドウで共有した場合、前述までの実験とは異なる動作をします。

以下のオペレーションをしたとしましょう。

・PowerPointのウィンドウを選択して画面共有を行う
・PowerPointのスライドショーを開始する

この場合、以下のようにフルスクリーンとなったウィンドウがキャプチャ対象となります。

これはWindows、MacともにChromiumベースのブラウザでは同じ挙動になります。

Chromiumの経緯

このPowerPointを対象としたChromiumの挙動は以下のIssueで指摘されたものです。

Issue 3852: Fullscreen powerpoint presentation not shared to far end
https://bugs.chromium.org/p/webrtc/issues/detail?id=3852

2014年に起票されたレポートですが、PowerPointのフルスクリーンを確実に認識するのは難しく、対応されたのは2019年末のコミットになります。

実装の内容としてはPowerPointのフルスクリーンを検知するための処理が特別に作成されています。

Windows
https://webrtc.googlesource.com/src/+/refs/heads/main/modules/desktop_capture/win/full_screen_win_application_handler.cc#89

MacOS
https://webrtc.googlesource.com/src/+/refs/heads/main/modules/desktop_capture/mac/full_screen_mac_application_handler.cc#52

windowsの場合はウィンドウのクラス名やスタイルでフルスクリーンか編集画面かを判断していますし、MacOSの場合はウィンドウのタイトル名でPowerPointかどうか判定してます。

このことはOfficeのバージョンの違いで、動作しなくなる可能性があることを意味します。

たとえばWindowsの場合、編集画面の判定をPPTFrameClassというクラス名か否かで判定しています。
これは2021年時点のMicrosoft365 2108では問題なく動作しています。
しかし、Office2000ではFF9FrameClassという名前になっているので動作することはないでしょう。

W3Cでの取り扱い

前述のPowerPointのウィンドウを対象とした画面共有の挙動はChrome固有の動作です。
ただ、2015年の時点でW3CのIssueで取り上げられたことはあります。

Fullscreen needs handling (was: Powerpoint is special) #29
https://github.com/w3c/mediacapture-screen-share/issues/29

We will have to detect when a particular Powerpoint window has become full screen and shift the window share to the presentation. We might want to extend the same sort of privilege to other applications that shift to a full screen mode.

残念ながら、フルスクリーンという概念はプラットフォーム固有のものという話になり議論が進まずIssueは閉じられました。

まとめ

Screen Capture APIを使用することで簡単に画面共有が行えます。
・共有する範囲はブラウザによってことなりますが、画面全体を共有する場合とウィンドウを共有する場合があります。
・ウィンドウを共有した場合の振る舞いはOSとブラウザによって異なります。
・WindowsのChromiumの場合は共有対象が表示した子ダイアログも共有の対象となる場合もあります。共有対象となるにはウィンドウのスタイルにWS_POPUPが指定されている必要があります。これは.NETのFormを単純に作成しただけだと、このスタイルはつきません。Win32のファイルダイアログやメッセージボックスにはついています。
・PowerPointをウィンドウ共有した場合、Windows,MacのChromiumはフルスクリーンを追跡する実装になっています。ただし、確実に追跡できる実装ではないためOfficeのバージョンの違いで動作しなくなる可能性があります。もし仕様を決める場合は、特定のバージョンのOfficeでしか動かないことをアナウンスする必要があります。

Discussion