ルームのシグナリング

リアルタイムのルームに参加するクライアントを自作する人向けの、シグナリングプロトコル(WebSocket・senca.rooms.v1)の仕様です。メッセージの形式、エラー、DataChannel、認証、イベントを定めています。

リアルタイム通信

このページの見出し
  1. トランスポートとバージョン
  2. 全体の流れ
  3. 受信スロット
  4. メッセージ(クライアント → サーバー)
  5. メッセージ(サーバー → クライアント)
  6. DataChannel
  7. エラー
  8. 転送フィルター
  9. 認証
  10. イベント(SSE と Webhook)
  11. REST API
  12. レイヤーの選択とスポットライト
  13. カスケード
  14. 例
  15. 関連するページ

SENCA のルームに参加するクライアント(ブラウザー以外のアプリや独自の SDK)を自分で作る人のための、シグナリングプロトコルの仕様です。 ブラウザーから使うだけなら、ブラウザー用 SDK(@senca/sdk)がここに書いた手順をすべて代わりに行います。その場合は リアルタイムのルーム と 管理 API と SDK から読み始めてください。 サーバーの設定や REST API の使い方も リアルタイムのルーム にあります。

このページの要点:

  • シグナリング(映像・音声をやり取りする前に、接続の条件を交換する手順)は、WebSocket の GET /signaling(サブプロトコル senca.rooms.v1)で行います。JSON のメッセージを 1 つずつ送り合います。
  • 最初のメッセージは connect(10 秒以内)で、参加トークンを渡します。
  • offer は常にクライアントが送り、サーバー(ICE-lite)は answer だけを返します。
  • 未知の type や未知のフィールドを送ると bad_message になります。サーバーが黙って無視することはありません。
  • ルームが別のノードにあれば redirect が届くので、その URL へ接続し直します。

以下では、トラックを送信する参加者を配信者、他の参加者のトラックを受信する参加者を受信者と呼びます。 受信スロットはクライアントの SDP にある受信用の m 行 1 つのこと、レイヤーはサイマルキャストで送る解像度やビットレートの違う映像の 1 本 1 本(rid で識別します)のことです。

トランスポートとバージョン

項目値
通信路WebSocket(RFC 6455)。GET /signaling に、サブプロトコル senca.rooms.v1 を指定して接続します(必須)。圧縮の拡張(permessage-deflate)は使いません
フレーム1 つのテキストフレームに 1 つの JSON メッセージ(UTF-8)を入れます。バイナリフレームは使いません
切り替え接続後は、同じ JSON を DataChannel signaling で送受信することもできます(「DataChannel」)
バージョンconnect.version = 1。サーバーは connected.version で応答します。一致しなければ unsupported_version で閉じます
種類すべてのメッセージは type を持ちます。未知の type や未知のフィールドは bad_message になります(黙って無視はしません)
上限メッセージ 256 KiB、メタデータ 8 KiB、アプリケーションのメッセージ 64 KiB、毎秒 50 メッセージ(超えると rate_limited、4 倍を超えると切断)

バージョンは、意味が変わる変更をするときだけでなく、フィールドを追加するときにも上げます。 未知のフィールドは拒否されるので、古いサーバーに新しいフィールドを送ると bad_message になってしまうためです。クライアントは connected.version を見て、送る内容を決めてください。

全体の流れ

ルームのシグナリングの流れ。WebSocket で接続し、connect、offer と answer、通知、再ネゴシエーション、ping と pong、disconnect の順にやり取りする

offer は常にクライアントが送ります(サーバーは answer だけを返します)。そのため、双方が同時に offer を送って衝突すること(glare)は起きません。 クライアントは、1 つの offer に対する answer を受け取るまで、次の offer を送ってはいけません(送ると invalid_state)。 answer を待っている間に renegotiate が届いたら、answer を受け取った後で、新しい offer を送ってください(再ネゴシエーション)。

受信スロット

クライアントの受信用の m 行 1 つが、受信スロット 1 つになります(recvonly か sendrecv の m 行です)。サーバーはルームのトラックを受信スロットに割り当て、 SFU 側の送信トラックの送信元を切り替えて流します。受信者からは 1 本の SSRC に見えます。送信元が切り替わるときは、キーフレームから始まります。

