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
| Attribute | What |
|---|---|
api | our API's address (https://api.nearglass.com) |
stream | the stream id (st_...) |
publishable-key | your pk_...: with api and stream, public admission |
token-url | server admission: your backend's route, POSTed { seat, join } for each viewer token (below) |
auto-join | take an offered seat without the viewer pressing Join (default: the viewer presses Join, so a tab nobody watches holds no seat) |
hls-url | an LL-HLS playlist, when our answer names none (the fallback) |
mode | latency (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-audio | sound 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
| Name | What |
|---|---|
hls | hls.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) |
backgroundAudio | the background-audio attribute, as a boolean |
descriptor | an 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 |
controller | read 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:
| Event | detail |
|---|---|
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) |
note | a 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:
| State | The bar says |
|---|---|
admitting | Getting a seat... |
waiting | In line, number 37... (or "In line..." with no number); with every place taken: The stream is full. Waiting for a seat... |
offer | A seat is free. Join within 10 s. (the number of seconds is the offer's own) |
connecting | Connecting... |
seat-lost | Your seat was given up. Reload to watch again. |
unsupported | the message from the player: This browser cannot play this stream., or the reason (fallback) |
error | Cannot play: followed by the cause, for example admission: HTTP 401 |
live, fallback, closed | nothing; 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 statesadmitting,joining,livein order, the bar reading "Low latency", the canvas shown and the fallback video hidden; token-urlalone, through a small route that callsviewer-tokens: live, held for 27 s (longer than the seat's 20 s timeout) with onetoken-urlcall, 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
descriptorproperty: 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.