Skip to content

接続と通話の状態 (Presence) ​

内部ドキュメント

このドキュメントは内部実装の詳細です。ゲーム開発者向けの情報は docs.code.uzu-app.com を参照してください。

プレイヤーの通話の状態を、サーバーと各端末の見え方から決めて、シナリオに渡す仕組み。 シナリオはこれを読んで「誰の声が届いていないか」「誰が別の部屋にいるか」を出す。

他の人については、ゲームの接続ではなく声が届くかだけを出す。アプリが裏に回ると ゲームの画面(WebView)は止まるが、通話(ネイティブ)は続く。接続で判断すると、 LINE を見ながら話している人を「落ちた」と扱ってしまう。ゲームの接続はサーバーの中だけで 使う(通話の判定・無人凍結・後片付け)。

通話の部屋とマイクの方針も同じ仕組みで配る。部屋とマイクはロジックの voicePlan が state から返す宣言だけで決まり、プレイヤーの端末から直接変える手段は無い。

判断の置き場所 ​

部品ごとに、知っていることと、直したときに届く速さが違う。判断はサーバーに集める。

部品知っていること役割
サーバー(room-core)roster、ゲームの接続、ゲームの state通話の申告を確定させ、どの部屋にいるべきか・全員を止めるかを決めて全員へ配る。デプロイで即直り、全員が同じ判断を見る。本番と uzu dev で同じコードが動く
アプリ(packages/uzu_code とホスト)通話(Agora)。自分の通話の状態、今いる部屋、同じ部屋で聞こえる人自分の通話を見たまま申告し、配られた部屋とマイクの方針に合わせ、声が届かない人の帯とマイクの鍵を描く。何秒待つかといった時間の判断はここに置く
SDKサーバーの presence、アプリの見たまま、自分の WebSocket の状態伝言を中身を見ずに運び、3 つの入力を決まった表どおりに並べてシナリオに渡す。シナリオに焼き込まれて変わらないので、判断を置かない

全体の流れ ​

  • サーバーに届く通話の状態は本人の申告だけ。アプリが裏に回ったり落ちたりすると申告は止まり、サーバーでは unknown になる
  • そのため声が届くかは、最後は各端末の見え方で決める。部屋割りで同じ部屋にいるはずの人が聞こえなくなったことは、各端末の missing にしか出ない
  • ロジックには接続も通話も渡さない。部屋とマイクを voicePlan で宣言するだけにする

伝言の通り道 ​

アプリとサーバーの伝言は、bridge の platform チャネルと WebSocket の __platform のあいだを SDK が素通しで運ぶ。申告や一時停止の要求の中身は、アプリとサーバーの更新だけで変えられる。

json
// bridge(アプリ ⇄ SDK)
{ "channel": "platform", "type": "voice.report", "payload": { "session": "connected", "room": null } }

// WebSocket(SDK ⇄ サーバー)
{ "type": "__platform", "message": { "type": "voice.report", "payload": { "session": "connected", "room": null } } }
type向きpayload
voice.reportアプリ → サーバー自分の通話の申告(VoiceReport)
pause.requestアプリ → サーバー緊急一時停止
pause.resumeアプリ → サーバー緊急一時停止の解除
presenceサーバー → アプリ判断済みの状態(PresenceSnapshot)。SDK も読んでシナリオの Presence を組み立てる

アプリと SDK のあいだの sdk チャネルのメッセージ(voiceChanged / connectionChanged / setListeningRoom / setMicMuted)は ブリッジ仕様 にある。

申告 (VoiceReport) ​

見たままを送り、判断は載せない。発話とミュートは頻繁に変わり、サーバーの判断にも使わないので載せない。

フィールド値
sessionoff / connecting / connected / reconnecting / failed
reasonoff なら table(卓が通話を使わない)/ this-device(この端末は通話に入らない)、failed なら mic-permission / network
room今入っている部屋。null は全体の部屋

サーバーは形を検め、合わなければ捨てる。申告はその接続の席のものとして扱い、本文に席 ID を載せさせない。

presence (PresenceSnapshot) ​

状態をまるごと送る。差分や命令にしないので、取りこぼしても次の 1 通で直る。時刻はサーバーの実時刻(ms)。

