@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 +77 -65
- package/dist/havik-player.global.js +1 -1
- package/dist/havik-player.global.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/API.md +134 -134
- package/docs/STABILITY.md +8 -7
- package/embed/index.html +1 -1
- package/examples/vanilla.html +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
# @oddin-gg/havik-player
|
|
2
2
|
|
|
3
|
-
HTML5 player SDK for Oddin live streams
|
|
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
|
|
10
|
-
| **B
|
|
11
|
-
| **C
|
|
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
|
|
17
|
-
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
-
>
|
|
23
|
-
>
|
|
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_…`)
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
play/pause, volume, a live badge
|
|
79
|
-
Picture-in-Picture, fullscreen, buffering
|
|
80
|
-
keyboard a11y. Re-skin it to match your brand with a `theme
|
|
81
|
-
`--havik-*` CSS variables
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
164
|
-
> which is what havik-streams matches against
|
|
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
|
|
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
|
|
176
|
-
waits for the pushed go-live, then resolves once
|
|
177
|
-
waiting for kickoff
|
|
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
|
|
191
|
-
stops and fires `ended
|
|
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
|
|
200
|
-
| ---------------------------- | ------------------- |
|
|
201
|
-
| `INVALID_URN` | 400 | malformed URN
|
|
202
|
-
| `NOT_FOUND` | 404 | unknown URN **or** not entitled (indistinguishable)
|
|
203
|
-
| `GONE` | 410 | ended past catchup window
|
|
204
|
-
| `TOO_EARLY` | 425 | upcoming, not live yet
|
|
205
|
-
| `UNAVAILABLE` | 503 | should-be-live, origin warming up
|
|
206
|
-
| `UNAUTHORIZED` / `FORBIDDEN` | 401 / 403 | bad key / origin not allowed / DRM rejected **server-side**
|
|
207
|
-
| `DRM_CLIENT` | 0 or license status | **client-side** DRM failure
|
|
208
|
-
| `RATE_LIMITED` | 429 | per-IP limiter
|
|
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
|
|
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
|
|
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
|
|
228
|
-
|
|
229
|
-
Steps 1 and 2 are applied on our side
|
|
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
|
|
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
|
|
267
|
-
state changes, errors, session end
|
|
268
|
-
CDN logs via a server-minted CMCD session id (`sid`). The
|
|
269
|
-
JS memory
|
|
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
|
|
279
|
-
via [`clearDeviceId()`](docs/API.md#getdeviceid--cleardeviceid). The QoE
|
|
280
|
-
above are anonymous
|
|
281
|
-
the in-memory `sid` is not a cross-session identifier
|
|
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),
|
|
287
|
-
the SDK must be used **client-side
|
|
288
|
-
browser, not during SSR
|
|
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
|
|
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
|
|
320
|
-
and PlayReady (Microsoft) are trademarks of their respective owners and
|
|
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.
|