The Streaming Protocol¶
The wire protocol between the Selkies server and the web client, for anyone implementing a client, embedding the engine, or debugging with a network inspector. Everything rides one WebSocket (proxied at <base>/websocket), carrying a mix of binary frames and terse text messages. There is no negotiation dance: connect, receive settings, start receiving media.
Connection and roles¶
The client connects to wss://host/<subfolder>websocket. Role assignment happens one of two ways:
- Fragment mode (default): the URL fragment the page was opened with decides the role,
#shared(view only),#collab(full control),#player2through#player4(gamepad slot only),#display2-rightand friends (second monitor surface). No fragment means primary. - Token mode (when the server was started with
SELKIES_MASTER_TOKEN): the client must present?token=<token>in the WebSocket URL. Tokens and their roles are registered by the orchestrator viaPOST /tokenson the internal control port with the master token as a bearer credential. Close codes:4001invalid token,4002revoked,4029reconnecting too fast.
On success the server sends MODE websockets, an auth confirmation with the assigned role, and a server_settings JSON message containing every tunable setting with its value, allowed range or enum, and locked flag, this single message is what renders the sidebar UI, which is why locking a setting server side removes the control everywhere.
Binary messages, server to client¶
The first byte of every binary frame is a type tag:
| Tag | Payload | Header layout (big endian) |
|---|---|---|
0x00 | Full frame H.264 | [1] keyframe flag, [2:4] uint16 frame id, payload follows |
0x01 | Opus audio packet | 2 byte header, then the Opus packet |
0x03 | JPEG stripe | [2:4] frame id, [4:6] stripe Y offset, then the JPEG |
0x04 | H.264 stripe | [1] frame type (1 IDR, 2 I, 0 other), [2:4] frame id, [4:6] stripe Y offset, [6:8] width, [8:10] height, then Annex B NALs |
These headers are produced by pixelflux itself (see the wire format details); the Python server broadcasts them untouched. A full frame is just a stripe at Y offset 0 with full height. The client feeds H.264 to a WebCodecs VideoDecoder per stream, decodes JPEG stripes with createImageBitmap, and composites stripes onto the canvas at their Y offsets. The frame type byte reflects what the encoder actually emitted, clients use it to recover decoder state after drops.
Binary messages, client to server¶
| Tag | Meaning |
|---|---|
0x01 | File upload chunk (between FILE_UPLOAD_START and FILE_UPLOAD_END text messages) |
0x02 | Microphone PCM, s16le mono 24kHz |
Text messages, client to server¶
Compact comma separated commands. The important families:
| Command | Parameters | Description |
|---|---|---|
kd,<keysym> | keysym | Key down event |
ku,<keysym> | keysym | Key up event |
kr | — | Reset keyboard modifiers |
m,<x>,<y>,<mask>,<mag> | x, y, mask, mag | Absolute pointer event with button mask and scroll magnitude |
m2,<dx>,<dy>,<mask>,<mag> | dx, dy, mask, mag | Relative pointer event (trackpad and pointer lock modes) |
r,<WxH>,<displayId> | WxH, displayId | Resize request |
s,<dpi> | dpi | Set display DPI |
js,c\|d\|b\|a,... | Event-specific | Gamepad connect (c), disconnect (d), button (b), or axis (a) events |
cw,<b64> | b64 | Clipboard write (text) |
cb,<mime>,<b64> | mime, b64 | Binary clipboard write |
cws / cbs | — | Chunked clipboard transfer start (text / binary) |
cwd / cbd | Chunk data | Chunked clipboard transfer data (text / binary) |
cwe / cbe | — | Chunked clipboard transfer end (text / binary) |
co,end,<text> | text | Commit composed text (IME) |
SETTINGS,<json> | json | Change stream settings (validated and clamped server-side) |
CLIENT_FRAME_ACK <id> | id | Backpressure acknowledgement |
START_VIDEO | — | Start video stream |
STOP_VIDEO | — | Stop video stream |
START_AUDIO | — | Start audio stream |
STOP_AUDIO | — | Stop audio stream |
FILE_UPLOAD_START:<relpath>:<size> | relpath, size | Begin file upload |
FILE_UPLOAD_END:<path> | path | Complete file upload |
cmd,<shell> | shell | Run a command in the session (gated by SELKIES_COMMAND_ENABLED) |
_f,<fps> | fps | Client FPS telemetry |
_l,<ms> | ms | Client latency telemetry |
Every input bearing message is filtered by role: viewers get settings and video control only, players get their js slot, mk_control grants of mouse and keyboard can be toggled live in token mode.
Text and JSON messages, server to client¶
cursor,{...}: PNG cursor images with hotspots, delivered out of band so the canvas cursor is pixel perfect without burning it into the video.clipboard,<b64>and the chunked variants: server clipboard changes.system_stats,gpu_stats,network_statsJSON blobs every few seconds for the Stats UI.stream_resolutionanddisplay_config_updatefor resize and multi monitor layout changes.VIDEO_STARTED,AUDIO_STOPPED,PIPELINE_RESETTING <display>lifecycle notices, andsystem,{"action":"reload"}when the client should reconnect fresh.
Backpressure¶
The server stamps every video frame with a wrapping uint16 id; the client periodically acknowledges the last id it presented. The server computes each client's desync, subtracts an RTT allowance (smoothed over recent samples), and if a client falls more than the allowed window behind, frames are withheld for that client before encode where possible, keeping H.264 reference chains valid. A client stalled beyond a timeout is disconnected. The intended behavior: one person on hotel wifi does not blur the session for four people on a LAN.
Settings flow¶
Client sends SETTINGS,{"framerate":60,"h264_crf":20,...}; the server validates each key against its schema (range clamp, enum membership, locked flag) and applies what survives, live where pixelflux supports it (quality, framerate, paint over) or with a pipeline restart where it does not (encoder, color mode). The authoritative state then flows back in server_settings so all connected clients converge.
Implementing a client: a minimal path¶
- Open the WebSocket with
binaryType = 'arraybuffer', sendSTART_VIDEOandSTART_AUDIOafter receivingserver_settings. - Demux on byte zero. Feed
0x00and0x04frames (strip the header, respect the keyframe flag) into a WebCodecs H.264 decoder configured from the stripe dimensions; paint0x03JPEGs at their offsets; queue0x01Opus into an audio decoder. - Send
CLIENT_FRAME_ACKwith the latest presented frame id a few times per second. - Map your input events to the
kd,ku,morm2grammar.
That is a functioning viewer; everything else (clipboard, files, gamepads, stats) is additive. Start from the example client in the pixelflux repository: example/screen_to_browser.py plus example/index.html are a complete working server and client pair, with the frame parsing in about a page of code.