ts
type PresenceSnapshot = {
  serverNow: number; // since を端末の時計に直すのに使う
  seats: Record<SeatId, {
    // プレイヤーだけ
    // ゲームの接続。今の SDK は読まず、アプリは退出の判定に not-joined かだけを読む。wire 3 の最初の SDK がこの項目を読むので配り続ける
    connection: { status: 'online' } | { status: 'offline'; since: number } | { status: 'not-joined' };
    voice:
      | { status: 'connected'; room: string | null }
      | { status: 'lost'; reason: 'reconnecting' | 'failed'; since: number }
      | { status: 'off' }
      | { status: 'unknown' };
    plan: { room: string | null; mic: 'free' | 'muted' | 'locked' }; // voicePlan() の結果
  }>;
  observers: { count: number }; // 観測席は人数だけ
  pause: null | { reason: 'emergency'; by: SeatId } | { reason: 'waiting'; for: SeatId[]; since: number };
};

サーバーは状態が変わったときに全員へ配り、接続した端末には必ず送る。

サーバーでの確定 ​

javascript/uzu_code/room-core/src/presence-tracker.ts。入力は 2 つだけで、transport が数えている「今つながっている接続の一覧」と、各端末の申告。

ゲームの接続 ​

シナリオにもアプリの帯にも出さない。サーバーが通話の判定(接続が切れたら申告は届かない)と無人凍結に使う。アプリは退出の判定で not-joined かだけを読む。

値意味
online生きている接続が 1 本以上ある。同じ人の端末が複数なら 1 台でも生きていれば online
offline接続が 0 本になってから 5 秒以上戻らない。since は切れた時刻
not-joinedゲームが始まってから、まだ一度も入っていない。ゲームは最初の 1 人がつながった時点で始まるので、遅れて入る人を切断と分ける
  • 切れた側は猶予(5 秒)続いたものだけを確定し、戻る側は待たずに確定する。瞬断や回線の切り替えで、全員の表示を揺らさない
  • 数えるのは roster にいる席の接続だけ。観測席(観戦・進行管理)は人数だけ数え、監視で開いた接続(monitor=1)はどちらにも数えない
  • transport は接続・切断・起床のたびに、今の接続の一覧を渡す(LiveConnection = { connectionId, seatId, monitor })。room-core は一覧と突き合わせるだけで、接続を自分では数えない

無言の切断 ​

電波が切れた端末は close を送らず、ソケットは OPEN のまま残る。GameHost は起床のたびに、heartbeat(SDK が 25 秒ごとに送る __ping)への自動応答の最終時刻(getWebSocketAutoResponseTimestamp)を見て、60 秒途絶えたソケットを閉じる。応答がまだ一度も無い接続は、受け入れた時刻を起点にする。

閉じたソケットは次の接続の一覧から消え、そこから 5 秒の猶予を経て offline が確定する。GameHost が起きるのは接続がある間は 60 秒ごと(heartbeat の alarm)なので、無言の切断が offline になるまでは最悪で 60 秒 + 60 秒 + 5 秒ほどかかる。uzu dev の GameRoom は無言の切断を見ない。

通話 ​

席の通話は、生きている端末の申告のうち off 以外で一番新しいものから決める。どれも off なら off。

  • 「通話中」を優先しないのは、古い接続の申告が新しい接続の状態を覆い隠すため。WebView が落ちて読み込み直すと、古い接続は無言のまま切られるまで残り、最後の connected を持ち続ける。通話は同じ人で 1 台だけ(後から入った端末が取る)なので、一番新しい申告が今の通話を表す
  • off を後回しにするのは、通話に入らない端末(通話を取られた側・通話を持たないホスト)が、通話中の端末の状態を消さないようにするため
申告確定する値
connectedすぐ connected(部屋つき)
offすぐ off
reconnecting / failed待ち始めてから 5 秒続いたら lost(reason は reconnecting / failed)。マイクの権限が無く通話に入れない人もここに入る。猶予は申告が届いた時刻ではなく待ち始めた時刻から数えるので、もう一方の端末の申告が消えて古い申告が繰り上がっても、すぐには lost にならない
connecting通話中だった人なら reconnecting と同じく待つ。lost の人はそのまま。それ以外は unknown
  • 申告が 1 つも無いときは、ゲームの接続が online のあいだは今の値を保つ。端末が切れてつなぎ直す途中(デプロイで全員が一度切れる等)に通話まで揺らさないため
  • ゲームの接続が offline になったら unknown(申告が届かない)
  • サーバーは切れた端末の申告を捨てる。アプリは WebSocket がつながり直したら申告を送り直す

再起動 ​