一度決まった割り当てはむやみに変えません。新しいトラックは、同じ種類の空いている受信スロットに入ります。 配信者が退出した受信スロットは空き(state: "empty")になり、次の配信者に再利用されるので、m 行が増え続けることはありません。

受信スロットが足りなくなると、サーバーは renegotiate {audio, video} を送ります。値は追加する数ではなく、種類ごとの受信スロットの合計数です。 クライアントは recvonly の transceiver を足りない分だけ追加し、新しい offer を送ってください。受信スロットは種類ごとに 32 個までです。

どのスロットに誰の映像が来ているかは、slots[].connection_id・client_id・track_id でわかります。ontrack で得た transceiver の mid と照らし合わせてください。 スロットの state は次の 3 つのどれかです。

state意味
active転送中
paused割り当て済みだが転送していない(スポットライトの対象外で unfocused: "off" の場合)
empty空き

自分のトラック、同じ bundle_id の接続のトラック、転送フィルターで除外されたトラックは受信しません。 また、役割が sendonly の接続と、権限が subscribe: false の接続は何も受信しません。 multistream: false の接続は、種類ごとに 1 本だけ受信します(スポットライトで注目している人、いなければ最初の配信者のトラックです)。

メッセージ(クライアント → サーバー)

connect(最初のメッセージ。10 秒以内に送ります)

フィールド型既定意味
version1必須プロトコルのバージョン。現在は 1 です
room_idstring必須参加するルームの ID。1〜128 文字の英数字と -_.:@
tokenstring参加トークン(「認証」)。認証 Webhook だけで参加を認める場合は省略できます
rolesendrecv・sendonly・recvonly必須参加するときの役割。トークンの role の範囲内で選びます(sendrecv のトークンならどれでも選べます)
client_idstringトークンに client_id があれば、同じ値にする必要があります。トークンだけで参加を認めるルームでは、トークンに無い値は名乗れません(転送フィルターの条件に使われるためです)。認証 Webhook は、この値を見て参加を認めるかどうかを判断できます
bundle_idstringconnection_id同じ値を持つ接続どうしは、互いのトラックを受信しません(同じ人のカメラと画面共有など)。client_id と同じく、トークンだけで参加を認めるルームではトークンの bundle_id だけを使います
metadataJSON認証 Webhook にだけ渡す値です(他の参加者には公開しません)
multistreambooltruefalse にすると、種類ごとに 1 本だけ受信します
simulcast{rids, default_rid?}送信するレイヤーの rid を低い順に 1〜4 個(各 16 文字までの英数字)。default_rid は、受信者が最初に受け取るレイヤーです
spotlight{number, focus_rid?, unfocused?}ルームの設定ルームのスポットライトの範囲内で、人数とレイヤーを選びます
audio / video{enabled, codec?, bitrate_kbps?}音声・映像の送信の設定。codec がルームのコーデックと違うと unsupported_codec になります
data_channels[{label, direction, ordered, max_retransmits?, max_packet_life_time?}]使用する DataChannel(「DataChannel」)。クライアントが同じラベルで開きます
signaling{data_channel, close_websocket}falseシグナリングを DataChannel へ切り替えるかどうか(「DataChannel」)
e2eeboolfalseE2EE を使うかどうか。ルームの e2ee と違うと e2ee_mismatch になります
forwarding_filter「転送フィルター」の形式この接続で受信するトラックを絞り込みます
sdk{name, version}SDK の名前とバージョン。ログに記録するためのものです

その他のメッセージ

typeフィールド意味
offersdp送信用と受信用の m 行を含む offer。BUNDLE と rtcp-mux が必須です
candidatecandidate, sdp_mid?, sdp_mline_index?, username_fragment?ICE の候補を順次送る trickle ICE(空文字列は候補の終わりを表します)。サーバーは ICE-lite なので、送らなくてもかまいません
pongstats?ping への応答。stats に getStats の要約(64 KiB まで)を含められます
layertrack_id, rid(null で自動)受信しているトラックのレイヤーを選びます
messagelabel, to?, data または data_base64アプリケーションのメッセージ(「DataChannel」)。to は connection_id の配列で、空なら全員に送ります。送信者は、label を送信できる向き(sendrecv・sendonly)で宣言している必要があります。受け取るのは、label を受信できる向きで宣言し、権限でも許可されている参加者だけです
statsreportsgetStats の要約を送ります(REST API の統計に表示されます)
disconnectreason?切断します

