Error codes
What the control API, the player, the relay and the C library report when something fails, what each means and how to fix it.
Taken from crates/nearglass-control-api/src, js/player/src,
crates/nearglass-ffi/include/nearglass.h and docs/RELAY-GATE.md on commit
79b96c7 of the SDK repository. The route list is in the
control API reference; the player in the
player reference.
Control API
Every error is JSON with the same shape, and the HTTP status says the class:
{ "error": { "code": "tenant_viewer_cap", "message": "this tenant's 200 seats are all taken" } }Branch on code, not on message: the message is for people, and its
wording can change. Not every non-2xx answer is an error:
POST /v1/streams/{id}/viewer-tokens answers 409 with a body that has
state (wait, offer or full) and no error when the viewer is in the
waiting line; that is the line, not a failure (Viewer tokens).
| Status | Meaning |
|---|---|
| 400 | the request is wrong: fix it |
| 401 | the key is missing or not accepted |
| 402 | an account limit stops this: raise it |
| 403 | the key is right but not allowed to do this |
| 404 | nothing there, or it belongs to another tenant |
| 409 | the request is fine but conflicts with state or a limit |
| 410 | the seat is gone |
| 429 | too many requests |
| 5xx | the service, not you |
Codes
| Status | code | Meaning | Fix |
|---|---|---|---|
| 400 | invalid_request | a field is missing, out of range or unknown, or the body is not JSON. Unknown fields are refused. The message names the field | read the message and correct the field. Limits are in the control API reference |
| 401 | unauthorized | no Authorization: Bearer key, or the key is unknown. For /v1/play/{id}/hls/...: no valid ?seat= | send the right kind of key for the route: sk_ from your backend, pk_ from a page, never pk_ for a backend route. A lost secret key cannot be shown again: replace it |
| 402 | spend_cap_reached | this month's spending cap is reached, so no new viewer gets a seat. Viewers already in keep watching | raise the cap with PATCH /v1/tenant, or wait for the month to end (Usage and spending caps) |
| 403 | server_admission | POST /v1/play/{id}/admit on a stream whose admission is server | take seats from your backend with viewer-tokens, or create the stream with admission: public |
| 403 | operator_only | a tenant tried to change its name or limits, or the default tenant's cap | ask us. Only the operator sets these |
| 404 | not_found | no such stream, seat, turn or tenant, or it is another tenant's | check the id. Ids are per tenant: another tenant's stream answers 404, never 403 |
| 404 | no_hls | this deployment packages no LL-HLS, or the stream takes no encoder ingest so has none | use a stream with encoder ingest (rtmp, rtmps, srt or whip) on a deployment that has the fallback |
| 404, 502 | hls_unavailable | the stream has no LL-HLS right now: not live yet, or the part is gone | retry. The player does |
| 502 | hls_origin | the LL-HLS packager did not answer | retry; if it persists, tell us |
| 404 | no_fleet | POST /v1/fleet/heartbeat on an API with no fleet file | operator only |
| 409 | namespace_in_use | another stream has this namespace | pick another, or leave namespace out |
| 409 | too_many_streams | at most 1000 streams, or your tenant's max_streams | delete a stream you no longer use |
| 409 | too_many_places | seats plus places in line, over every stream, would pass 20000 | lower viewer_cap or queue_max on this stream, or on another |
| 409 | tenant_viewer_cap | on create: viewer_cap is above your tenant's. On viewer-tokens and admit: every seat of your tenant's streams is taken | lower viewer_cap, free seats, or ask us to raise the limit. The player waits and asks again every 10 s |
| 409 | no_stream_key | the stream publishes with tokens only (ingest: ["sdk"]) so has no key to rotate; or it takes no encoder ingest so the gateway can have no token for it | use publisher-tokens for an SDK publisher |
| 409 | not_interactive | controller tokens on a stream whose mode is broadcast | create the stream with mode: interactive |
| 409 | turn_exists | this turn_id already has a token | use a new turn_id, or end the turn first |
| 409 | above_max_spend_cap | spend_cap_usd is above the most your tenant may set | set a lower cap, or ask us |
| 409 | too_many_tenants, default_tenant, too_many_keys | operator calls: the tenant limit, the default tenant (configured by the environment), or four secret keys already | operator only. For keys: revoke_others rotates |
| 409 | cursor | POST /v1/meter/records: the batch does not start at the cursor | resend from the cursor in the answer. Meter shipper only |
| 410 | seat_lost | the seat is gone: it lapsed (no heartbeat), was kicked, or the stream was deleted | ask for a new seat. The player goes to seat-lost |
| 429 | rate_limited | public calls (admit, token, hb, leave) pass a limit per client address (a burst of 20, refilling 2 a second); or more than 6 tokens for one seat in a minute | slow down. A reconnect that never got a session reuses its token, so a retry loop is not what trips this |
| 500 | internal | an unexpected failure | retry; if it persists, tell us |
| 503 | clock_unsynchronized | the API's clock is not synchronized, so it mints no tokens: an expiry would mean nothing | retry. On your own deployment: timedatectl must say "System clock synchronized: yes" |
The limit of 6 tokens a minute per seat is the value in the code, with the
other numbers above. The per-address limit counts the address the API sees: behind a
proxy, set NEARGLASS_API_TRUST_FORWARDED=1 (Configuration).
Messages worth knowing
For invalid_request, the message starts with the field:
| Message starts | Cause |
|---|---|
name: | 1 to 100 characters |
namespace: | 1 to 8 segments of A-Za-z0-9._-, separated by /, not under control |
ingest: | at least one of rtmp, rtmps, srt, whip, sdk |
viewer_cap: | at least 1, and with queue_max at most 20000 |
ttl: | 1 to 3600 s for viewer and controller tokens; 1 to 604800 s (7 days) for publisher tokens |
turn_id: | 1 to 59 printable characters, no / or spaces |
seat: | an id this API returned (se_ and 32 hex digits) |
spend_cap_usd: | 0 or more |
webhook_url: | an http or https URL, at most 512 characters, and a public address (not loopback, private, link-local or internal names) |
from, to: | YYYY-MM-DD, from not after to; and at most 92 days a call |
body: or query: | not valid JSON, or an unknown or mistyped field |
Player
Messages in the bar
The <nearglass-player> element writes these to its status bar (the full table
is in the player reference).
| Text | State | Cause and fix |
|---|---|---|
| Set token-url, or api, stream and publishable-key. | none | the element has no way to be admitted. Set the attributes, or the descriptor property |
| The stream is full. Waiting for a seat... | waiting | the stream's viewer cap or your tenant's cap is reached. The player asks again by itself |
| In line, number N... | waiting | in the waiting line. Wait |
| Your seat was given up. Reload to watch again. | seat-lost | the seat lapsed (no heartbeat reached the API) or your backend kicked it. The player has stopped: reload the page to watch again |
| This browser cannot play this stream. / the reason | unsupported | no MoQ in this browser and no LL-HLS either. Fallback reasons |
| Cannot play: and the message | error | admission or the first descriptor failed. See below |
| Every place on the stream is taken. Waiting for one... | note | every relay refused the viewer as full (see relay refusals). The player waits and retries, with longer waits each round |
| Reconnecting... | note | the session dropped. The player reconnects by itself |
| Connecting... | note | opening a session |
| Waiting for the stream... | note | the relay has no publisher yet. The player asks again until the encoder announces |
| Paused while the tab is hidden | note | the page is hidden, so only the sound (if on) keeps running |
| Jumping to live... | note | the player dropped to the live edge |
| decoder: and a message | note | the browser's decoder reported an error. The player recovers at the next keyframe |
| Recovering (...)... | note | the LL-HLS fallback, through hls.js, hit a playback error and is recovering |
Messages in error and in thrown errors
play() goes to error with { message } when admission or the first
descriptor fails, and stops. The message has the form
<what>: HTTP <status> <code>: <message>, where <what> is admission
(/v1/play/{id}/admit), token (/v1/play/{id}/token) or token-url (your
route), and <code> and <message> are the control API's, from the table
above (a backend that flattens the error is read too). Examples: admission: HTTP 403 server_admission, token-url: HTTP 401 unauthorized.
| Message | Cause | Fix |
|---|---|---|
admission: HTTP 401 ... | the publishable key is wrong or missing | check publishable-key; it must be a pk_ key of the stream's tenant |
admission: HTTP 403 server_admission | the stream is not public | use token-url, or create the stream with admission: public |
admission: HTTP 404 not_found | no such stream for this key | check stream and the key's tenant |
admission: HTTP 402 spend_cap_reached | the spending cap is reached | raise the cap |
token-url: HTTP ... | your route answered an error | your route must pass viewer-tokens' status and body through. 409 with a state is the line, and is handled |
descriptor: not an object | the descriptor is not a JSON object | return the API's JSON unchanged |
descriptor: no token, no relays, no namespace | a required field is missing | return the API's JSON unchanged |
not hex | relay_cert_sha256 is not a hex string | return the API's JSON unchanged |
seat lost | the token call answered 410 | the player goes to seat-lost |
closed | close() was called during admission | none |
play() also logs one console warning: "nothing keeps this seat (the answer
has no play_url, and no api and stream are set); it lapses in 20 s". Your
token-url route is not passing through the API's play_url. See
Viewer tokens.
Engine errors
createPlayer reports failures through events, not exceptions. Sessions
that end are recorded with a reason (sessionInfo().ended):
| Reason | Meaning |
|---|---|
stall | no object for 1 s: resubscribed |
hidden | the silence began with the page hidden. iOS stops a background page's WebTransport and then suspends the page |
renewed | replaced by a fresh session because requests were refused, or the tab came back after a long time hidden (the renew event) |
rotated | handed over to a fresh session before a token or session limit (the rotation event) |
restart | the player restarted the session itself, for example to jump to live |
Relay refusals a client sees
A relay that checks tokens refuses a client for these reasons. The player turns each into the behaviour in the right column. The numbers are the MoQ request error codes the player reads.
| Refusal | Code | What the player does |
|---|---|---|
| not found | 4 or 16 | the relay has no publisher for the track yet (right after a relay restart, or before the encoder announces). Asks again on the same session after 100 ms, then every 250 ms |
| expired token | 5 | the session's token is past its expiry plus the watch grace. Gives up the session at once and connects a fresh one with a fresh token, waiting longer if that is refused too (to a limit of 30 s) |
| full | 0x5746 (EDGE_FULL) | the relay is saturated and keeps its viewers. The player tries the next nearest relay; with every relay full it waits (the relay's retry interval, doubling each round to 16 s) and shows the waiting state |
| any other refusal | after 2 s of refusals on a session, a fresh session and tokens, backing off to 30 s |
Failures before the first request, as the relay records them and counts them
in moq_relay_seat_refused_total{reason}:
| Reason | Cause | Fix |
|---|---|---|
scope_full ("scope full") | the scope's max_sessions is reached. It counts every role | raise max_sessions for the scope, or free a session. A seat frees one |
token_id_in_use ("token in use") | a token with this cti already has a live session | use a fresh token for a new connection. A session that ended without a close keeps its id until the idle timeout |
token_closed ("token closed") | the token's id was closed by the admin port (a kicked or lost seat) | ask for a new seat |
A relay also refuses every token while the kernel says its clock is not
synchronized, and a token issued in the relay's future (iat). A publisher
session is closed at its token's exp plus the scope's clock_skew_secs; a
viewer's at exp plus skew plus the scope's watch_grace_secs. The player
renews before then.
C library (nearglass.h)
Functions return 0 on success and a negative code on failure. Constructors
(nearglass_pub_new, nearglass_sub_new) return NULL and write a message into the
err buffer you pass (if it is not NULL).
| Constant | Value | Meaning | Returned when |
|---|---|---|---|
NEARGLASS_OK | 0 | success | |
NEARGLASS_ERR_ARG | -1 | a bad argument | a NULL handle, bad UTF-8, an unknown track number, or nearglass_pub_push with a NULL or empty access unit |
NEARGLASS_ERR_QUEUE | -2 | the frame queue is full; the frame was dropped | nearglass_pub_push. Never blocks: the encoder is faster than the link |
NEARGLASS_ERR_CLOSED | -3 | the handle is closed | any call on a handle that has shut down |
Also in nearglass.h:
| Constant | Value | Meaning |
|---|---|---|
NEARGLASS_ABI_VERSION | 2 | the interface version; nearglass_abi_version() returns the library's |
NEARGLASS_KEYFRAME | 0x1 | flag for nearglass_pub_push: the access unit is a keyframe |
NEARGLASS_FRAME_HIDDEN | 0x1 | nearglass_frame.flags: decode, do not show (an intra refresh start) |
NEARGLASS_FRAME_LATE | 0x2 | nearglass_frame.flags: later than the stale budget; decode, do not show |
NEARGLASS_EVENT_COMMAND | 1 | control event: apply the payload |
NEARGLASS_EVENT_STOP | 2 | control event: set every output to its safe state |
NEARGLASS_STOP_DEADMAN | 1 | stop reason: no valid command within the dead-man time |
NEARGLASS_STOP_RELEASED | 2 | stop reason: the driver released the dead-man control |
NEARGLASS_STOP_FLAG | 3 | stop reason: the driver sent stop |
NEARGLASS_STOP_TURN_ENDED | 4 | stop reason: the turn's deadline passed, or the device ended the turn |
NEARGLASS_STOP_DISCONNECTED | 5 | stop reason: the device lost its relay session |