play-server のデプロイでは GameHost ごと作り直され、全員の WebSocket が切れる。facet(room-core)も、コードの更新やメモリからの退避で作り直される。どちらでも「今誰の接続があるか」は消えるので、席ごとの記録(接続・通話・部屋割り)と申告を storage の presence に残し、作り直したら戻す。

戻した直後の最初の突き合わせでは、確定待ちの起点を今(起動した時刻)へずらす。すぐつなぎ直した人は offline にならない。最初の突き合わせより先にメッセージの処理が来ても、そこまでは切断と通話の途切れを確定しない。

戻したあと、部屋割りは state から評価し直す。state の保存は遅らせて書き、presence はすぐ書くので、その間に落ちると保存された部屋割りが state と食い違う。

観測席の人数 ​

観測席の人数は、接続ではなく席 ID(人)で数える。同じ人が 2 台でつないだときや、つなぎ直しで古い接続が残っている間に、人数が増えて見えない。人数だけの変化は、3 秒の間隔を空けて配る。ライブ配信で入退室が集中しても配信が膨らまない。新しく接続した端末には、その時点の presence を必ず送る。

無人凍結 ​

無人凍結 の猶予は、観戦者と監視用の接続も含めた全部の接続で数える。無人凍結は配っていないので、誰かが見ている部屋で止めると、その人の画面だけ時計が進み、戻ったときに巻き戻る。

ロジックに渡すもの ​

ロジックに渡すのは voicePlan だけ。接続や通話の状態は渡さない(手番を飛ばすといった判断に、裏に回っただけの人を巻き込まないため)。戻らない人を待つ自動中断も、この版では入れない(一時停止の理由の型 waiting だけを置く)。

ts
type GameLogic<S> = {
  // …setup / actions / serverActions / update / deadlines / tickRate
  voicePlan?(args: { state: S; playerId: SeatId }): { room: string | null; mic: 'free' | 'muted' | 'locked' };
};

voicePlan ​

state が変わるたびに、サーバーがプレイヤー 1 人ずつについて呼び、結果が変わったときだけ presence を配る。部屋割りが変わるときは、presence を state より先に配る。新しい場面の state を受けた時点で、その場面の plan(マイクのロック等)がアプリから見えている必要があるため。deadlines の at と同じく、state だけから決まる軽い純関数にする。書かなければ全員が全体の部屋、マイクは本人の自由。

プレイヤーの部屋は state に持たせ、voicePlan はそこから決める。読み込み直しや再接続のあとに戻る先は state にしかなく、作品の画面から部屋を移す口はない(観測席の setListeningRoom だけが例外で、どの部屋を聞くかは観測者それぞれの画面の選択であってゲームの state ではない)。

room や mic を書かなかったら既定(全体の部屋・free)とみなす。{ mic: 'locked' } のように room を書き忘れただけで、マイクの方針ごと黙って効かなくなるのを避ける。投げたり、型の違う値(room が文字列でも null でもない、mic が 3 つのどれでもない)を返したりしたら、直前の結果を使い続け、席ごとに 1 回だけログを出す。

mic はマイクを制限するだけで、オンにすることはない。作品が本人の同意なく声を流さないため、マイクをオンにできるのは本人だけにしている。

mic場面に入ったとき場面の中場面を抜けたとき
free何もしない(オンにもしない)本人の自由何もしない
mutedミュートする本人は戻せるミュートのまま(本人がオンに戻す)
lockedミュートする本人も解除できない。押すと緊急一時停止を選べるダイアログ〃
  • 一時停止の間はロックを外し(ミュートは切れたまま)、再開するとまたロックしてミュートする。停止の要求中(サーバーの確定待ち)は外さない
  • 観測席は voicePlan の対象外

シナリオに出すもの ​

ts
onPresenceChange(listener: (presence: Presence, changes: PresenceChange[]) => void): () => void; // 戻り値で購読を解除する
getPresence(): Presence;
onPauseChange(listener: (pause: Pause) => void): () => void;
getPause(): Pause;
setListeningRoom(room: string | null): void; // 観測席が聴く部屋を選ぶ。プレイヤー席では何も起きない
muteMic(): void; // 本人のマイクをミュートする。作品からマイクをオンにする手段は無い

Presence は me(自分)/ others(自分以外のプレイヤー)/ observers(人数と、今話している人)を持つ。誰か(名前・キャラクター)は名簿から、自分の席 ID と席種は onState の引数から引く。

