@oddin-gg/havik-player 1.18.1 → 2.0.0

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/README.md CHANGED
@@ -1,26 +1,35 @@
1
1
  # @oddin-gg/havik-player
2
2
 
3
- HTML5 player SDK for Oddin live streams — **LL-HLS + DRM** against the
3
+ HTML5 player SDK for Oddin live streams: **LL-HLS + DRM** against the
4
4
  [havik-streams](https://github.com/oddin-gg/havik-streams) API, in three
5
5
  integration modes:
6
6
 
7
7
  | Mode | Entry | You provide | SDK owns |
8
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 |
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
12
 
13
13
  Widevine (Chrome/Firefox/Edge/Android) is fully supported. Safari/iOS plays via
14
14
  native passthrough; full FairPlay EME is a tracked fast-follow. See [Limitations](#limitations).
15
15
 
16
- > 📖 **Hosted docs & embed page:** [player-dev.oddin-video.gg](https://player-dev.oddin-video.gg) (source: [havik-player-site](https://github.com/oddin-gg/havik-player-site) — the site pins and bundles this SDK's released npm package) · **Full API reference:** [docs/API.md](docs/API.md) · **Examples** (vanilla / React / Vue): [examples/](examples/) · **API stability & versioning:** [docs/STABILITY.md](docs/STABILITY.md) · **Service status & endpoint directory:** [status-dev.oddin-video.gg](https://status-dev.oddin-video.gg)
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 — email **[havik-support@oddin.gg](mailto:havik-support@oddin.gg)** with the origins
23
- > you need allowlisted — and allow a few minutes for propagation. Never use a `["*"]`
16
+ > 📖 **Hosted docs and embed page:** [player-dev.oddin-video.gg](https://player-dev.oddin-video.gg).
17
+ > The site pins and bundles this SDK's released npm package; its source is
18
+ > [havik-player-site](https://github.com/oddin-gg/havik-player-site).
19
+ >
20
+ > - **Full API reference:** [docs/API.md](docs/API.md)
21
+ > - **Examples** (vanilla / React / Vue): [examples/](examples/)
22
+ > - **API stability and versioning:** [docs/STABILITY.md](docs/STABILITY.md)
23
+ > - **Service status and endpoint directory:** [status-dev.oddin-video.gg](https://status-dev.oddin-video.gg)
24
+
25
+ > ⚠️ **Before you integrate.** One prerequisite sits outside your code. The
26
+ > api-key's **AllowedOrigins** (and, for iframes, **AllowedIframeParents**) **and**
27
+ > the DRM service's **CORS allow-list** must both include every origin you serve
28
+ > the player from. Otherwise requests fail their preflight and DRM never starts.
29
+ >
30
+ > Onboard your origins with Oddin first. Email
31
+ > **[havik-support@oddin.gg](mailto:havik-support@oddin.gg)** with the origins you
32
+ > need allowlisted, then allow a few minutes for propagation. Never use a `["*"]`
24
33
  > allow-list in production.
25
34
 
26
35
  ---
@@ -38,12 +47,12 @@ Or via CDN (`<script>` / iframe), exposing `window.HavikPlayer`:
38
47
  ```
39
48
 
40
49
  You need two things to play a stream: a **match URN** (`od:match:…`) and a
41
- **publishable api-key** (`pk_live_…` / `pk_test_…`) whose **AllowedOrigins**
42
- includes the origin your player runs on. See [Auth & onboarding](#auth--onboarding).
50
+ **publishable api-key** (`pk_live_…` / `pk_test_…`). The key's
51
+ **AllowedOrigins** must include the origin your player runs on. See [Auth & onboarding](#auth--onboarding).
43
52
 
44
53
  ---
45
54
 
46
- ## Mode A — Managed player
55
+ ## Mode A: Managed player
47
56
 
48
57
  The SDK owns the `<video>`, bundles hls.js, and wires LL-HLS + Widevine DRM.
49
58
 
@@ -74,11 +83,12 @@ buffering recovery, and silent DRM **license-URL refresh** for long sessions.
74
83
 
75
84
  ### Controls & skinning
76
85
 
77
- Out of the box, Mode A renders a **branded, fully skinnable control bar** — seek,
78
- play/pause, volume, a live badge + go-live, quality / audio / subtitle menus,
79
- Picture-in-Picture, fullscreen, buffering + error/retry overlays, auto-hide, and
80
- keyboard a11y. Re-skin it to match your brand with a `theme` (or by overriding the
81
- `--havik-*` CSS variables); or opt out with `controls: 'native'` / `'none'`.
86
+ By default, Mode A renders a **branded, fully skinnable control bar**. It covers
87
+ seek, play/pause, volume, a live badge and go-live, quality / audio / subtitle
88
+ menus, Picture-in-Picture, fullscreen, buffering and error/retry overlays,
89
+ auto-hide, and keyboard a11y. Re-skin it to match your brand with a `theme`, or
90
+ by overriding the `--havik-*` CSS variables; or opt out with
91
+ `controls: 'native'` / `'none'`.
82
92
 
83
93
  ```ts
84
94
  await createPlayer({
@@ -93,7 +103,7 @@ await createPlayer({
93
103
 
94
104
  See [Controls & theming](docs/API.md#controls--theming) for the full theme/CSS-variable reference.
95
105
 
96
- ## Mode B — Bring your own player
106
+ ## Mode B: Bring your own player
97
107
 
98
108
  The SDK resolves the stream and gives you the DRM header helpers; you own the
99
109
  `<video>` and the playback engine.
@@ -111,15 +121,15 @@ const stream = await resolveStream({
111
121
 
112
122
  // stream = { manifestUrl, drmEnabled, drm: { widevine: { licenseUrl } }, … }
113
123
  // Wire into YOUR player. For hls.js, attach the license headers in licenseXhrSetup
114
- // (NOT xhrSetup — EME license requests route through licenseXhrSetup):
124
+ // (NOT xhrSetup: EME license requests route through licenseXhrSetup):
115
125
  const headers = licenseRequestHeaders(stream, cred); // { 'x-api-key', 'X-Match-Urn', 'X-Device-Id', 'Content-Type' }
116
126
  ```
117
127
 
118
- > **DRM rule:** POST to `drm.widevine.licenseUrl` **verbatim** — its signed query
128
+ > **DRM rule:** POST to `drm.widevine.licenseUrl` **verbatim**: its signed query
119
129
  > string _is_ the token. Never rewrite/normalize the URL, and use the **same
120
130
  > api-key** on playback and license requests or the license 403s.
121
131
 
122
- Discovery helpers (there is no push channel — polling only):
132
+ Discovery helpers (there is no push channel, so polling only):
123
133
 
124
134
  ```ts
125
135
  import { fetchCatalog, watchStatus } from '@oddin-gg/havik-player';
@@ -134,7 +144,7 @@ const watcher = watchStatus({
134
144
  // watcher.stop();
135
145
  ```
136
146
 
137
- ## Mode C — Iframe embed
147
+ ## Mode C: Iframe embed
138
148
 
139
149
  Drop in the hosted player page and control it over `postMessage`:
140
150
 
@@ -160,21 +170,21 @@ config from `window.HAVIK_EMBED_CONFIG` (server-injected) or the URL query.
160
170
  A ready-to-host template lives in the repo at [`embed/index.html`](embed/index.html) (not part of the npm package).
161
171
 
162
172
  > **Iframe auth:** the key's **AllowedIframeParents** must include the embedding
163
- > page's origin — iframe navigations send a `Referer`/`Sec-Fetch-Dest: iframe`,
164
- > which is what havik-streams matches against (no `Origin` header on a top-level
165
- > iframe nav).
173
+ > page's origin. Iframe navigations send a `Referer`/`Sec-Fetch-Dest: iframe`,
174
+ > which is what havik-streams matches against; there is no `Origin` header on a
175
+ > top-level iframe nav.
166
176
 
167
177
  ---
168
178
 
169
179
  ## Live pickup (`waitForLive`)
170
180
 
171
181
  A live match isn't playable until the stream is actually live. With `waitForLive`,
172
- the player arms and auto-attaches the instant it goes live — no user action.
182
+ the player arms and auto-attaches the instant it goes live, with no user action.
173
183
 
174
184
  **By default it does this over a push channel (SSE), not polling.** While the
175
- live-state stream is connected the player makes **zero** `/v1/playback` requests — it
176
- waits for the pushed go-live, then resolves once (so it doesn't repeatedly poll while
177
- waiting for kickoff). If the push channel is unavailable it transparently falls back
185
+ live-state stream is connected the player makes **zero** `/v1/playback` requests.
186
+ It waits for the pushed go-live, then resolves once, so it doesn't repeatedly poll
187
+ while waiting for kickoff. If the push channel is unavailable it transparently falls back
178
188
  to **polling** `425 TOO_EARLY` + `Retry-After` (scaled ~30s→1s near kickoff, never
179
189
  faster than the server asks). Set `liveStateEvents: false` to force polling.
180
190
 
@@ -187,8 +197,8 @@ waitForLive: {
187
197
  }
188
198
  ```
189
199
 
190
- **End of stream** is detected automatically: when the live stream finishes the player
191
- stops and fires `ended` — it never sits buffering on a stream that's over. See
200
+ **End of stream** is detected automatically: when the live stream finishes the
201
+ player stops and fires `ended`. It never sits buffering on a stream that's over. See
192
202
  [`subscribeLiveState`](docs/API.md#subscribelivestate) to consume the live-state
193
203
  channel directly.
194
204
 
@@ -196,37 +206,37 @@ channel directly.
196
206
 
197
207
  All API errors are a typed `PlaybackError` with `{ code, httpStatus, retryAfterMs?, requestId? }`:
198
208
 
199
- | code | HTTP | meaning | retry? |
200
- | ---------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
201
- | `INVALID_URN` | 400 | malformed URN | no |
202
- | `NOT_FOUND` | 404 | unknown URN **or** not entitled (indistinguishable) | no |
203
- | `GONE` | 410 | ended past catchup window | no |
204
- | `TOO_EARLY` | 425 | upcoming, not live yet | yes (waitForLive) |
205
- | `UNAVAILABLE` | 503 | should-be-live, origin warming up | yes |
206
- | `UNAUTHORIZED` / `FORBIDDEN` | 401 / 403 | bad key / origin not allowed / DRM rejected **server-side** | no |
207
- | `DRM_CLIENT` | 0 or license status | **client-side** DRM failure — key system unusable on this platform, CDM/EME error, or license transport exhausted (not an entitlement denial) | sometimes — the control bar offers retry (see docs/API.md) |
208
- | `RATE_LIMITED` | 429 | per-IP limiter | back off |
209
+ | code | HTTP | meaning | retry? |
210
+ | ---------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
211
+ | `INVALID_URN` | 400 | malformed URN | no |
212
+ | `NOT_FOUND` | 404 | unknown URN **or** not entitled (indistinguishable) | no |
213
+ | `GONE` | 410 | ended past catchup window | no |
214
+ | `TOO_EARLY` | 425 | upcoming, not live yet | yes (waitForLive) |
215
+ | `UNAVAILABLE` | 503 | should-be-live, origin warming up | yes |
216
+ | `UNAUTHORIZED` / `FORBIDDEN` | 401 / 403 | bad key / origin not allowed / DRM rejected **server-side** | no |
217
+ | `DRM_CLIENT` | 0 or license status | **client-side** DRM failure: key system unusable on this platform, CDM/EME error, or license transport exhausted (not an entitlement denial) | sometimes; the control bar offers retry (see docs/API.md) |
218
+ | `RATE_LIMITED` | 429 | per-IP limiter | back off |
209
219
 
210
220
  ---
211
221
 
212
222
  ## Auth & onboarding
213
223
 
214
224
  The `x-api-key` is a **publishable** key, by design embeddable in browser JS. Its
215
- only guardrail is the **server-side per-key allowlist** — so onboarding each
225
+ only guardrail is the **server-side per-key allowlist**, so onboarding each
216
226
  integrator origin is a hard dependency:
217
227
 
218
228
  1. **havik-streams**: the key's `AllowedOrigins` must include every origin your
219
229
  player runs on (and `AllowedIframeParents` for Mode C). Never ship `["*"]` to
220
- prod — it disables the guardrail.
230
+ prod; it disables the guardrail.
221
231
  2. **havik-drm (`drm-proxy`)**: its `CORS_ALLOWED_ORIGINS` must also include those
222
232
  origins, or the cross-origin license POST fails its preflight and DRM never
223
233
  starts (silent black video).
224
234
  3. Scope the key's entitlements to the tournaments you serve.
225
235
 
226
236
  **Where to send onboarding requests:** email
227
- [havik-support@oddin.gg](mailto:havik-support@oddin.gg) with the origins (and iframe
228
- parents, for Mode C) you need allowlisted, plus the tournaments you expect to serve.
229
- Steps 1 and 2 are applied on our side — you cannot self-serve them.
237
+ [havik-support@oddin.gg](mailto:havik-support@oddin.gg) with the origins you need
238
+ allowlisted, and the iframe parents for Mode C. Include the tournaments you expect
239
+ to serve. Steps 1 and 2 are applied on our side; you cannot self-serve them.
230
240
 
231
241
  Revocation isn't instant (≤5min validation cache + ≤24h rotation grace).
232
242
 
@@ -255,7 +265,7 @@ The key's `AllowedOrigins` must include `http://localhost:5173`.
255
265
  passthrough, which only plays DRM where the OS already trusts the origins. Full
256
266
  FairPlay EME (via a Shaka adapter behind the `PlaybackEngine` seam) is a tracked
257
267
  fast-follow.
258
- - **HLS only** (no DASH); **Widevine + FairPlay** key systems (no PlayReady — Edge
268
+ - **HLS only** (no DASH); **Widevine + FairPlay** key systems (no PlayReady; Edge
259
269
  uses Widevine). Container CMAF/fMP4, encryption cbcs.
260
270
  - hls.js is pinned exactly (see `package.json` for the current version).
261
271
  LL-HLS+DRM playback is sensitive to hls.js regressions, so bumps are
@@ -263,10 +273,11 @@ The key's `AllowedOrigins` must include `http://localhost:5173`.
263
273
 
264
274
  ## Analytics (QoE beacons)
265
275
 
266
- Mode A emits anonymous QoE beacons (startup time, heartbeat byte/quality deltas,
267
- state changes, errors, session end) to the Havik analytics plane, correlated with
268
- CDN logs via a server-minted CMCD session id (`sid`). The `sid` lives **only in
269
- JS memory** — never in storage or cookies — and dies with the player. Opt out or
276
+ Mode A emits anonymous QoE beacons to the Havik analytics plane: startup time,
277
+ heartbeat byte and quality deltas, state changes, errors, and session end. They
278
+ are correlated with CDN logs via a server-minted CMCD session id (`sid`). The
279
+ `sid` lives **only in JS memory**, never in storage or cookies, and dies with the
280
+ player. Opt out or
270
281
  redirect with the `analytics` option on `createPlayer`
271
282
  (`analytics: false | { endpoint?: string }`); without a server-minted `sid` the
272
283
  emitter is inert.
@@ -275,17 +286,18 @@ emitter is inert.
275
286
 
276
287
  The SDK persists a random per-device id in `localStorage` and sends it as
277
288
  `X-Device-Id` on every DRM license request (required by the license server). It is a
278
- stable cross-session identifier — disclose it in your privacy policy and offer a reset
279
- via [`clearDeviceId()`](docs/API.md#getdeviceid--cleardeviceid). The QoE beacons
280
- above are anonymous (no IP is stored and the user agent is minimized on arrival;
281
- the in-memory `sid` is not a cross-session identifier). No other personal data is
289
+ stable cross-session identifier, so disclose it in your privacy policy and offer a
290
+ reset via [`clearDeviceId()`](docs/API.md#getdeviceid--cleardeviceid). The QoE
291
+ beacons above are anonymous. No IP is stored, the user agent is minimized on
292
+ arrival, and the in-memory `sid` is not a cross-session identifier. No other personal data is
282
293
  collected by the SDK.
283
294
 
284
295
  ## Browser & runtime support
285
296
 
286
- - **Browser-only.** Importing the package is side-effect-safe (verified in CI), but
287
- the SDK must be used **client-side** — call `createPlayer`/`mountEmbed` in the
288
- browser, not during SSR (e.g. dynamic-import it in a Next.js client component).
297
+ - **Browser-only.** Importing the package is side-effect-safe (verified in CI),
298
+ but the SDK must be used **client-side**. Call `createPlayer`/`mountEmbed` in
299
+ the browser, not during SSR; in Next.js, dynamic-import it in a client
300
+ component.
289
301
  - Supported targets: see [`.browserslistrc`](.browserslistrc) (Chrome/Edge ≥94,
290
302
  Firefox ≥91, Safari/iOS ≥15). DRM = Widevine on Chrome/Edge/Firefox/Android;
291
303
  FairPlay (Safari/iOS) is not yet implemented.
@@ -307,7 +319,7 @@ See [SECURITY.md](SECURITY.md) for the vulnerability-disclosure policy.
307
319
 
308
320
  ## License
309
321
 
310
- ISC — see [LICENSE](LICENSE).
322
+ ISC; see [LICENSE](LICENSE).
311
323
 
312
324
  ### Third-party licenses
313
325
 
@@ -316,6 +328,6 @@ embeds, **hls.js** ([Apache-2.0](https://github.com/video-dev/hls.js/blob/master
316
328
  © Dailymotion). Full attribution is in [THIRD_PARTY_NOTICES.txt](THIRD_PARTY_NOTICES.txt),
317
329
  which ships with the package and whose attribution is preserved in the bundle banner.
318
330
 
319
- DRM uses the browser's built-in EME/CDM; Widevine™ (Google), FairPlay®/AirPlay® (Apple)
320
- and PlayReady (Microsoft) are trademarks of their respective owners and no CDM is
321
- distributed with this package.
331
+ DRM uses the browser's built-in EME/CDM. Widevine™ (Google), FairPlay®/AirPlay®
332
+ (Apple) and PlayReady (Microsoft) are trademarks of their respective owners, and
333
+ no CDM is distributed with this package.