Skip to content

Protocol messages ​

couchpad expects a small JSON protocol over one WebSocket per device. Every message is an object with a string t. The server identifies the host connection as 'tv' and each phone by a playerId it assigns.

Client to server ​

MessageFromMeaning
{t:'room.create', household?}hostMake a room
{t:'room.resume', code, tvToken}hostReclaim a room after a reload or reconnect
{t:'room.join', code, name, rejoinToken?}phoneJoin, or take back a seat with the token
{t:'signal', to, data}bothWebRTC offer, answer or candidate. Phones send to:'tv', the host sends to:<playerId>
{t:'relay', to?, data}bothFallback transport, same addressing as signal
{t:'ping'}bothSocket keepalive; answer {t:'pong'}

Server to client ​

MessageToMeaning
{t:'room.created', code, tvToken}hostcode is 4 letters from BCDFGHJKLMNPQRSTVWXZA
{t:'room.resumed', players?:[{id,name,avatar?}]}hostAfter a resume, this live list is the truth
{t:'room.joined', playerId, token, name}phonetoken is the rejoin token
{t:'player.joined', player}hostA phone joined or rejoined
{t:'player.left', playerId}hostA phone's socket closed
{t:'tv.left'} / {t:'tv.back'}phonesHost presence. Phones re-offer WebRTC on tv.back
{t:'signal' | 'relay', from, data}eitherfrom is 'tv' or a playerId. The server sets it; never trust the sender
{t:'pong'}eitherReply to ping
{t:'error', code, message?}eitherSee below

Unknown t values are ignored, so you can multiplex your own game events on the same socket.

Error codes ​

room.notFound, room.full, room.badRequest, room.notInRoom, room.targetGone, room.badToken, room.rateLimited. Servers may add their own; couchpad passes unknown codes through as is.

Peer messages ​

These travel inside a DataChannel as JSON strings, or as objects inside relay.data. Both sides validate them with PeerMsgSchema.

ts
{ t: 'input', frame: InputFrame }
{ t: 'screen', name: string, data?: unknown }
{ t: 'ping', id: number }
{ t: 'pong', id: number }

Signal payloads ​

ts
{ kind: 'offer' | 'answer', sdp: string }
{ kind: 'candidate', candidate: RTCIceCandidateInit }

Relay chunks ​

Relay payloads over 28 KB are split before sending:

ts
{ t: '~chunk', id: number, seq: number, total: number, part: string }

total is at most 64 and seq is 0-based. The receiver reassembles per sender and chunk id and drops partials after 10 s. A server only forwards relay.data, so it needs no chunking logic.

A complete session ​

TV     → server  {t:'room.create', household:'living-room'}
server → TV      {t:'room.created', code:'KMPV', tvToken:'…'}
phone  → server  {t:'room.join', code:'KMPV', name:'Asha'}
server → phone   {t:'room.joined', playerId:'p1', token:'…', name:'Asha'}
server → TV      {t:'player.joined', player:{id:'p1', name:'Asha'}}
phone  → server  {t:'signal', to:'tv', data:{kind:'offer', sdp:'…'}}
server → TV      {t:'signal', from:'p1', data:{kind:'offer', sdp:'…'}}
TV     → server  {t:'signal', to:'p1', data:{kind:'answer', sdp:'…'}}
server → phone   {t:'signal', from:'tv', data:{kind:'answer', sdp:'…'}}
  … candidates both ways …
phone ⇄ TV       DataChannels 'input' and 'state' open: mode 'direct'
TV    → phone    {t:'screen', name:'play', data:{widgets:[…]}}
phone → TV       {t:'input', frame:{t:1234.5, axes:{x:0.4, y:-1}}}

MIT licensed. Extracted from the Fire TV party game Cadabra.