LL-HLS fallback
When the player switches from MoQ to LL-HLS, how it switches back, what plays it, and the limits.
The player plays over MoQ (WebTransport and WebCodecs) wherever it can. Where
it cannot, it plays the same stream as LL-HLS (Low-Latency HLS), a few
parts of a second at a time over ordinary HTTPS, so a viewer sees a picture
instead of an error. You do nothing for this: it is built into
<nearglass-player> and play(). This page is for knowing what
happens, so your page can show it honestly and you can plan around the limits.
The bar under the video says which mode is playing: Low latency for MoQ,
Compatibility mode, about 1 s behind for LL-HLS. Your own UI can read the
same from the modechange event.
When it switches
There are two kinds of switch.
When a browser cannot play at all
Checked before the player connects to anything. The browser goes to LL-HLS at once and stays there:
reason | Why |
|---|---|
no-webtransport | the browser has no WebTransport |
no-webcodecs | no WebCodecs video decoder (Firefox for Android) |
no-h264 | the decoder cannot play H.264 at all |
webkit-flow-control | the browser is Safari, or any browser on an iPhone or iPad (all use WebKit), whose WebTransport stops delivering data within minutes (Phones, iPhone and Safari) |
A browser that fails the check without a playlist to fall back to gets the
state unsupported, and the bar shows the reason in a sentence, for example
"This browser cannot play Nearglass yet. It has no WebTransport. Use Chrome or
Edge on a computer, or Chrome on Android." The sentences are the player's
FALLBACK_MESSAGES, and FALLBACK_REASONS and SUPPORTED_BROWSERS hold
their parts, if your page wants to show them its own way.
When MoQ does not start
A browser that could play MoQ is given a short time to start:
- no relay session within 3 s of starting (
connect-timeout), for example a network that blocks UDP, or - a session but no video frame within 3 s more (
no-keyframe).
Then the player starts LL-HLS, and keeps trying MoQ in the background. The
moment MoQ shows a live frame, the player stops LL-HLS and switches back
(the bar returns to "Low latency", modechange fires). Both numbers are
exported (CONNECT_DEADLINE_MS, KEYFRAME_DEADLINE_MS from @nearglass/player).
These deadlines apply to the start only. Once MoQ has played, a later stall is the engine's to recover (it resubscribes, and reconnects with a fresh session); the player does not drop to LL-HLS because of it.
What your page sees
For a switch, in order:
statechangewithstate: 'fallback'andreason(andpermanent: truefor the first kind above);modechangewithmode: 'll-hls'; the bar's mode label changes;- the canvas is hidden and a
<video>(thefallbackpart) shows the picture.
When MoQ takes over again: statechange live, modechange with
mode: 'moq'. If nothing can play, modechange with mode: 'none' and
statechange unsupported with a message.
The fallback's sound is the stream's AAC track in the <video>. The Sound
button is always shown in this mode. Like MoQ sound, it starts off and a
hidden page turns it off (Sound).
Where the playlist comes from
The playlist's address is the descriptor's hls field, which our API fills in
for a stream that has one. Or you give it: the hls-url attribute (the
hlsUrl prop) names a playlist when the descriptor does not. The player adds
the viewer's seat to the address (?seat=), because the playlist is served
only to a held seat and is metered against it, like MoQ
(GET /v1/play/{id}/hls/{file}).
A seat on LL-HLS is a seat like any other: it counts against the viewer cap, takes part in the waiting line, is heartbeated by the player, and is metered as viewer time.
What plays it
| Browser | Plays LL-HLS with |
|---|---|
| Safari, every browser on iPhone and iPad | the browser's own HLS |
a browser where video.canPlayType('application/vnd.apple.mpegurl') answers yes | the browser's own HLS |
| anything else with Media Source Extensions (Firefox for Android) | hls.js, if the page gives it to the player |
| none of these | nothing: unsupported, and the bar adds "This browser cannot play the compatibility stream either." |
hls.js
hls.js is not bundled. Load it with a script tag before the player, and the
player finds globalThis.Hls. Or set the class on the element before it is
added to the page (Browsers without native HLS).
The player reads it when it starts: a class set on an element that is already
running is not seen until the player restarts, and a browser that needs it
then gets unsupported.
The player sets hls.js to low-latency mode, with a short live-sync target and
catch-up up to 1.5 times real time, and widens the target a little if the
player is seeking in a loop. After an error the browser's own HLS is reloaded
after 1 s, and hls.js recovers by itself where it can ("Reconnecting..." and
"Recovering (...)" are the note event's text).
hls.js with React
In React the class goes in the hls prop, which sets it after the element has
started. Until that is changed, load hls.js with a script tag, or assign it to
globalThis.Hls before the first render. See
Things to know.
Limits
- Only streams sent from an encoder have LL-HLS (SRT, RTMP, WHIP, with a
stream key). A stream a device publishes with the SDK (
"ingest":["sdk"]) has none: the descriptor has nohls, and a browser that needs the fallback getsunsupported. Give anhls-urlonly if you package it yourself. - It is behind MoQ, and not by a fixed amount. The player cannot make LL-HLS as fast as MoQ. For an auction or a bet, show the mode (the bar does) and decide whether a compatibility-mode viewer may act.
- Driving needs MoQ. A controller seat (Controller seats) needs WebTransport; there is no LL-HLS path for sending commands.
- Delay figures are not measured on real phones and networks; see the placeholder at the top.
- The service has to package it. LL-HLS is produced by the service from
the encoder's stream. A self-hosted setup with no packager has none, unless
you supply an
hls-url.
Trying it
To see the fallback in a desktop browser without a phone, load the page with WebTransport removed before the player loads:
<script>Object.defineProperty(window, 'WebTransport', { value: undefined });</script>
<script type="module" src="https://cdn.nearglass.com/player@0.1.0-rc.1/src/element.js"></script>
<nearglass-player api="https://api.nearglass.com" stream="st_..." publishable-key="pk_..." auto-join></nearglass-player>The statechange events then give fallback with reason: 'no-webtransport'
(or unsupported if the stream has no LL-HLS).
What was run
In headless Chrome against the packaged player, the control API and a relay on one host, with a stand-in for hls.js and an empty playlist (this host has no LL-HLS packager, so no real LL-HLS picture was played):
| Case | Result |
|---|---|
WebTransport removed, hls-url given, hls.js available before the start | state fallback with reason: 'no-webtransport' and permanent: true within a fraction of a second; mode ll-hls; the bar reads "Compatibility mode, about 1 s behind"; the canvas hidden, the video shown; the Sound button shown; hls.js loaded the playlist with ?seat= added |
the same, el.hls set 300 ms after the element started | state unsupported; the bar shows the no-WebTransport sentence and "This browser cannot play the compatibility stream either." |
the same, el.hls set before the element was added | works as the first case |
| WebTransport removed, no playlist | unsupported; the bar shows "This browser cannot play Nearglass yet. It has no WebTransport. Use Chrome or Edge on a computer, or Chrome on Android." |
| WebTransport removed, a playlist, neither native HLS nor hls.js | unsupported, with the extra sentence |
| WebKit simulated (its WebTransport member and an Apple vendor string), a playlist, native HLS faked | fallback with reason: 'webkit-flow-control'; engine native; with an empty playlist the note "Reconnecting..." came once a second (the 1 s reload) |
| WebKit simulated, no playlist | unsupported; the bar shows the WebKit sentence |
| connected to a stream with no publisher, a playlist given | joining, the note "Waiting for the stream...", then fallback with reason: 'no-keyframe' and permanent: false 3.0 s after the start; when the publisher started, live and mode moq, hls.js destroyed, the canvas shown again |
Phones and Safari
How the player behaves on iPhone, Safari and Android, why Apple devices use the LL-HLS fallback, and what to expect from sound and background tabs.
Use Nearglass with AI and vision models
Live frames into Python as numpy arrays, frames out of Python, and model results tied to the exact frame they came from.