Play

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:

reasonWhy
no-webtransportthe browser has no WebTransport
no-webcodecsno WebCodecs video decoder (Firefox for Android)
no-h264the decoder cannot play H.264 at all
webkit-flow-controlthe 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:

  1. no relay session within 3 s of starting (connect-timeout), for example a network that blocks UDP, or
  2. 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:

  • statechange with state: 'fallback' and reason (and permanent: true for the first kind above);
  • modechange with mode: 'll-hls'; the bar's mode label changes;
  • the canvas is hidden and a <video> (the fallback part) 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

BrowserPlays LL-HLS with
Safari, every browser on iPhone and iPadthe browser's own HLS
a browser where video.canPlayType('application/vnd.apple.mpegurl') answers yesthe 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 thesenothing: 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 no hls, and a browser that needs the fallback gets unsupported. Give an hls-url only 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):

CaseResult
WebTransport removed, hls-url given, hls.js available before the startstate 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 startedstate 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 addedworks as the first case
WebTransport removed, no playlistunsupported; 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.jsunsupported, with the extra sentence
WebKit simulated (its WebTransport member and an Apple vendor string), a playlist, native HLS fakedfallback 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 playlistunsupported; the bar shows the WebKit sentence
connected to a stream with no publisher, a playlist givenjoining, 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

On this page