メッセージ(サーバー → クライアント)

typeフィールド意味
connectedversion, connection_id, client_id, bundle_id, room_id, role, multistream, e2ee, video_codec, audio_codec, max_video_bitrate_kbps?, spotlight?, participants, recv_slots {audio, video}, data_channels, signaling_data_channel, ping_interval_ms, metadata参加に成功しました。role・metadata は、認証 Webhook が変更した後の値です。最初の offer には、recv_slots の数だけ受信用の m 行を入れてください
answersdp, slots[], published[] {mid, track_id, kind, rids}offer への answer と、すべての受信スロットの割り当てです
slotsslots[]割り当てが変わった受信スロットだけを知らせます
renegotiateaudio, video受信スロットをこの数まで増やして、新しい offer を送ってください
notifyevent と内容(下表)ルームで起きたことを知らせます
pushdataREST API から送られたメッセージです
messagelabel, from?, data または data_base64シグナリングで届いたアプリケーションのメッセージです
ping10 秒ごとに送ります。10 秒以内に pong が無ければ ping_timeout で切断します
switched以後のシグナリングは DataChannel signaling で行います(「DataChannel」)
redirectlocation別のノードへ接続し直してください(この後 disconnect {reason: "redirected"} を送って閉じます。例外は「カスケード」)
errorcode, message, fatal, request?エラーを知らせます(「エラー」)
disconnectreason切断します(この後に閉じます)

notify の event には次のものがあります。

event内容
connection.createdparticipant {connection_id, client_id, bundle_id, role, metadata, tracks[], node?}(node は別のノードにいる参加者に付きます)
connection.updatedconnection_id, metadata
connection.destroyedconnection_id, client_id?, reason
track.addedtrack {track_id, connection_id, kind, rids[]}
track.removedtrack_id, connection_id
spotlight.changedfocused[](connection_id の配列。新しく注目した順)
layer.changedtrack_id, mid, rid(現在受信しているレイヤー)
network.statuslevel(0 = 良好〜3 = 不良), bandwidth_bps, rtt_ms(自分の回線の状態。2 秒に 1 回まで)

DataChannel

アプリケーションのメッセージ

アプリケーションのメッセージには、ラベルが # で始まる DataChannel を使います。connect.data_channels で宣言したうえで、クライアントが開きます (pc.createDataChannel('#chat', {ordered, maxRetransmits}))。宣言していないラベルや、ルームの data_labels・トークンの permissions.data に一致しないラベルは、サーバーが閉じます。

届いたメッセージは、同じラベルを開いていて direction が受信できる向き(sendrecv・recvonly)の他の参加者全員へ、そのまま転送されます。 テキストはテキストのまま、バイナリはバイナリのままです。送信者自身には返らず、direction: "recvonly" の接続から送ったメッセージは転送されません。

特定の相手にだけ送るときは、シグナリングの message {label, to: [connection_id], data} を使います。受信者には message {label, from, data} が届きます。

シグナリングの切り替え

参加した後は、シグナリングを WebSocket から DataChannel に移せます。 connect に signaling: {data_channel: true} と data_channels: [{label: "signaling"}] を付け、クライアントが signaling を開いてください。 サーバーは WebSocket で switched を送り、以後のメッセージを DataChannel で送ってきます。クライアントも DataChannel で送ってかまいません(WebSocket で送っても受け付けます)。

close_websocket: true にしておけば、WebSocket を閉じても切断されません(false なら切断されます)。 DataChannel が閉じるとシグナリングは WebSocket に戻り、WebSocket も閉じていればその時点で切断されます。

エラー

{"type":"error","code":"room_full","message":"room is full","fatal":true,"request":"connect"}

