@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 ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Oddin.gg
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # @oddin-gg/havik-player
2
+
3
+ HTML5 player SDK for Oddin live streams — **LL-HLS + DRM** against the
4
+ [havik-streams](https://github.com/oddin-gg/havik-streams) API, in three
5
+ integration modes:
6
+
7
+ | Mode | Entry | You provide | SDK owns |
8
+ | ----------------------------- | ---------------------------- | ---------------------- | -------------------------------------------------- |
9
+ | **A — Managed** | `createPlayer()` | a `<video>` element | hls.js, LL-HLS, DRM/EME, retries |
10
+ | **B — Bring-your-own-player** | `resolveStream()` | the player + `<video>` | auth → catalog → playback resolution + DRM helpers |
11
+ | **C — Iframe embed** | `mountEmbed()` / hosted page | an iframe slot | everything, behind a `postMessage` API |
12
+
13
+ Widevine (Chrome/Firefox/Edge/Android) is fully supported. Safari/iOS plays via
14
+ native passthrough; full FairPlay EME is a tracked fast-follow. See [Limitations](#limitations).
15
+
16
+ > 📖 **Full API reference:** [docs/API.md](docs/API.md) · **Examples** (vanilla / React / Vue): [examples/](examples/) · **API stability & versioning:** [docs/STABILITY.md](docs/STABILITY.md)
17
+
18
+ > ⚠️ **Before you integrate — a server-side prerequisite that is out of your code's hands:**
19
+ > the api-key's **AllowedOrigins** (and, for iframes, **AllowedIframeParents**) **and** the
20
+ > DRM service's **CORS allow-list** must both include every origin you serve the player from,
21
+ > or requests fail their preflight and DRM never starts. Onboard your origins with Oddin
22
+ > first; allow a few minutes for propagation. Never use a `["*"]` allow-list in production.
23
+
24
+ ---
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install @oddin-gg/havik-player
30
+ ```
31
+
32
+ Or via CDN (`<script>` / iframe), exposing `window.HavikPlayer`:
33
+
34
+ ```html
35
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player/dist/havik-player.global.js"></script>
36
+ ```
37
+
38
+ You need two things to play a stream: a **match URN** (`od:match:…`) and a
39
+ **publishable api-key** (`pk_live_…` / `pk_test_…`) whose **AllowedOrigins**
40
+ includes the origin your player runs on. See [Auth & onboarding](#auth--onboarding).
41
+
42
+ ---
43
+
44
+ ## Mode A — Managed player
45
+
46
+ The SDK owns the `<video>`, bundles hls.js, and wires LL-HLS + Widevine DRM.
47
+
48
+ ```ts
49
+ import { createPlayer } from '@oddin-gg/havik-player';
50
+
51
+ const player = await createPlayer({
52
+ video: document.querySelector('video')!,
53
+ baseUrl: 'https://streams.oddin.gg',
54
+ matchUrn: 'od:match:1234',
55
+ credential: { apiKey: 'pk_live_…' },
56
+ autoplay: true,
57
+ muted: true, // required for reliable autoplay
58
+ waitForLive: true, // arm on an upcoming match, auto-play at go-live
59
+ });
60
+
61
+ player.on('statechange', (s) => console.log('state', s)); // loading|waiting|playing|buffering|paused|ended|error
62
+ player.on('waiting', (w) => console.log('go-live in', w.retryInMs, 'ms')); // armed countdown
63
+ player.on('stats', (s) => console.log(s)); // dropped frames, latency, bitrate, level
64
+ player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
65
+
66
+ // later
67
+ player.destroy();
68
+ ```
69
+
70
+ The managed player handles `425 TOO_EARLY` retries (honoring `Retry-After`),
71
+ buffering recovery, and silent DRM **license-URL refresh** for long sessions.
72
+
73
+ ### Controls & skinning
74
+
75
+ Out of the box, Mode A renders a **branded, fully skinnable control bar** — seek,
76
+ play/pause, volume, a live badge + go-live, quality / audio / subtitle menus,
77
+ Picture-in-Picture, fullscreen, buffering + error/retry overlays, auto-hide, and
78
+ keyboard a11y. Re-skin it to match your brand with a `theme` (or by overriding the
79
+ `--havik-*` CSS variables); or opt out with `controls: 'native'` / `'none'`.
80
+
81
+ ```ts
82
+ await createPlayer({
83
+ video,
84
+ baseUrl,
85
+ matchUrn,
86
+ credential: { apiKey },
87
+ controls: 'custom', // 'custom' (default, branded bar) | 'native' | 'none'
88
+ theme: { accent: '#14b8a6', surface: '#0f172a' }, // merged over the Oddin defaults
89
+ });
90
+ ```
91
+
92
+ See [Controls & theming](docs/API.md#controls--theming) for the full theme/CSS-variable reference.
93
+
94
+ ## Mode B — Bring your own player
95
+
96
+ The SDK resolves the stream and gives you the DRM header helpers; you own the
97
+ `<video>` and the playback engine.
98
+
99
+ ```ts
100
+ import { resolveStream, licenseRequestHeaders, getDeviceId } from '@oddin-gg/havik-player';
101
+
102
+ const cred = { apiKey: 'pk_live_…' };
103
+ const stream = await resolveStream({
104
+ baseUrl: 'https://streams.oddin.gg',
105
+ matchUrn: 'od:match:1234',
106
+ credential: cred,
107
+ waitForLive: true,
108
+ });
109
+
110
+ // stream = { manifestUrl, drmEnabled, drm: { widevine: { licenseUrl } }, … }
111
+ // Wire into YOUR player. For hls.js, attach the license headers in licenseXhrSetup
112
+ // (NOT xhrSetup — EME license requests route through licenseXhrSetup):
113
+ const headers = licenseRequestHeaders(stream, cred); // { 'x-api-key', 'X-Match-Urn', 'X-Device-Id', 'Content-Type' }
114
+ ```
115
+
116
+ > **DRM rule:** POST to `drm.widevine.licenseUrl` **verbatim** — its signed query
117
+ > string _is_ the token. Never rewrite/normalize the URL, and use the **same
118
+ > api-key** on playback and license requests or the license 403s.
119
+
120
+ Discovery helpers (there is no push channel — polling only):
121
+
122
+ ```ts
123
+ import { fetchCatalog, watchStatus } from '@oddin-gg/havik-player';
124
+
125
+ const catalog = await fetchCatalog({ baseUrl, credential: cred }); // tournaments → matches (metadata only)
126
+ const watcher = watchStatus({
127
+ baseUrl,
128
+ matchUrn,
129
+ credential: cred,
130
+ onChange: (m) => console.log(m.status),
131
+ });
132
+ // watcher.stop();
133
+ ```
134
+
135
+ ## Mode C — Iframe embed
136
+
137
+ Drop in the hosted player page and control it over `postMessage`:
138
+
139
+ ```ts
140
+ import { mountEmbed } from '@oddin-gg/havik-player';
141
+
142
+ const embed = mountEmbed({
143
+ container: document.querySelector('#player')!,
144
+ src: 'https://player.oddin.gg/embed/', // the hosted embed page
145
+ baseUrl: 'https://streams.oddin.gg',
146
+ matchUrn: 'od:match:1234',
147
+ // apiKey is DEV-ONLY here; in production the hosted page injects it server-side.
148
+ onEvent: (msg) => console.log(msg), // { type: 'oddin:ready' | 'oddin:state' | 'oddin:waiting' | 'oddin:stats' | 'oddin:error' }
149
+ });
150
+
151
+ embed.play();
152
+ embed.setMuted(false);
153
+ // embed.destroy();
154
+ ```
155
+
156
+ Both ends verify `event.origin` on every message. The embed page reads its
157
+ config from `window.HAVIK_EMBED_CONFIG` (server-injected) or the URL query.
158
+ A ready-to-host page is shipped at `embed/index.html`.
159
+
160
+ > **Iframe auth:** the key's **AllowedIframeParents** must include the embedding
161
+ > page's origin — iframe navigations send a `Referer`/`Sec-Fetch-Dest: iframe`,
162
+ > which is what havik-streams matches against (no `Origin` header on a top-level
163
+ > iframe nav).
164
+
165
+ ---
166
+
167
+ ## Live pickup (`waitForLive`)
168
+
169
+ A live match isn't playable until the stream is actually live. With `waitForLive`,
170
+ the player arms and auto-attaches the instant it goes live — no user action.
171
+
172
+ **By default it does this over a push channel (SSE), not polling.** While the
173
+ live-state stream is connected the player makes **zero** `/v1/playback` requests — it
174
+ waits for the pushed go-live, then resolves once (so it doesn't repeatedly poll while
175
+ waiting for kickoff). If the push channel is unavailable it transparently falls back
176
+ to **polling** `425 TOO_EARLY` + `Retry-After` (scaled ~30s→1s near kickoff, never
177
+ faster than the server asks). Set `liveStateEvents: false` to force polling.
178
+
179
+ ```ts
180
+ waitForLive: {
181
+ timeoutMs: 30 * 60_000, // overall arm budget (default: unbounded)
182
+ floorMs: 1000, // min poll interval (default 1000, server minimum)
183
+ ceilMs: 30_000, // cap far-from-kickoff backoff
184
+ onState: (s) => console.log(s.phase, s.retryInMs, s.attempt),
185
+ }
186
+ ```
187
+
188
+ **End of stream** is detected automatically: when the live stream finishes the player
189
+ stops and fires `ended` — it never sits buffering on a stream that's over. See
190
+ [`subscribeLiveState`](docs/API.md#subscribelivestate) to consume the live-state
191
+ channel directly.
192
+
193
+ ## Error handling
194
+
195
+ All API errors are a typed `PlaybackError` with `{ code, httpStatus, retryAfterMs?, requestId? }`:
196
+
197
+ | code | HTTP | meaning | retry? |
198
+ | ---------------------------- | --------- | --------------------------------------------------- | ----------------- |
199
+ | `INVALID_URN` | 400 | malformed URN | no |
200
+ | `NOT_FOUND` | 404 | unknown URN **or** not entitled (indistinguishable) | no |
201
+ | `GONE` | 410 | ended past catchup window | no |
202
+ | `TOO_EARLY` | 425 | upcoming, not live yet | yes (waitForLive) |
203
+ | `UNAVAILABLE` | 503 | should-be-live, origin warming up | yes |
204
+ | `UNAUTHORIZED` / `FORBIDDEN` | 401 / 403 | bad key / origin not allowed / DRM rejected | no |
205
+ | `RATE_LIMITED` | 429 | per-IP limiter | back off |
206
+
207
+ ---
208
+
209
+ ## Auth & onboarding
210
+
211
+ The `x-api-key` is a **publishable** key, by design embeddable in browser JS. Its
212
+ only guardrail is the **server-side per-key allowlist** — so onboarding each
213
+ integrator origin is a hard dependency:
214
+
215
+ 1. **havik-streams**: the key's `AllowedOrigins` must include every origin your
216
+ player runs on (and `AllowedIframeParents` for Mode C). Never ship `["*"]` to
217
+ prod — it disables the guardrail.
218
+ 2. **havik-drm (`drm-proxy`)**: its `CORS_ALLOWED_ORIGINS` must also include those
219
+ origins, or the cross-origin license POST fails its preflight and DRM never
220
+ starts (silent black video).
221
+ 3. Scope the key's entitlements to the tournaments you serve.
222
+
223
+ Revocation isn't instant (≤5min validation cache + ≤24h rotation grace).
224
+
225
+ **Forward-compatible credentials:** `credential` also accepts an async function
226
+ `() => Promise<{ apiKey }>`, so you can later swap in a short-lived-token mint
227
+ service without changing your integration. (No such service exists today.)
228
+
229
+ ---
230
+
231
+ ## Demo app
232
+
233
+ A full catalog-browser + live-watch + iframe demo lives in `demo/`:
234
+
235
+ ```bash
236
+ cp demo/.env.local.example demo/.env.local # set VITE_HAVIK_BASE_URL + VITE_HAVIK_API_KEY
237
+ npm run dev # http://localhost:5173
238
+ ```
239
+
240
+ The key's `AllowedOrigins` must include `http://localhost:5173`.
241
+
242
+ ---
243
+
244
+ ## Limitations
245
+
246
+ - **FairPlay (Safari/iOS)**: not implemented in v1. Safari uses native HLS
247
+ passthrough, which only plays DRM where the OS already trusts the origins. Full
248
+ FairPlay EME (via a Shaka adapter behind the `PlaybackEngine` seam) is a tracked
249
+ fast-follow.
250
+ - **HLS only** (no DASH); **Widevine + FairPlay** key systems (no PlayReady — Edge
251
+ uses Widevine). Container CMAF/fMP4, encryption cbcs.
252
+ - hls.js is pinned to `1.7.0-beta.1` (the first version that plays this LL-HLS+DRM
253
+ content; 1.6.x has a fatal sourceBuffer bug). Pin to `1.7.0` stable when released.
254
+
255
+ ## Privacy
256
+
257
+ The SDK persists a random per-device id in `localStorage` and sends it as
258
+ `X-Device-Id` on every DRM license request (required by the license server). It is a
259
+ stable cross-session identifier — disclose it in your privacy policy and offer a reset
260
+ via [`clearDeviceId()`](docs/API.md#getdeviceid--cleardeviceid). No other personal
261
+ data is collected by the SDK.
262
+
263
+ ## Browser & runtime support
264
+
265
+ - **Browser-only.** Importing the package is side-effect-safe (verified in CI), but
266
+ the SDK must be used **client-side** — call `createPlayer`/`mountEmbed` in the
267
+ browser, not during SSR (e.g. dynamic-import it in a Next.js client component).
268
+ - Supported targets: see [`.browserslistrc`](.browserslistrc) (Chrome/Edge ≥94,
269
+ Firefox ≥91, Safari/iOS ≥15). DRM = Widevine on Chrome/Edge/Firefox/Android;
270
+ FairPlay (Safari/iOS) is not yet implemented.
271
+
272
+ ## Development
273
+
274
+ ```bash
275
+ npm run build # ESM lib (dist/index.js) + IIFE bundle (dist/havik-player.global.js)
276
+ npm run typecheck
277
+ npm run lint
278
+ npm test # vitest
279
+ npm run test:coverage
280
+ npm run dev # demo app
281
+ ```
282
+
283
+ ## Security
284
+
285
+ See [SECURITY.md](SECURITY.md) for the vulnerability-disclosure policy.
286
+
287
+ ## License
288
+
289
+ ISC — see [LICENSE](LICENSE).
290
+
291
+ ### Third-party licenses
292
+
293
+ This package depends on, and the CDN bundle (`dist/havik-player.global.js`) statically
294
+ embeds, **hls.js** ([Apache-2.0](https://github.com/video-dev/hls.js/blob/master/LICENSE),
295
+ © Dailymotion). Full attribution is in [THIRD_PARTY_NOTICES.txt](THIRD_PARTY_NOTICES.txt),
296
+ which ships with the package and whose attribution is preserved in the bundle banner.
297
+
298
+ DRM uses the browser's built-in EME/CDM; Widevine™ (Google), FairPlay®/AirPlay® (Apple)
299
+ and PlayReady (Microsoft) are trademarks of their respective owners and no CDM is
300
+ distributed with this package.
@@ -0,0 +1,59 @@
1
+ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
2
+ ============================================
3
+
4
+ @oddin-gg/havik-player incorporates third-party software. The pre-built browser
5
+ bundle (dist/havik-player.global.js) statically embeds hls.js; the npm package
6
+ declares it as a runtime dependency. The applicable licenses and required
7
+ attributions are reproduced below.
8
+
9
+
10
+ --------------------------------------------------------------------------------
11
+ hls.js
12
+ --------------------------------------------------------------------------------
13
+ Project: hls.js
14
+ Homepage: https://github.com/video-dev/hls.js
15
+ License: Apache License, Version 2.0
16
+ Full text: http://www.apache.org/licenses/LICENSE-2.0
17
+
18
+ Copyright (c) 2017 Dailymotion (http://www.dailymotion.com)
19
+
20
+ Licensed under the Apache License, Version 2.0 (the "License");
21
+ you may not use this file except in compliance with the License.
22
+ You may obtain a copy of the License at
23
+
24
+ http://www.apache.org/licenses/LICENSE-2.0
25
+
26
+ Unless required by applicable law or agreed to in writing, software
27
+ distributed under the License is distributed on an "AS IS" BASIS,
28
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29
+ See the License for the specific language governing permissions and
30
+ limitations under the License.
31
+
32
+ src/remux/mp4-generator.js and src/demux/exp-golomb.ts implementation in this project
33
+ are derived from the HLS library for video.js (https://github.com/videojs/videojs-contrib-hls)
34
+
35
+ That work is also covered by the Apache 2 License, following copyright:
36
+ Copyright (c) 2013-2015 Brightcove
37
+
38
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
44
+ THE SOFTWARE.
45
+
46
+
47
+ --------------------------------------------------------------------------------
48
+ DRM / EME — trademark & licensing notice
49
+ --------------------------------------------------------------------------------
50
+ This SDK uses the browser's built-in Encrypted Media Extensions (EME) and the
51
+ Content Decryption Module (CDM) provided by the user's browser/operating system.
52
+ No DRM client or CDM is distributed with this package.
53
+
54
+ - Widevine™ is a trademark of Google LLC.
55
+ - FairPlay® and AirPlay® are trademarks of Apple Inc.
56
+ - PlayReady is a trademark of Microsoft Corporation.
57
+
58
+ These names are used for identification only. The corresponding CDMs are licensed
59
+ separately by their respective owners and are not provided by Oddin.gg.