概要
UDPベースのリアルタイム・マルチルームオーディオ配信プロトコル。 RTPを基盤にOSTPool拡張ヘッダを追加し、LAN・P2P・WAN全ての接続モードを統一的に扱う。
プロトコル比較
| 特徴 | WebRTC | AES67 | OSTP |
|---|---|---|---|
| トポロジー | Mesh P2P | Multicast | Hybrid (LAN/P2P/WAN Swarm) |
| 最大受信者数 | ~100 (SFU) | LAN限定 | 100,000+ (Swarm) |
| LAN/WAN | WAN中心 | LANのみ | 両方対応 |
| 著作権追跡 | ✗ | ✗ | ✅ SSRC fingerprint |
| 経済レイヤー | ✗ | ✗ | ✅ TIP / SUPPORT / CHARGE |
| 初回音声到達 | 500–2000ms (ICE) | <5ms | <5ms (LAN) / <100ms (WAN) |
| DJデュアルデッキ | ✗ | ✗ | ✅ stream_id encoding |
| ファイルプリバッファ | ✗ | ✗ | ✅ 50ms切替 |
アーキテクチャ
OSTProはMedia Plane(UDPによる音声配信)とControl Plane(WebSocketによる制御)の2層構造を持つ。
Source → Relay ├── Swarm Node 1 │ ├── Leaf Node A │ └── Leaf Node B └── Swarm Node 2 ├── Leaf Node C ├── Leaf Node D └── Leaf Node E
solunad :8400/ws (DCP) └── Monitor commands └── Player control └── System info relay.solun.art:5100 (RSP) └── JOIN / HELLO / MEMBERS └── TIP / SUPPORT / CHARGE └── RTCP receiver reports
接続モード
239.69.0.1:5004
UDP hole-punch
PEER ヒントを配信。NATトラバーサルで直接接続。relay.solun.art:5100
パケットフォーマット
OSTProパケットはRTP (RFC 3550) ベースに、RFC 5285準拠のRTP拡張ヘッダ(Profile=0x4F53 "OS")を付加した構造を持つ。
3.1 Modern OSTProパケット (28バイトヘッダ)
stream_id フィールド詳細
stream_id[15:12] = deck_id // 0 = Deck A, 1 = Deck B stream_id[11:8] = ch_count // 0=legacy 2ch, 1=mono, 2=stereo, 6=5.1, 8=7.1 stream_id[7:0] = reserved // MUST be 0x00
3.2 ペイロードタイプ (PT)
| PT | 名前 | 説明 |
|---|---|---|
| 96 | PCM24 | 24-bit integer PCM、48000Hz |
| 97 | F32 | 32-bit float PCM、48000Hz |
| 98 | Opus | Opus compressed (RFC 6716)、可変ビットレート |
| 10 | AES67/L24 | AES67 互換 24-bit PCM (互換レイヤー) |
| 11 | AES67/L16 | AES67 互換 16-bit PCM (互換レイヤー) |
| 126 | NACK | 再送要求 (RFC 4585 Generic NACK) |
| 127 | FEC/XOR | XOR パリティパケット (N=5) |
3.3 Legacy フォーマット (後方互換)
X=0の場合、RTPヘッダ12バイトの直後(offset 12)に4バイトのマジック 0x4F535450("OSTP")を検出することで識別する。
Modern形式への移行推奨。レガシー識別子は受信側でのみサポート。
チャンネルアドレッシング
チャンネル名は人間が読めるUTF-8文字列で、LAN・WAN両方のアドレスに変換される。
名前規則
| 制約 | 値 |
|---|---|
| エンコーディング | UTF-8 NFC正規化 |
| 長さ | 1–64バイト |
| 使用可能文字 | Printableユニコード + /(パス区切り) |
| 禁止文字 | 制御文字 (U+0000–U+001F)、NULL |
| パス最大深度 | 4階層 |
名前例
"test" // シンプルな名前 "soluna/stage-a" // 階層: ブランド/ステージ "venue/tokyo/floor-1" // 階層: 会場/都市/フロア "dj/akira/set-20260316" // 階層: アーティスト/セット
LAN → Multicastアドレス導出
チャンネル名のFNV-1a 32bitハッシュを計算し、Organization-Localマルチキャスト範囲にマップする:
h = FNV1a_32(channel_name) X = (h >> 8) & 0xFF Y = h & 0xFF multicast_addr = 239.69.X.Y port = 5004 // AES67互換
WAN → Relay接続
// UDP unicast to relay relay.solun.art:5100 // TCP/WS control JOIN:channel_name:password:device_name\n
リレープロトコル (RSP)
Relay Session Protocol。TCP上のテキストベースプロトコル。各コマンドは \n で終端。
接続先: relay.solun.art:5100
接続フロー
コマンド一覧
| コマンド | 方向 | 説明 |
|---|---|---|
| JOIN:<ch>:<pw>:<name> | C→R | チャンネル参加。pw省略可 |
| HELLO | C→R | キープアライブ (5秒毎、MUST) |
| MEMBERS | C→R | メンバー一覧取得 |
| WALLET | C→R | 残高照会 (Solana) |
| TIP:<amount> | C→R | DJ/配信者へチップ送信 (ENAI) |
| SUPPORT:<amount> | C→R | DJ/イベント支援 (ENAI) |
| REPORT:<ssrc>:<recv>:<lost>:<loss%>:<jitter>:<seq> | C→R | RTCP受信レポート |
| OK:joined | R→C | 接続成功 |
| ROLE:<role> | R→C | ロール通知 (dj/owner/listener) |
| PEER:<ip>:<port> | R→C | P2Pピアヒント (NAT traverse用) |
| ERROR:<code>:<msg> | R→C | エラー通知 |
輻輳制御
RFC 8085準拠のUDP輻輳制御。損失率・ジッターを監視し、ビットレートラダーを自動調整する。
ビットレートラダー
| Tier | ビットレート | 品質 | 想定用途 |
|---|---|---|---|
| 0 | 32 kbps | 音声 | 緊急/低速WAN (3G等) |
| 1 | 64 kbps | 良好 | 標準モバイル (4G) |
| 2 | 128 kbps | Hi-Fi | デフォルト (WiFi/5G) |
| 3 | 192 kbps | スタジオ | 高品質WiFi/有線 |
| 4 | 320 kbps | マスター | 録音/アーカイブ/LAN |
状態遷移ルール
3層回復メカニズム
XOR FECパケットフォーマット
PT = 127 // FEC/XOR stream_id = 0xFFFF // FECパケット識別 seq = base_seq // 保護対象の先頭シーケンス番号 Payload = XOR(pkt[N], pkt[N+1], pkt[N+2], pkt[N+3], pkt[N+4])
RTCP受信レポート (JSON over WebSocket)
{
"command": "rx.receiver_report",
"ssrc": 3141592653,
"packets_received": 25000,
"packets_lost": 12,
"loss_rate_pct": 0.05,
"jitter_ms": 1.8,
"last_seq": 25012,
"rtt_ms": 45.2
}
ファイル配信プロトコル
次の曲ファイルを再生前に全受信者へ配信するプリバッファ機構。これにより曲切替を 50ms 以内に同期実行できる。
| 方式 | 切替遅延 |
|---|---|
| 通常アップロード (逐次転送) | 3,000 – 30,000 ms (ファイルサイズに依存) |
| プリバッファ済み (OSTP) | 50 ms ✅ |
配信フロー
// ── 75%再生時点でプリバッファ開始 ───────────────────────────── iOS/Android ──POST /api/player/upload?name=track.mp3&mode=next──→ solunad solunad ←── event: player.next_file_start {name, size} ──────→ 全受信者 ←── binary: [0xFC][0xFD][hi][lo][32KB chunk] × N ──→ 全受信者 solunad ←── event: player.next_file_done ────────────────────→ 全受信者 各受信者 ──command: player.next_file_ready ──────────────────────→ solunad // ── 曲切替時 (全員準備完了後) ──────────────────────────────── solunad ←── event: player.switch {switch_delay_ms: 50, file_pos_ms: 0} → 全受信者 各受信者 ── 50ms後にローカルAVPlayer/MediaPlayerで再生開始 ─────────
バイナリフレームフォーマット
| マジック | 用途 |
|---|---|
| 0xFA 0xFB | 現在曲チャンク (ライブ配信) |
| 0xFC 0xFD | 次曲プリバッファチャンク |
[magic 2B][size_hi 1B][size_lo 1B][payload 最大32768B]
— サイズフィールドはbig-endian。最大チャンクサイズ = 32,768バイト = 32KB。
デーモン制御プロトコル (DCP)
solunadデーモンをWebSocket経由で制御するJSONプロトコル。接続先: ws://host:8400/ws
メッセージフォーマット
// リクエスト (id は任意の整数) { "id": 42, "command": "monitor.set_delay", "ms": 40 } // レスポンス { "id": 42, "success": true, "data": "" } // ブロードキャストイベント (id なし) { "event": "player.switch", "switch_delay_ms": 50, "file_pos_ms": 0 }
コマンド一覧
Monitor (TX再生制御)
| コマンド | 説明 |
|---|---|
| monitor.stats | 送信統計 (送信パケット数、ビットレート、接続数) |
| monitor.start | 音声送信開始 |
| monitor.stop | 音声送信停止 |
| monitor.set_volume | 音量設定 (0.0–1.0) |
| monitor.set_mute | ミュート切替 (bool) |
| monitor.set_buffer | バッファサイズ設定 (ms) |
| monitor.set_delay | 遅延補正設定 (ms) |
| monitor.list_devices | 利用可能なオーディオデバイス一覧 |
Player
| コマンド | 説明 |
|---|---|
| player.status | 現在の再生状態 |
| player.play | 再生開始 |
| player.pause | 一時停止 |
| player.stop | 停止 |
| player.seek | シーク (pos_ms) |
| player.file_ready | 現在曲のプリバッファ完了通知 (受信者→送信者) |
| player.next_file_ready | 次曲のプリバッファ完了通知 (受信者→送信者) |
System / Channel
| コマンド | 説明 |
|---|---|
| system.info | デーモンバージョン、OS、ビルド情報 |
| time.ping | RTT測定 (遅延補正計算に使用) |
| mode.get | 現在の動作モード取得 (tx/rx/loopback) |
| mode.set | 動作モード切替 |
| channel.get | 現在のチャンネル名取得 |
| channel.set | チャンネル名変更 |
| rx.receiver_report | RTCP受信レポート送信 (→ relay) |
Time Sync アルゴリズム
// クライアントサイド実装例 (TypeScript) const t1 = Date.now() ws.send(JSON.stringify({ command: "time.ping" })) // recv: {"data": "{\"pong\":true}"} const t2 = Date.now() const latency = Math.max(20, Math.floor((t2 - t1) / 2) + 15) // 遅延補正をデーモンに適用 ws.send(JSON.stringify({ command: "monitor.set_delay", ms: latency })) // ジッターバッファも同期 jitterBufferTarget = latency
DJデュアルデッキ
stream_idフィールドを利用して2つのデッキ(Deck A / Deck B)を同時に扱う。 クロスフェード・BPM同期・エフェクトをプロトコルレベルでサポート。
stream_id エンコーディング
| deck_id | 意味 |
|---|---|
| 0x0 | Deck A (プライマリ) |
| 0x1 | Deck B (セカンダリ) |
| 0x2–0xF | Reserved (将来の拡張用) |
動作モード
パラメータコマンド
// クロスフェード位置設定 (0.0 = Deck A, 1.0 = Deck B) { "command": "deck.set_param", "deck": 0, "param": "crossfade", "value": 0.7 } // BPM設定 { "command": "deck.set_param", "deck": 1, "param": "bpm", "value": 128.0 } // モード切替 { "command": "deck.set_mode", "mode": "param_only" }
等パワークロスフェードアルゴリズム
// crossfade: 0.0 = Deck A only, 1.0 = Deck B only θ = crossfade × π / 2 gain_A = cos(θ) // Deck A: 1.0 → 0.0 (equal-power curve) gain_B = sin(θ) // Deck B: 0.0 → 1.0 (equal-power curve) output = gain_A × deck_A + gain_B × deck_B // MUST smooth over ≥ 10ms (480 samples @ 48kHz) to prevent clicks // クリックノイズ防止のため ≥480サンプルかけてスムーシングすること
セキュリティ
OSTProのセキュリティモデルはコンテンツの機密性に応じて段階的に適用される。
暗号化
| 要件 | ケース |
|---|---|
| OPTIONAL DTLS-SRTP | パブリックチャンネル (無料コンテンツ) |
| REQUIRED DTLS-SRTP | 有料コンテンツ / クローズドチャンネル / ペイウォール |
セッショントークン
// 256-bit random, base64url encoded token = base64url(crypto.randomBytes(32)) // WebSocket Authorizationヘッダで送信 Authorization: Bearer <token> // トークン有効期限: セッション終了まで (最大24時間)
レート制限
| 対象 | 制限 | 窓 |
|---|---|---|
| JOIN試行 | 10回 | 1分/IP |
| NACK送信 | 10パケット | 1秒/セッション |
| UDP受信 | 10,000パケット | 1秒/IP |
| CHARGE (支払い) | nonce + 300秒ウィンドウ | リプレイ防止 |
プライバシー
IANA登録
OSTProが使用するポート番号・ペイロードタイプ・マルチキャストアドレスのIANA登録状況。
| リソース | 値 | 状態 |
|---|---|---|
| RTP Extension Profile | 0x4F53 ("OS") | 申請予定 |
| UDP Port (audio) | 5004 | 既存割当 (AES67互換) |
| UDP/TCP Port (relay) | 5100 | 申請予定 |
| TCP/WS Port (DCP) | 8400 | 申請予定 |
| Payload Type 96 | PCM24 | Dynamic |
| Payload Type 97 | F32 | Dynamic |
| Payload Type 98 | Opus | Dynamic |
| Payload Type 126 | NACK | Dynamic |
| Payload Type 127 | FEC/XOR | Dynamic |
| Multicast (LAN) | 239.69.0.0/16 | Organization-Local |
実装状況
各プラットフォームでのOSTProサポート状況。✅ = 実装済み・テスト済み、🔜 = 実装計画あり (DTLS)。
| プラットフォーム | 受信 RX | 送信 TX | FEC (動的) | NACK | DTLS | RTCP | TURN | DJ Deck |
|---|---|---|---|---|---|---|---|---|
| Mac (solunad) | ✅ | ✅ | ✅ | ✅ | 🔜 | ✅ | ✅ | ✅ |
| iOS (Swift) | ✅ | ✅ | ✅ | ✅ | 🔜 | ✅ | ✅ | ✅ |
| Android (Kotlin) | ✅ | ✅ | ✅ | ✅ | 🔜 | ✅ | 🔜 | 🔜 |
| Windows | ✅ | ✅ | ✅ | ✅ | 🔜 | ✅ | 🔜 | 🔜 |
| Web (Browser) | ✅ | ✅ | ✅ | ✅ | 🔜 | ✅ | 🔜 | 🔜 |
| Linux / RPi | ✅ | ✅ | ✅ | ✅ | 🔜 | ✅ | 🔜 | 🔜 |
| ESP32 | ✅ | ✅ | ✅ | ✅ | 🔜 | ✅ | — | — |
FEC (動的): loss率に応じ N=0/10/5/3/2 を自動切替 (OSTP v0.9.3 §3.7)
RTCP: ビットレート適応 + RFC 3550 §6.4.1 ジッタバッファ駆動
TURN: Symmetric NAT フォールバック —
TURN_ALLOC / TURN_OK (OSTP v0.9.3 §5.x)
v0.9.3 実装済み修正
| 修正 | 詳細 | 影響 |
|---|---|---|
media_timestamp ms化 | ナノ秒 (4.3秒ロールオーバー) → ミリ秒 (49日ウィンドウ) | 全プラットフォーム同期修正 |
| Dynamic FEC | RTCP loss% → N自動調整 (0/10/5/3/2) | WiFi損失への適応 |
| RTCP → ジッタバッファ | RFC 3550 inter-arrival jitter → target_fill_frames | iOS バッファ精度向上 |
| TURN フォールバック | TURN_ALLOC / TURN_OK リレー確認 | Symmetric NAT 対応 |
| Gossip + dual-parent | GOSSIP_PEERS 8候補 / PARENT_FAIL <80ms 再接続 | スワームチャーン耐性 |
| RTCP APP SWCH | PT=204 同期ファイル切替をリレー経由で全員転送 | player.switch 精度 |
| TIP on-chain検証 | Solana RPC getTransaction で tx_signature 確認 | 不正チップ防止 |
| Control/Data HOL解消 | ファイルチャンク port 8401 / コントロール JSON port 8400 | 大容量ファイル中の遅延防止 |
ドキュメント
OSTP仕様書・Internet-Draft・APIリファレンスへのリンク。