Zapping should not wait on the network. The app knows which node serves each channel (routes), builds the playback token itself, and opens the stream straight on that node. The panel is only involved for the slow path.

 app ──(1) GET playback-routes, every 60 s, in the background──► gateway ─► panel
 app ──(2) build token locally (AES-GCM + HMAC, < 1 ms)
 app ──(3) GET https://node/<token>~<session_token>──────────────► streaming node

Needs the capabilities client_playback_tokens and playback_routes. Otherwise use the slow path (7.5) for everything.

7.1 Routes: playback-routes

GET /api/v2/playback-routes
Authorization: Bearer <session_token>
If-None-Match: "<ETag of the previous answer>"
Accept-Encoding: gzip
{
  "ttl": 60,
  "nodes": {
    "3": { "hosts": ["lb1.example.com", "203.0.113.5"], "http_ports": [80],
           "https_ports": [443], "prefix": "", "direct": true, "thumbnails": true,
           "reports": true },
    "5": { "hosts": ["lb2.example.com"], "http_ports": [80], "https_ports": [],
           "prefix": "", "direct": true, "thumbnails": false, "reports": false }
  },
  "default": { "nodes": [3, 5], "spread": 2, "pictures": [3] },
  "channels": { "123": { "nodes": [5], "spread": 1 }, "140": { "nodes": [], "spread": 1 } }
}

Refresh every ttl seconds with If-None-Match; 304 means nothing changed. Send Accept-Encoding: gzip: on a large panel the map is about 260 KB and about 35 KB compressed. Each encoding has its own ETag.

To play channel id:

  1. Take channels[id], or default if it is not listed. nodes: [] means the channel is not available right now: show it as unavailable.
  2. Pick one of the first spread nodes at random (they are already running it); on failure, continue with the rest in order.
  3. Use the node's hosts in order (names before IPs), https when it has an HTTPS port, and put prefix before the path. https_ports lists only ports the node saw open in its last heartbeat and http_ports leaves out the ones it saw closed: prefer https whenever the node has one. With a reverse proxy, hosts are the proxies' and the node's own IPs are left out. Empty hosts means the node cannot be reached right now (its reverse proxy is down): skip it.
  4. Use app-built tokens on nodes with direct: true. A node with direct: false may refuse them: use the slow path (7.5) there.
  5. Send playback reports (7.7) only to nodes with reports: true.
  6. pictures (capability picture_routes) lists, in the same order, the nodes of nodes where the channel runs always-on: only those keep its picture (7.6). Missing means no node has one: show the logo.

A web app served over HTTPS cannot load http:// media, and certificates are not valid for bare IPs. Skip IP hosts for HTTPS and nodes without an HTTPS port.

Movies and episodes: GET /api/v2/playback-routes?type=vod&ids=4321,4322 (movie ids) (type=saga for episodes; up to 200 ids):

{ "ttl": 60, "nodes": { "3": { "…": "…" } }, "items": { "4321": [3] } }

Ids with no live node (or that do not exist) are left out of items. The node checks the line's package when it opens the stream.