Skip to content

Managed player

Mode A: the SDK owns your <video> element completely, including hls.js, low-latency tuning, DRM/EME, retries, live pickup, stats and a branded, skinnable control bar. This is the mode the hosted embed runs internally.

Create a player

ts
import { createPlayer } from '@oddin-gg/havik-player';

const player = await createPlayer({
  video: document.querySelector('video')!,
  baseUrl: 'https://feed-dev.oddin-video.gg',
  matchUrn: 'od:match:1234',
  credential: { apiKey: 'pk_test_…' },
  autoplay: true,
  muted: true, // required for reliable autoplay
  waitForLive: true,
});

createPlayer resolves once the stream is attached, or armed if waitForLive is set and the match is upcoming. Terminal errors (NOT_FOUND, GONE, UNAUTHORIZED) reject the promise; the live wait itself never blocks it. The full options table is in the API reference. The defaults are tuned for Oddin's streams, and most integrations set nothing beyond the required four options plus muted.

Events

ts
const off = player.on('statechange', (s) => console.log(s));
player.on('waiting', (w) => console.log(`live in ~${Math.round(w.retryInMs / 1000)}s`));
player.on('stats', (s) => console.log(s.latencySeconds, s.bandwidthKbps));
player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
player.on('autoplayblocked', () => showTapToPlay());
// off() unsubscribes; player.destroy() tears everything down.

States: idle | waiting | loading | playing | buffering | paused | ended | error. The events table lists every event and payload. Two of them usually need UI of their own:

  • autoplayblocked: the browser refused autoplay. Show a tap-to-play affordance and call player.play() from the gesture.
  • ended: the live stream finished. The player stops cleanly and (with the default controls) shows a branded "This stream has ended" card instead of a frozen frame.

Live pickup (waitForLive)

A live match isn't playable until the stream actually starts. With waitForLive, the player arms and attaches the instant it goes live, with no user action and no reload. By default this rides a push channel (SSE), falling back to polling (425 TOO_EARLY + Retry-After) if the channel is unavailable:

ts
waitForLive: {
  timeoutMs: 30 * 60_000, // overall arm budget (default: unbounded)
  ceilMs: 30_000,         // cap far-from-kickoff backoff
  onState: (s) => console.log(s.phase, s.retryInMs),
}

End of stream is detected automatically: the player fires ended instead of sitting on a dead manifest.

Low latency

Playback targets ~2 s behind the live edge. A drift ceiling (~12 s) forces catch-up after a stall. An over-seek clamp makes "drag to the right end" mean go live, instead of landing on the bleeding edge. ABR starts on a sustainable rendition (~360p seed) and ramps up within seconds. Expect ~4–5 s glass-to-glass on the default configuration.

If you do need to tune, liveLatencyTarget is the only latency setting most integrations touch; everything else derives from it. startLevel and maxBitrate bound ABR, and hlsConfig passes options straight through to hls.js. Details: Low-latency & ABR.

Controls

The default control bar is branded and covers seek, play/pause, volume, the live badge and go-live, quality/audio/subtitle menus, PiP and fullscreen. It also covers buffering and error/retry overlays, keyboard shortcuts and screen-reader support. Options:

ts
controls: 'custom' | 'native' | 'none'; // default 'custom'

'native' uses the browser's <video controls>; 'none' renders nothing and you drive the player API yourself. Skinning the custom bar with a theme object; see Controls & theming.

Long sessions

The player refreshes the DRM license URL every 8 minutes (licenseRefreshMs), so multi-hour sessions never die on an expired license token. reload() re-resolves playback in place if you need a hard refresh. retry() recovers from fatal errors (re-resolve and re-attach), and the default error card wires it to a Retry button.

Cleanup

ts
player.destroy();

Tears down hls.js, detaches EME, clears listeners, and releases the element. The instance is inert afterwards. Do this on unmount: a leaked player keeps a media pipeline alive.

ISC licensed. Bundles hls.js (Apache-2.0).