Play

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/player

A 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

PropBecomesWhat
api, stream, publishableKeythe attributes api, stream, publishable-keypublic admission
tokenUrltoken-urlserver admission
hlsUrlhls-urlan LL-HLS playlist, when our answer names none
modemode'latency' (default), 'smooth' or 'paced'
autoJoinauto-jointake an offered seat without Join
backgroundAudiobackground-audiosound plays on while the page is hidden; default off
descriptorthe descriptor propertyan object, or a function returning one, in place of tokenUrl
hlsthe hls propertyhls.js's Hls class
onStateChangethe statechange eventcalled with the event's detail: { state, ... }
onModeChangethe modechange eventcalled with { mode, label }
onNotethe note eventcalled with the status text
id, className, style, and any other proppassed 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:

FieldWhat
state'idle' until the first event, then the player's state (admitting, waiting, offer, connecting, live, fallback, ...; see Events)
infothe state event's detail, for example { state: 'waiting', position: 3 }
mode'moq', 'll-hls' or 'none'
labelthe mode's text, as the bar shows it
notethe 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 descriptor function 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 in useCallback. The same goes for an object that changes each render.
  • The hls prop reaches the element after it has started. The element reads hls when 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 with hls={Hls}. Until that is changed, make Hls global 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. onStateChange and the other callbacks do not restart it, and may change on every render.
  • Server rendering. Importing @nearglass/react on the server does nothing harmful: the element file only defines the element where the browser has customElements. 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:

FileWhat
app/api/watch/[id]/route.jsPOST /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.jsthe 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.

On this page