リアルタイム通信
このページの見出し
ブラウザーやアプリの参加者どうしが、WebRTC で映像・音声・データをやり取りする場が「ルーム」です。 SENCA はルームの中で SFU(Selective Forwarding Unit。受け取った映像・音声を変換せず、必要な相手へ転送するだけのサーバー)として動きます。
このページの要点:
- ルームは
POST /api/v1/roomsで作ります。参加者には参加トークン(POST /rooms/{id}/tokens)を渡し、WebSocket のGET /signalingで接続してもらいます。 - 送信のみ・受信のみといった役割、1 人が複数の映像を送るマルチストリーム、サイマルキャスト(同じ映像を画質違いで同時に送り、受信側ごとに選ぶ方式)、スポットライト(話している人だけを高画質にする機能)が使えます。
- 転送フィルター、DataChannel のメッセージ、E2EE のパススルー、統計にも対応します。参加者にはメタデータと ID(
metadata・client_id・bundle_id)を付けられます。 - ルームはグリッドのワークロードとして 1 つのノードに置かれます。別のノードに接続した参加者は、配置先へ案内(リダイレクト)されるか、カスケードでそのまま受け付けられます。
- ルームで起きたことは SSE と Webhook で受け取れます。
基本の考え方
ルームはコントロールプレーンで管理するリソースで、POST /api/v1/rooms で作成し、どのノードからでも読み書きできます。ルームの ID はテナントをまたいで一意です。
作ったルームは、グリッドのワークロード(room:{id})として、ちょうど 1 つのノードに配置されます(クラスターとグリッド)。
そのノードがルームを開き、参加者のピア接続を受け付けます。どこに配置されたかは、ルームの placement_status でわかります。
参加者はどのノードに接続してもかまいません。シグナリングの GET /signaling は、最初の connect の room_id を見ます。
ルームが別のノードにあれば redirect {location} を返して接続を閉じ、クライアントを配置先へ案内します(リダイレクト)。
参加者・メッセージ・統計の REST API は、受け付けたノードがルームの配置先へ転送します。
ただし cascade: true のルームではリダイレクトせず、接続したノードで参加者を受け付けます(「ルームの移動と障害」のカスケード)。
ドレインやノードの停止で配置先が変わったときは、元のノードがまだ動いていれば、現在の参加者に redirect を送って新しいノードへ入り直してもらいます。
参加トークンは、どのノードでも発行・検証できます。署名鍵をコントロールプレーンに保存した鍵から導出しているので、全ノードで同じ鍵になるためです。
準備(ノードの起動)
ルームを使うノードでは WebRTC を有効にします。
| フラグ | 要否 | 意味 |
|---|---|---|
--webrtc-udp ADDR | 必須 | WebRTC の UDP ソケット(WHIP・WHEP と共用)。0.0.0.0:ポート で待ち受けると、ICE の候補にはノードのネットワークインターフェースのアドレスが入ります |
--webrtc-public-ip IP | NAT・Docker の内側では必須 | クライアントから見えるノードの IP アドレス。ICE の候補にはこのアドレスが入ります(カンマ区切りか、オプションの繰り返しで複数指定できます) |
--public-http-url URL | 推奨 | ルーム・トークンの応答に含まれる signaling_url の元になります(http:// は ws://、https:// は wss:// に変え、/signaling を付けます)。指定しないと、自ノードに配置されたルームの signaling_url は null になります |
--https ADDR + --tls-cert・--tls-key | 任意 | wss:// で受け付けます(/signaling は HTTP と HTTPS の両方の待ち受けで動作します) |
senca-node --data-dir /var/lib/senca --http 0.0.0.0:8080 \
--webrtc-udp 0.0.0.0:20000 --webrtc-public-ip 203.0.113.10 --public-http-url https://media.example.com
--webrtc-udp を指定していないノードでは、GET /signaling が HTTP 503(text/plain、webrtc (--webrtc-udp) is off)を返します。
そのノードに配置されたルームでは、参加者・メッセージ・統計の API も 503 CAPACITY_UNAVAILABLE になります。
クラスターでの注意: グリッドは、WebRTC が有効なノードかどうかを見ずにルームを配置します。WebRTC が無効なノードに置かれたルームには、誰も参加できません。
全ノードで --webrtc-udp を有効にするか、WebRTC が有効なノードに --label webrtc=on を付け、ルームの placement.labels で配置先を絞ってください。
リダイレクトと API の転送には、クラスター設定にある各ピアの api(管理 API の URL)を使います。
api が無いノードに配置されたルームは、他のノードからは「配置先が決まっていない」ように見えます(「ルームの REST API」の「ルームを配置したノードへの問い合わせ」)。
ルームの REST API
すべて /api/v1 の下にあり、共通の規約(Bearer、problem+json、Idempotency-Key、ETag・If-Match、limit・cursor、未知のフィールドは 400)に従います
(API と SDK)。
| 操作 | メソッドとパス | 権限 | 結果 |
|---|---|---|---|
| 作成 | POST /rooms {id?, settings?, metadata?, placement?} | stream:write | 201、Location・ETag |
| 一覧 | GET /rooms?limit&cursor | stream:read | 200 {items, next_cursor}(ID 順。limit の既定は 100、1〜1000) |
| 取得 | GET /rooms/{id} | stream:read | 200、ETag: "{generation}" |
| 更新 | PATCH /rooms/{id} {settings?, metadata?, placement?} | stream:write | 200。If-Match 必須(無ければ 428、古ければ 412) |
| 削除 | DELETE /rooms/{id} | stream:write | 204。参加者は room_deleted で切断されます |
| 参加トークン | POST /rooms/{id}/tokens | stream:write | 201(「参加トークン」) |
| 参加者の一覧 | GET /rooms/{id}/participants?limit&cursor | stream:read | 200 {items, next_cursor} |
| 参加者の取得 | GET /rooms/{id}/participants/{connection_id} | stream:read | 200 |
| 参加者のメタデータの変更 | PATCH /rooms/{id}/participants/{connection_id} {metadata} | stream:write | 200。全員に connection.updated が通知されます |
| 参加者の切断 | DELETE /rooms/{id}/participants/{connection_id} | stream:write | 204。理由は kicked |
| メッセージの送信 | POST /rooms/{id}/messages {to?, label?, data} | stream:write | 200 {delivered}(「メッセージ」) |
| 統計 | GET /rooms/{id}/stats | stream:read | 200(「参加者と統計」) |
参加者の変更・切断は、どのノードに送っても、参加者が接続しているノード(カスケードのエッジノードを含む)に転送されて実行されます。
作成
id には 1〜128 文字の英数字と -_.:@ が使えます。省略すれば、24 文字の 16 進数の ID が自動で付きます。
同じ ID のルームがすでにあると、それがどのテナントのルームでも 409 CONFLICT です。1 つのテナントで作れるルームは 10000 個までで、超えると 403 QUOTA_EXCEEDED になります。
metadata には任意の JSON を 8 KiB まで入れられます。
placement は、入力・出力と同じ配置の指定です(node・device・labels・anti_affinity・pin_policy・priority・strategy)。
node にはノードの ID か名前を書け、保存するときに ID に変換されます。degrade はパイプライン専用なので、ルームに指定すると 400 です。
詳しくは クラスターとグリッド を参照してください。
ルームの設定(settings)
省略したフィールドは既定値になり、未知のフィールドは 400 です。作成時にも更新時にも下の範囲を検査し、
外れていれば 400 INVALID_CONFIG を返します(details.errors に該当するフィールドが入ります)。
| フィールド | 既定 | 範囲・意味 |
|---|---|---|
max_participants | 50 | 1〜10000。ルームの参加者数の上限です。グリッドでの資源の見積もり(下記)にも使います |
e2ee | false | E2EE のルームにします(「E2EE」) |
video_codec | vp8 | vp8・vp9・h264・av1。ルームで 1 つです(SFU は変換しないため、全員が同じコーデックで送信します) |
audio_codec | opus | opus のみ |
max_video_bitrate_kbps | なし | 50〜50000。参加時の connected メッセージで、送信する参加者に伝える映像のビットレートの上限(推奨値) |
spotlight | なし | {number, unfocused?, focus_rid?}。number は 1〜16、unfocused は lowest(既定)か off、focus_rid は 1〜16 文字の英数字(「スポットライト」) |
recording | none | none・per_participant・composite。per_participant(参加者ごと)か composite(全員を並べた 1 本)にすると、録画用のメディアパイプラインが自動で作られます(「ルームの REST API」の「ルームの録画」)。e2ee: true と composite は併用できません(422) |
forwarding_filter | なし | ルーム全体の転送フィルター(「マルチストリーム・サイマルキャスト・レイヤーの選択・転送フィルター」) |
data_labels | ["#*"] | 使用を許可する DataChannel のラベルのパターン。# で始まる 64 バイトまでの文字列を 32 個まで指定できます。末尾の * は前方一致を表します |
empty_timeout_secs | 60 | 自動で作成されたルーム(auto: true)が空になってから削除されるまでの秒数 |
cascade | false | true にすると、ルームの配置先でないノードに接続した参加者も、そのノードで受け付けます(「ルームの移動と障害」のカスケード)。false なら配置先へリダイレクトします |
auth_webhook_url | なし | このルームの認証 Webhook の URL(http://・https://)。ノードの --rooms-auth-webhook より優先されます(「参加トークン」) |
更新
settings には変えたいフィールドだけを書きます(JSON Merge Patch、RFC 7396)。書かなかったフィールドはそのまま残り、null にしたフィールドは既定値に戻ります。
spotlight のような入れ子のオブジェクトも同じ規則です。変更を反映した結果は、作成時と同じ規則で検査されます。
参加者がいる間は、e2ee・video_codec・audio_codec を変えられません(409)。配置の指定は placement: null で解除できます。
ルームの録画
settings.recording を per_participant か composite にすると、そのルームの録画用のメディアパイプライン(ID は room-rec.{ルームの ID}、
managed: true)が自動で作られ、以後はルームの設定に合わせて更新されます(メディアパイプライン)。
| 値 | 録画されるもの |
|---|---|
per_participant | 参加者ごとにエクスポートしたストリームを、それぞれ録画します。E2EE のルームでも、暗号化されたまま録画できます |
composite | 映像のある参加者を格子に並べた 1 本のストリーム(AV1、1280x720)を作り、それを録画します |
none に戻すとメディアパイプラインは削除されますが、エクスポートしたストリームと録画は残ります。
このメディアパイプラインを API から直接変更・削除することはできません(409)。録画の方法を変えたいときは、ルームの設定を変更してください。
ルームの設定の変更は、ルームを配置したノードが検知して反映します。転送フィルターやスポットライトを変えた場合は、その場で受信スロットを再割り当てします。
ルームの表示
POST・GET・PATCH の応答には、コントロールプレーンに保存されたルームの情報が含まれます(参加者数などは含みません)。
| フィールド | 意味 |
|---|---|
id・generation・settings・metadata | ルームの ID、世代(更新のたびに増える番号。ETag に使います)、設定、メタデータ |
auto | 参加トークンの発行時に自動で作成されたルームかどうか |
created_at・updated_at | 作成日時と更新日時(RFC 3339) |
signaling_url | 接続先。ルームが別のノードにあればそのノードの URL、そうでなければこのノードの --public-http-url から作った URL(無ければ null) |
placement | 配置の指定 |
placement_status | 配置の状態。state は running・degraded・pending・unschedulable・not_scheduled のどれかです。配置済みなら node・node_name・device・level・level_label・since・reason が、配置できなければ reasons が入ります |
グリッドでの資源の見積もり
ルームに必要な資源は、max_participants(N)から見積もります。
| 資源 | 見積もり |
|---|---|
| CPU | 200 + 20 × N ミリコア |
| メモリ | 64 + 2 × N MB |
| 受信 | 2 × N Mbit/s |
| 送信 | 2 × N × min(N, 8) Mbit/s |
ルームで動作中のメディアパイプライン(desired_state: running)があれば、1 つにつき CPU 20 × N ミリコアとメモリ 8 × N MB を加えます。
既定の 50 人なら、CPU 1200 ミリコア、メモリ 164 MB、受信 100 Mbit/s、送信 800 Mbit/s です。
5 人で使うルームなのに既定の 50 のままだと、その分ノードの容量を余計に押さえてしまいます。max_participants を実際の人数に合わせれば、1 つのノードにより多くのルームを置けます。ノードの容量は --net-mbps などで設定します(クラスターとグリッド)。
ルームを配置したノードへの問い合わせ
参加者・メッセージ・統計の API に応答するのは、ルームを配置したノードです。
ルームが別のノードにあれば、リクエストを受けたノードが同じヘッダー(Authorization・Content-Type・If-Match・Idempotency-Key・X-Request-Id)のまま
配置先の /api/v1 へ転送し、ステータスコード・本文・ETag をそのまま返します。転送のタイムアウトは 10 秒です。
転送されてきたリクエストを、さらに別のノードへ転送することはありません。そうなる場面では 503(retryable: true)を返します。
このほかに返るエラーは次のとおりです。
| 状況 | 応答 |
|---|---|
| 配置先がまだ決まっていない(作成直後など) | 503 CAPACITY_UNAVAILABLE(retryable: true) |
| ルームの処理が 5 秒以内に応答しない | 503(retryable: true) |
| 配置先のノードがまだルームを開いていない | 404 |
応答に含まれる時刻(joined_at・at など)は RFC 3339 の形式です。
参加トークン
参加者がルームに入るには、参加トークンが要ります。アプリケーションのサーバーで POST /rooms/{id}/tokens を呼んで発行し、ブラウザーやアプリへ渡してください。リクエストの本文は次のとおりです。
| フィールド | 既定 | 意味 |
|---|---|---|
role | 必須 | sendrecv(送受信)・sendonly(送信のみ)・recvonly(受信のみ)。参加時に選べる役割の上限です(sendrecv ならどれでも選べます) |
client_id | なし | 参加者を見分けるための ID。1〜255 文字で、制御文字は使えません |
bundle_id | なし | 同じ利用者の複数の接続をまとめる ID。1〜255 文字で、制御文字は使えません |
metadata | なし | 他の参加者に公開するメタデータ(8 KiB まで) |
permissions | 下の既定値 | {publish, subscribe, data}(「権限」) |
ttl_secs | 600 | トークンの有効期間(秒)。10〜86400 |
応答(201):
{"token":"eyJ…","room_id":"daily","expires_at":"2026-11-03T10:10:00Z","signaling_url":"wss://media.example.com/signaling"}
まだ無いルームのトークンを発行すると、そのルームが自動で作られます(既定の設定で、auto: true が付きます)。
自動で作られたルームは、空のまま empty_timeout_secs(既定 60 秒)が過ぎると削除されます。誰も参加しないまま時間が過ぎたルームも同じです。
空かどうかは、ルームを配置したノードが 5 秒ごとに確かめます。なお、他のテナントのルームの ID を指定すると 404 になります。
トークンは JWT 互換の形式で、Ed25519(EdDSA、kid: senca-rooms-1)で署名されます。iss は "senca"、aud は "senca-rooms"、sub はテナントです。
クラスターの初期化前で署名鍵がまだ無いときは、503(retryable: true)が返ります。
権限(permissions)
| フィールド | 既定 | 意味 |
|---|---|---|
publish | ["audio","video"] | 送信を許可するトラックの種類。許可されていない種類を送ると forbidden になります |
subscribe | true | 他の参加者のトラックを受信できるかどうか |
data | ["#*"] | 使用を許可する DataChannel のラベルのパターン |
検証の規則
参加時(connect)に、ルームを配置したノードが次の規則で検証します。
- トークンは必須です(既定)。トークンの無い
connectはauth_failedになります(ルームのシグナリングの「エラー」)。--rooms-auth-mode onlyのときだけ、認証 Webhook がトークンの代わりに参加を認めます。 exp(有効期限)は必須です。時計のずれは 30 秒まで許容します。現在からexpまでが 24 時間を超えるトークンは拒否します。発行時刻のiatが先の時刻になりすぎているトークンも拒否します。- トークンの
roomとconnect.room_idが違うとforbiddenになります。connect.roleがトークンのroleの範囲を超えた場合もforbiddenです。 client_id・bundle_idは、クライアントが自分で名乗った値を信用しません(転送フィルターの条件に使われるためです)。- トークンに
client_idがあれば、それを使います。connect.client_idを付ける場合は、同じ値でなければforbiddenです。 - トークンに
client_idが無いのにconnect.client_idを付けると、forbidden(client_id must be issued in the token)になります。 bundle_idも同じです。トークンに無いのにconnect.bundle_idを付けるとforbiddenになります。どちらにも無ければ、bundle_idはconnection_idと同じ値になります。
- トークンに
- 参加者のメタデータには、トークンの
metadataを使います。connect.metadataは他の参加者には公開されません(認証 Webhook に渡すためのフィールドです)。
認証 Webhook
参加の可否を自分のサーバーでも判断したいときは、認証 Webhook を使います。
ルームの設定 auth_webhook_url(無ければノードの --rooms-auth-webhook)に URL があると、ノードはトークンを検証した後、
その URL へ署名付きの POST で問い合わせます(本文と応答の形式は ルームのシグナリングの「認証」)。
どちらの URL も無いルームでは、トークンだけで参加を認めます。
Webhook が allowed: false を返すと auth_failed になり、reason はそのままクライアントに届きます。
許可する場合は、応答で role・client_id・metadata・permissions を上書きできます。5 秒以内に応答が無いときや、2xx 以外が返ったときは拒否として扱います。
署名用のシークレットは --rooms-webhook-secret-file で渡します(16 バイト以上。Senca-Signature ヘッダーに使います)。
シークレットの無いノードでは、Webhook の URL を持つルームには誰も参加できません。
宛先は --egress-allow の方針で検査されるので、社内のアドレスへ送るときは許可しておいてください。
シグナリング
シグナリングは、WebSocket の GET /signaling(subprotocol senca.rooms.v1 が必須)で JSON のメッセージをやり取りします。
メッセージの詳しい形式は ルームのシグナリング にあり、ここではノードでの処理の流れだけを示します。
- アプリケーションのサーバーが
POST /rooms/{id}/tokensでトークンを発行し、tokenとsignaling_urlをブラウザーへ渡します。 - クライアントが
signaling_urlへ接続し、10 秒以内にconnect {version: 1, room_id, token, role, ...}を送ります(HTTP のAuthorizationヘッダーは使いません)。 - 接続を受けたノードは、ルームの配置先を確認します(作成直後のルームでは、配置先が決まるまで最大 3 秒待ちます)。
- ルームが別のノードにある場合は、
{"type":"redirect","location":"ws://…/signaling"}を送り、クローズコード 4300 で閉じます。クライアントはその URL へ接続し直します。 - 配置先が決まらない場合は、
{"type":"error","code":"capacity_unavailable","fatal":true,…}を送り、クローズコード 4503 で閉じます。 - ルームがこのノードにある場合は、
connectを処理してconnectedを返します。
- ルームが別のノードにある場合は、
- クライアントが SDP の offer を送ります。offer を送るのは常にクライアントです。 サーバーは ICE-lite で動作し、answer だけを返します。ICE と DTLS の接続が確立すると、メディアが流れ始めます。
- サーバーは 10 秒ごとに
pingを送ります。10 秒以内にpongが返らなければ、ping_timeoutで切断します。
主なクローズコード(WebSocket を閉じるときの番号)は次のとおりです。
| クローズコード | 発生する場面 |
|---|---|
| 4408 | WebSocket を開いてから 10 秒以内に最初のメッセージが届かない |
| 1013 | ルームの処理をまだ開始できない(署名鍵が無い、クラスターの初期化前) |
| 4300 | 上記のリダイレクト |
| 4503 | 上記の capacity_unavailable、または room_full |
その他のエラーコード、クローズコード、切断の理由は、ルームのシグナリングの「エラー」の表のとおりです。
ルームが存在しない場合(コントロールプレーンに無い、または配置先のノードがまだルームを開いていない場合)は room_not_found になります(ノードは参加時にルームを作成しません)。
マルチストリーム・サイマルキャスト・レイヤーの選択・転送フィルター
受信スロット
クライアントの SDP にある受信用の m 行 1 つが、受信スロット 1 つになります。サーバーはルームのトラックを受信スロットに割り当て、足りなければ renegotiate を送ってスロットを増やしてもらいます。
受信スロットは種類(音声・映像)ごとに 32 個までです。multistream: false の参加者は、種類ごとに 1 本だけ受信します。
自分のトラックと、同じ bundle_id を持つ接続のトラックは届きません。
サイマルキャストとレイヤーの選択
サイマルキャストで送る参加者は、connect.simulcast {rids, default_rid?} で、送信するレイヤーの rid を低い順に 1〜4 個指定します(1〜16 文字の英数字、重複不可)。
どのレイヤーを受け取るかは、受信者ごと・トラックごとに決まります。優先順位は次のとおりです。
- クライアントが送った
layer {track_id, rid}(rid: nullで自動に戻ります) - スポットライト
simulcast.default_rid- 自動。推定帯域から音声の分として 64 kbit/s を引き、0.9 を掛け、受信している映像の数で割ります。その帯域に収まる最も高いレイヤーを選びます。
自動で選ぶ場合、レイヤーを下げるのは帯域の不足が 1 秒続いてからです(推定値が必要量の 0.6 倍を下回ったときはすぐ下げます)。上げるのは、帯域に収まる状態が 2 秒続いてからです。
切り替えは新しいレイヤーのキーフレームが来た時点で行い、それまでは今のレイヤーを転送し続けます。いま受信しているレイヤーは notify {event: "layer.changed"} でわかります。
転送フィルター
ルームの settings.forwarding_filter と、接続ごとの connect.forwarding_filter の両方の条件を満たすトラックだけが転送されます。
{"action":"block","rules":[[{"field":"kind","operator":"is_in","values":["video"]},
{"field":"client_id","operator":"is_not_in","values":["teacher"]}]]}
この例は「先生(client_id が teacher)以外の映像は受信しない」という指定です。rules は、AND で結んだ規則の組を OR でつないだものです(1〜10 組、各組 1〜10 規則)。
field には送信する側の connection_id・client_id・bundle_id・kind・metadata.<key>(キーは 64 バイトまで、値が文字列・数値・真偽値の場合)を指定します。
operator は is_in・is_not_in、values は 1〜100 個(各 255 バイトまで。kind の場合は audio・video)です。
action は block(一致したものを転送しない)か allow(一致したものだけを転送する)です。
スポットライト
大人数の会議で全員を高画質で受け取ると、帯域がすぐに足りなくなります。スポットライトを使うと、いま話している number 人の映像だけを高いレイヤーで転送できます。
ルームの settings.spotlight で有効にしてください。
注目していない参加者の映像は、最も低いレイヤーで転送するか(unfocused: "lowest")、転送を止めます("off"。受信スロットは残り、paused になります)。
話しているかどうかは、RFC 6464 の音量情報で判定します。音量情報が無ければ Opus のパケットサイズから推定します。
音声が 200 ms 続けば話し始め、1.5 秒無音が続けば話し終わりとみなします。一度注目した人は、2 秒間は注目から外しません。
注目する人の組はルームで 1 つで、変わると spotlight.changed が通知されます。ただし受信者ごとに自分は除かれるので、各受信者が注目するのは自分以外の number 人です。
注目した人の映像は focus_rid のレイヤーで転送します。focus_rid が無ければ、帯域に合わせて自動で選びます。
クライアントも connect.spotlight {number, focus_rid?, unfocused?} で、ルームの設定の範囲内で値を選べます(number はルームの値を超えないよう切り詰めます)。
メッセージ(DataChannel・プッシュ)
参加者どうしのメッセージには、ラベルが # で始まる DataChannel を使います。クライアントは connect.data_channels で宣言してから DataChannel を開きます。
受け付けるのは、ルームの data_labels とトークンの permissions.data の両方に一致するラベルだけです。
届いたメッセージは、同じラベルの DataChannel を受信用に開いている他の参加者全員へ、そのまま転送されます。
特定の参加者にだけ送りたいときは、シグナリングの message {label, to: [connection_id], data} を使います。受信者には from 付きで届きます。
アプリケーションのサーバーから送るときは、POST /rooms/{id}/messages を使います。本文は次のとおりです。
| フィールド | 意味 |
|---|---|
to | 宛先の connection_id の配列。省略するか空にすると全員に送ります。ルームにいない ID が含まれていると 404 になります |
label | 指定すると、そのラベルの DataChannel を開いている参加者へ送ります(# で始まること、data が文字列であること) |
data | JSON(64 KiB まで)。label が無ければ、シグナリングの push {data} として届きます |
応答は {"delivered": N} で、N はメッセージを届けた参加者の数です。
大きさの上限は、アプリケーションのメッセージが 64 KiB、シグナリングのメッセージが 256 KiB です。
1 つの接続から送れるのは毎秒 50 メッセージまでで、超えると rate_limited、その 4 倍を超えると切断されます。
参加者と統計
参加者の情報(GET …/participants)には、次のフィールドが含まれます。
connection_id・client_id・bundle_id・role・metadatatracks[] {track_id, connection_id, kind, rids?}: 送信しているトラックjoined_at(参加した日時)・peer_connected(ピア接続ができているか)・signaling(websocketかdatachannel)subscriptions[]: 受信スロットの一覧。mid・kind・state(active・paused・empty)・track_id・connection_id・client_idが入ります
統計(GET …/stats)は {room_id, at, participants[]} の形で返ります。参加者ごとに次の値が含まれます。
connection_id・rtt_ms(往復の遅延)・bandwidth_bps(推定帯域)・send_bitrate_bpsretransmitted_packets・nack_requested・received_bytes・forwarded_bytes・forwarded_framespublished[] {track_id, kind, layers[] {rid, bitrate_bps}}: 送信しているトラックとレイヤーごとのビットレートsubscriptions[] {mid, track_id, state, rid}: 受信スロットの状態client: クライアントがpong.statsやstatsで送った getStats の要約
ルームの移動と障害
ドレイン(POST /nodes/{id}/drain)やノードの停止で、グリッドがルームを別のノードへ移すことがあります。
このとき元のノードは、現在の参加者に redirect を送り、disconnect {reason: "redirected"} を送ってから接続を閉じてルームを手放します。
新しいノードがルームを開き、参加者はそこへ入り直します。connection_id とトラック ID は新しくなります。
元のノードが異常停止した場合は、リダイレクトを送れません。参加者のピア接続とシグナリングが切れるので、クライアントが自分で入り直します。
入り直す先はどのノードでもよく、新しい配置先へリダイレクトされます。SDK の url に複数のノードを書いておけば、停止したノードを飛ばして接続できます。
グリッドがルームを移すまでにかかる時間は クラスターとグリッド を参照してください。
どちらの場合も、移動前のルームの状態(参加者・トラック・スポットライト)は引き継がれず、新しいノードでは空のルームから始まります。
カスケード(ルームを複数のノードにまたがらせる)
ルームの設定を cascade: true にする(またはノードで --rooms-cascade を指定する)と、参加者は近くのノードに接続したまま同じルームに参加できます。
ノード間の中継を使うので、クラスターを組んでいることが前提です。
- ルームの配置先のノードをホームノード、それ以外で参加者を受け付けるノードをエッジノードと呼びます。エッジノードでのルームは参加者が来たときに開かれ、 参加者がいなくなってから 20 秒で閉じます。ドレイン中のノードはエッジノードにならず、ホームノードへリダイレクトします。
- 別のノードの参加者は、
nodeフィールド付きの参加者として見えます。映像・音声・DataChannel のメッセージは、ノード間の中継で届きます。 REST の参加者一覧(ホームノードへ転送されます)では、別のノードの参加者はsignaling: "remote"と表示されます。 max_participantsは、ノードをまたいで合計した人数で判定します。別々のノードで同時に参加すると、短い間だけ上限を超えることがあります。- ホームノードが停止すると、グリッドがルームを別のノードへ移します。移動先がエッジノードだった場合、そこにいた参加者は切断されずにそのまま続けられます。
停止したノードにいた参加者は、別のノードへ接続し直せば入り直せます(SDK の
urlに複数のノードを書いておきます)。 - 別のノードにいる参加者を切断・更新する REST API は、どのノードで呼んでも、参加者がいるノードへ転送されます。
- メディアパイプラインは、エッジノードにいる参加者のトラックも、そのノードから中継で受け取ってエクスポートします。
- カスケードの状態は、
GET /api/v1/nodesの自ノードのroomsで確認できます。
E2EE(パススルー)
settings.e2ee: true のルームでは、クライアントが SFrame などで暗号化したメディアを、SFU は復号せずにそのまま転送します。
参加者は connect.e2ee をルームの e2ee と同じ値にしてください。違うと e2ee_mismatch になります。参加者がいる間は、ルームの e2ee を変えられません。
暗号化・復号と鍵の配布は、アプリケーションが行います。ブラウザー SDK はルームの設定と食い違っていないかを確かめるだけで、暗号化の機能(Encoded Transform)は持ちません。
サーバーは中身を見られないので、サーバーで中身を処理する操作はすべて 422(UNSUPPORTED_CODEC、details.reason: "e2ee")になります。対象は次のとおりです。
- ルームの操作:
recording: "composite"の指定、合成(composite)か文字起こし(captions)を含むメディアパイプラインの作成・変更、 合成・文字起こしのメディアパイプラインがあるルームをe2ee: trueに変えること。 - E2EE のルームからエクスポートしたストリーム(
metadata.e2ee: "true"が付きます)の利用: 静止画の取得(GET /streams/{id}/snapshot)、 字幕(captions)、静止画の保存(snapshots)、静止画の予約、パイプラインの入力(source)。録画のサムネイルも作られません。
つまり E2EE のルームでできるのは、参加者ごとのエクスポートと、暗号化されたままの録画だけです。
イベント(SSE)
ルームのイベントは GET /api/v1/events(API と SDK)に流れます。イベントは 2 種類あり、取りこぼしたときの扱いが違います。
制御のイベントは、クラスターの合意ログに記録されます。SSE の id を持つため、Last-Event-ID で途中から受信を再開できます。
event | 発生する場面 | data |
|---|---|---|
room.updated | ルームの作成、自動作成、更新 | {auto} |
room.deleted | ルームの削除(自動作成されたルームの自動削除を含む) | {} |
ルームの中のイベント(参加者やトラックの出入りなど)は、参加者がいるノードの購読者にだけ流れます。
カスケードでは、エッジノードにいる参加者のイベントはそのエッジノードに流れ、room.started・room.stopped はホームノードにだけ流れます。
こちらは SSE の id を持たないので、Last-Event-ID で再開できず、取りこぼしたイベントも再送されません。
event | 発生する場面 |
|---|---|
room.started | ノードがルームを開いた |
room.stopped | ノードがルームを閉じた(削除や、別のノードへの移動のため) |
room.connection.created | 参加者が参加した |
room.connection.updated | 参加者のメタデータが変わった |
room.connection.destroyed | 参加者が退出した(data に reason・duration_ms が入ります) |
room.track.published | トラックが公開された |
room.track.unpublished | トラックの公開が終わった |
room.spotlight.changed | スポットライトで注目する人の組が変わった |
ルームの中のイベントの形式:
{"type":"room.connection.destroyed","resource_id":"daily","event_id":"…","node":"…","occurred_at":"2026-11-03T10:00:00Z",
"data":{"connection_id":"…","client_id":"alice","reason":"normal","duration_ms":61234}}
ルームの中のイベントを漏れなく受け取るには、ルームを配置したノード(placement_status.node)の SSE を購読してください。
イベントはテナントで絞り込まれるため、他のテナントのルームのイベントは流れません。
イベント Webhook
--rooms-event-webhook URL と --rooms-webhook-secret-file FILE を指定すると、各ノードは自ノードのルームの中のイベントを、
その URL へ署名付きで POST します(本文の形式は ルームのシグナリングの「イベント」。type には
connection.created などの Webhook 用の名前が入ります)。
イベントは少なくとも 1 回届けるので、同じイベントが 2 回以上届くことがあります。受信側では、本文の id(Senca-Webhook-Id ヘッダーと同じ値)で重複を取り除いてください。
2xx が返れば配信完了です。408・429・5xx、接続の失敗、10 秒のタイムアウトのときは、1、2、4…秒と間隔を空けて再送し、10 回失敗したら諦めます。
それ以外の 4xx が返ったときは、再送せずにすぐ諦めます。諦めた件数と最近のイベントの ID は、GET /api/v1/nodes の自ノードの rooms.event_webhook で確認できます。
宛先は --egress-allow の方針で検査されます。Webhook を設定しても、SSE にはこれまでどおりイベントが流れます。
上限
| 項目 | 値 |
|---|---|
| ルームの参加者数 | max_participants(既定 50、1〜10000)。超えると room_full |
| ノードあたりの参加者数・ルーム数 | それぞれ 10000(超えると room_full) |
| テナントあたりのルーム数 | 10000 |
| 受信スロット | 種類ごとに 32 |
| サイマルキャストのレイヤー | 1〜4 |
| メタデータ | 8 KiB(ルーム・参加者・トークン) |
| アプリケーションのメッセージ | 64 KiB |
| シグナリングのメッセージ | 256 KiB、毎秒 50 個 |
| トークンの有効期間 | 10〜86400 秒(既定 600) |
connect までの期限 | 10 秒 |
| ping | 10 秒ごと、pong の期限は 10 秒 |
| ピア接続の期限 | 参加から 30 秒以内に接続できなければ peer_failed |
現在のノードで使えないもの
次の機能は ルームのシグナリング の仕様にありますが、現在のバージョンのノードでは使えません。
- 満員のときに別のノードへ案内する
overflow_redirect: 設定できません。ノードが満員ならroom_fullになります。 - 参加者が自分のメタデータを変更するメッセージ: シグナリング v1 にはありません。アプリケーションのサーバーから
PATCH /rooms/{id}/participants/{connection_id}で変更してください。 - 接続の再開(resume): 切断されたら、新しい接続として入り直します。
例
curl
API=https://media.example.com/api/v1
H="Authorization: Bearer $SENCA_TOKEN"
curl -s -X POST -H "$H" -H 'Content-Type: application/json' "$API/rooms" \
-d '{"id":"demo","settings":{"max_participants":10,"video_codec":"vp8","spotlight":{"number":2}},"metadata":{"title":"standup"}}'
curl -s -H "$H" "$API/rooms/demo"
curl -s -X POST -H "$H" -H 'Content-Type: application/json' "$API/rooms/demo/tokens" \
-d '{"role":"sendrecv","client_id":"alice","metadata":{"name":"Alice"},"ttl_secs":600}'
curl -s -H "$H" "$API/rooms/demo/participants"
curl -s -H "$H" "$API/rooms/demo/stats"
curl -s -X POST -H "$H" -H 'Content-Type: application/json' "$API/rooms/demo/messages" -d '{"data":{"notice":"5 分後に終わります"}}'
curl -s -X POST -H "$H" -H 'Content-Type: application/json' "$API/rooms/demo/messages" -d '{"label":"#chat","data":"hello"}'
curl -s -X PATCH -H "$H" -H 'Content-Type: application/json' -H 'If-Match: "1"' "$API/rooms/demo" \
-d '{"settings":{"max_participants":20,"spotlight":{"number":2}}}'
curl -s -X DELETE -H "$H" "$API/rooms/demo/participants/$CONNECTION_ID"
curl -s -X DELETE -H "$H" "$API/rooms/demo"
ブラウザー SDK(@senca/sdk)
import { Senca } from '@senca/sdk';
const camera = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720 }, audio: true });
const session = await Senca.connect({
url: 'wss://media.example.com/signaling', // トークンの応答の signaling_url(複数書くと、入り直すときに順に切り替える)
room: 'demo',
token: () => fetch('/my-backend/room-token').then((r) => r.text()), // 入り直すたびに取得し直す
publish: { stream: camera, simulcast: true },
dataChannels: ['#chat'],
});
for await (const track of session.remoteTracks()) {
const video = track.attach(document.createElement('video'));
document.body.append(video);
track.ended.then(() => video.remove());
}
await session.send('#chat', { text: 'hello' }); // 全員へ(DataChannel)
for await (const m of session.messages('#chat')) console.log(m.sender?.clientId, m.json());
const t = [...session.tracks.values()].find((t) => t.kind === 'video');
await t.setLayer('r0'); // 'r0' | 'r1' | 'r2' | 'auto'
await session.close();
SDP のやり取り、受信スロットの管理、再ネゴシエーション、ping への応答、リダイレクト(3 回まで)、再接続は SDK が自動で行います。 再接続は 0.5 秒後から始め、間隔を倍々に延ばしながら(最大 10 秒)10 回まで試みます。 オプションとイベントの一覧は、SDK の README と API と SDK にあります。
関連するページ
- ルームのシグナリング(プロトコルの仕様)
- メディアパイプライン(ルームの録画・合成・送信・文字起こし)
- クラスターとグリッド
- 管理 API と SDK・API の一覧
この文書の原文は、リポジトリの docs/guide/ja/10-rooms.md にあります。内容は現在の版(v0.0.1)に合わせています。