me は自分の WebSocket の状態(me.connection)と自分の通話(me.voice)を持つ。others は通話(voice)だけを持ち、値は here / elsewhere / lost / left / off / unknown の 6 つ。

組み立ての表 ​

SDK(javascript/uzu_code/sdk/src/presence.ts)は時間を測らない。何秒待つかはアプリとサーバーが決めた値がそのまま届く。

SDK は作品に焼き込まれて変わらないが、サーバーとアプリはあとから値や項目を増やす。知らない値・欠けた項目は意味の分かる値に倒す(他の人は unknown、自分の通話は connecting、マイクは open)。アプリから届く知らない通話の状態も捨てずに connecting として受ける。組み立てに失敗したら直前の値で続け、例外を WebSocket の状態の処理(再接続の予約)まで通さない。

自分については次の 2 行。

入力出す値
自分の WebSocket の状態me.connection(connecting / online / reconnecting / offline)
アプリの voiceChanged.selfme.voice。届く前と、知らない値のときは connecting

他の人の voice は、上から順に最初に当たった行で決める。

入力出す値
アプリの left に載っているleft(since は left の時刻)。まだ聞こえていても、サーバーが lost でも left を採る
サーバーの voice が lostlost(since はサーバーの時刻を端末の時刻に直す)
自分が通話にいて、heard に載っているhere(マイク・発話・音質はアプリの見たまま)。サーバーが unknown(裏に回って申告が止まった)でも、聞こえていれば同じ部屋にいる
自分が通話にいて、missing に載っているlost(since は missing の時刻)
サーバーの voice が offoff
サーバーの voice が unknownunknown(no-report)。まだ一度も入っていない人もここ
サーバーの voice が connected、自分が通話にいないunknown(me-not-connected)。相手の問題ではない
〃、相手の部屋が自分の部屋と違うelsewhere
〃、同じ部屋here(話していない)。部屋を移った直後など、まだ声が届き始めていない
heard のうち roster にいない人で話している人observers.speaking
サーバーの voice が知らない値unknown(no-report)
  • since はサーバーの時刻を serverNow との差で端末の時刻に直す
  • changes は自分の接続の変化、全員の通話の状態の変化、観測席の人数の変化だけ。発話やミュートの変化では空で、値だけが新しくなる。席が初めて現れたことは変化にしない
  • 発話は 1 秒に数回変わるので、描画のフレームごとにまとめて届ける
  • 一時停止の理由(getPause / onPauseChange)は presence の pause から出す。時計は理由を見ずに wire の frozen だけで止める(緊急停止)
  • run() より前にアプリから届いた platform の伝言(通話の申告など)は捨てず、種類ごとに最新の 1 通を持って、サーバーへの出口ができたら送る。アプリは SDK の ready で通話の申告を送り直すが、ready は run() より前に出る

ローカル実行 ​

roomId なしのローカル実行(uzu dev の一人用など)はサーバーが無いので、me を online・通話 off(this-device)に固定する。アプリからの pause.request / pause.resume は SDK が自分で処理する。mobile の一人用はサーバーにつながるので、ほかと同じ。

アプリ ​

PlayScreenController ​

dart/packages/uzu_code/lib/play_screen_controller.dart。

