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
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:
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 licenseXhrSetup — not xhrSetup; EME license requests route through licenseXhrSetup:
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:
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.