fatal: true の場合、サーバーは続けて disconnect {reason: "error"} を送り、WebSocket を下表のクローズコードで閉じます。fatal: false の場合は そのメッセージだけを拒否し、接続は続きます(参加後の bad_message・invalid_state、2 回目以降の offer の誤りなど)。

codeクローズコード発生する場面
bad_message4400JSON の形式、型、未知のフィールド、値の範囲に誤りがある(参加前なら fatal: true)
unsupported_version4422version が 1 でない
auth_failed4401トークンが無い、不正、期限切れのいずれか。または、認証 Webhook が拒否したか、5 秒以内に応答しなかった
forbidden4403別のルームのトークンを使った、役割や client_id がトークンと合わない、許可されていない種類のトラックを送った
room_not_found4404ルームが存在しない(ノードは参加時にルームを作成しません)
room_full4503ルームまたはノードの上限に達した(仕様上は、overflow_redirect を設定したサーバーなら redirect を返します。現在のノードでは設定できません)
capacity_unavailable4503ルームを配置できるノードが今は無い(グリッドが配置先を決められない場合)
role_violation4403recvonly の接続が送信用の m 行を含めた
e2ee_mismatch4422connect.e2ee とルームの e2ee が違う
unsupported_codec4422ルームのコーデックと違う
invalid_sdp4400SDP を解釈できない、mid の無い m 行がある
negotiation_failed4500answer を作成できない(BUNDLE、rtcp-mux、コーデックの条件が合わない)
invalid_state4400最初のメッセージが connect でない、offer の処理中に次の offer を送った、認証中にメッセージを送った
rate_limited4429毎秒 50 メッセージを超えた
unknown_track4404layer で指定した track_id・rid が存在しないか、受信していない
unknown_label4403使えないラベルで message を送った
timeout440810 秒以内に connect が届かない
internal4500サーバー内部のエラー

切断の理由は、disconnect.reason、connection.destroyed.reason、イベントで使います。値は次のとおりです。

理由意味
normal通常の退出
websocket_closedWebSocket が閉じた
ping_timeoutpong が期限内に返らなかった
peer_failedICE・DTLS に失敗した、または参加から 30 秒以内に接続できなかった
kickedREST API で切断された
room_deletedルームが削除された
errorエラーで切断された
redirected別のノードへ案内された
node_shutdownノードが停止した

転送フィルター