API役割
presenceサーバーが配った最新の presence。新しい SDK の作品でだけ届く
gameConnectionSDK の WebSocket の状態(connectionChanged)
updateVoice(self, heard, missing, left)自分の通話の状態と退出した人を渡す。voiceChanged は中身が変わるたびに、voice.report は session / reason / room が変わったときだけ送る
voiceSelf / heardVoices / missingVoices / leftSeatsupdateVoice で渡した値。アプリの画面が描き直す契機に使う
plannedElsewhere(seatId)作品の部屋割りで、その人が自分と別の部屋にいるか(観測席は聞いている部屋と比べる。一時停止中は false、分からなければ null)。話者表示の段分けに使う
otherVoice(seatId)他のプレイヤーの通話を、この端末から見た値で返す(here / elsewhere / lost / off / unknown)。SDK が作品へ渡す others[].voice と同じ規則(組み立ての表)で、アプリの画面と作品の画面で同じ人の状態が食い違わない。presence が届かない古い SDK の作品では、この端末の通話で聞こえているかだけで決める
myPlan / micLocked自分の部屋割りとマイクの方針。観測席と、presence が届いていない間は null
onPresence / onSetListeningRoom / onSetMicMuted / onSetMicEnabledホストが結線するコールバック。onSetMicMuted / onSetMicEnabled はミュートのときだけ呼ばれる
voiceOffReason通話を持たないホストの既定(thisDevice / table)。SDK の ready で off を申告する
  • SDK の ready で、自分の通話の状態を渡し直し、申告も必ず送る(ページを読み込み直すと SDK は何も覚えていない)。あわせてゲームの接続の状態(gameConnection)を「つなぎ始め」に戻し、新しいページの WebSocket が online になったときにも申告を送り直す
  • シナリオからのマイクの解除(setMicMuted の muted: false)と、古い作品の setMicEnabled(true) は捨てる(handleSetMicMuted / handleSetMicEnabled)。ホストが最後の守りになる
  • isPaused は、サーバーが確定した停止だけ。停止の要求中(確定待ち)はマイクのロックを外さず、HUD の鍵と方針の判定がずれない
  • connectionChanged で online に戻ったら申告を送り直す(サーバーは切れた端末の申告を捨てる)
  • 部屋とマイクを presence の plan に合わせるのはホストの責務。方針の状態機械(MicPolicyTracker)と、同じ部屋にいるはずなのに声が届かない相手の判定(MissingVoiceTracker、猶予 5 秒)は lib/voice_policy.dart にあり、mobile とエミュレータが共有する
  • 「同じ部屋にいるはずの人」(expectedVoicesOf)は、部屋割り(plan。一時停止中は全員が全体の部屋)で自分と同じ部屋にいて、次のどちらかに当たる人
    • この端末の通話で一度は聞こえたことがある。猶予 5 秒。アプリごと落ちた人もサーバーでは unknown になるだけだが、部屋割りでは同じ部屋にいるはずなので、聞こえなくなれば載る
    • まだ一度も聞こえていないが、サーバー上は自分と同じ部屋の通話に入っている(本人の申告)。猶予 15 秒。片方向にしか声が届かない人を拾う。ゲームを開く前・遅れて入る人は通話にいないので待たない
    • 退出した人は、どちらに当たっても載せない

退出 ​

退出ボタンで抜けた人(一時退出・イベントからの退出・終了)と、アプリが落ちた人は、プレイ画面の接続でも通話でも同じに見える(どちらも WebSocket が切れて unknown、通話から消える)。そこで退出は卓の参加者の状態で決める(LeftSeatTracker)。

  • 卓で isPlaying でない参加者のうち、presence の席の接続が not-joined でない人(ゲームに一度でも入った人)を退出とする。isPlaying はまだ来ていない人も false なので、接続で分ける
  • isPlaying はプレイ画面の subscription で全員の端末に届く。ホストは 1 秒ごとの組み直しで拾う
  • since はこの端末で退出に気付いた時刻。画面を開き直すと、すでに抜けていた人も開いた時刻になる
  • 戻ってきた人は isPlaying が true に戻るので、退出の扱いが解ける
  • 退出した人は missing に載せず、SDK へ voiceChanged の left で渡す。退出に対応していない SDK(0.9 系)の作品では、サーバーの値(切断後は unknown)で出る
  • アプリが落ちた人・オフラインになった人は今までどおり missing に載り、lost と「〇〇との通話が切れています」の帯が出続ける

HUD ​

  • 切断の帯(PresenceBanners): 自分のゲームの接続が reconnecting / offline(offline は強い色)、自分の通話が reconnecting / failed(つなぎ直しをあきらめた。理由が network のとき)、他のプレイヤーは「〇〇との通話が切れています」(自分の端末の missing)と、退出に気付いてから 5 秒だけ出す「〇〇が退出しました」(left、経過時間は出さない)。他の人のゲームの接続では出さない。経過時間つきで、多くても 3 本。自分のゲームの接続が切れている間は自分の帯だけにする。名前は GamePlayScreen.seatName(mobile はキャラクターの名前)
  • マイクの鍵: 自分の plan が locked で一時停止中でなければ、ホストはマイクボタンを鍵にする。押すと「この場面ではマイクを使えません」のダイアログ(LockedMicDialog)。目立つほうは「閉じる」で、「緊急一時停止する」は止められる席(canControlPause)にだけ出す

話者表示 ​

UZU メニューの先頭と緊急一時停止の overlay に、プレイヤー全員の通話の状態を出す(UzuCodeSpeakers)。作品が話者を描かない・壊れたときの逃げ道なので、作品側からは消せない。

