React and Next.js
The NearglassPlayer component and useNearglassPlayer hook from @nearglass/react, and a Next.js page with its token route.
@nearglass/react is a hundred lines over the <nearglass-player> element:
the component renders the element, turns props into its attributes and its
events into callbacks, and a hook gives you the player's state to draw your
own UI. Everything the element does (admission, the waiting line,
heartbeats, relay choice, the fallback) is the element's.
npm install @nearglass/react @nearglass/playerA page that signs viewers in
import { useRef } from 'react';
import Hls from 'hls.js'; // optional: LL-HLS where the browser has no native HLS
import { NearglassPlayer, useNearglassPlayer } from '@nearglass/react';
export function Watch({ streamId }) {
const ref = useRef(null);
const { state, label } = useNearglassPlayer(ref);
return (
<>
<NearglassPlayer ref={ref} tokenUrl={`/api/watch/${streamId}`} hls={Hls} />
<p aria-live="polite">{label} {state}</p>
</>
);
}(About the hls prop, see Things to know: it is applied
late today.)
tokenUrl is your backend's route (server admission): the player passes its
seat back on every call, waits in line on a 409 and keeps the seat by itself.
What the route receives and answers:
Mint viewer tokens on your server.
For a public stream, use the publishable key instead of the route:
<NearglassPlayer api="https://api.nearglass.com" stream={streamId} publishableKey="pk_..." autoJoin />Props
| Prop | Becomes | What |
|---|---|---|
api, stream, publishableKey | the attributes api, stream, publishable-key | public admission |
tokenUrl | token-url | server admission |
hlsUrl | hls-url | an LL-HLS playlist, when our answer names none |
mode | mode | 'latency' (default), 'smooth' or 'paced' |
autoJoin | auto-join | take an offered seat without Join |
backgroundAudio | background-audio | sound plays on while the page is hidden; default off |
descriptor | the descriptor property | an object, or a function returning one, in place of tokenUrl |
hls | the hls property | hls.js's Hls class |
onStateChange | the statechange event | called with the event's detail: { state, ... } |
onModeChange | the modechange event | called with { mode, label } |
onNote | the note event | called with the status text |
id, className, style, and any other prop | passed to the element |
The ref is the element itself: ref.current.join(), .setMuted(bool),
.controller. Style it as in Styling (the
--nearglass-player-* custom properties and ::part()), from a stylesheet or
style.
There is no onAudioChange prop. For the sound's state
(Sound), listen on the ref:
useEffect(() => {
const el = ref.current;
const on = (e) => setSoundOn(e.detail.state !== 'muted');
el.addEventListener('audiochange', on);
return () => el.removeEventListener('audiochange', on);
}, []);The hook
useNearglassPlayer(ref) follows the element and returns, re-rendering on each
change:
| Field | What |
|---|---|
state | 'idle' until the first event, then the player's state (admitting, waiting, offer, connecting, live, fallback, ...; see Events) |
info | the state event's detail, for example { state: 'waiting', position: 3 } |
mode | 'moq', 'll-hls' or 'none' |
label | the mode's text, as the bar shows it |
note | the latest status line |
Call it in the component that holds the ref, as above. It reads the ref in an effect, so the element must be rendered in the same component or a child of it.
Things to know
- A
descriptorfunction must keep its identity. Setting the property restarts the player, and a function written inline in the JSX is a new function on every render. Define it outside the component, or wrap it inuseCallback. The same goes for an object that changes each render. - The
hlsprop reaches the element after it has started. The element readshlswhen it starts, and a class set later is not seen until the player restarts (checked in a browser with the element, not through React). So a browser that needs hls.js may get "cannot play" even withhls={Hls}. Until that is changed, makeHlsglobal before the first render (import Hls from 'hls.js'; globalThis.Hls = Hls;at the top of your module), which the element finds when it starts. See the fallback. - A changed prop restarts the player, as a changed attribute does, and a
restart takes a new seat.
onStateChangeand the other callbacks do not restart it, and may change on every render. - Server rendering. Importing
@nearglass/reacton the server does nothing harmful: the element file only defines the element where the browser hascustomElements. The component renders an unknown tag on the server and the element wakes up in the browser. In Next.js, put the component in a file marked'use client'.
Next.js
The react-next sample is the whole shape, three small files:
| File | What |
|---|---|
app/api/watch/[id]/route.js | POST /api/watch/<stream>, the player's tokenUrl: checks the viewer, asks the control API for a seat with the secret key (the page's seat and join passed on), answers the API's status and body |
lib/watch.js | the call itself, in plain fetch, with no framework |
app/watch/[id]/page.jsx | /watch/<stream>: a client component with <NearglassPlayer tokenUrl=...> |
'use client';
import { use, useRef } from 'react';
import { NearglassPlayer, useNearglassPlayer } from '@nearglass/react';
export default function Watch({ params }) {
const { id } = use(params);
const ref = useRef(null);
const { state, label } = useNearglassPlayer(ref);
return (
<main>
<NearglassPlayer ref={ref} tokenUrl={`/api/watch/${id}`} />
<p aria-live="polite">{label} {state}</p>
</main>
);
}npm install
NEARGLASS_API=https://api.nearglass.com NEARGLASS_SECRET_KEY=sk_... npm run dev
# open http://localhost:3000/watch/st_...The secret key stays in the route, in an environment variable; the page gets
only the descriptor. The sample's currentUser is a stand-in that accepts
everyone: replace it with your own sign-in check before you use it. The
sample uses Next.js 15 (next ^15.0.0) and React 19.
What was run
The wrapper's own tests (npm test in js/react) and the sample's (npm test
in samples/react-next: the route's logic against a stand-in API) pass. The
route's call was also run against the real control API by tools/smoke.sh:
it seats a viewer, keeps the seat when the page passes it back, queues the
next viewer past the cap, and the token it returns plays.