close #2183 Reusing a room means whoever enters first becomes the host, and moving the session to someone else's library meant everyone leaving and re-entering in the right order. The host can now promote any connected guest from the participant list (Watch Together screen and the in-player session sheet), with playback, control mode, and reconnect identities carried across. The relay owns the swap: a new `transferHost` message validates the sender is the live host and the target a connected modern-protocol guest, then swaps `HostPeerID` and the reconnect verifiers (each peer keeps its own token), persists the room, and broadcasts `hostChanged` to every peer. Clients flip roles in place: the controller swaps its role engine while keeping the session, message queue, and player attachment. A promoted guest seeds the coordinator with the room's last known intent (a paused room stays paused) and the known-peer roster so the fresh epoch re-gates instead of solo-starting; a demoted host falls back to a reconciler with a fresh clock sync and asks the new authority for state. Guests re-pin the host identity, reset their sequence, and re-converge their clocks. A rejected transfer (`not_host` / `peer_not_found`) surfaces as a toast instead of tearing the session down. Pre-transfer app builds in the room ignore `hostChanged` and stop following the room at the next transfer; targets on an old sync protocol are not offered the action.
47 lines
1.1 KiB
JSON
47 lines
1.1 KiB
JSON
{
|
|
"protocolVersion": 2,
|
|
"legacyProtocolVersion": 0,
|
|
"clientMessageTypes": {
|
|
"create": "create",
|
|
"join": "join",
|
|
"broadcast": "broadcast",
|
|
"sendTo": "sendTo",
|
|
"ping": "ping",
|
|
"leave": "leave",
|
|
"endSession": "endSession",
|
|
"transferHost": "transferHost"
|
|
},
|
|
"serverMessageTypes": {
|
|
"created": "created",
|
|
"joined": "joined",
|
|
"peerJoined": "peerJoined",
|
|
"peerLeft": "peerLeft",
|
|
"message": "message",
|
|
"error": "error",
|
|
"pong": "pong",
|
|
"left": "left",
|
|
"ended": "ended",
|
|
"hostChanged": "hostChanged"
|
|
},
|
|
"errorCodes": {
|
|
"rateLimited": "rate_limited",
|
|
"invalidMessage": "invalid_message",
|
|
"roomExists": "room_exists",
|
|
"roomNotFound": "room_not_found",
|
|
"roomFull": "room_full",
|
|
"notInRoom": "not_in_room",
|
|
"alreadyInRoom": "already_in_room",
|
|
"peerIdUnavailable": "peer_id_unavailable",
|
|
"protocolMismatch": "protocol_mismatch",
|
|
"notHost": "not_host",
|
|
"peerNotFound": "peer_not_found"
|
|
},
|
|
"limits": {
|
|
"maxRoomSize": 8,
|
|
"maxMessageSize": 65536,
|
|
"maxSessionIdLength": 64,
|
|
"maxPeerIdLength": 128
|
|
},
|
|
"idPattern": "^[A-Za-z0-9_-]+$"
|
|
}
|