7.7 Playback reports: stalls, crashes and blocks

The app tells the panel why playback stutters, so the provider can answer "is it your connection, your device, or us?" without asking the user for anything. Reports are small, rare and never about healthy playback.

Send reports only when the user has "Send diagnostics" on (on by default). Node reports need reports: true on the node in playback-routes (7.1); diagnostics needs the capability playback_reports in identity. Without them, send nothing.

What to report

  • stall: one record per stall after playing, at most one every 30 s per device. Not in the 3 s after a zap or a seek by the user, and not while an on-demand channel starts. The player's own seeks (jump to live, adjustments) do not open that grace.
  • crash: one record in the next batch when the app was closed while playing. Set a persistent mark on playing; clear it on a clean exit and when the app goes to the background (TV systems kill background apps).
  • block: one record per confirmed block of a streaming host: the device has internet (the connectivity check answers) and still cannot reach the host, twice, 30 s or more apart, on the same network. A captive portal is not a block.
  • Nothing else: no healthy sessions, no channels that are down, no start-up times.

Where to send them

Records Where
stall, crash The node where they happened: POST <node base>/app-diag
block The panel: POST /api/v2/diagnostics, through the gateway

Keep one queue per node. Send one batch per node at most every minute, and drop records older than 24 h (a node that cannot be reached for 24 h loses them). Never send stalls to diagnostics or blocks to a node.

POST /app-diag on the node

<node base> is scheme://host:port + prefix of that node, built as in 7.1 (the host the player used, or the next one in hosts).

POST https://lb1.example.com:443/app-diag
Content-Type: text/plain;charset=UTF-8

{"st":"<session_token>","r":[{"t":"stall","seq":812,"ago":4200,"ch":4512,"eng":"hls","h":1,"v":"net","busy":0.97,"buf":0.2,"slow":0,"x5":0,"rc":200,"err":"net.timeout","bwm":"1840000:2.1","dec":1480,"drop":3,"dropq":1,"long":0,"fix":"reload","code":"7KQ2-M9XA-T4PE","net":"wifi","rssi":-71}]}
  • The body is JSON, 16 KB at most (the session token alone takes 1-3 KB), with the current session token in st and up to 30 records in r (the rest are ignored).
  • Send it as text/plain with no Authorization or custom headers: it is a CORS simple request, with no preflight (nodes do not answer preflights for POST). A web app may use fetch(url, { method: "POST", mode: "no-cors", body }): the answer is then opaque, so a resolved promise means delivered and a network error means keep the batch.
  • The node sets the time, its own id and the device's IP. Never send them.
Status Meaning What the app does
204 Accepted. Invalid records are dropped silently Drop the batch
400 Not a JSON object with st and r Drop the batch
404 The session token is not valid here (expired, or another panel) refresh-session, then send again once; drop it if it fails again
413 Body over 16 KB Send fewer records
429 One batch per device per minute (Retry-After) Keep the records, send them after Retry-After
503 The node cannot store them now (Retry-After) Keep the records, send them later
network error Keep the records

POST diagnostics on the API

POST /api/v2/diagnostics
Authorization: Bearer <session_token>
Content-Type: application/json

{"r":[{"t":"block","seq":813,"ago":61000,"host":"lb1.example.com","type":"dns","times":2,"node":3,"code":"7KQ2M9XAT4PE","net":"wifi"}]}
{ "accepted": 1 }

Up to 30 records, 16 KB at most, one batch per device per minute (429 rate_limited with Retry-After otherwise: keep the records and send them later). Errors are the JSON errors of section 4.

Record fields

All records:

Field Type Meaning
t string stall, crash or block
seq integer 1 to 2147483647. A counter per device, shared by every record (node and API), kept across restarts and never reused: the panel drops a repeated (device, seq), so resending after a lost answer is safe
ago integer Milliseconds since the event, up to 86400000. The panel stores its own clock minus ago; the device's clock is never used
code string Support code of the playback session: 12 Crockford base32 characters (0-9, A-Z without I, L, O, U), dashes allowed. Show the same code in the stats overlay, in "No connection" and in errors: the provider finds the reports by it
net string wifi, eth, cell or other

stall and crash:

Field Type Meaning
ch integer Content id
ct string channel (default), vod (movie id) or saga (episode id)
eng string Player: hls (hls.js), native (the TV's own player) or exo (ExoPlayer)
h integer Index in the node's hosts of the host in use
fix string What fixed it: none, reload (playlist reloaded), recreate (player recreated), host (another host), node (another node)

stall only (the inputs of the local verdict, so the panel can recalibrate without a new app):

Field Type Meaning
v string The local verdict: net (the connection is not enough), server (5xx, timeouts or slow first bytes), device (dropped frames), content (a splice or discontinuity), none (no data)
busy number Share of the last 30 s spent downloading, 0 to 1
buf number Buffered seconds at the stall
slow integer Fragments (not the last of the playlist) with a first byte of 2 s or more in the window
x5 integer 5xx answers or timeouts in the last 10 s
rc integer HTTP status of the last failed request, 0 if none
err string Last player error: hls.js details or the ExoPlayer error mapped to net.* / fmt.*
bwm string "<bytes>:<seconds>" downloaded in the window
dec, drop integer Decoded and dropped frames in the window
dropq integer 1 if the frame counters move with playback; 0 if they do not (that device never gets the device verdict)
long integer 1 if the stall lasted 30 s or more
rssi integer Wi-Fi signal in dBm, -150 to 0

block only:

Field Type Meaning
host string The host that could not be reached (a name or an IP). Required
type string dns (the name fails and the node's IP works), ip (both fail and the panel's API works), dns_api (the API's own domain; report it once the app got through another way). Required
times integer Confirmations behind the record
node integer Key of the node in playback-routes; leave it out for the API's domain

Strings are ASCII; text fields are lowercased and anything else is dropped (the field, not the record). Unknown fields are ignored. Records with a bad t, seq or ago (or a block without a valid host and type) are dropped.

Where the platform lets you, send a User-Agent of the form <YourApp>/<version> (<model>) (for example MyPlayer/1.4.2 (Fire TV Stick 4K)) on playback and report requests, so the requests of your app are easy to tell apart. The panel does not count watch time per app from it: stalls are turned into a rate with the watch time of the devices that send playback reports. The App/Device column of Live Connections comes from a fixed list of known players, read from the User-Agent of the stream request; an app that is not on that list shows as "Other".