---
title: "Streaming API v2: How playback works"
description: "Play straight from the streaming nodes: the routes map and how to pick a node for each channel, movie or episode."
---

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`

```http
GET /api/v2/playback-routes
Authorization: Bearer <session_token>
If-None-Match: "<ETag of the previous answer>"
Accept-Encoding: gzip
```

```json
{
  "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):

```json
{ "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.
