Skip to content

Bring your own player

Mode B: you keep your player stack — hls.js, Shaka, or a wrapper you already ship — and the SDK does the Havik-specific work: authenticated stream resolution, DRM license wiring, and live-state discovery.

Resolve a stream

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

const cred = { apiKey: 'pk_test_…' };

const stream = await resolveStream({
  baseUrl: 'https://feed-dev.oddin-video.gg',
  matchUrn: 'od:match:1234',
  credential: cred,
  waitForLive: true,
});

// stream = { manifestUrl, drmEnabled, drm: { widevine: { licenseUrl } }, … }

Feed stream.manifestUrl to your player. For DRM streams, wire the license request yourself:

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

const headers = licenseRequestHeaders(stream, cred);
// { 'x-api-key', 'X-Match-Urn', 'X-Device-Id', 'Content-Type' }

With hls.js, attach them in licenseXhrSetupnot xhrSetup; EME license requests route through licenseXhrSetup:

ts
const hls = new Hls({
  emeEnabled: true,
  drmSystems: { 'com.widevine.alpha': { licenseUrl: stream.drm.widevine.licenseUrl } },
  licenseXhrSetup: (xhr) => {
    for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
  },
});

The one DRM rule

POST to drm.widevine.licenseUrl verbatim — its signed query string is the token. Never rewrite or normalize the URL, and use the same api-key on playback and license requests, or the license request 403s.

Live-edge behavior is yours

The managed player's latency target, drift ceiling, and over-seek clamp don't come with Mode B — your player owns seeking. In particular, clamp seeks that land closer to live than your sync target, or a scrub to the far right strands playback on the bleeding edge where the newest parts aren't reliably published yet. See Low-latency & ABR for what the managed mode does, as a spec to replicate.

Discovery: catalog & status

There is no push channel for the catalog — poll it:

ts
import { fetchCatalog, watchStatus, subscribeLiveState } from '@oddin-gg/havik-player';

// tournaments → matches (metadata only)
const catalog = await fetchCatalog({ baseUrl, credential: cred });

// poll one match's status
const watcher = watchStatus({
  baseUrl,
  matchUrn,
  credential: cred,
  onChange: (m) => console.log(m.status),
});
// watcher.stop();

For go-live and end-of-stream, subscribeLiveState exposes the same push channel (SSE) the managed player uses — see the API reference.

Device identity

License requests carry a stable per-browser device id (X-Device-Id), generated and persisted by the SDK. getDeviceId() / clearDeviceId() expose it — useful for support tickets and for honoring "reset this device" flows.

When to prefer Mode A

Mode B hands you the sharp edges: live-edge seeking policy, retry ladders, license refresh on long sessions, autoplay policy handling. If you don't already own a player you're invested in, the managed player does all of that and stays skinnable down to CSS variables.

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