ブログ一覧

Playwright の CI でフェイクカメラが NotFoundError になる

更新: Web開発者向け

音声を使う Web アプリの E2E テストをコンテナの CI に載せたら、getUserMedia が NotFoundError: Requested device not found で失敗しました。Chromium にはテスト用のフェイクメディアデバイス(--use-fake-device-for-media-stream)があり、手元の実機ではこのフラグで getUserMedia が通ります。同じフラグが、コンテナでは効きませんでした。

Linux コンテナ + Playwright 1.62 の Chromium で 4 手法を試して全滅した記録と、最終的にテスト環境ではなくアプリ側を直して E2E を通した話です(2026-08-20 時点)。

まず実測: 4 手法の全滅記録

1. 既定の headless + フェイクフラグ。 --use-fake-ui-for-media-stream--use-fake-device-for-media-stream を launch 引数に渡す構成です。getUserMedia は NotFoundError、enumerateDevices() は空配列を返しました。フェイクデバイスが「拒否されている」のではなく「存在しない」状態です。

const devices = await page.evaluate(async () => {
  const list = await navigator.mediaDevices.enumerateDevices();
  return list.map((d) => `${d.kind}:${d.label}`);
});
// コンテナ内では [] が返る。実機では audioinput / videoinput が並ぶ

2. ヘッドフル + xvfb-run。 headless 側の制限を疑い、仮想ディスプレイでヘッドフル起動(xvfb-run 経由)に切り替えました。結果は同じです。chrome://version の Command Line 欄で、フラグがプロセスに到達していることは確認できているので、「フラグが渡っていない」線は消えました。

3. PulseAudio ダミーデバイス。 オーディオサブシステムが無いのが原因かと思い、pactl load-module module-null-sink でダミーシンクを立ててから起動しました。変化なし。

4. ファイル指定式。 --use-file-for-fake-video-capture=<.y4m>--use-file-for-fake-audio-capture=<.wav> で実ファイルを与え、context.grantPermissions(['camera', 'microphone']) も付与しました。変化なし。

同種の報告は Playwright リポジトリにもあります(microsoft/playwright#5417、Docker イメージ上でフェイクメディア系フラグが効かない)。

分かっていること・分かっていないこと

切り分けで言えるのは次までです。

  • フラグは Chromium プロセスに到達している(chrome://version で確認)
  • 同じフラグ構成で、ホスト実機の Chromium では getUserMedia が通る
  • コンテナ内では enumerateDevices() が空配列

実デバイスが 1 つも無いコンテナのメディアデバイス層では、フェイクデバイスの登録自体が成立していないように見えます。ただしここは推定で、Chromium のソースまでは追っていません。追わなかった理由は次の節のとおりで、追う必要がなくなったからです。

どう直したか: テスト環境ではなくアプリを直す

ここで発想を変えました。もともとアプリの仕様には「マイクが取れない環境では音声出力のみで動作を続ける」というエラー縮退の要件があり、実装を後回しにしていました。テスト環境を直す代わりに、この縮退を先に実装します。

let micStream = null;
try {
  micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (e) {
  // NotFoundError / NotAllowedError など。マイクなしモードで続行する
  micStream = null;
}
// micStream が null でも初期化を続け、音声出力だけで動かす

E2E テストはこの縮退経路を通って、実 API との通信まで到達できるようになりました。「テストのためにモックを増やす」のではなく、「本番の縮退仕様を先に実装したら、テストが後からついてきた」形です。縮退はテスト専用の分岐ではありません。マイクの権限を拒否した利用者や、入力デバイスの無い端末が本番で実際に踏む経路なので、テストの都合でアプリを歪めたことにはなりません。

どこで止まるか

  • デバイスが実際に取れた場合の経路(取得成功側)は、この CI では検証できないままです。そこは実機での手動確認に残っています
  • この手が使えるのは、「デバイス無しでも意味のある動作」を仕様として定義できるアプリに限られます。録画や通話そのものが目的のアプリでは成立しません
  • コンテナでフェイクデバイスを生やす正攻法は見つけていません。この全滅記録は 2026-08-20 時点、Playwright 1.62 の Chromium でのものです

この構成で気をつけること

  • getUserMedia の失敗を調べるときは、まず enumerateDevices() の中身と chrome://version のフラグ到達を見る。「フラグが効いていない」のか「デバイス層に何も無い」のかで打ち手が変わる
  • headless、ヘッドフル + xvfb、PulseAudio ダミー、ファイル指定式の 4 手法がすべて外れたら、コンテナ側の制約を疑って撤退する
  • アプリの仕様にエラー縮退の要件があるなら、それを先に実装する。E2E はその経路で実 API まで通せる

フェイクカメラを生やす試みは全敗でしたが、そのおかげで後回しにしていた縮退仕様が先に実装され、E2E は実 API まで通りました。テスト環境の制約が、アプリの実装の穴を先に埋めさせてくれることもあります。

関連

この記事をシェア