VRChat のワールドに本物のブラウザを詰め込む
vrc-browser-bridge は本物のブラウザを VRChat のワールドに詰め込んで、スクリーンとして使う。VRChat の Udon サンドボックスには WebView も CEF もなく、Chromium を埋め込むこともできず、リアルタイムの socket すら開けない。標準的な意味での組み込みブラウザは、この環境には端から存在しない。このプロジェクトのやり方は制約を回避すること、つまりブラウザを外に置いておく。ローカルで本物の Chromium を走らせ、その画面を低遅延の RTSP ストリームとしてワールドに送り込んで一枚のスクリーンにし、ワールド内でのプレイヤーの操作を送り返して動かす。Chromium は常にページ状態の唯一の権威であり、ワールドで見えているのはその投影に遠隔操作の層を一枚重ねたものにすぎない。
分解すると 2 本のストリームと 1 本のループになる。コントロール方向は mock または Udon のイベントが WebSocket 経由で入ってきて、それを Playwright/CDP で受け止めて Chromium に落とす。メディア方向は Chromium の画面が FFmpeg を通って MediaMTX へ渡り、RTSP として push され、ワールド内の AVPro で再生される。もう一本ログのループがある。VRChat が output_log に内容を書き込み、Companion がそれを tail して、VRCBRIDGE プレフィックス付きの行を拾い出してコントロールサーバーに送り返す。ログを経由するのは Udon にまともな外向きのネットワーク経路がないからで、ログこそが外に向けて安定して話せる唯一の場所になっている。
イベントプロトコルは @vrcbb/protocol として単独に切り出した。各メッセージは JSON で、固定の v: 1、sessionId、単調増加する seq を持ち、座標は一律 0..1 に正規化されている。入ってきたらまず検証を通し、それから sessionId + playerId + source + seq で重複排除とソートをする。マルチプレイ環境ではイベントの順序の乱れや再送は日常茶飯事で、この層を作らないと後段はすべてオカルトなバグだらけになる。コントロールは open と locked に分かれる。open はすべての正当なイベントを受け付け、locked はまずコントロールロックを奪う必要がある(lock_request / lock_release、30 秒の TTL 付き)。公共スクリーンで複数人が同時にクリックできては困るからだ。操作はポインタの移動/押下/離上、スクロールホイール、テキスト送信、Enter/Escape/Tab/Ctrl+L、URL 送信、進む/戻る/更新をカバーし、画質は 360p/720p/1080p/auto をランタイムで切り替えられる。コントロールサーバーは別途 /health、/media-state、/snapshot を開けていて、snapshot はデバッグ用に JPEG を一枚そのまま吐く。
記しておく価値のある点が二つ。一つは、先に mock 入力ページを作ったこと。VRChat につながなくてもローカルの閉ループを回せるので、一行変えるたびにヘッドセットをかぶってワールドに入って検証する手間が省ける。もう一つは、Windows では PowerShell の実行ポリシーが .ps1 の shim を弾くので、npm.cmd / npx.cmd を直接使って回避したこと。FFmpeg と MediaMTX はバンドルせず外部バイナリとして扱い、コマンドを実行せず表示するだけの --dry-run も残してある。
リポジトリは npm workspaces の monorepo で、全編 TS strict、control-server / companion / media-pipeline / mock-input がそれぞれ独立した app となり、プロトコルは単独のパッケージになっている。現時点ではまだ WIP で、ローカルの閉ループは通り、メディアストリームとログの経路を少しずつ接続している最中、バージョンはまだすべて 0.1.0 だ。