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.
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#
wt_joinwith the room id and your stream session.- Apply the
wt_statesnapshot; rememberhost_user_id. - Report position every 2 seconds; snap on corrective
wt_state. - Answer every
wt_seek_readybarrier promptly. - On
wt_next, start the named file and sendwt_ready. wt_leaveon the way out.