Nearglass

Concepts

Streams, keys, seats and tokens, the relay and the control plane, the viewer cap, controllers, metering and the fallback.

The few ideas the API, the player and the publishers are built on. Each section ends with where it is specified, for readers who want the detail.

encoder or device --SRT/RTMP or SDK--> ingest --MoQ--> relays --MoQ--> players (seats)
                                                          |
your backend --secret key--> control API --tokens-------- +
your page    --publishable key--^         (seats, line, usage)

The pieces

PieceWhat it doesWho runs it
Publishersends one stream: an encoder over SRT, RTMP or WHIP into our ingest gateway, or your device with the SDK straight to a relayyou
Ingest gatewaytakes SRT, RTMP and WHIP from encoders and publishes them over MoQ; also makes the LL-HLS copyus (or you, self-hosted)
Relayfans one stream out to many viewers over MoQ, and admits only sessions with a valid tokenus
Playerplays the stream: <nearglass-player> in a page, @nearglass/react, or the C library in a native appyour viewers
Control APIthe control plane: streams, keys, seats and tokens, usage, spending caps, webhooks, the fleetus; your backend and pages call it

The live MoQ stream never passes through the control API (it only hands a seated viewer the LL-HLS fallback's files), and the control API never connects to a relay: the relays only check the tokens it signs, and an agent beside each relay reports to it (see Fleet and regions).

Streams

A stream is one live source and everyone watching it. You create it with POST /v1/streams and get back its id (st_ and 24 hex digits) and where to send video. Its settings:

SettingDefaultWhat it does
name(required)yours, 1 to 100 characters
modebroadcastinteractive adds controllers
ingest["rtmp", "srt"]how video arrives: rtmp, rtmps, srt, whip (from an encoder, with a stream key) or sdk (your device publishes with the SDK and a publisher token)
viewer_cap100seats: viewers watching at once (the cap)
queue_maxtwice the capplaces in the waiting line
admissionserverwho hands out seats: your backend (server) or any page with your publishable key (public)
namespacelive/<id>the stream's name on the relays; set it when a device already uses one
max_kbps0 (none)a bitrate limit, recorded with the stream but not enforced yet: ingest accepts a higher bitrate

A stream has no "start" call: it is live while video arrives. Deleting it stops admitting at once and closes every live session it has, viewers and publisher alike.

Changing a stream. PATCH /v1/streams/{id} changes name, viewer_cap, queue_max and admission; the fields you leave out stay. Nobody already watching is disturbed:

  • A cap lowered below the viewers seated keeps them all in (their tokens go on renewing) and seats nobody new until fewer are left than the new cap.
  • A raised cap seats the people in line, first in line first, as their pages next ask (every 2 s).
  • A new admission changes only how new viewers come in; seats already held keep working.

mode, ingest, namespace and max_kbps are fixed when the stream is created: to change one of them, create a new stream.

Reference: POST /v1/streams, PATCH /v1/streams/{id}.

Keys

KeyPrefixWho holds itWhat it can do
Secret keysk_your backendeverything for your account: streams, tokens, usage, settings
Publishable keypk_your web pagesask for a seat on a public stream, nothing else
Stream keystk_your encodersend video to one stream (it is in the SRT and RTMP addresses)

Secret keys are stored by us as hashes only; a lost one cannot be shown again, only replaced. A stream key is shown once, when the stream is created or its key rotated (POST /v1/streams/{id}/stream-key, which also ends the live ingest session). Your account is a tenant: its own keys, its own signing key for tokens, and its own space on the relays, so a token made for your streams is refused for anyone else's, whatever the stream is called.

Seats and tokens

A viewer watches through a seat. A seat is what the viewer cap counts, what the waiting line leads to, and what metering bills by the viewer-hour.

The relay lets a viewer in with a viewer token: a short signed token (an ES256 Common Access Token) for one stream, minted by the control API for one seat. Facts that shape how you use them:

  • A token is renewed while the viewer watches. It lives 120 s by default (ttl, at most an hour). The relay keeps a joined viewer's session past the token's expiry for a watch grace, and the player moves to a fresh token with its seat before the grace runs out, by itself (POST /v1/play/{id}/token, or your token-url with server admission). See Token renewal.
  • One token, one session. The relay allows one live session per token, so a token copied to a second browser is refused there.
  • A seat is kept by a heartbeat. The player calls POST /v1/play/{id}/hb every 5 s. A seat with no heartbeat for 20 s (60 s for a hidden tab) is freed, and the relay closes its sessions. Closing the page gives the seat up at once (/leave).
  • Limits per seat: at most 6 tokens a minute, and a seat holds its two newest tokens (a third closes the oldest's session).
  • Kicking a viewer (DELETE /v1/streams/{id}/viewers/{seat}) frees the seat and closes its sessions at once.

Publishers have tokens too: the ingest gateway gets one per stream from the API by itself; a device that publishes with the SDK (ingest: ["sdk"]) gets a publisher token from your backend (POST /v1/streams/{id}/publisher-tokens, an hour by default, up to 7 days).

Reference: viewer-tokens, publisher-tokens.

The viewer cap and the waiting room

Each stream has a cap on seats (viewer_cap). When it is full, a new viewer gets a place in line instead of a seat:

StateMeansThe player shows
inseated: a token and the relays to usethe video
waitin line, at position (from 1)"In line, number 37..."
offera seat is free for this viewer for 10 sa Join button (or joins by itself with auto-join)
fullthe line is full too"The stream is full. Waiting for a seat..." and retries

The offer exists so a tab nobody is looking at does not take a seat from someone who is. The API hands out seats up to the cap, and the relay lets in only tokens the API minted for a seat, one session each, so a page that skips the API cannot get in at all. The relay also caps your account's live sessions as a whole (Admission and caps).

Two ways to hand out seats, chosen per stream with admission:

  • server (the default): your backend calls POST /v1/streams/{id}/viewer-tokens with your secret key, after its own login, and passes the answer to the player. You decide who watches. See Mint viewer tokens on your server.
  • public: the player itself calls POST /v1/play/{id}/admit with your publishable key. Anyone with the page can take a seat (rate limited per address: a burst of 20, then 2 a second). For open events and the Quickstart.

Controllers

An interactive stream adds a channel back to the source: a driver sends commands (a joystick's state, 50 times a second by default) that reach the device through the same relays, while everyone else watches. This is the machine-control shape: one device, one driver at a time, many watchers.

  • Your backend runs the queue of drivers. For each turn it mints a controller token (POST /v1/streams/{id}/controller-tokens with a turn_id), which lets that driver watch and send commands for that turn only, on control/<namespace>/<turn_id>.
  • The device decides when a turn ends (its own deadline), and stops by itself if commands stop arriving: the driver holds a dead-man, and the player's controller sends one stop and then silence when the tab is hidden, the window loses focus or the video stalls for 500 ms.
  • Your backend ends a turn early with DELETE /v1/streams/{id}/controller-tokens/{turn_id}, which closes the driver's control session at the relay.

See Controller seats.

Metering

What we bill and you can check:

  • Viewer-hours: the time each viewer session lasts, summed per stream per day. A session under 10 s is not counted as time (a join that never saw a picture), but its bytes are.
  • GiB: the bytes the relays sent, recorded beside viewer-hours, because egress is most of the cost and the bill must be checkable against it.

The relays write one record per closed session; LL-HLS viewers are recorded by our HLS edge the same way, per seat. Days are UTC, and a session counts on the day it ended, so a viewer watching now appears once their session closes (a page moves to a new session about every 11 minutes). GET /v1/usage gives days, streams and the month so far, with MoQ and LL-HLS shown apart. A spending cap stops new viewers, never the ones already watching. See Usage and spending caps.

Fallback

The player plays over MoQ (Media over QUIC: WebTransport and WebCodecs in the browser) wherever it can, and falls back to LL-HLS (Low-Latency HLS, about 1 s behind) where it cannot:

Browser or networkHow it plays
Chrome, Edge (desktop)MoQ
Chrome for Android, Samsung InternetMoQ (H.264 decode depends on the device)
Firefox (desktop)MoQ
Safari on Mac, iPhone and iPad (every iOS browser)LL-HLS
Firefox for AndroidLL-HLS
A network that blocks UDPLL-HLS

The switch is automatic: a browser without WebTransport or WebCodecs goes to LL-HLS at once; otherwise the player gives MoQ 3 s to connect and 3 s more for a first frame, plays LL-HLS meanwhile if that runs out, and switches back to MoQ as soon as a frame arrives. The bar under the video says which: "Low latency" or "Compatibility mode, about 1 s behind". A fallback viewer takes a seat and is metered like any other.

Limits today:

  • LL-HLS exists only for streams sent from an encoder (SRT, RTMP, WHIP): a stream a device publishes with the SDK has no LL-HLS.
  • Safari and most browsers play LL-HLS natively. Where a browser does not, give the player hls.js (el.hls = Hls); it is not bundled.
  • The fallback's delay has not been measured on real phones and networks yet, and LL-HLS has not been played on an iPhone through our service yet; treat "about 1 s" as the design figure.

Reference: GET /v1/play/{id}/hls/{file}.

Fleet and regions

A region is one origin relay and its edge relays. Your publisher connects to the origin of the stream's home region; the other regions' origins pull the stream from it, and each edge pulls from its own region's origin. Viewers connect to edges only.

The API lists, the player measures. Every token answer carries a short list of relays, the least loaded edges that are up in each region. The player opens a connection to each of them at once and plays from the first to answer, which is the nearest by round trip. If that edge goes away, the player moves to the next one on the list, in another region if it has to; the relay still checks the token, so the list is a hint, never access.

Each relay host runs a small agent beside the relay. It reports the relay's health and load to the control API every few seconds and takes back what the relay needs: your account's token scope, the routes to the origin, and the sessions to close when a seat is lost or a viewer kicked. A kick or a lost seat therefore reaches every relay in every region within a heartbeat.

See Fleet and regions and Relay selection.

On this page