Skip to content

A small TypeScript library for TV and web games


Players scan a QR code on the TV and their phone becomes a gamepad. Input streams over a WebRTC DataChannel, and when the network blocks that, a WebSocket relay carries it instead. Your game never has to know which one it got.

npm install github:MaanavKrishna/couchpad

Watch a session survive the network

Scroll through four networks and watch what couchpad sends. Every line in the log is a real couchpad message.

Built for phones on a couch

Phones lock, refresh and wander between rooms. These are the numbers couchpad uses to keep a seat alive, straight from the source.

4 sto open WebRTC, then relay
If both DataChannels are not open within OPEN_TIMEOUT_MS, the peer fails over to the WebSocket relay for good. Input is relayed while it waits, so nothing blocks.peer.ts: OPEN_TIMEOUT_MS
28 KBrelay chunk size
Relay payloads above RELAY_CHUNK_BYTES are split into ~chunk envelopes that fit 32 KB API Gateway frames, and reassembled per sender. Partials expire after 10 s.util.ts: RELAY_CHUNK_BYTES
30 Hzcontinuous input
Stick, tilt and held buttons go on an unordered, no-retransmit channel. Button presses skip the throttle so an edge is never late.client.ts: INPUT_HZ
100 msheld-input keepalive
While anything is held the latest frame is resent. A release is sent twice, 30 ms apart, and blur or a hidden tab sends a zeroed frame.client.ts: KEEPALIVE_MS
2 sping and ICE grace
A ping every 2 s measures round-trip time. A disconnected ICE state gets 2 s to recover before the link drops to relay.client.ts: PING_INTERVAL_MS
250 ms–8 sreconnect backoff
Socket drops retry with ±30% jitter. Backoff only resets after a socket stays open for 3 s, so flapping links keep backing off.signaling.ts: BACKOFF_MS
64queued reliable messages
While no path is up, reliable sends wait in a bounded queue and flush in order when the link returns.client.ts: RELIABLE_QUEUE_MAX
200 KBlargest drawing
The draw pad exports a JPEG of at most 256 px, lowering quality until it fits 25 KB. Anything over 200 KB is dropped before send.util.ts: MAX_DRAW_BYTES
  • 1 runtime dependency (zod)
  • 8 widget kinds
  • 39 tests, no browser needed
  • 13–16 ms phone to Fire TV in Cadabra

Try the input path

This joystick runs the same rules as CouchClient.sendInput(): moves are coalesced to 30 Hz, a button edge goes out at once, and a held control is resent every 100 ms.

Quick start

The TV creates a room and tells each phone which controls to show. The phone renders them and streams input back.

ts
import { CouchHost } from 'couchpad';

const host = new CouchHost({ wsUrl: 'wss://your-server/ws', household: 'living-room' });
const { code } = await host.create(); // show a QR to https://your-site/?room=CODE

host.on('player', (id: string) => host.sendTo(id, {
  t: 'screen', name: 'play',
  data: { widgets: [{ kind: 'joystick', id: 'move' }, { kind: 'button', id: 'jump', label: 'JUMP' }] },
}));
host.on('input', (id: string, frame) => console.log(id, frame.axes, frame.buttons));
ts
import { CouchClient, mountController, type ControlWidget } from 'couchpad';

const client = new CouchClient({ wsUrl: 'wss://your-server/ws' });
client.onMessage((m) => {
  if (m.t !== 'screen') return;
  const { widgets } = m.data as { widgets: ControlWidget[] };
  mountController(document.body, widgets, (f) => client.sendInput(f));
});

const room = new URLSearchParams(location.search).get('room')!;
await client.join(room, 'Asha'); // client.mode is now 'direct' or 'relay'
bash
npm install github:MaanavKrishna/couchpad

Used by Cadabra

Cadabra is a Fire TV party game where the room invents the game and then plays it. Every phone joins by QR and becomes the controller through couchpad.

It carries input from up to 4 phones to a Fire TV Stick HD, measured at 13 to 16 ms on a home network. Its production signaling runs on API Gateway WebSocket, Lambda and DynamoDB, which is where the 28 KB chunking came from.

See Cadabra on GitHub
A phone that has joined a Cadabra room, showing couchpad's joystick and buttons, next to the TV game it controls.
A phone joined to Cadabra, playing as the controller.

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