npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oddin-gg/havik-player

v1.0.2

Published

Havik HTML5 player SDK — LL-HLS + DRM playback for Oddin live streams (managed, bring-your-own-player, and iframe-embed modes).

Downloads

85

Readme

@oddin-gg/havik-player

HTML5 player SDK for Oddin live streams — LL-HLS + DRM against the havik-streams API, in three integration modes:

| Mode | Entry | You provide | SDK owns | | ----------------------------- | ---------------------------- | ---------------------- | -------------------------------------------------- | | A — Managed | createPlayer() | a <video> element | hls.js, LL-HLS, DRM/EME, retries | | B — Bring-your-own-player | resolveStream() | the player + <video> | auth → catalog → playback resolution + DRM helpers | | C — Iframe embed | mountEmbed() / hosted page | an iframe slot | everything, behind a postMessage API |

Widevine (Chrome/Firefox/Edge/Android) is fully supported. Safari/iOS plays via native passthrough; full FairPlay EME is a tracked fast-follow. See Limitations.

📖 Full API reference: docs/API.md · Examples (vanilla / React / Vue): examples/ · API stability & versioning: docs/STABILITY.md

⚠️ Before you integrate — a server-side prerequisite that is out of your code's hands: the api-key's AllowedOrigins (and, for iframes, AllowedIframeParents) and the DRM service's CORS allow-list must both include every origin you serve the player from, or requests fail their preflight and DRM never starts. Onboard your origins with Oddin first; allow a few minutes for propagation. Never use a ["*"] allow-list in production.


Install

npm install @oddin-gg/havik-player

Or via CDN (<script> / iframe), exposing window.HavikPlayer:

<script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player/dist/havik-player.global.js"></script>

You need two things to play a stream: a match URN (od:match:…) and a publishable api-key (pk_live_… / pk_test_…) whose AllowedOrigins includes the origin your player runs on. See Auth & onboarding.


Mode A — Managed player

The SDK owns the <video>, bundles hls.js, and wires LL-HLS + Widevine DRM.

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

const player = await createPlayer({
  video: document.querySelector('video')!,
  baseUrl: 'https://streams.oddin.gg',
  matchUrn: 'od:match:1234',
  credential: { apiKey: 'pk_live_…' },
  autoplay: true,
  muted: true, // required for reliable autoplay
  waitForLive: true, // arm on an upcoming match, auto-play at go-live
});

player.on('statechange', (s) => console.log('state', s)); // loading|waiting|playing|buffering|paused|ended|error
player.on('waiting', (w) => console.log('go-live in', w.retryInMs, 'ms')); // armed countdown
player.on('stats', (s) => console.log(s)); // dropped frames, latency, bitrate, level
player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));

// later
player.destroy();

The managed player handles 425 TOO_EARLY retries (honoring Retry-After), buffering recovery, and silent DRM license-URL refresh for long sessions.

Controls & skinning

Out of the box, Mode A renders a branded, fully skinnable control bar — seek, play/pause, volume, a live badge + go-live, quality / audio / subtitle menus, Picture-in-Picture, fullscreen, buffering + error/retry overlays, auto-hide, and keyboard a11y. Re-skin it to match your brand with a theme (or by overriding the --havik-* CSS variables); or opt out with controls: 'native' / 'none'.

await createPlayer({
  video,
  baseUrl,
  matchUrn,
  credential: { apiKey },
  controls: 'custom', // 'custom' (default, branded bar) | 'native' | 'none'
  theme: { accent: '#14b8a6', surface: '#0f172a' }, // merged over the Oddin defaults
});

See Controls & theming for the full theme/CSS-variable reference.

Mode B — Bring your own player

The SDK resolves the stream and gives you the DRM header helpers; you own the <video> and the playback engine.

import { resolveStream, licenseRequestHeaders, getDeviceId } from '@oddin-gg/havik-player';

const cred = { apiKey: 'pk_live_…' };
const stream = await resolveStream({
  baseUrl: 'https://streams.oddin.gg',
  matchUrn: 'od:match:1234',
  credential: cred,
  waitForLive: true,
});

// stream = { manifestUrl, drmEnabled, drm: { widevine: { licenseUrl } }, … }
// Wire into YOUR player. For hls.js, attach the license headers in licenseXhrSetup
// (NOT xhrSetup — EME license requests route through licenseXhrSetup):
const headers = licenseRequestHeaders(stream, cred); // { 'x-api-key', 'X-Match-Urn', 'X-Device-Id', 'Content-Type' }

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

Discovery helpers (there is no push channel — polling only):

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

const catalog = await fetchCatalog({ baseUrl, credential: cred }); // tournaments → matches (metadata only)
const watcher = watchStatus({
  baseUrl,
  matchUrn,
  credential: cred,
  onChange: (m) => console.log(m.status),
});
// watcher.stop();

Mode C — Iframe embed

Drop in the hosted player page and control it over postMessage:

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

const embed = mountEmbed({
  container: document.querySelector('#player')!,
  src: 'https://player.oddin.gg/embed/', // the hosted embed page
  baseUrl: 'https://streams.oddin.gg',
  matchUrn: 'od:match:1234',
  // apiKey is DEV-ONLY here; in production the hosted page injects it server-side.
  onEvent: (msg) => console.log(msg), // { type: 'oddin:ready' | 'oddin:state' | 'oddin:waiting' | 'oddin:stats' | 'oddin:error' }
});

embed.play();
embed.setMuted(false);
// embed.destroy();

