6 min read

Media over QUIC Transport を一通り実装してみた

ここ 1 ヶ月ほど Media over QUIC Transport (MOQT) を実装して、一通り動いたので感想などを雑に書いていく。

前提

  • 商用 WebRTC 全スタック実装者
  • Erlang/OTP による QUIC 実装者
  • WebRTC SFU Sora 開発/設計担当

QUIC ライブラリ選定

まず実装するにあたり QUIC ライブラリや HTTP/3 自体は実装しないことにした。自分が WebRTC を始めたときとは違いすでに QUIC が RFC になってから 4 年経過しており、HTTP/3 が RFC になってから 3 年立っている。

巨人達が QUIC ライブラリを OSS として公開してくれているわけで、まずはそれに乗っかれば良い。もともと WebRTC でもクライアントは libwebrtc という巨人に乗っかっていた。サーバーはいつか自前でやるが、今はまだその時ではない。

言語は Python を選択

まず MOQT のクライアントとリレーサーバーを実装するにあたり、Python を採用する事にした。これは「テストがしやすい」という判断。自分が普段から使っている pytest ベースでテストが書ける。また Python で作って公開する事で MOQT を触るハードルが一気に下がると考えたのもある。

pytest 経由で PBT や pyroute2 を使ったネットワーク障害テストも実現しやすいというのもある。

QUIC は msquic を採用

Python を採用するということは非同期 I/O 周りを自前でやるのは現実的ではないので Sans I/O なライブラリは採用できない。Google の Quiche や ngtcp2 などは採用からはずれた。Rust ベースも PyO3 に慣れていないので Cloudflare Quiche も採用しなかった。

  • 非同期 I/O がしっかりしていること
  • macOS / Linux / Windows へのビルドが難しくないこと
  • CMake でビルドできること
  • C/C++ でできていること
  • 継続的なメンテナンスがあること

ということで Microsoft が公開している msquic を採用することにした。Windows でのビルドに絶対的な信頼がおけるのも強い。msquic は自前で非同期 I/O をしっかり作り込んでいるのも魅力で、依存ライブラリが OpenSSL くらいなのもよかった。

HTTP/3 は nghttp3 を採用

MOQT は QUIC と WebTransport (over HTTP/3) の二つの経路があるので、msquic だけだと満たせない。特にブラウザからテストするには WebTransport への対応が必須になる。

ここは Sans I/O の nghttp3 を採用することにした。

  • nghttp3 の開発者のスポンサーをしている
  • nghttp3 の開発者にアドバイザーをお願いしている

ブラウザ部分に影響するのは融通が利くほうがありがたいという判断。これで一通り方針は決まった。

また WebTransport over HTTP/3 はフォールバックで WebTransport over HTTP/2 なる、ここでは HTTP/2 の実装が必要になるが nghttp3 の開発者は nghttp2 の開発者でもあるというのも大きい。

ちなみに WebTransport 部分は Python 側でとりあえず書くと言う判断をこのときはした。

戦略

時雨堂では Python ライブラリを作る際は基本的に C/C++ ベースであり、nanobind と scikit-build-core と cmake という構成を取っている。

なので msquic と nghttp3 を利用して HTTP/3 を実装し、その上に Python で WebTransport を実装するという戦略をとることにした。

のだが、nghttp3 の開発者に色々相談していたら WebTransport 実装を作ってくれてしまった ... 。ということでバインディングと MOQT だけ作れば良くなった。

msquic と nghttp3 の Python バインディング

msquic と nghttp3 は C で書かれており癖が凄く少なくコールバックを活用して実装していくだけ。とりあえずさくっと WebTransport over HTTP/3 までは実装ができた。

ブラウザで確認しようと思ったらなんと、Chrome はまだ WebTransport は draft-02 で、nghttp3 は最新の draft-14 ... 、feature フラグを使っても 07 までということで、nghttp3 を fork して draft-07 に対応することにした。

で、無事動作した。

base64 のハッシュを渡す動く仕組み便利

ちなみにこの WebTransport DevTools は Preact と Vite で作った。

MOQT Python 版の実装

MOQT は基本的にリレーサーバーがキモとなる。サーバー=リレーサーバーになる。ただし、Python でリレーサーバーは現実的ではないので C++ で完全に隠蔽したリレーサーバーを作る事にした。クライアント側は Python で上手くラップしたりするが、リレーサーバーだけはもう 100% C++ くらいの勢い。

0:00
/0:06

1 -> 3 としょぼいが動いた

ただ転送しているだけで、何か特殊なことをしていたりはしない。とりあえず動いたバージョン。

MOQT ブラウザ版

ここでお気付きだと思うが、MOQT のブラウザ版も作った。もちろん依存 0 で TypeScript でゴリゴリ書いた。

結局ブラウザは早いので TypeScript で特に困ることないし、柔軟性やデバッグを考えると圧倒的に便利。

低レベル API と高レベル API を用意して、高レベル API は MediaStream だけ渡せば良い Publisher / Subscriber を実装した。

<del>地獄の始まり</del>

<del>さて、ここからが問題。当たり前だが QUIC なので「ストリーム単位での順番保証がない」なのに「MOQT は 1 ストリーム 1 オブジェクト」ということで視聴側でオーダリングが必要になる。つまりバッファリングと並び替え。

そもそも WebCodecs API は順番に入れないと破綻するので、その辺りはシビア。

とりあえず実装した。もちろんタイムアウトも指定して一生待ったりはしないようにしてる。</del>