ゲームデータの取得中(ゲームの画面を出す前)も、ローディング画面のカードと UZU メニューに出す(UzuCodeLocalSpeakers)。まだ作品とつながっておらず presence が無いので、この端末の通話で分かること(自分の状態と、同じ部屋で聞こえている相手)だけを同じ規則で出す。段分けと観戦の人数は出さない。通話を使わない作品と、通話がつながっていない間はどちらも出さない。

  • 1 人 1 行。名前はキャラクターの名前(帯と同じ)を 5 文字と「…」に切る。状態はアイコンで出し、文言は長押しのツールチップと読み上げに持たせる
  • 顔の枠は「話している」と「声が届いていない」(lost)、名前の右のアイコンはミュート・通話にいない(off)・自分の通話の問題(つないでいる・マイクの権限がない・つながらない・通信が不安定)・ミュートを解除できない(locked)。同じことを 2 か所に出さない
  • 段は作品の部屋割り(presence の plan.room、plannedElsewhereOf)だけで分け、自分と別の部屋の人は扉のアイコンの区切りの下に並べる。観測席は自分が聞いている部屋(setListeningRoom)と比べる。通話の状態(otherVoice)は通話に入っている人の部屋しか分からないので、段分けに使うと、まだ通話に来ていない人が部屋を移っていないのに同じ部屋にいるように見える。一時停止の間は全員が同じ段。部屋割りが届かない古い SDK の作品では段を分けない
  • 観戦の人数は目のアイコンで出す(管理画面から見ている運営も入る)
  • 申告が届かない人(unknown)は何も出さない。アプリが裏に回っただけのことが多い
  • 4 人までは 1 列、5 人からは 2 列。幅に余裕がある overlay では最大 4 列まで広げる
  • UZU メニューは状態表示を項目の上に積む。積むと画面の高さに収まらず、左右に並べる幅があるときだけ項目の左に並べる(横向きの電話は高さが 284〜349px しかない)

ホストごとの申告 ​

ホスト通話の状態
mobile(複数人)Agora の状態から申告する。通話中 → connected、入室中 → connecting、再接続中 → reconnecting、マイクの権限なし → failed(mic-permission)、つながらない → failed(network)、作品が通話を使わない・イベントで通話が有効でない → off(table)、通話を別の端末に取られている間 → off(this-device)。方針によるミュートは本人の好み(localVoiceMutePreference)に書かない。ライブ配信の観戦者のマイクは listen-only。自分の通話の品質は送信の品質から決める(bad 以下で poor)
mobile(一人用)off(table)
mobile(プレビュー)off(this-device)
エミュレータ画面ごとの操作盤の値から組む。声が届かない相手(missing)も本番と同じ規則で数える(Emulator)
UZU TOKYOoff(table)
uzu dev harness画面ごとのマイクボタンのメニュー(エミュレータの操作盤と同じ OFF / SPK / LSN / MUTE / ERR / RECON / FAIL に、声が届かない LOST を足したもの)から組む。部屋は部屋割りに即座に従い、同じ部屋で通話に入っている席は聞こえている扱い。声が届かないプレイヤー(LOST / RECON / FAIL)は、部屋割りで同じ部屋にいる他の席の missing に猶予なしで載せる。観測席は全体の部屋のままで、setListeningRoom と一時停止は扱わない

mobile の通話エンジン(AbstRTCEngine)は、登録した時点の線の状態を linkState で、送信の品質を sendQuality / onSendQualityChanged で、部屋の移動で前の部屋を抜けたことを onRoomLeft で知らせる。

  • 部屋を移ったら、自分以外の聞こえている相手の記録を消す。Agora は抜けた本人に、前の部屋の相手の退出を知らせないため。新しい部屋の相手は、入り直したときに戻る
  • 部屋の移動中のマイクの切り替えは、値だけ覚えて入り直したあとに当てる。移動中にエンジンを直接呼ぶと Agora が -8(状態が不正)で断り、オフのつもりの人がミュートのまま残る
  • 通話を別の端末に取られて取り返したら、エンジンが替わったことを検知して、部屋とマイクの方針を当て直す。新しいエンジンがミュートなしで入ったら、ミュートし直す

