Transport is Phoenix Channels over a single WebSocket at
wss://ws.frostbyte.tv/socket/websocket. The sync server lives on its own
subdomain so the apex frostbyte.tv can be a pure static landing page with
its own caching profile.
Phoenix Channels already handles framing, multiplexing, heartbeats, and
reconnection. This document defines the event names and payload shapes
that flow through the room:<room_code> topic.
All timestamps are integer milliseconds since the Unix epoch unless noted. Payloads are JSON (Phoenix's default serializer).
room:<room_code>
Where <room_code> is a 6-character alphanumeric string, e.g. ABC123.
Sent when the extension joins the channel. Phoenix forwards this to
FrostbyteWeb.RoomChannel.join/3.
{
"user_name": "faris",
"client_time_ms": 1744329600000,
"capabilities": ["youtube", "netflix", "twitch"]
}user_name: display name the other peers see. No uniqueness constraint.client_time_ms: the client's wall clock at the moment of sending. Used by the server to estimate initial clock offset.capabilities: list of platforms this client can play. Lets the server reject joins to a room whose current content the client can't handle.
Join reply (server → client, as the phx_join ok payload):
{
"user_id": "u_8f2a",
"role": "controller",
"server_time_ms": 1744329600123,
"session": {
"content": { "platform": "youtube", "id": "dQw4w9WgXcQ" },
"paused": true,
"position_ms": 0,
"rate": 1.0,
"updated_at_server_ms": 1744329600000
},
"peers": [
{ "user_id": "u_8f2a", "user_name": "faris", "role": "controller" }
]
}user_id: server-assigned, unique within the room.role:controller(can sendstate_change) orviewer(read-only).server_time_ms: server wall clock when the reply was built.session: current authoritative session state. Client should seek toposition_msand applypaused+ratebefore listening for broadcasts.peers: everyone currently in the room, including the joiner.
Sent periodically by each client to measure and maintain clock offset from the server. Fire several times during the first few seconds, then slow to once every 30 seconds.
{ "client_time_ms": 1744329605123 }Server replies (as the event's reply payload):
{
"client_time_ms": 1744329605123,
"server_time_ms": 1744329605145
}The client uses the round trip to estimate RTT and the offset between its wall clock and the server's. Full algorithm in sync-algorithm.md.
Sent by a controller to change playback state. Viewers that send this
receive an error reply and no broadcast happens.
{
"action": "pause",
"position_ms": 48230,
"client_time_ms": 1744329650000
}action: one ofplay,pause,seek.position_ms: the video position at the moment the controller acted, from their local<video>.currentTime.client_time_ms: the controller's wall clock at the moment they acted. The server uses this together with the controller's known clock offset to reconstruct the intended server-time the action should take effect.
Server replies { "ok": {} } if accepted, or an error payload if not.
Grant or revoke controller permission on another user in the room.
{ "user_id": "u_3c11", "role": "controller" }Switch the room to a new piece of content. Resets position to 0 and pauses everyone.
{ "platform": "youtube", "id": "dQw4w9WgXcQ" }The authoritative session clock has changed. Every client should apply this
at the specified execute_at_server_ms.
{
"action": "pause",
"position_ms": 48230,
"rate": 1.0,
"execute_at_server_ms": 1744329650200,
"updated_at_server_ms": 1744329650045
}execute_at_server_ms: the server time at which every client should apply this change. The client converts to its local time using its measured offset and schedules asetTimeout. The 200ms-ish buffer is chosen to exceed typical WS RTT so the effect lands simultaneously everywhere.updated_at_server_ms: the server time at which this state became authoritative. Used for drift projection between broadcasts.
Sent automatically by Phoenix.Presence whenever the room membership
changes. Standard Phoenix Presence format:
{
"joins": { "u_3c11": { "metas": [{ "user_name": "jeff", "role": "viewer" }] } },
"leaves": { "u_0a22": { "metas": [{ "user_name": "ali", "role": "viewer" }] } }
}A user's role in the room changed. Separate from presence_diff so that
clients can update the roster without a presence churn event.
{ "user_id": "u_3c11", "role": "controller" }The room switched content. Clients should load the new content and wait for
the next state_broadcast before playing.
{ "platform": "netflix", "id": "81123456" }Transient or recoverable error. Fatal errors disconnect the channel instead.
{ "code": "not_controller", "message": "only controllers can pause" }Defined error codes (v1):
| code | meaning |
|---|---|
not_controller |
caller tried a controller-only action without role |
bad_payload |
payload failed schema validation |
content_mismatch |
caller's capabilities cannot play current content |
rate_limited |
too many messages in the current window |
This is protocol v1. Breaking changes require bumping to v2. The version number is implicit in the Phoenix Channel vsn query parameter and in server-side channel module routing. Clients and server are expected to be updated together for v1. Forward compatibility is not a goal in v1.