Documentation
API reference

Start here

Search every guideesc close

Developers

Watch Together over WebSocket

The wire contract for synced playback rooms: every frame in both directions, who may do what, and how rooms live and die.

6 min read

Watch Together keeps a group of viewers on the same server in sync: one room, one title, shared play/pause/seek. The web app ships it today, and the protocol is open for any client. This page is the behavioral contract; the frame shapes themselves are machine-readable in the API reference as the WtClientMessage (what you send) and WtServerMessage (what you receive) schemas.

Everything below applies to servers that advertise the ws.wt_contract_v1 capability (in GET /api/v1/system/info). Older servers speak the same frames but without the error codes, typed skip direction, and host handover described here.

Transport#

There is no separate socket. Watch Together frames travel on the server's one WebSocket at /api/v1/ws, authenticated like every other request. Each frame is a single JSON text message tagged by a type field; every Watch Together type starts with wt_. Frames the server does not recognize are ignored, so you can probe safely, but note the flip side: a malformed frame gets no reply at all. If you expect an answer and hear nothing, check your JSON before suspecting the server.

Positions are always position_ticks: 100-nanosecond units, ten million per second. It is the same unit the stream endpoints use.

The room lifecycle#

Create. Send wt_create with the media_id and media_file_id you are playing, your current position_ticks, and is_playing. For an episode, also send episode_id; that is what makes the room episode-shaped, so the server can advance everyone to the next episode together. You get back wt_created with the room_id and yourself as host. A user can be in one room at a time; creating a new room quietly leaves the old one.

Invite. Any participant can send wt_invite with a list of server user_ids. Each invitee receives wt_invited (host name, title, artwork); you receive one wt_invite_sent per user. There are no invite links or tokens: the room_id itself is the credential, and any signed-in user of the server who has it can join. Treat it accordingly.

Join. Send wt_join with the room_id and your stream_session_id (from the playback-info response for the same file). You receive a wt_state snapshot, which includes host_user_id, plus one wt_joined per existing participant so you can build the roster. Everyone else sees a single wt_joined for you. Rooms hold up to 8 people.

Leave and die. Send wt_leave when you exit. When the last participant leaves, the room is destroyed. A dropped connection is not a leave: you have a 5-minute grace to reconnect (just wt_join the same room again), and a room where everyone is disconnected is reaped after that grace passes. Peers are not told about mere disconnects, only explicit leaves, so a roster can briefly list someone who has lost their connection.

End of the movie. When a movie room finishes its file you receive wt_ended and the room is gone. An episode room instead broadcasts wt_next with the next episode's media_file_id; each client then starts playback of that file and reports wt_ready with its new stream_session_id. When every connected member is ready, the server broadcasts a fresh wt_state at position zero and the room plays on.

Who may do what#

Play, pause, and seek belong to everyone: any participant's wt_play, wt_pause, or wt_seek applies, last writer wins, and there are no sequence numbers. The server's clock is the authority; every accepted action produces a broadcast wt_state carrying the authoritative playback_state (playing or paused) and position_ticks.

Only the host may change episodes (wt_skip_episode with direction of next or prev). The creator starts as host. If the host leaves and others remain, the earliest joiner is promoted, and the room hears a wt_state whose host_user_id names the new host. Keep the host from your join snapshot and update it whenever a wt_state carries the field.

Seeks are a barrier#

A seek is coordinated so nobody plays ahead while a slow member is still buffering at the new position. When any participant seeks, the server pauses the room at the target and broadcasts wt_state with a fresh seek_id and waiting_for_seek_ready: true. Every connected member must answer wt_seek_ready echoing that seek_id once it can play at the target. When the last answer arrives, the server restores the intended state (playing, unless the seek said otherwise) and broadcasts a wt_state without a seek_id. Play or pause frames sent during the barrier are not lost; they adjust what the room does once the barrier lifts. A member that disconnects mid-barrier stops being waited on.

Ignore any wt_seek_ready bookkeeping for a seek_id you no longer hold: stale ids are dropped silently on the server too.

Staying in sync#

Send wt_report about every 2 seconds with your position_ticks and a buffering flag, even while paused. Reports are advisory with one exception: if your reported position drifts 3 seconds or more from the server's clock, you alone receive a corrective wt_state; snap to it. For sub-threshold drift, do what the web player does and nudge your playback rate a few percent rather than seeking. A generous deadband keeps a room settled instead of thrashing, and remember that on a transcoding session a seek restarts the transcode, so hard corrections are expensive off DirectPlay.

Reporting buffering: true pauses the whole room and tells everyone who is waiting (wt_buffering); the next buffering: false resumes it (wt_resumed plus a wt_state). Use it honestly and sparingly: after a buffering resume the server suppresses further buffering pauses for a few seconds to stop flapping.

Errors#

Errors come back addressed to you as wt_error with a machine-readable code and a human-readable message. The codes are a closed enum in the spec (WtErrorCode): things like room_not_found, room_full, already_in_room, not_participant, not_host, no_next_episode. Branch on code; show message if you like, but do not parse it.

A minimal well-behaved guest#

  1. wt_join with the room id and your stream session.
  2. Apply the wt_state snapshot; remember host_user_id.
  3. Report position every 2 seconds; snap on corrective wt_state.
  4. Answer every wt_seek_ready barrier promptly.
  5. On wt_next, start the named file and send wt_ready.
  6. wt_leave on the way out.