Nearglass
Reference

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).

StatusMeaning
400the request is wrong: fix it
401the key is missing or not accepted
402an account limit stops this: raise it
403the key is right but not allowed to do this
404nothing there, or it belongs to another tenant
409the request is fine but conflicts with state or a limit
410the seat is gone
429too many requests
5xxthe service, not you

Codes

StatuscodeMeaningFix
400invalid_requesta field is missing, out of range or unknown, or the body is not JSON. Unknown fields are refused. The message names the fieldread the message and correct the field. Limits are in the control API reference
401unauthorizedno 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
402spend_cap_reachedthis month's spending cap is reached, so no new viewer gets a seat. Viewers already in keep watchingraise the cap with PATCH /v1/tenant, or wait for the month to end (Usage and spending caps)
403server_admissionPOST /v1/play/{id}/admit on a stream whose admission is servertake seats from your backend with viewer-tokens, or create the stream with admission: public
403operator_onlya tenant tried to change its name or limits, or the default tenant's capask us. Only the operator sets these
404not_foundno such stream, seat, turn or tenant, or it is another tenant'scheck the id. Ids are per tenant: another tenant's stream answers 404, never 403
404no_hlsthis deployment packages no LL-HLS, or the stream takes no encoder ingest so has noneuse a stream with encoder ingest (rtmp, rtmps, srt or whip) on a deployment that has the fallback
404, 502hls_unavailablethe stream has no LL-HLS right now: not live yet, or the part is goneretry. The player does
502hls_originthe LL-HLS packager did not answerretry; if it persists, tell us
404no_fleetPOST /v1/fleet/heartbeat on an API with no fleet fileoperator only
409namespace_in_useanother stream has this namespacepick another, or leave namespace out
409too_many_streamsat most 1000 streams, or your tenant's max_streamsdelete a stream you no longer use
409too_many_placesseats plus places in line, over every stream, would pass 20000lower viewer_cap or queue_max on this stream, or on another
409tenant_viewer_capon create: viewer_cap is above your tenant's. On viewer-tokens and admit: every seat of your tenant's streams is takenlower viewer_cap, free seats, or ask us to raise the limit. The player waits and asks again every 10 s
409no_stream_keythe 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 ituse publisher-tokens for an SDK publisher
409not_interactivecontroller tokens on a stream whose mode is broadcastcreate the stream with mode: interactive
409turn_existsthis turn_id already has a tokenuse a new turn_id, or end the turn first
409above_max_spend_capspend_cap_usd is above the most your tenant may setset a lower cap, or ask us
409too_many_tenants, default_tenant, too_many_keysoperator calls: the tenant limit, the default tenant (configured by the environment), or four secret keys alreadyoperator only. For keys: revoke_others rotates
409cursorPOST /v1/meter/records: the batch does not start at the cursorresend from the cursor in the answer. Meter shipper only
410seat_lostthe seat is gone: it lapsed (no heartbeat), was kicked, or the stream was deletedask for a new seat. The player goes to seat-lost
429rate_limitedpublic 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 minuteslow down. A reconnect that never got a session reuses its token, so a retry loop is not what trips this
500internalan unexpected failureretry; if it persists, tell us
503clock_unsynchronizedthe API's clock is not synchronized, so it mints no tokens: an expiry would mean nothingretry. 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 startsCause
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).

TextStateCause and fix
Set token-url, or api, stream and publishable-key.nonethe element has no way to be admitted. Set the attributes, or the descriptor property
The stream is full. Waiting for a seat...waitingthe stream's viewer cap or your tenant's cap is reached. The player asks again by itself
In line, number N...waitingin the waiting line. Wait
Your seat was given up. Reload to watch again.seat-lostthe 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 reasonunsupportedno MoQ in this browser and no LL-HLS either. Fallback reasons
Cannot play: and the messageerroradmission or the first descriptor failed. See below
Every place on the stream is taken. Waiting for one...noteevery relay refused the viewer as full (see relay refusals). The player waits and retries, with longer waits each round
Reconnecting...notethe session dropped. The player reconnects by itself
Connecting...noteopening a session
Waiting for the stream...notethe relay has no publisher yet. The player asks again until the encoder announces
Paused while the tab is hiddennotethe page is hidden, so only the sound (if on) keeps running
Jumping to live...notethe player dropped to the live edge
decoder: and a messagenotethe browser's decoder reported an error. The player recovers at the next keyframe
Recovering (...)...notethe 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.

MessageCauseFix
admission: HTTP 401 ...the publishable key is wrong or missingcheck publishable-key; it must be a pk_ key of the stream's tenant
admission: HTTP 403 server_admissionthe stream is not publicuse token-url, or create the stream with admission: public
admission: HTTP 404 not_foundno such stream for this keycheck stream and the key's tenant
admission: HTTP 402 spend_cap_reachedthe spending cap is reachedraise the cap
token-url: HTTP ...your route answered an erroryour route must pass viewer-tokens' status and body through. 409 with a state is the line, and is handled
descriptor: not an objectthe descriptor is not a JSON objectreturn the API's JSON unchanged
descriptor: no token, no relays, no namespacea required field is missingreturn the API's JSON unchanged
not hexrelay_cert_sha256 is not a hex stringreturn the API's JSON unchanged
seat lostthe token call answered 410the player goes to seat-lost
closedclose() was called during admissionnone

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):

ReasonMeaning
stallno object for 1 s: resubscribed
hiddenthe silence began with the page hidden. iOS stops a background page's WebTransport and then suspends the page
renewedreplaced by a fresh session because requests were refused, or the tab came back after a long time hidden (the renew event)
rotatedhanded over to a fresh session before a token or session limit (the rotation event)
restartthe 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.

RefusalCodeWhat the player does
not found4 or 16the 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 token5the 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)
full0x5746 (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 refusalafter 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}:

ReasonCauseFix
scope_full ("scope full")the scope's max_sessions is reached. It counts every roleraise 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 sessionuse 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).

ConstantValueMeaningReturned when
NEARGLASS_OK0success
NEARGLASS_ERR_ARG-1a bad argumenta NULL handle, bad UTF-8, an unknown track number, or nearglass_pub_push with a NULL or empty access unit
NEARGLASS_ERR_QUEUE-2the frame queue is full; the frame was droppednearglass_pub_push. Never blocks: the encoder is faster than the link
NEARGLASS_ERR_CLOSED-3the handle is closedany call on a handle that has shut down

Also in nearglass.h:

ConstantValueMeaning
NEARGLASS_ABI_VERSION2the interface version; nearglass_abi_version() returns the library's
NEARGLASS_KEYFRAME0x1flag for nearglass_pub_push: the access unit is a keyframe
NEARGLASS_FRAME_HIDDEN0x1nearglass_frame.flags: decode, do not show (an intra refresh start)
NEARGLASS_FRAME_LATE0x2nearglass_frame.flags: later than the stale budget; decode, do not show
NEARGLASS_EVENT_COMMAND1control event: apply the payload
NEARGLASS_EVENT_STOP2control event: set every output to its safe state
NEARGLASS_STOP_DEADMAN1stop reason: no valid command within the dead-man time
NEARGLASS_STOP_RELEASED2stop reason: the driver released the dead-man control
NEARGLASS_STOP_FLAG3stop reason: the driver sent stop
NEARGLASS_STOP_TURN_ENDED4stop reason: the turn's deadline passed, or the device ended the turn
NEARGLASS_STOP_DISCONNECTED5stop reason: the device lost its relay session

On this page