観測席と監視用の接続 ​

  • 観測席(観戦・進行管理)は roster に載らないまま接続する。presence では人数だけを数え、voicePlan の対象にしない。全体の部屋にミュートで入り(本人は戻せる)、聴く部屋は本人が setListeningRoom で選ぶ。ライブ配信の観戦者は聴くだけ
  • UZU ADMIN のイベント詳細と Studio のテストプレイ履歴は、エミュレータを monitor=1 を付けて開く。エミュレータがゲームの URL に、SDK が WebSocket の接続 URL に引き継ぐ。サーバーはこの接続を数えず、緊急一時停止も受けず、通話の申告も捨てる。各画面はプレイヤーと同じ席でつながるので、印が無いと落ちているプレイヤーの申告が残って見える
  • 席 ID は接続 URL の自己申告なので、改造したクライアントなら他人の席を名乗れる。roster での判定と端末の数え方は、正規のアプリから来ていることに頼っている

新旧の SDK の同居 ​

wire 2 / bridge 1 の作品(旧 SDK)も、同じアプリと同じ play-server で動く。

層すること
play-server__platform は全員へ配る。旧 SDK は知らない type として捨てる。緊急停止は __pause / __resume も受ける
アプリ旧 SDK 向けのメッセージ(playersChanged / sdk の requestPause 等)と新しいメッセージの両方を送り、両方を受ける。旧 SDK は platform チャネルと知らない type を捨て、新 SDK は旧メッセージを捨てる。部屋とマイクを plan で動かすのは presence が届いたときだけなので、旧 SDK の作品では changeRoom / setMicEnabled の経路だけが動く
backendSupportedWireRange は 2〜3、アプリが名乗る bridge の範囲は 0〜2

旧 SDK の作品をすべて再 publish し終えたら、下限を上げるのと同じ変更で旧い経路を消す(バージョンと互換性)。

将来 Agora の Webhook を足すとき ​

今は入れていない。自動一時中断(#24926)で「誰かに声が届いているか」をサーバーが知る必要が出たときの候補で、足すときに確かめることをここに置く。

  • 公開済みの作品は壊れない。 SDK は知らない値・欠けた項目に耐えるので、サーバーが通話の状態に値を足しても、それより前に publish した作品はその値を unknown として扱う
  • サーバーの判定は作り直さなくてよい。 席の通話は今「端末の申告」から決めている。Webhook は「Agora 上でその席がチャンネルにいるか」という入力を足すだけになる
  • チャンネル名から卓を引ける必要がある。 通話のチャンネル名は mobile が組んでいて、play-server は知らない。今の組み方は {agoraRoomId}_{部屋の番号}(dev / stg は先頭に {env}_)で、部屋の番号は UZU Code の部屋名の FNV-1a 32bit ハッシュ(全体の部屋は 0。uzu_code_room_id.dart)。通知を卓(GameHost)へ届けるには、サーバーが agoraRoomId から卓を引け、部屋名から同じ番号を計算できる決まりにしておく。Agora の uid はプレイヤーの席 ID(ユーザー ID)
  • App ID が全環境で 1 つ。 dev / stg / prd が同じ Agora プロジェクトなので、通知は全環境から同じ URL に届く。チャンネル名の {env}_ で受け口が振り分けるか、環境ごとにプロジェクトを分けるかを決める
  • 通知の性質。 退室には理由が付く(2 が通信のタイムアウト = 落ちた)。順番は保証されず、同じ通知が 2 回届くことがあり、3 回送り直して届かなければ失われる。取りこぼしは REST のチャンネルの一覧で補うが、REST はアカウント単位で毎秒 10 回までなので、全卓を短い間隔で取りに行くことはできない

数値 ​

値場所
切断を確定するまでの猶予 5 秒DEFAULT_PRESENCE_TIMING.graceMs(room-core)
観測席の人数を配る間隔 3 秒DEFAULT_PRESENCE_TIMING.observersIntervalMs(room-core)
無言の切断とみなすまで 60 秒SILENT_DROP_MS(play-server の GameHost)
heartbeat 25 秒ReconnectableWebSocket の既定(SDK)
GameHost が起きる間隔 60 秒HEARTBEAT_INTERVAL_MS(play-server の GameHost)
声が届かない相手を載せるまでの猶予 5 秒(一度は聞こえた人)MissingVoiceTracker.grace(packages/uzu_code)
〃 15 秒(一度も聞こえていない人)MissingVoiceTracker.neverHeardGrace(packages/uzu_code)
退出の帯を出す長さ 5 秒PresenceBanner.leftNoticeDuration(packages/uzu_code)