@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.
- package/LICENSE +15 -0
- package/README.md +300 -0
- package/THIRD_PARTY_NOTICES.txt +59 -0
- package/dist/havik-player.global.js +147 -0
- package/dist/havik-player.global.js.map +1 -0
- package/dist/index.cjs +108 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +795 -0
- package/dist/index.d.ts +795 -0
- package/dist/index.js +1919 -0
- package/dist/index.js.map +1 -0
- package/docs/API.md +1115 -0
- package/docs/STABILITY.md +56 -0
- package/embed/index.html +26 -0
- package/examples/README.md +17 -0
- package/examples/Vue.vue +37 -0
- package/examples/react.tsx +47 -0
- package/examples/vanilla.html +41 -0
- package/package.json +101 -0
|
@@ -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.
|
package/embed/index.html
ADDED
|
@@ -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.
|
package/examples/Vue.vue
ADDED
|
@@ -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 <script> 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
|
+
}
|