Nearglass
Play

Web player

The <nearglass-player> element in a plain page, with its attributes, events, styling, sound and server admission.

@nearglass/player is the browser player: plain ES modules, no dependencies, no build step. Its <nearglass-player> element is the whole player in one tag (admission, the waiting line, heartbeats, relay choice, the LL-HLS fallback), and works the same in plain HTML, Vue, Svelte and Angular. React has a small wrapper.

@nearglass/player and @nearglass/react are licensed under Apache-2.0, so a page that embeds the player raises no licensing question for your web app. The rest of the SDK is AGPL-3.0-or-later or commercial.

Plain HTML

<script type="module" src="https://cdn.nearglass.com/player@0.1.0-rc.1/src/element.js"></script>

<!-- Public admission: anyone with the page takes a seat -->
<nearglass-player api="https://api.nearglass.com" stream="st_..." publishable-key="pk_..."></nearglass-player>

With a bundler: npm install @nearglass/player, then import '@nearglass/player/element.js'; (or import '@nearglass/player';, which defines the element too). A page that loads the player from another origin, as from a CDN, works: the player starts its receive worker through a small script on the page's own origin.

The element needs one of two ways to get a seat. With api, stream and publishable-key it asks our API itself (public admission, for a stream created with "admission":"public"). With token-url it asks your backend (server admission, below). With neither, the bar says "Set token-url, or api, stream and publishable-key."

Attributes

AttributeWhat
apiour API's address (https://api.nearglass.com)
streamthe stream id (st_...)
publishable-keyyour pk_...: with api and stream, public admission
token-urlserver admission: your backend's route, POSTed { seat, join } for each viewer token (below)
auto-jointake an offered seat without the viewer pressing Join (default: the viewer presses Join, so a tab nobody watches holds no seat)
hls-urlan LL-HLS playlist, when our answer names none (the fallback)
modelatency (default), smooth or paced: how frames are put on screen. latency shows the newest frame at once. smooth holds frames briefly to even out a bunching link. paced is smooth that shows every frame in order, where it can
background-audiosound plays on while the page is hidden (below); without it the sound turns off

Changing an attribute (other than background-audio) restarts the player, and so takes a new seat. Removing the element from the page closes it and gives the seat up; moving it in the DOM is a remove and an add, so it restarts.

Properties and methods

NameWhat
hlshls.js's Hls class, for LL-HLS in browsers with no native HLS. Default: globalThis.Hls if the page loaded hls.js with a script tag. The player reads it when it starts, so set it before the element is added to the page (below)
backgroundAudiothe background-audio attribute, as a boolean
descriptoran object, or a function returning one (or a promise of one), in place of token-url: the API's descriptor JSON, for a page that gets it some other way (below). Setting it restarts the player
controllerread only: play()'s controller, or null before the player starts (lower level). el.controller.stats() answers the state, mode, seat, relay, region, round trips and the engine's counters
join()take the seat offered
setMuted(bool)the sound off or on; returns whether it took. Turning it on has to come from a click or tap

Events

Events fire on the element, with detail:

Eventdetail
statechange{ state, ...info }. States: admitting, waiting (position, or full: true when every place is taken), offer (seconds), connecting, joining, live, stalled, fallback (reason), seat-lost, unsupported (message), error (message), closed
modechange{ mode, label }: mode is moq, ll-hls or none; label is the text the bar shows (Low latency, or the compatibility label)
notea status line (a string), such as "Reconnecting..."
audiochange{ state, reason }, the sound's state (below)
const el = document.querySelector('nearglass-player');
el.addEventListener('statechange', (e) => console.log(e.detail.state));
el.addEventListener('modechange', (e) => console.log(e.detail.label));

The bar's own text, for each state:

