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 afterplaying, 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 onplaying; 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
stand up to 30 records inr(the rest are ignored). - Send it as
text/plainwith noAuthorizationor custom headers: it is a CORS simple request, with no preflight (nodes do not answer preflights forPOST). A web app may usefetch(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".