これ完全に自分の勘違いで、1 ストリーム 1 サブグループだったので、別に地獄ではなかった。自分がずっと 1 ストリーム 1 オブジェクトと思い込んで実装していた。ということで 1 ストリーム 1 サブグループに実装し直した。

Discord で指摘してくれた kixelated に感謝。

Joining Fetch

MOQT の数少ない良い機能として Joining Fetch という仕組みがある。これは新しくサブスクライバーが SUBSCRIBE するたび配信側にキーフレーム要求が飛ばないようにする仕組み。

リレーサーバーであるキャッシュをする仕組みで、キャッシュから事前に「古いキーフレーム」を受け取って、キーフレームの生成をさせないというもの。

これがマジで怠い。ストリームは全て並列で送られてくるわけで。さらに FETCH してキャッシュを取り出してから、SUBSCRIBE する間にも配信されているわけでその間の差分も含めてサブスクライバーには転送しなければいけない。実装した。

キーフレーム間隔を 240 秒にしても配信側はキーフレームを発行せずに視聴側はちゃんと映像が表示されている。

0:00
/0:04

これ本当にめんどくさかった

Low Overhead Media Container (LOC)

そもそも MOQT だけでは音声や映像をおくるのは現実的ではない。その為に最低限の拡張機能が必要になる。MOQT はタイムスタンプすら送る仕組みがないし、キーフレームかどうかを判定する仕組みもない。そこで LOC というのが必要になる。実装した。

MOQT Streaming Format (MSF)

さらに MOQT は残念ながら WebRTC の SDP のような仕組みは定義されておらず、その上で別の仕組みを実装する必要がある。今までは WARP Streaming Format と呼ばれていたのが MOQT Streaming Format と名前が変わっている。

Rename draft from ‘warp’ to ‘msf’ by wilaw · Pull Request #77 · moq-wg/msf

MOQT draft 10 付近前提としていて色々古い SUBSCRIBE_DONE はないよ?

それがここ1ヶ月前くらいの話。実装した。

ちなみにこの Catalog って JSON です

この CATALGO と表示されている部分が MSF の一部。これで、カタログを受け取って視聴側がデコーダーを初期化してみたいな挙動が実現できた。

Rust 版も作った

Python 版はクライアント/サーバー(テスト用) /リレー、ブラウザ版はクライアント。もう少し検証用に別のライブラリが欲しいということで Rust 版を作る事にした。

Rust 版は Cloudflare Quiche を採用。最近 tokio-quiche ができたのもあり、動くものを作るには楽できるだろうという判断。非同期 I/O が簡単に実現できるの大事。WebTransport は nghttp3 を参考に実装。これはまだ動かしてない。


2025 年末時点での MOQT について

そもそもブラウザの WebTransport が draft-02 で微妙というのが一番だるい。Safari は対応していない(Safari 27 とかで動きそうではあるが)。あと WebCodecs API と WebTransport API を上手くラップしてさらにそこに MOQT を実装し、そのうえに LOC を実装し、さらにその上に MSF を実装し、オーダリングを実装する必要がある。

つまり ...

MOQT Streaming Format over Low Overhead Media Container over Media over QUIC Transport over WebTransport over HTTP/3 over QUIC over UDP

これを実装する必要がある。なにより音声や映像の変換処理は WebCodecs API なので、そしてクライアント側でやる事が多すぎる。

つまり WebRTC はただのぬるま湯だった。申し訳ない、自分が甘えすぎていたという現実を突きつけられた。

基本的にまだ何もしなくていい

MOQT 全然仕様が決まってないし、普通に不安定なので今は踏み込むのは本当におすすめしない。もちろん新しい技術が好き!というであれば良いが。

ちなみに社員達にはやるだけ無駄と判断して MOQT は一切触らせていない。

既存の技術と対等に戦うにはまだまだ先だし、そもそもスケールアウトやスケールアップは基本的にプロトコル関係ない分散システムの話なので気にするところではない。

MOQT はスケールするけど WebRTC はスケールしないとか言ってる人たちは相手にしなくていい。両方スケールさせればいい。

今後

使いやすい Python と Rust とブラウザ向けのライブラリを公開するので待っていて欲しい。

Python でコーデックとかマイクとかカメラからデータ取得したり、再生とか保存とかどうするの?あと変換は?って思うかもしれないが、心配しないで欲しい。全部作ってある。

全て Python 3.12-14 かつ no-GIL (Free Threading) 対応で作っているので、Python だけで気軽に MOQT が使えるようになる。これで LLM とも気軽に組み合わせられる。

また MOQT をブラウザで確認したい人向けには MOQT DevTools の公開を準備しているのでお待ちして欲しい。また Python 版の MOQT リレーサーバーも気軽に検証できるようにサービスとして公開しようと考えていので待ってて欲しい。

追記

ブラウザ向けライブラリを npm で公開した。ソースコードはもう少し先。

https://www.npmjs.com/package/moqt-js


利用技術まとめ

  • MOQT Python クライアント / リレーサーバー
    • msquic
    • nghttp3
    • nanobind
    • scikit-build-core
    • cmake
  • MOQT TypeScript クライアアント
    • 依存なし
  • MOQT Rust クライアント/サーバー
    • Cloudflare Quiche
  • MOQT DevTools
    • Preact
    • Preact/Signals
    • Tailwind CSS
    • Vite
    • Vitest
    • Fast-Check
0:00
/0:06

Joining Fetch の高速化が課題

デバッグログも作り込んだ

おまけ

Python 向けとブラウザ向けのライブラリはこんな感じで使えるようになります。

moqt.md
GitHub Gist: instantly share code, notes, and snippets.