Both ends verify event.origin on every message. The embed page reads its config from window.HAVIK_EMBED_CONFIG (server-injected) or the URL query. A ready-to-host page is shipped at embed/index.html.

Iframe auth: the key's AllowedIframeParents must include the embedding page's origin — iframe navigations send a Referer/Sec-Fetch-Dest: iframe, which is what havik-streams matches against (no Origin header on a top-level iframe nav).


Live pickup (waitForLive)

A live match isn't playable until the stream is actually live. With waitForLive, the player arms and auto-attaches the instant it goes live — no user action.

By default it does this over a push channel (SSE), not polling. While the live-state stream is connected the player makes zero /v1/playback requests — it waits for the pushed go-live, then resolves once (so it doesn't repeatedly poll while waiting for kickoff). If the push channel is unavailable it transparently falls back to polling 425 TOO_EARLY + Retry-After (scaled ~30s→1s near kickoff, never faster than the server asks). Set liveStateEvents: false to force polling.

waitForLive: {
  timeoutMs: 30 * 60_000, // overall arm budget (default: unbounded)
  floorMs: 1000,          // min poll interval (default 1000, server minimum)
  ceilMs: 30_000,         // cap far-from-kickoff backoff
  onState: (s) => console.log(s.phase, s.retryInMs, s.attempt),
}

End of stream is detected automatically: when the live stream finishes the player stops and fires ended — it never sits buffering on a stream that's over. See subscribeLiveState to consume the live-state channel directly.

Error handling

All API errors are a typed PlaybackError with { code, httpStatus, retryAfterMs?, requestId? }:

| code | HTTP | meaning | retry? | | ---------------------------- | --------- | --------------------------------------------------- | ----------------- | | INVALID_URN | 400 | malformed URN | no | | NOT_FOUND | 404 | unknown URN or not entitled (indistinguishable) | no | | GONE | 410 | ended past catchup window | no | | TOO_EARLY | 425 | upcoming, not live yet | yes (waitForLive) | | UNAVAILABLE | 503 | should-be-live, origin warming up | yes | | UNAUTHORIZED / FORBIDDEN | 401 / 403 | bad key / origin not allowed / DRM rejected | no | | RATE_LIMITED | 429 | per-IP limiter | back off |


Auth & onboarding

The x-api-key is a publishable key, by design embeddable in browser JS. Its only guardrail is the server-side per-key allowlist — so onboarding each integrator origin is a hard dependency:

  1. havik-streams: the key's AllowedOrigins must include every origin your player runs on (and AllowedIframeParents for Mode C). Never ship ["*"] to prod — it disables the guardrail.
  2. havik-drm (drm-proxy): its CORS_ALLOWED_ORIGINS must also include those origins, or the cross-origin license POST fails its preflight and DRM never starts (silent black video).
  3. Scope the key's entitlements to the tournaments you serve.

Revocation isn't instant (≤5min validation cache + ≤24h rotation grace).

Forward-compatible credentials: credential also accepts an async function () => Promise<{ apiKey }>, so you can later swap in a short-lived-token mint service without changing your integration. (No such service exists today.)


Demo app

A full catalog-browser + live-watch + iframe demo lives in demo/:

cp demo/.env.local.example demo/.env.local   # set VITE_HAVIK_BASE_URL + VITE_HAVIK_API_KEY
npm run dev                                   # http://localhost:5173

The key's AllowedOrigins must include http://localhost:5173.


Limitations

  • FairPlay (Safari/iOS): not implemented in v1. Safari uses native HLS passthrough, which only plays DRM where the OS already trusts the origins. Full FairPlay EME (via a Shaka adapter behind the PlaybackEngine seam) is a tracked fast-follow.
  • HLS only (no DASH); Widevine + FairPlay key systems (no PlayReady — Edge uses Widevine). Container CMAF/fMP4, encryption cbcs.
  • hls.js is pinned to 1.7.0-beta.1 (the first version that plays this LL-HLS+DRM content; 1.6.x has a fatal sourceBuffer bug). Pin to 1.7.0 stable when released.

Privacy

The SDK persists a random per-device id in localStorage and sends it as X-Device-Id on every DRM license request (required by the license server). It is a stable cross-session identifier — disclose it in your privacy policy and offer a reset via clearDeviceId(). No other personal data is collected by the SDK.

Browser & runtime support

  • Browser-only. Importing the package is side-effect-safe (verified in CI), but the SDK must be used client-side — call createPlayer/mountEmbed in the browser, not during SSR (e.g. dynamic-import it in a Next.js client component).
  • Supported targets: see .browserslistrc (Chrome/Edge ≥94, Firefox ≥91, Safari/iOS ≥15). DRM = Widevine on Chrome/Edge/Firefox/Android; FairPlay (Safari/iOS) is not yet implemented.

Development

npm run build       # ESM lib (dist/index.js) + IIFE bundle (dist/havik-player.global.js)
npm run typecheck
npm run lint
npm test            # vitest
npm run test:coverage
npm run dev         # demo app

Security

See SECURITY.md for the vulnerability-disclosure policy.

License

ISC — see LICENSE.

Third-party licenses

This package depends on, and the CDN bundle (dist/havik-player.global.js) statically embeds, hls.js (Apache-2.0, © Dailymotion). Full attribution is in THIRD_PARTY_NOTICES.txt, which ships with the package and whose attribution is preserved in the bundle banner.

DRM uses the browser's built-in EME/CDM; Widevine™ (Google), FairPlay®/AirPlay® (Apple) and PlayReady (Microsoft) are trademarks of their respective owners and no CDM is distributed with this package.