{"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 規則)。各規則は配信者の connection_id・client_id・bundle_id・kind・ metadata.<key>(値が文字列・数値・真偽値の場合)を条件にします。block は一致したトラックを転送せず、allow は一致したトラックだけを転送します。 ルームの設定のフィルターと接続のフィルターの両方を満たすトラックだけを転送します。ルームのフィルターを変更すると、その場で受信スロットを再割り当てします。

認証

参加トークン(JWT 互換)

参加トークンは、SENCA の署名鍵(Ed25519 の EdDSA、または P-256 の ES256)で署名します。POST /rooms/{id}/tokens で発行できます(現在のノードは Ed25519 で署名します)。 アルゴリズムが none・HS 系(共有鍵)・RS 系のトークンと、crit ヘッダーを持つトークンは拒否します。アルゴリズムは鍵によって決まり、ヘッダーの alg が鍵と合わなければ拒否します。

{"iss":"senca","aud":"senca-rooms","exp":1767225600,"iat":1767222000,"jti":"...",
 "room":"daily","role":"sendrecv","client_id":"alice","metadata":{"name":"Alice"},
 "permissions":{"publish":["audio","video"],"subscribe":true,"data":["#chat"]}}

exp は必須で、時計のずれは 30 秒まで許容します。有効期間(exp - iat)は 24 時間までです。metadata は他の参加者に公開されます。 permissions を省略したトークンでは、音声と映像の送信、他の参加者のトラックの受信、すべての DataChannel のラベル(#*)が許可されます。

認証 Webhook

認証 Webhook を設定すると、サーバーは参加のたびに Webhook の URL へ署名付きの POST を送り(「署名」)、参加を認めるかどうかを尋ねます。5 秒以内に応答が無ければ拒否です。 問い合わせ方は 2 通りあり、既定の after_token ではトークンを検証した後にさらに問い合わせます。only ではトークンを使わず、Webhook の応答だけで参加を認めます。

{"version":1,"type":"connection.auth","request_id":"…","node":"node-a","room_id":"daily",
 "connection_id":"…","client_id":"alice","role":"sendrecv","metadata":{…クライアントの connect.metadata…},
 "token_claims":{…トークンがあれば…},"remote_addr":"203.0.113.5:53211"}

Webhook は 2xx で次のように応答します(未知のフィールドがあると拒否します)。

{"allowed":true,"role":"recvonly","client_id":"alice","metadata":{"name":"Alice"},
 "permissions":{"publish":[],"subscribe":true,"data":["#chat"]}}

allowed: false なら、reason を添えて auth_failed をクライアントへ返します。2xx 以外の応答や、解釈できない本文も拒否として扱います。

問い合わせ先は、ルームの設定 auth_webhook_url(http か https)があればその URL、無ければノードの --rooms-auth-webhook の URL です。 問い合わせ方は --rooms-auth-mode(after_token または only)で選びます。 どちらの URL も無いルームはトークンだけで参加を認め、トークンに無い client_id・bundle_id は名乗れません。

署名用のシークレットは --rooms-webhook-secret-file で渡します。シークレットが無いノードでは、Webhook の URL を持つルームへの参加はすべて拒否されます。 宛先は --egress-allow の方針で検査されるので、プライベートアドレスへ送るには許可が要ります。 リクエストには Senca-Webhook-Id ヘッダー(request_id と同じ値)も付きます。

署名(Webhook 共通)

Webhook のリクエストには、Senca-Signature: t=<UNIX 秒>,v1=<hex(HMAC-SHA256(secret, "<t>.<本文>"))> の形式の署名が付きます。シークレットを入れ替えている間は、v1= が複数付きます。 受信側は、t と現在時刻の差が 300 秒以内であること(|now - t| <= 300)と、いずれかの v1 が一致することを確かめてください。 イベントの Webhook には、Senca-Webhook-Id(イベントの ID)と Senca-Webhook-Attempt(何回目の送信か)も付きます。

イベント(SSE と Webhook)

SSE(/api/v1/events)と Webhook には、同じ形式の入れ物(エンベロープ)に包んでイベントを送ります。

{"id":"01j…","version":1,"at":1767222000123,"node":"node-a","room_id":"daily",
 "type":"connection.destroyed","data":{"connection_id":"…","client_id":"alice","reason":"normal","duration_ms":61234}}

type には、room.created・room.updated・room.destroyed・connection.created・connection.updated・connection.destroyed・ track.published・track.unpublished・spotlight.changed があります。

ノードの SSE では、名前を次のように変え、data に内容を入れて送ります。room.created は room.started、room.destroyed は room.stopped になり、 そのほかは room.connection.*・room.track.*・room.spotlight.changed のように room. が付きます。room.updated は、ルームの設定の変更を表す制御のイベントとして送られます。

ノードに --rooms-event-webhook URL と --rooms-webhook-secret-file を指定すると、上の形式のエンベロープに署名して POST します(type は Webhook 用の名前のままです)。 各ノードが送るのは、自ノードにいる参加者のイベントで、どのノードからかは node でわかります。 カスケードのエッジノードは room.created・room.updated・room.destroyed を送らず、これらはホームノードだけが送ります。

Webhook は少なくとも 1 回届けるので、同じイベントが重複して届くことがあります。受信側は id で重複を取り除いてください。 2xx が返れば配信完了です。408・429・5xx、接続の失敗、10 秒のタイムアウトのときは、1、2、4…秒(±20 %、最大 1 時間)と間隔を空けて再送し、10 回失敗したら送信をやめます。 それ以外の 4xx が返ったときは、すぐにやめます。送信をやめたイベントの数と ID は、GET /api/v1/nodes の rooms.event_webhook で確認できます。

REST API

ルームの REST API は、/api/v1 の共通の規約(Bearer、problem+json、Idempotency-Key、If-Match・ETag、limit・cursor、時刻は RFC 3339)に従います。 参加者・メッセージ・統計には、ルームを配置したノードが応答します(どのノードに送っても、配置先のノードへ転送されます)。 参加者の変更・切断は、参加者が接続しているノード(カスケードのエッジノードを含む)へさらに転送されます。ルームの settings を更新すると、 書いた項目だけが変わります(JSON Merge Patch)。 シグナリングも、どのノードに接続してもかまいません(ルームが別のノードにあれば redirect が返ります)。

操作パス
ルームの作成POST /rooms {id?, settings?, metadata?, placement?} → 201(ルームはグリッドのワークロードとして 1 つのノードに配置されます。placement で配置先の指定や優先度を決められます)
一覧・取得GET /rooms?limit&cursor・GET /rooms/{id}(ETag: "{generation}")
更新PATCH /rooms/{id} {settings?, metadata?, placement?}(If-Match 必須。参加者がいる間は e2ee・video_codec・audio_codec を変更できません: 409)
削除DELETE /rooms/{id}(参加者は room_deleted で切断されます)
参加者GET /rooms/{id}/participants・GET …/{connection_id}(subscriptions は受信スロット)
参加者のメタデータの変更PATCH /rooms/{id}/participants/{connection_id} {metadata} → 全員に connection.updated が届きます
切断DELETE /rooms/{id}/participants/{connection_id}(kicked)
メッセージの送信POST /rooms/{id}/messages {to?, label?, data}(label があれば DataChannel、無ければ push)
統計GET /rooms/{id}/stats(参加者ごとの RTT、推定帯域、送受信量、送信しているレイヤーのビットレート、受信しているレイヤー、クライアントの getStats の要約)
参加トークンPOST /rooms/{id}/tokens {role, client_id?, bundle_id?, metadata?, permissions?, ttl_secs?} → {token, room_id, expires_at(RFC 3339), signaling_url}。ttl_secs の既定は 600 で、10〜86400 を指定できます。ルームが無ければ自動で作成します(auto: true。空のまましばらく経つと削除されます)。署名は Ed25519 です

ルームの設定(settings)には、次のフィールドがあります(かっこ内は既定値や選べる値です)。 max_participants(50)、e2ee、video_codec(vp8・vp9・h264・av1)、audio_codec(opus)、 max_video_bitrate_kbps、spotlight {number, unfocused: "lowest"|"off", focus_rid?}、recording(none・per_participant・ composite。composite は E2EE と併用できません)、forwarding_filter、data_labels(["#*"])、empty_timeout_secs(60。トークンの発行で自動作成されたルームに適用)、 cascade、auth_webhook_url。詳しくは リアルタイムのルーム の「ルームの設定」を参照してください。

E2EE のルーム(SFrame などで暗号化)では、SFU は復号せずにそのまま転送します。サーバーでの合成、合成した映像の録画、文字起こしはできません。 E2EE のルームに合成か文字起こしを含むメディアパイプラインを作成しようとすると、422(details.reason: "e2ee")で拒否されます。

レイヤーの選択とスポットライト

レイヤーは、受信者ごと・トラックごとに選びます。優先順位は次のとおりです。

  1. クライアントが送った layer
  2. スポットライト(注目している配信者は focus_rid か自動で選んだレイヤー、注目していない配信者は最も低いレイヤーか転送なし)
  3. simulcast.default_rid
  4. 自動: 推定帯域に 0.9 を掛け、受信している映像の数で割ります。その帯域に収まる最も高いレイヤーを選びます。 レイヤーを下げるのは帯域の不足が 1 秒続いてから(大きく不足している場合はすぐ)、上げるのは帯域に収まる状態が 2 秒続いてからです。

切り替えは、新しいレイヤーのキーフレームから行います(それまでは現在のレイヤーを転送します)。

スポットライトは、音量(RFC 6464 の -dBov。無ければ Opus のパケットサイズから推定)で話している人を検出します。 音声が 200 ms 続けば話している、1.5 秒無音なら話していないと判定します。注目が頻繁に切り替わらないよう、一度注目したら 2 秒間は外しません。 注目する人の組はルームで 1 つです(変わると spotlight.changed が届きます)。各受信者にとっては、自分を除いた number 人が注目の対象になります。

カスケード

ルームは複数のノードにまたがれます(クラスターとグリッド)。別のノードにいる参加者は node 付きの参加者として見え、 そのトラックも普通の参加者と同じように受信スロットに割り当てられます。 グリッドの配置や上限のためにこのノードでルームを受け付けられないときは、redirect {location} で別のノードへ案内します。

最初の connect を受けた時点でルームが別のノードにあれば、redirect を送ってからクローズコード 4300 で閉じます。このときは disconnect を送りません。 connect が 10 秒以内に届かなければ、クローズコード 4408 で閉じます。

ルームを配置したノード(グリッドの配置先)をホームノードと呼びます。ルームの設定が cascade: true のとき(またはノードで --rooms-cascade を指定したとき)は、 別のノードに接続した参加者をリダイレクトせず、そのノードで受け付けます。このノードがエッジノードです。 cascade が false のルームではホームノードへ案内します。ドレイン中のノードが新たにエッジノードになることはありません。

ノード間で 7 秒間同期できなかったノードにいる参加者は、参加者の一覧から外れます。 max_participants はノードをまたいだ合計人数で判定するため、別々のノードで同時に参加すると、短い間だけ上限を超えることがあります。

ホームノードが停止すると、グリッドがルームを別のノードへ移します。移動先がエッジノードなら、そこにいた参加者はそのまま続けられ、そのノードが新しいホームノードになります。 停止したノードにいた参加者は、別のノードへ接続し直せば入り直せます。参加者がいなくなったエッジノードは、20 秒後にそのルームを閉じます。

例

クライアント(→)とサーバー(←)のやり取りの例です。

→ {"type":"connect","version":1,"room_id":"daily","role":"sendrecv","token":"eyJ…","client_id":"alice",
   "simulcast":{"rids":["r0","r1","r2"]},"data_channels":[{"label":"#chat"}],"video":{"codec":"vp8"}}
← {"type":"connected","version":1,"connection_id":"6x0…","client_id":"alice","bundle_id":"6x0…","room_id":"daily",
   "role":"sendrecv","multistream":true,"e2ee":false,"video_codec":"vp8","audio_codec":"opus",
   "participants":[{"connection_id":"9k2…","client_id":"bob","bundle_id":"9k2…","role":"sendrecv","metadata":null,
   "tracks":[{"track_id":"9k2….0","connection_id":"9k2…","kind":"video"},{"track_id":"9k2….1","connection_id":"9k2…","kind":"audio"}]}],
   "recv_slots":{"audio":1,"video":1},"data_channels":[{"label":"#chat","direction":"sendrecv","ordered":true}],
   "signaling_data_channel":false,"ping_interval_ms":10000,"metadata":{"name":"Alice"}}
→ {"type":"offer","sdp":"v=0…m=video … a=sendonly … a=simulcast:send r0;r1;r2 … m=audio … a=sendonly … m=application … m=audio … a=recvonly … m=video … a=recvonly …"}
← {"type":"answer","sdp":"v=0…","published":[{"mid":"0","track_id":"6x0….0","kind":"video","rids":["r0","r1","r2"]},
   {"mid":"1","track_id":"6x0….1","kind":"audio"}],
   "slots":[{"mid":"3","kind":"audio","state":"active","track_id":"9k2….1","connection_id":"9k2…","client_id":"bob"},
            {"mid":"4","kind":"video","state":"active","track_id":"9k2….0","connection_id":"9k2…","client_id":"bob"}]}
← {"type":"notify","event":"connection.created","participant":{"connection_id":"p7q…","client_id":"carol",…}}
← {"type":"renegotiate","audio":2,"video":2}
→ {"type":"layer","track_id":"9k2….0","rid":null}
← {"type":"notify","event":"layer.changed","track_id":"9k2….0","mid":"4","rid":null}
← {"type":"ping"}
→ {"type":"pong"}

関連するページ

この文書の原文は、リポジトリの docs/realtime/signaling.md にあります。内容は現在の版(v0.0.1)に合わせています。