dwgx@blog:~$dwgx
> cd ../posts

Reflection King:VRChat プレイヤーに動画を食わせる取得・変換バックエンド

// created

Reflection King は VRChat プレイヤーに動画を食わせるバックエンドだ。あのプレイヤーは偏食で、直接 GET できるおとなしいメディアアドレスしか受け付けない。ログインが必要だったり、JS を走らせて解析する必要があったり、認識できないエンコードのソースに当たるとサボる。こいつの仕事はこうだ。ページの URL を放り込むと、解析・変換して、サーバー上の raw アドレスとして着地させてくれる。プレイヤー側はそれをそのまま持っていって再生するだけ。毎回手で yt-dlp を回して画像ホスティングに上げる手間が省ける。

バックエンドは Axum、その前に React コンソールを立ててタスクの状態を見る。中核は永続化されたタスクキューで、SQLite に保存されるので、プロセスを再起動してもタスクは失われない。URL が入ってくるとまずリソース発見にかける。内部で yt-dlpyou-getstreamlink を数珠つなぎにしていて、ストリームアドレスを抜き出せるなら抜き出す。抜き出せない複雑なサイトは Playwright sidecar に渡し、実際にブラウザを立ち上げて嗅ぎ回らせる。発見の段階では候補が一つとは限らないことが多いので、候補ごとにスコアを付け、明らかに詰むものは先に弾く。地域制限、DRM、広告挿入はすべてリスクとしてマークして、変換を半分走らせてから行き止まりだと気づく事態を防ぐ。候補が決まれば ffmpeg の出番だ。変換すべきものは変換し、remux できるものは remux し、最後に統一して MP4 に faststart を付ける。こうすればファイル全体を待たずにダウンロードしながら再生できる。成果物は /media/{id}/{file} という raw URL にぶら下がり、HTTP Range に対応しているのでシークバーを引っ張ってもつっかえない。

三つ踏んだ。一つ目はセキュリティ。これは本質的に「URL を渡せばそこにリクエストしに行く」ものなので、典型的な SSRF の温床だ。プライベート IP や内部ネットワークのレンジは、リクエストを出す前に必ず塞がないといけない。さもないと公開デプロイは内部ネットワークへの扉を全開にしているのと同じだ。管理インターフェースにはキーを必須にし、sidecar の CDP/VNC/デバッグポートは一つも露出させてはいけない。二つ目はログイン状態。ログインしないとそもそも見えないソースがあるので、サーバー側でブラウザの Profile/Cookie をインポートする仕組みを追加し、sidecar にセッションを持たせて探らせるようにした。ここが一番デリケートで、.env、Cookie JSON、ブラウザ Profile、SQLite のデータベースは絶対に Git に入れない。三つ目はサイト対応。汎用パーサーではカバーしきれないので、Bilibili、Hanime1、それに一群の MacCMS 系リソースサイトは個別にアダプターを書く必要がある。ここが一番時間を食うし一番達成感がない。サイトがリニューアルするたびにアダプターも追随して直す羽目になる。

公開 VPS へのデプロイは一行のスクリプトで済む。

curl -fsSL https://raw.githubusercontent.com/dwgx/Reflection_King/master/install.sh | sudo bash

ローカルで Rust + Playwright をコンパイルしたくなければ、GHCR のプリビルドイメージを引いて docker compose で回すほうが速い。RK_PUBLIC_BASE_URL には外部クライアントが本当にアクセスできるアドレスを入れないといけない、これは忘れないこと。さもないと /media/... はローカルマシンでしか有効にならず、VRChat 側が受け取っても再生できない。より細かいことはリポジトリの docs/DEPLOYMENT.mddocs/SECURITY.md に書いてある。

プロジェクトはいくつかの crate に分けてある。reflection-core には設定、モデル、タスクストレージ、URL の安全性、ダウンロードと変換を置く。reflection-api は HTTP API とキューのスケジューリング。ブラウザ探索は独立して reflection-browser sidecar にした。コンソールは単独の React アプリだ。あとは reflection-worker のガワも残してあって、将来 worker を切り出して独立に走らせるとき用だが、今はまだそこまで至っていない。