@oddin-gg/havik-player 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ # API stability, versioning & support
2
+
3
+ ## Semantic versioning
4
+
5
+ `@oddin-gg/havik-player` follows [SemVer](https://semver.org/). Releases are automated
6
+ by semantic-release from Conventional Commits: `fix:` → patch, `feat:` → minor,
7
+ breaking change → major. **Pin a version in production** (npm and the CDN URL).
8
+
9
+ ## What is public vs internal
10
+
11
+ **Stable public API** (changes only follow SemVer; the runtime export set is frozen by
12
+ `test/public-api.test.ts`):
13
+
14
+ - `createPlayer` + the `Player` interface (Mode A)
15
+ - `resolveStream`, `fetchCatalog`, `flattenMatches`, `watchStatus`,
16
+ `licenseRequestHeaders`, `getDeviceId`, `clearDeviceId`, `isLowLatencyManifest` (Mode B)
17
+ - `mountEmbed` / `bootEmbed` + `EmbedHandle` / the `oddin:*` postMessage protocol (Mode C)
18
+ - `PlaybackError` and all exported `type` definitions
19
+
20
+ **Advanced / experimental** (public but may evolve faster): `pollToLive`, `isOddinMessage`.
21
+
22
+ **Internal** — **not** part of the public API and excluded from the type declarations
23
+ (`stripInternal`): the `PlaybackEngine` engine seam, `EngineEvent`, `resolvePlaybackOnce`,
24
+ `resolveCredential`. Do not depend on these.
25
+
26
+ ## Deprecation policy
27
+
28
+ - Anything to be removed is first marked `@deprecated` (with the replacement) for **at
29
+ least one minor release** before removal.
30
+ - Removals and breaking signature changes ship only in a **major**, and are called out in
31
+ the `CHANGELOG.md` and the GitHub release notes.
32
+
33
+ ## postMessage protocol (Mode C)
34
+
35
+ The host↔iframe protocol is versioned via `PROTOCOL_VERSION`, sent in `oddin:ready`.
36
+ Changes are additive within a major; `mountEmbed` logs a warning if the embed page's
37
+ protocol version differs from the host SDK's — pin the embed page version to the SDK
38
+ version to avoid CDN auto-update skew.
39
+
40
+ ## Browser support
41
+
42
+ See [`.browserslistrc`](../.browserslistrc). DRM: Widevine on Chrome/Edge/Firefox/Android;
43
+ FairPlay (Safari/iOS) is not yet implemented.
44
+
45
+ ## Supported versions
46
+
47
+ Security and bug fixes target the latest published minor.
48
+
49
+ ## Export control / cryptography note
50
+
51
+ This SDK performs no proprietary cryptography and bundles no DRM client/CDM. It relies
52
+ solely on the browser's built-in Encrypted Media Extensions (EME), the OS/browser-provided
53
+ Content Decryption Module, and (for the device id) the standard Web Crypto API. It is
54
+ publicly available open-source software. For US export classification this typically falls
55
+ under license-exception TSU / ECCN 5D002 NLR-eligible, but confirm the appropriate
56
+ classification with your legal/compliance team for your jurisdiction.
@@ -0,0 +1,26 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="referrer" content="no-referrer" />
7
+ <title>Havik Player</title>
8
+ </head>
9
+ <body>
10
+ <!--
11
+ Reference hosted iframe embed page (Mode C) — this file is a TEMPLATE, not a
12
+ production page. Before deploying:
13
+ 1) PIN a version + add Subresource Integrity on the script below, e.g.
14
+ src=".../@oddin-gg/havik-player@X.Y.Z/dist/havik-player.global.js"
15
+ integrity="sha384-…" crossorigin="anonymous"
16
+ (the unpinned URL below tracks `latest` and auto-upgrades — do not ship it).
17
+ 2) INJECT config server-side as window.HAVIK_EMBED_CONFIG (baseUrl, matchUrn,
18
+ apiKey, parentOrigin). When injected, the URL query string is IGNORED for
19
+ security; the api-key must NOT travel in the query in production.
20
+ -->
21
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@1.0.2/dist/havik-player.global.js" integrity="sha384-NUQrfF/31PIMZEq+eH+zrJfZUqC6hlPgS8PFbP5kWwNiF7XjF8qEckdFYbBVFM7H" crossorigin="anonymous"></script>
22
+ <script>
23
+ HavikPlayer.bootEmbed();
24
+ </script>
25
+ </body>
26
+ </html>
@@ -0,0 +1,17 @@
1
+ # Examples
2
+
3
+ Copy-paste integration references for `@oddin-gg/havik-player`. See
4
+ [../docs/API.md](../docs/API.md) for the full API.
5
+
6
+ | File | Stack | Notes |
7
+ |---|---|---|
8
+ | [`vanilla.html`](vanilla.html) | Plain `<script>` (CDN) | Self-contained page using `window.HavikPlayer`. |
9
+ | [`react.tsx`](react.tsx) | React 18+ | `useEffect` lifecycle with StrictMode-safe cleanup. |
10
+ | [`Vue.vue`](Vue.vue) | Vue 3 | `onMounted` / `onBeforeUnmount` lifecycle. |
11
+
12
+ For every framework the rule is the same: **create the player when the element
13
+ mounts, and call `player.destroy()` when it unmounts** (it tears down hls.js + the
14
+ EME/CDM session). The React example also guards against StrictMode's double-invoke.
15
+
16
+ Before any of these work, the api-key's **AllowedOrigins** (and the DRM service's
17
+ CORS allow-list) must include the page's origin — see the README's onboarding note.
@@ -0,0 +1,37 @@
1
+ <!-- Vue 3 integration example. createPlayer/destroy mirror the component lifecycle. -->
2
+ <script setup lang="ts">
3
+ import { onBeforeUnmount, onMounted, ref } from 'vue';
4
+ import { createPlayer, type Player } from '@oddin-gg/havik-player';
5
+
6
+ const props = defineProps<{ baseUrl: string; matchUrn: string; apiKey: string }>();
7
+
8
+ const videoEl = ref<HTMLVideoElement | null>(null);
9
+ let player: Player | null = null;
10
+
11
+ onMounted(async () => {
12
+ if (!videoEl.value) return;
13
+ try {
14
+ player = await createPlayer({
15
+ video: videoEl.value,
16
+ baseUrl: props.baseUrl,
17
+ matchUrn: props.matchUrn,
18
+ credential: { apiKey: props.apiKey },
19
+ autoplay: true,
20
+ muted: true,
21
+ waitForLive: true,
22
+ });
23
+ player.on('error', (e) => console.error(e.code, e.message));
24
+ } catch (err) {
25
+ console.error('havik-player', err);
26
+ }
27
+ });
28
+
29
+ onBeforeUnmount(() => {
30
+ player?.destroy();
31
+ player = null;
32
+ });
33
+ </script>
34
+
35
+ <template>
36
+ <video ref="videoEl" controls playsinline muted style="width: 100%" />
37
+ </template>
@@ -0,0 +1,47 @@
1
+ // React integration example. The key detail is lifecycle: createPlayer fully tears
2
+ // down hls.js + EME on destroy(), so the effect MUST destroy on unmount — and guard
3
+ // against React 18 StrictMode's double-invoke and rapid prop changes (the #1
4
+ // managed-player integration bug).
5
+ import { useEffect, useRef } from 'react';
6
+ import { createPlayer, type Player } from '@oddin-gg/havik-player';
7
+
8
+ interface Props {
9
+ baseUrl: string;
10
+ matchUrn: string;
11
+ apiKey: string;
12
+ }
13
+
14
+ export function HavikPlayer({ baseUrl, matchUrn, apiKey }: Props) {
15
+ const videoRef = useRef<HTMLVideoElement>(null);
16
+
17
+ useEffect(() => {
18
+ const video = videoRef.current;
19
+ if (!video) return;
20
+
21
+ let player: Player | null = null;
22
+ let cancelled = false;
23
+
24
+ createPlayer({
25
+ video,
26
+ baseUrl,
27
+ matchUrn,
28
+ credential: { apiKey },
29
+ autoplay: true,
30
+ muted: true,
31
+ waitForLive: true,
32
+ })
33
+ .then((p) => {
34
+ // If the effect was already cleaned up (StrictMode / fast nav), discard.
35
+ if (cancelled) p.destroy();
36
+ else player = p;
37
+ })
38
+ .catch((err) => console.error('havik-player', err));
39
+
40
+ return () => {
41
+ cancelled = true;
42
+ player?.destroy();
43
+ };
44
+ }, [baseUrl, matchUrn, apiKey]);
45
+
46
+ return <video ref={videoRef} controls playsInline muted style={{ width: '100%' }} />;
47
+ }
@@ -0,0 +1,41 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Havik Player — vanilla &lt;script&gt; example</title>
7
+ </head>
8
+ <body>
9
+ <video id="player" controls playsinline muted style="width: 100%; max-width: 960px; background: #000"></video>
10
+
11
+ <!-- In production, pin a version + add Subresource Integrity (see README). -->
12
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player/dist/havik-player.global.js"></script>
13
+ <script>
14
+ (async () => {
15
+ const player = await HavikPlayer.createPlayer({
16
+ video: document.getElementById('player'),
17
+ baseUrl: 'https://streams.oddin.gg',
18
+ matchUrn: 'od:match:1234',
19
+ credential: { apiKey: 'pk_live_…' }, // scope its AllowedOrigins to this page's origin
20
+ autoplay: true,
21
+ muted: true,
22
+ waitForLive: true, // arm now, auto-play the instant the match goes live
23
+ });
24
+
25
+ player.on('statechange', (s) => console.log('state', s));
26
+ player.on('autoplayblocked', () => {
27
+ // Browser blocked autoplay — show your own "tap to play" button, then call player.play().
28
+ });
29
+ player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
30
+
31
+ // Quality / captions menu data:
32
+ player.on('ready', () => {
33
+ console.log('qualities', player.getQualityLevels());
34
+ console.log('captions', player.getTextTracks());
35
+ });
36
+
37
+ // window.addEventListener('beforeunload', () => player.destroy());
38
+ })();
39
+ </script>
40
+ </body>
41
+ </html>
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@oddin-gg/havik-player",
3
+ "version": "1.0.2",
4
+ "description": "Havik HTML5 player SDK — LL-HLS + DRM playback for Oddin live streams (managed, bring-your-own-player, and iframe-embed modes).",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "license": "ISC",
8
+ "author": "Oddin.gg",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/oddin-gg/havik-html5.git"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "publishConfig": {
17
+ "registry": "https://registry.npmjs.org",
18
+ "access": "public"
19
+ },
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "unpkg": "./dist/havik-player.global.js",
24
+ "jsdelivr": "./dist/havik-player.global.js",
25
+ "exports": {
26
+ ".": {
27
+ "import": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "require": {
32
+ "types": "./dist/index.d.cts",
33
+ "default": "./dist/index.cjs"
34
+ }
35
+ },
36
+ "./global": "./dist/havik-player.global.js",
37
+ "./package.json": "./package.json"
38
+ },
39
+ "files": [
40
+ "dist/index.js",
41
+ "dist/index.js.map",
42
+ "dist/index.cjs",
43
+ "dist/index.cjs.map",
44
+ "dist/index.d.ts",
45
+ "dist/index.d.cts",
46
+ "dist/havik-player.global.js",
47
+ "dist/havik-player.global.js.map",
48
+ "embed",
49
+ "docs",
50
+ "examples",
51
+ "README.md",
52
+ "LICENSE",
53
+ "THIRD_PARTY_NOTICES.txt"
54
+ ],
55
+ "scripts": {
56
+ "dev": "vite",
57
+ "build": "npm run build:lib && node scripts/emit-cts.mjs && npm run build:global",
58
+ "build:lib": "vite build --config vite.lib.config.ts",
59
+ "build:global": "vite build --config vite.global.config.ts",
60
+ "build:demo": "vite build",
61
+ "typecheck": "tsc --noEmit",
62
+ "lint": "eslint .",
63
+ "format": "prettier --write .",
64
+ "format:check": "prettier --check .",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest",
67
+ "test:coverage": "vitest run --coverage",
68
+ "test:conformance": "playwright test -c conformance/playwright.config.ts",
69
+ "verify:ssr": "node scripts/verify-ssr.mjs",
70
+ "publint": "publint",
71
+ "size": "size-limit",
72
+ "lint:licenses": "node scripts/check-licenses.mjs",
73
+ "sbom": "npx --yes @cyclonedx/cyclonedx-npm@2 --omit dev --output-file sbom.json",
74
+ "prepack": "npm run build"
75
+ },
76
+ "dependencies": {
77
+ "hls.js": "1.6.16"
78
+ },
79
+ "devDependencies": {
80
+ "@eslint/js": "^10.0.1",
81
+ "@microsoft/api-extractor": "^7.58.9",
82
+ "@playwright/test": "^1.61.1",
83
+ "@semantic-release/changelog": "^6.0.3",
84
+ "@semantic-release/exec": "^7.1.0",
85
+ "@semantic-release/git": "^10.0.1",
86
+ "@size-limit/file": "^12.1.0",
87
+ "@types/node": "^26.0.1",
88
+ "@vitest/coverage-v8": "^4.1.9",
89
+ "eslint": "^10.6.0",
90
+ "globals": "^17.7.0",
91
+ "jsdom": "^29.1.1",
92
+ "prettier": "^3.9.3",
93
+ "publint": "^0.3.2",
94
+ "size-limit": "^12.1.0",
95
+ "typescript": "^6.0.3",
96
+ "typescript-eslint": "^8.62.0",
97
+ "vite": "^8.1.0",
98
+ "vite-plugin-dts": "^5.0.3",
99
+ "vitest": "^4.1.9"
100
+ }
101
+ }