pure Rust SRT実装(srt-tokio)でライブ配信リレーを作ったら不安定だった話 — libsrtとの成熟度の差

サーバー・インフラ

SRT (Secure Reliable Transport) とSRTLA (SRT Bonding) を使って、モバイル回線からVPS経由でOBS複数台に同時配信するリレーサーバーを構築しようとした。最初はpure Rustのsrt-tokioクレートで全部書こうとしたが、5秒〜5分でOBSが切断される不安定な動作に悩まされ、最終的にlibsrt(C実装)ベースのツールに切り替えたら安定した。

この記事は、その泥沼デバッグの記録と、「プロトコルの再実装は見た目以上に難しい」という教訓をまとめたものだ。

やりたかったこと

構成イメージ

// モバイル回線ボンディング → VPS → OBS複数台

[送信側] スマホ/カメラ
  ├─ SIM回線A ──┐
  ├─ SIM回線B ──┼─→ SRTLA受信SRTリレー (fan-out)[OBS #1]
  └─ SIM回線C ──┘                                         ├→ [OBS #2]
                                                                 └→ [OBS #3]

要件は以下の通り:

  • SRTLA(ボンディング)でモバイル複数回線を束ねて送信
  • VPS上で受信し、1入力 → N出力のfan-outでOBS複数台に同時配信
  • 可能な限りシンプルな構成で実現したい

最初のアプローチ: pure Rust(srt-tokio)で全部書く

srt-tokioはSRTプロトコルのpure Rust実装で、tokioランタイム上で動作する。libsrt(C実装)に依存しないため、クロスコンパイルが容易でRustエコシステムとの親和性も高い。

実装方針はシンプルだった:

// 概念的なコード(簡略化)
use tokio::sync::broadcast;

// 1. SRTリスナーで受信
let (tx, _) = broadcast::channel(1024);

// 2. 入力ストリームからパケットを読み、broadcastで配信
while let Some(packet) = input.next().await {
    tx.send(packet)?;
}

// 3. 各OBSクライアントはbroadcast::Receiverで受信
let mut rx = tx.subscribe();
while let Ok(packet) = rx.recv().await {
    output.send(packet).await?;
}

コンパイルは通る。接続もできる。映像も流れ始める。

しかし、5秒〜5分でOBSが切断される。

何が起きていたか — 泥沼のデバッグ

ここから始まったのは、一つ直すと別の問題が出る「モグラ叩き」状態だった。

問題1: peer_idle_timeout のデフォルト5秒

OBSが接続後ちょうど5秒で切断される現象。調査の結果、srt-tokioのpeer_idle_timeoutのデフォルト値が5秒に設定されていた。OBSはreceive-onlyモードで接続するため、OBS側からデータを送信しない。srt-tokio側は「相手からデータが来ない=アイドル状態」と判定し、5秒で切断していた。

⚠️ SRTの仕様上の注意
SRTではreceiver側からもACK/NAKなどの制御パケットが送信されるが、srt-tokioの実装ではこれらの制御パケットをアイドル判定に正しく反映できていなかった可能性がある。libsrtではこの問題は発生しない。

問題2: conn.next() が即None → 接続ループ離脱

peer_idle_timeoutを延長して5秒切断は回避できた。しかし今度は、接続直後や数分後にconn.next()(受信ストリームの次のパケットを待つ)がNoneを返し、接続ハンドラのループから抜けてしまう問題が発生。

OBSはreceive-onlyなので、サーバー→OBSの方向にしかデータは流れない。つまりconn.next()でOBSからのデータを待っても、来ないのが正常動作だ。しかしsrt-tokioの実装では、受信ストリームが閉じたと判断してNoneを返してしまう。

問題3: client_count がデクリメントされない

接続数を管理するclient_countが、上記の異常切断時に正しくデクリメントされず、サーバーの状態が不整合になる。新しいOBS接続を受け付けなくなったり、リソースリークが発生した。

問題4: latency値の調整が逆効果

SRTのlatencyパラメータ(バッファリング時間)を調整すれば改善するかと思い、値を変更。しかし、latencyを上げるとバッファが溢れやすくなり、下げるとパケットロス耐性が下がるというトレードオフに直面。どの値でも安定しない。

libsrtでは同じlatency値で問題なく動作しており、問題はlatency値ではなくSRTプロトコルの内部ステート管理にあった

共通する根本原因

これらの問題に共通するのは、SRTプロトコルのステートフルな部分の実装差異だ。SRTは単なるUDPラッパーではなく、以下のような複雑なメカニズムを持つ:

  • ハンドシェイク — caller/listener/rendezvousの3モード、暗号化ネゴシエーション
  • TSBPD (Time Stamp Based Packet Delivery) — タイムスタンプベースのパケット配信制御
  • ACK/NAK/ACKACK — 確認応答と再送要求のステートマシン
  • セッション管理 — keep-alive、タイムアウト、グレースフルシャットダウン
  • 輻輳制御 — ネットワーク状態に応じた送信レート調整

これらすべてが正しく実装されていないと、「接続はできるが安定しない」という最もデバッグしにくい状態になる。

srt-tokioとlibsrtの成熟度の差

libsrt(Haivision/srt)はHaivision社が開発するSRTプロトコルの参照実装で、2013年の初版から10年以上の実績がある。放送業界での採用実績も豊富で、エッジケースへの対応が積み重ねられている。一方、srt-tokioはコミュニティによるRust再実装であり、プロトコルの基本動作は実装されているものの、実際のネットワーク環境で発生するエッジケースへの対応はまだ発展途上だ。

実際に同じ送信元・同じOBSで試した結果、srt-tokioでは5秒〜5分で切断されるのに対し、libsrtベースのツールでは数時間安定動作した。違いはSRT実装だけだった。

解決策: libsrtベースのツールを組み合わせる

pure Rustでの完結は諦め、libsrtベースのツールを組み合わせる構成に切り替えた。

最終構成

// 3プロセス構成

[プロセス1] SRTLA Receiver (Rust/独自実装)
  └─ SRTLAパケットを受信し、通常のSRTストリームに変換
  └─ UDP/SRTでローカルに出力

[プロセス2] srt-live-transmit (C/libsrt)
  └─ プロセス1の出力を受け取り、srtrelayに転送
  └─ SRTプロトコルの正しいハンドシェイク・セッション管理を担当

[プロセス3] srtrelay (Go/libsrt)
  └─ 1入力 → N出力のfan-outを担当
  └─ OBS複数台が同時接続可能

各ツールの役割

ツール 言語 SRT実装 役割
SRTLA Receiver Rust 独自 SRTLAボンディング受信
srt-live-transmit C libsrt SRTストリームのブリッジ
srtrelay Go libsrt (CGo) 1入力 → N出力 fan-out

ポイントは、SRTプロトコルの複雑な部分(ハンドシェイク、TSBPD、ACK/NAK、セッション管理)はすべてlibsrtに任せていること。独自のRust実装はSRTLAの受信部分のみに限定した。SRTLAはSRTパケットをUDPレベルで複数回線に分散・再結合するレイヤーであり、SRTプロトコル本体と比べて実装がシンプルだ。

結果

✅ 安定動作を確認
3プロセス構成に切り替えた結果、OBSの切断問題は完全に解消。数時間の連続配信でも安定動作している。fan-outのレイテンシオーバーヘッドは約300msで、モバイル配信の通常レイテンシ(1500〜2000ms)と比較して誤差の範囲。

3プロセスに分かれているのはエレガントではないが、動くことが正義だ。各プロセスは独立してログを出力するため、問題発生時の切り分けもしやすい。

得られた教訓

1. プロトコルの再実装は見た目以上に難しい

特にSRTのようなステートフルなUDPプロトコルは、RFCやドキュメントだけでは実装しきれないエッジケースが無数にある。「コンパイルが通る」「接続できる」「映像が流れる」の先に、「安定して動き続ける」という巨大なハードルがある。

2. 参照実装が存在するなら、まずそれを使え

libsrtは10年以上の実績があり、放送業界で実際に使われている。pure Rustで再実装する技術的挑戦は価値があるが、プロダクション環境では参照実装を選ぶべきだ。必要に応じて後から差し替えることはできる。

3. 「コンパイルが通る ≠ 正しく動く」

Rustの型安全性やコンパイラの厳しさは、メモリ安全性やデータ競合の防止には非常に有効だ。しかし、プロトコルの意味的な正しさ(セマンティクス)はコンパイラで検証できない。SRTのエッジケースは、実際のネットワーク環境(パケットロス、遅延揺らぎ、帯域変動)でしか再現しない。

4. レイテンシの現実

3プロセス構成によるfan-outのオーバーヘッドは約300ms。モバイル回線ボンディングを使ったライブ配信では、送信側のレイテンシが1500〜2000msある。300msの追加レイテンシは、全体の配信遅延から見れば誤差の範囲だ。

まとめ — 成熟度の差は「年月」で埋まる

srt-tokioが悪いプロジェクトだと言いたいわけではない。pure RustのSRT実装という方向性自体は正しいし、将来的にはlibsrtと同等の安定性を達成する可能性もある。なお、srt-tokioのリポジトリ自体がREADME冒頭で「NOTE: THIS IS NOT PRODUCTION READY.」と明記しており、それをわかった上でチャレンジしてみた結果がこの記事だ。

しかし、プロトコル実装の成熟度は年月をかけて積み上げるものだ。libsrtが10年以上かけて潰してきたエッジケースの数と、コミュニティ再実装のそれとでは、現時点では大きな差がある。

ライブ配信は「止まったら終わり」の世界だ。視聴者はバッファリングや切断に敏感で、復帰までの数秒で離脱する。そういうプロダクション環境では、新しい実装の可能性より、枯れた実装の安定性を選ぶべきだと実感した。

判断基準 推奨
プロダクション環境でSRTを使いたい libsrt(C)またはlibsrtバインディング
SRTでfan-outしたい srtrelay + srt-live-transmit
SRTLAボンディングを自前実装したい SRTLA受信部のみ自作、SRT本体はlibsrtに委任
pure Rustで実験・学習したい srt-tokio(ただしプロダクション利用は慎重に)

SRT/SRTLAを使った屋外ライブ配信の機材選びについては「屋外ライブ配信カメラおすすめ比較【2026年版】」、配信中のバッテリー問題については「屋外配信向けモバイルバッテリーおすすめ比較【2026年版】」も参考にしてほしい。

コメント

タイトルとURLをコピーしました