Files
plezy/relay_protocol.json
edde746 89d54d8f1d feat(watch-together): let the host hand the room to another participant
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.
2026-08-31 08:55:45 +02:00

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_-]+$"
}