The render protocol

Docs Documentation

The render protocol

Developer guide for renderer.html: transports, the message flow, the timeline rule, the messages each way, network and data, and the rules a renderer must follow.

This guide is for developers who write their own renderer.html. Start from the Minimal example renderer in Settings → Player (also at /examples/minimal-renderer.html): about 300 lines of plain JavaScript that do everything below.

Finding the host

Every message is one JSON object with a string type and the protocol version v (currently 1). Send v on every message. Ignore any message type and any field you do not know: the protocol only grows by adding within a version. Payloads may arrive as objects or as JSON strings, so accept both.

Check for the host in this order when your script starts:

  • Raspberry Pi — the page address has ?host_ws=ws://127.0.0.1:<port>/render: open that WebSocket and send and receive JSON text.
  • Desktop app — window.__TAURI__?.event exists: listen for wf:render, emit wf:renderer.
  • Android TV (bridge) — window.WunderframeRenderer?.post exists: the host calls window.__wfRender(json), you call WunderframeRenderer.post(json).
  • Browser — you are in an iframe: message events from window.parent, and window.parent.postMessage(msg, "*").
  • Android TV (port) — none of the above: the host posts the string wf-render-port with a MessagePort. It may arrive after your script ran, so queue what you send until a transport exists.

The conversation

Message flow
renderer                     host
   | -- ready --------------->  |   as soon as the script runs
   | <---------------- load --  |   stage program P off-screen
   | -- prepared {P} -------->  |   pictures decoded, fonts loaded, first video buffered
   | <---------------- play --  |   show P from position X
   | -- shot {index, at} ---->  |   each time a shot comes on screen
   | <------------ position --  |   follower correction on a wall
   | <---------------- data --  |   new data, swapped at the next shot boundary
   | <---------------- load --  |   the next program Q, staged while P plays
   | -- prepared {Q} -------->  |
   | <---------------- play --  |   switch to Q

If the host sees no ready within 20 seconds, or the renderer crashes, that screen runs the stock renderer instead and reports why.

The timeline rule

Walls stay frame-accurate only if what you draw is a pure function of the timeline position.

  • play { epoch, position_ms, at } means: at host time at, the timeline was at position_ms.
  • The position now is position_ms + (hostNow() − at), where hostNow() is Date.now() plus the offset from the last clock message.
  • Recompute it every frame from the last anchor. Never add up frame times or count timer ticks.
  • A position message is a new anchor: jump to it. Seek videos to the position too.

Messages from the host

  • load — a program to prepare off-screen: its shots, blocks, fonts and the local address of every asset. Answer prepared when every picture is decoded and the first video can play.
  • play — show a prepared program from a position.
  • position — a correction from the wall’s leader.
  • data — a new data pack (records, feeds, lists). Swap it in at the next shot boundary.
  • overlay — what covers or marks the program: identify, the idle card, offline marker, a wall panel’s name.
  • presenter and cast — the live presenter or cast on this stage, with what you need to join their video.
  • monitor — someone is watching this screen; show a small dot if you can.
  • clock — the host’s offset to server time and the venue’s time zone.
  • vars — the stage’s current TV channel changed; switch that video’s source only.
  • unload, fetch_result — drop a program; the answer to your fetch.

Messages from the renderer

  • ready — first, unprompted, with v, your renderer_version (the name Devices shows) and the capabilities you implement. Announce only what you do.
  • prepared — a loaded program is ready to show.
  • shot — a shot came on screen: its index and when. This is the wall’s sync beat.
  • ended — one pass of the program is over.
  • planes and audio — on TV players, where the video layers go and which sounds to play (see below).
  • channel — how a TV channel you play in the page is doing.
  • fetch — ask the host to fetch something for you; the answer comes as fetch_result.
  • error and perf — what went wrong and how the page is doing; they appear on Devices. An error never stops the show: keep drawing what you can.

Network and data

A renderer has no network of its own. Everything it needs comes in load and data, with local addresses that work offline. Anything live goes through the host’s fetch, with limits on size and time and no cookies; keep the last good value. What you fetch yourself is not cached, not offline-safe and not in step across a wall — it will look broken on screens. During the self-test every fetch is refused.

Planes, video and sound on TV players

On Android TV and Raspberry Pi, video and TV channels play in native layers, not in your page. The host may open your page several times, each with ?plane=n, and stack them with video layers between them. Draw only the blocks that belong to your plane, and leave the video’s area transparent. load tells you the host’s limits and whether sound plays natively; then your page stays muted and announces sounds to the host instead.

Rules

  • One self-contained HTML file, at most 8 MB. No <script src>, stylesheet links, imports from a URL, relative paths, <base> or frames.
  • You never see credentials: no player key, no token. Do not try to read the host’s storage.
  • The download address is public. Do not put secrets in the file.
  • Keep v: 1 in ready, follow the timeline rule, and ignore what you do not know.