StateThe bar says
admittingGetting a seat...
waitingIn line, number 37... (or "In line..." with no number); with every place taken: The stream is full. Waiting for a seat...
offerA seat is free. Join within 10 s. (the number of seconds is the offer's own)
connectingConnecting...
seat-lostYour seat was given up. Reload to watch again.
unsupportedthe message from the player: This browser cannot play this stream., or the reason (fallback)
errorCannot play: followed by the cause, for example admission: HTTP 401
live, fallback, closednothing; the mode label is shown instead

Styling

The element draws the video and a bar under it (the mode, the status, the Join and Sound buttons); nothing is drawn over the picture. It takes the page's font. Colours and shape:

nearglass-player {
  --nearglass-player-bar-bg: #111;
  --nearglass-player-bar-fg: #eee;
  --nearglass-player-accent: #e5484d;
  --nearglass-player-radius: 0;
}
nearglass-player::part(bar) { font-size: 14px; }

Parts: video, canvas, fallback, bar, mode, status, join, sound. The video box is 16:9 and the picture is fitted inside it; size the element with CSS like any block. The hidden attribute hides the element (the player keeps running).

Sound

Browsers start video muted. The Sound button appears when the stream has audio (in the compatibility mode it is always there); el.setMuted(false) from your own control works after a click. Streams carry AAC-LC or Opus, and a viewer who stays muted costs no audio bytes.

In the background. By default, hiding the page turns the sound off, as if the viewer had pressed Sound. That covers another tab in front, the browser minimised, and a phone back on its home screen or locked. While the sound is off the player downloads and decodes no audio. It stays off when the page comes back, and the viewer taps Sound to hear it again. On an iPhone that tap is the user gesture the browser needs before it plays sound. The element's Sound button shows the sound off by itself.

Without this, a phone would keep playing in the background. The player asks iOS for the playback audio session so that sound plays with the silent switch on, and that session also lets a page play in the background.

With your own controls, listen for the sound's state and show your control off when the player turns the sound off:

el.addEventListener('audiochange', (e) => {
  // e.detail: { state, reason }. state is 'muted', 'starting', 'playing' or
  // 'unavailable'. 'muted' with reason 'hidden' (the page was hidden) or
  // 'no-output' (a start that never played: tap again)
  if (e.detail.state === 'muted') mySoundButton.setAttribute('aria-pressed', 'false');
});

For a page people listen to in the background (a radio-style stream, a talk), turn it on:

<nearglass-player token-url="/api/watch/st_..." background-audio></nearglass-player>

el.backgroundAudio = true does the same and does not restart the player. Your backend can also set it per viewer: add "background_audio": true to the descriptor answer your token-url route returns (it is not a field our API sets). The page's attribute wins over the descriptor; with neither it is off. The compatibility mode follows the same rule.

The video keeps its own hidden-tab rules either way: after 30 s hidden its download stops, and after 5 minutes the session closes, unless sound is playing in the background. A hidden tab's seat lives 60 s without a heartbeat instead of 20 s, because browsers slow a hidden page's timers. More on phones: Phones, iPhone and Safari.

Server admission

For viewers who sign in to your site, create the stream with "admission":"server" (the default) and let your backend hand out the seats: it checks the user, then asks our API for a seat and a token with your secret key. The page needs only your route:

<nearglass-player token-url="/api/watch/st_..."></nearglass-player>

The player POSTs { seat, join } to token-url (with the page's cookies) for each token it needs, passing back the seat it was given (so a reconnect keeps the viewer's seat), waits in line on a 409 with the same waiting text and Join button as public admission, heartbeats the seat while it plays, and gives it up when the element is removed or the page closes. What your route receives and answers is in Mint viewer tokens on your server.

A descriptor property (an object, or a function returning the API's descriptor JSON) takes the place of token-url when your page gets descriptors some other way; the player then still heartbeats the seat at the descriptor's play_url, but the line and passing the seat back are your function's to do. The function is called again for every fresh token, so it must be able to return a new one each time.

Browsers without native HLS

Safari and every iPhone browser play the LL-HLS fallback natively. For others (Firefox for Android), give the player hls.js:

import Hls from 'hls.js';
import '@nearglass/player/element.js';

const el = document.createElement('nearglass-player');
el.hls = Hls;                                  // before the element is added
el.setAttribute('token-url', '/api/watch/st_...');
document.body.append(el);

The player takes hls when it starts, which is when the element is added to the page. An hls set afterwards (a line that runs after the markup has loaded) is not seen until the player restarts, and a browser that needs the fallback then shows "This browser cannot play the compatibility stream either." If the element is already in your HTML, load hls.js with a script tag before the player: the player finds globalThis.Hls without any line of code.

hls.js is an optional peer dependency (^1.5.0), not bundled. More in the fallback.

Lower level

play() (@nearglass/player/play.js) is the element without the element: you give it a canvas and a <video> and it does admission, heartbeats, relays and the fallback, and returns a controller (on, join, setMuted, setBackgroundAudio, stats, close, and the properties state, mode, seat). It has events the element does not forward, for example relay (url, region, rttMs: the relay connected to). createPlayer() (@nearglass/player/player.js) is the playback engine alone, for a self-hosted relay and your own tokens. Their options are in the player's reference, docs/api/JS-API.md, and its types in types/.

What was run

Run in headless Chrome against the packaged player (the npm tarball, served as a CDN from another origin), the control API and a relay on one host, for this page:

  • public admission with auto-join: the states admitting, joining, live in order, the bar reading "Low latency", the canvas shown and the fallback video hidden;
  • token-url alone, through a small route that calls viewer-tokens: live, held for 27 s (longer than the seat's 20 s timeout) with one token-url call, and no seat left behind when the page closed;
  • a changed attribute (mode) restarts the player on a new seat;
  • a stream with one seat: a second page showed "In line, number 1...", then "A seat is free. Join within 10 s." with a Join button when the first page closed, and was live after Join;
  • the descriptor property: a new function set a second time closed the player and started it again;
  • the fallback cases on the fallback page.

Also in the repository's smoke test (tools/smoke.sh, the samples): the public page loaded from another origin, and the server-admission page held past the seat's 20 s timeout.

On this page