@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/docs/API.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Havik Player SDK
|
|
1
|
+
# Havik Player SDK API Reference
|
|
2
2
|
|
|
3
3
|
`@oddin-gg/havik-player` · HTML5 player SDK for Oddin live streams (LL‑HLS + DRM).
|
|
4
4
|
|
|
@@ -17,12 +17,12 @@ This is the complete API reference. For a quick start, see the [README](../READM
|
|
|
17
17
|
- [Live pickup](#live-pickup)
|
|
18
18
|
- [DRM](#drm)
|
|
19
19
|
- [Browser support](#browser-support)
|
|
20
|
-
- [Mode A
|
|
20
|
+
- [Mode A: Managed player](#mode-a-managed-player)
|
|
21
21
|
- [createPlayer](#createplayer)
|
|
22
22
|
- [Player](#player) · [Player events](#playereventmap)
|
|
23
23
|
- [Controls & theming](#controls--theming) · [HavikTheme](#haviktheme)
|
|
24
|
-
- [Mode B
|
|
25
|
-
- [Mode C
|
|
24
|
+
- [Mode B: Bring your own player](#mode-b-bring-your-own-player)
|
|
25
|
+
- [Mode C: Iframe embed](#mode-c-iframe-embed)
|
|
26
26
|
- [Reference](#reference)
|
|
27
27
|
- [Errors](#errors)
|
|
28
28
|
- [Data types](#data-types)
|
|
@@ -41,9 +41,9 @@ The SDK plays Oddin live streams in the browser over **Low‑Latency HLS** with
|
|
|
41
41
|
|
|
42
42
|
| Mode | Entry point | You provide | The SDK handles |
|
|
43
43
|
| ----------------------------- | ----------------------------------- | ----------------------- | -------------------------------------------------- |
|
|
44
|
-
| **A
|
|
45
|
-
| **B
|
|
46
|
-
| **C
|
|
44
|
+
| **A (managed)** | [`createPlayer()`](#createplayer) | a `<video>` element | hls.js, LL‑HLS, DRM/EME, retries, stats |
|
|
45
|
+
| **B (bring your own player)** | [`resolveStream()`](#resolvestream) | your player + `<video>` | auth → catalog → playback resolution + DRM helpers |
|
|
46
|
+
| **C (iframe embed)** | [`mountEmbed()`](#mountembed) | an iframe slot | everything, behind a `postMessage` API |
|
|
47
47
|
|
|
48
48
|
All modes are exported from the package root and are fully typed (TypeScript
|
|
49
49
|
definitions ship in the package).
|
|
@@ -84,20 +84,20 @@ IIFE bundle for `<script>`/iframe use.
|
|
|
84
84
|
|
|
85
85
|
Two values are required to play any stream:
|
|
86
86
|
|
|
87
|
-
- A **match URN
|
|
88
|
-
- A **publishable api‑key
|
|
87
|
+
- A **match URN**: identifies the event, e.g. `od:match:1234`.
|
|
88
|
+
- A **publishable api‑key**: e.g. `pk_live_…` (production) or `pk_test_…` (test).
|
|
89
89
|
The key must be configured with an **allowed origin list** that includes the
|
|
90
90
|
origin your player runs on. See [Onboarding & CORS](#onboarding--cors).
|
|
91
91
|
|
|
92
|
-
You also need the **API base URL
|
|
92
|
+
You also need the **API base URL**: `https://feed-dev.oddin-video.gg` for the
|
|
93
93
|
integration environment, `https://feed.oddin-video.gg` for production. Treat the
|
|
94
|
-
endpoint directory as the source of truth rather than hard-coding either
|
|
94
|
+
endpoint directory as the source of truth rather than hard-coding either. See
|
|
95
95
|
[Service URL discovery](#service-url-discovery).
|
|
96
96
|
|
|
97
97
|
### Credentials
|
|
98
98
|
|
|
99
99
|
Every API call carries your api‑key. Pass it as a [`Credential`](#credential), or as a
|
|
100
|
-
function returning one
|
|
100
|
+
function returning one; see [`CredentialSource`](#credentialsource). The function form is
|
|
101
101
|
called per request, so it supports transparent refresh if you later move to
|
|
102
102
|
short‑lived tokens.
|
|
103
103
|
|
|
@@ -111,17 +111,17 @@ const credential = async () => ({ apiKey: await myTokenService.get() });
|
|
|
111
111
|
|
|
112
112
|
### The play sequence
|
|
113
113
|
|
|
114
|
-
1. **(Optional) Discover** live/upcoming matches
|
|
115
|
-
watch a single match's status
|
|
116
|
-
2. **Resolve playback
|
|
114
|
+
1. **(Optional) Discover** live/upcoming matches with [`fetchCatalog()`](#fetchcatalog), or
|
|
115
|
+
watch a single match's status with [`watchStatus()`](#watchstatus).
|
|
116
|
+
2. **Resolve playback**: [`resolveStream()`](#resolvestream) returns a
|
|
117
117
|
[`StreamDescriptor`](#streamdescriptor) with the manifest URL and DRM config.
|
|
118
|
-
3. **Play
|
|
118
|
+
3. **Play**: feed the descriptor to the managed player (Mode A) or your own player
|
|
119
119
|
(Mode B).
|
|
120
120
|
4. **DRM licenses** are acquired automatically by the player's EME layer as the CDM
|
|
121
121
|
encounters encrypted media.
|
|
122
122
|
|
|
123
123
|
Match status is available as a **push** channel ([`subscribeLiveState`](#subscribelivestate),
|
|
124
|
-
SSE)
|
|
124
|
+
SSE), used automatically by the managed player, with polling
|
|
125
125
|
([`watchStatus`](#watchstatus) / `waitForLive`) as the fallback. The authoritative
|
|
126
126
|
signal that a stream is playable is still a successful playback resolution (not catalog
|
|
127
127
|
status, which can briefly lead the stream going live).
|
|
@@ -134,10 +134,10 @@ action. By default it does this over the **push** channel (SSE,
|
|
|
134
134
|
[`subscribeLiveState`](#subscribelivestate)): while connected it makes no
|
|
135
135
|
`/v1/playback` requests, waits for the pushed `live`, then resolves once. If the SSE
|
|
136
136
|
endpoint is unavailable it **falls back to polling** `TOO_EARLY` + the server's
|
|
137
|
-
`Retry-After`. End-of-stream is detected automatically too
|
|
137
|
+
`Retry-After`. End-of-stream is detected automatically too: the player stops and
|
|
138
138
|
fires `ended`. And ended is not necessarily forever: if the encoder reconnects after
|
|
139
139
|
an outage long enough to tear the session down, the recreated session serves under a
|
|
140
|
-
**new manifest URL
|
|
140
|
+
**new manifest URL**: so on the next pushed `live` the player **rejoins**
|
|
141
141
|
automatically (re-resolves and re-attaches, `rejoinOnLive` option). Set
|
|
142
142
|
`liveStateEvents: false` to force polling-only.
|
|
143
143
|
|
|
@@ -150,7 +150,7 @@ See [`WaitForLive`](#waitforlive) for tuning, and [`createPlayer`](#createplayer
|
|
|
150
150
|
PlayReady is not used (Windows/Edge play via Widevine).
|
|
151
151
|
- **Container/encryption:** CMAF/fMP4, `cbcs`.
|
|
152
152
|
- The playback descriptor's `drm.*.licenseUrl` values are **pre‑signed**. When you
|
|
153
|
-
integrate your own player (Mode B), you must **POST to the license URL verbatim
|
|
153
|
+
integrate your own player (Mode B), you must **POST to the license URL verbatim**:
|
|
154
154
|
its signed query string is the access token; do not rewrite, re‑encode, or strip
|
|
155
155
|
parameters. Use the **same api‑key** to resolve playback and to fetch licenses.
|
|
156
156
|
The managed and iframe players handle this for you.
|
|
@@ -164,7 +164,7 @@ See [`WaitForLive`](#waitforlive) for tuning, and [`createPlayer`](#createplayer
|
|
|
164
164
|
>
|
|
165
165
|
> **iOS third‑party browsers (Chrome/Firefox/Edge on iOS):** iOS has no
|
|
166
166
|
> Widevine CDM, so protected playback in these browsers depends entirely on
|
|
167
|
-
> whether the WebKit build grants them FairPlay. Don't assume either way
|
|
167
|
+
> whether the WebKit build grants them FairPlay. Don't assume either way:
|
|
168
168
|
> call [`detectDrmSupport`](#detectdrmsupport) and let the probe decide, so
|
|
169
169
|
> viewers see an accurate message instead of a black player.
|
|
170
170
|
|
|
@@ -175,7 +175,7 @@ See [`WaitForLive`](#waitforlive) for tuning, and [`createPlayer`](#createplayer
|
|
|
175
175
|
| Chrome, Edge (desktop & Android) | hls.js | Widevine ✓ |
|
|
176
176
|
| Firefox | hls.js | Widevine ✓ |
|
|
177
177
|
| Safari (macOS, iOS 17.1+) | hls.js (via MMS) | FairPlay ✓ (`com.apple.fps`) |
|
|
178
|
-
| Older Safari / iOS | native HLS (fallback) | FairPlay
|
|
178
|
+
| Older Safari / iOS | native HLS (fallback) | FairPlay, OS passthrough only |
|
|
179
179
|
| Chrome/Firefox/Edge **on iOS** | hls.js | probe with `detectDrmSupport` (no Widevine on iOS) |
|
|
180
180
|
|
|
181
181
|
Requires a browser with Media Source Extensions (or ManagedMediaSource) and
|
|
@@ -183,7 +183,7 @@ Encrypted Media Extensions (all evergreen browsers).
|
|
|
183
183
|
|
|
184
184
|
---
|
|
185
185
|
|
|
186
|
-
## Mode A
|
|
186
|
+
## Mode A: Managed player
|
|
187
187
|
|
|
188
188
|
The SDK owns the `<video>` element, bundles hls.js, and wires LL‑HLS + DRM
|
|
189
189
|
automatically. It also handles live‑pickup retries, buffering recovery, stats, and
|
|
@@ -219,87 +219,87 @@ player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
|
|
|
219
219
|
|
|
220
220
|
#### `CreatePlayerOptions`
|
|
221
221
|
|
|
222
|
-
| Option | Type | Default | Description
|
|
223
|
-
| --------------------- | --------------------------------------- | -------------------------------- |
|
|
224
|
-
| `video` | `HTMLVideoElement` | — | **Required.** The element the SDK will control.
|
|
225
|
-
| `baseUrl` | `string` | — | **Required.** API base URL, e.g. `https://feed-dev.oddin-video.gg`.
|
|
226
|
-
| `matchUrn` | `string` | — | **Required.** The match to play, e.g. `od:match:1234`.
|
|
227
|
-
| `credential` | [`CredentialSource`](#credentialsource) | — | **Required.** Your api‑key (or a function returning one).
|
|
228
|
-
| `autoplay` | `boolean` | `true` | Start playback once ready.
|
|
229
|
-
| `muted` | `boolean` | `false` | Mute the element. Set `true` for reliable autoplay under browser sound policies.
|
|
230
|
-
| `controls` | `'custom' \| 'native' \| 'none'` | `'custom'` | Which controls to render. See [Controls & theming](#controls--theming).
|
|
231
|
-
| `theme` | [`Partial<HavikTheme>`](#haviktheme) | Oddin brand | Re-skin the custom control bar (ignored unless `controls: 'custom'`). See [Controls & theming](#controls--theming).
|
|
232
|
-
| `statusOverlays` | `boolean` | `true` | Branded full-bleed cards for the stopped/pre-play states (ended, fatal error, pre-play, armed-waiting) instead of a frozen frame + a dead play button. Themed via the control-bar tokens + `theme.logoUrl`. Set `false` to render your own UI from the `ended`/`error` events. Ignored unless `controls: 'custom'`.
|
|
233
|
-
| `endedMessage` | `string` | "This stream has ended" | Message on the end-of-stream card.
|
|
234
|
-
| `interruptedMessage` | `string` | "Reconnecting — please stand by" | Message shown **instead of** `endedMessage` when the server reports the stream stopped but the match is still running (`endedReason: 'interrupted'
|
|
235
|
-
| `errorMessage` | `string` | the error's message | Message on the fatal-error card (otherwise the error's own message).
|
|
236
|
-
| `waitForLive` | [`WaitForLive`](#waitforlive) | — | Arm on an upcoming match and auto‑attach at go‑live.
|
|
237
|
-
| `lowLatency` | `'auto' \| boolean` | `'auto'` | `'auto'` lets the player decide from the manifest.
|
|
238
|
-
| `engine` | `'auto' \| 'hls' \| 'native'` | `'auto'` | `'auto'` prefers hls.js wherever it is supported
|
|
239
|
-
| `userId` | `string` | — | Optional viewer id (sent as `X-User-Id` on license requests).
|
|
240
|
-
| `debug` | `boolean` | `false` | Turn on hls.js's internal console logging
|
|
241
|
-
| `hlsConfig` | `Partial<HlsConfig>` | — | Escape hatch for advanced hls.js tuning.
|
|
242
|
-
| `statsIntervalMs` | `number` | `2000` | How often `'stats'` is emitted.
|
|
243
|
-
| `licenseRefreshMs` | `number` | `480000` (8 min) | Silent DRM license‑URL refresh interval for long sessions. `0` disables.
|
|
244
|
-
| `poster` | `string` | — | Poster image shown before playback starts.
|
|
245
|
-
| `startLevel` | `number` | `-1` | Initial rendition index (`-1` = auto). With auto, ABR starts on a conservative, sustainable rendition (~360p) and ramps up as bandwidth allows
|
|
246
|
-
| `maxBitrate` | `number` | — | Cap ABR to renditions at/below this bitrate (bps).
|
|
247
|
-
| `liveLatencyTarget` | `number` | `2` | Target live‑edge latency in seconds (hls.js `liveSyncDuration`). The player derives a matching latency **ceiling** internally and catches up at 1.5×, so this is the only live‑latency knob you need. Lower = closer to live but more rebuffer‑prone. Live/LL only.
|
|
248
|
-
| `snapToLiveOnRefocus` | `boolean` | `true` | Snap to the live edge when a backgrounded tab is refocused while far behind. Live/LL only.
|
|
249
|
-
| `liveStateEvents` | `boolean` | `true` | Use the push-based **live-state SSE** channel for go-live and end-of-stream (see [Live state](#live-state-sse)). Graceful: falls back to polling if the endpoint is unreachable. Set `false` to disable.
|
|
250
|
-
| `rejoinOnLive` | `boolean` | `true` | Rejoin automatically when a stream that ended comes back live. An ingest outage longer than the bridge's reconnect grace recreates the session under a **new manifest URL** (the old playlist freezes at HTTP 200), so the player keeps the live-state subscription open after `ended` (until `gone`) and re-arms on a pushed `live`. Set `false` to keep `ended` terminal. Needs `liveStateEvents`.
|
|
251
|
-
| `eventsBaseUrl` | `string` | derived | Override the SSE endpoint base. Default: `baseUrl` with the host prefixed by `events.` (e.g. `feed.<d>` → `events.<d>`).
|
|
252
|
-
|
|
253
|
-
### Low
|
|
222
|
+
| Option | Type | Default | Description |
|
|
223
|
+
| --------------------- | --------------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
224
|
+
| `video` | `HTMLVideoElement` | — | **Required.** The element the SDK will control. |
|
|
225
|
+
| `baseUrl` | `string` | — | **Required.** API base URL, e.g. `https://feed-dev.oddin-video.gg`. |
|
|
226
|
+
| `matchUrn` | `string` | — | **Required.** The match to play, e.g. `od:match:1234`. |
|
|
227
|
+
| `credential` | [`CredentialSource`](#credentialsource) | — | **Required.** Your api‑key (or a function returning one). |
|
|
228
|
+
| `autoplay` | `boolean` | `true` | Start playback once ready. |
|
|
229
|
+
| `muted` | `boolean` | `false` | Mute the element. Set `true` for reliable autoplay under browser sound policies. |
|
|
230
|
+
| `controls` | `'custom' \| 'native' \| 'none'` | `'custom'` | Which controls to render. See [Controls & theming](#controls--theming). |
|
|
231
|
+
| `theme` | [`Partial<HavikTheme>`](#haviktheme) | Oddin brand | Re-skin the custom control bar (ignored unless `controls: 'custom'`). See [Controls & theming](#controls--theming). |
|
|
232
|
+
| `statusOverlays` | `boolean` | `true` | Branded full-bleed cards for the stopped/pre-play states (ended, fatal error, pre-play, armed-waiting) instead of a frozen frame + a dead play button. Themed via the control-bar tokens + `theme.logoUrl`. Set `false` to render your own UI from the `ended`/`error` events. Ignored unless `controls: 'custom'`. |
|
|
233
|
+
| `endedMessage` | `string` | "This stream has ended" | Message on the end-of-stream card. |
|
|
234
|
+
| `interruptedMessage` | `string` | "Reconnecting — please stand by" | Message shown **instead of** `endedMessage` when the server reports the stream stopped but the match is still running (`endedReason: 'interrupted'`, an ingest outage that outlived the bridge's hold). The player rejoins automatically when the stream returns **while `rejoinOnLive` is on** (the default); override this wording if you disable auto-rejoin, since nothing will re-attach on its own. |
|
|
235
|
+
| `errorMessage` | `string` | the error's message | Message on the fatal-error card (otherwise the error's own message). |
|
|
236
|
+
| `waitForLive` | [`WaitForLive`](#waitforlive) | — | Arm on an upcoming match and auto‑attach at go‑live. |
|
|
237
|
+
| `lowLatency` | `'auto' \| boolean` | `'auto'` | `'auto'` lets the player decide from the manifest. |
|
|
238
|
+
| `engine` | `'auto' \| 'hls' \| 'native'` | `'auto'` | `'auto'` prefers hls.js wherever it is supported, including modern Safari/iOS via ManagedMediaSource, and falls back to native HLS otherwise. |
|
|
239
|
+
| `userId` | `string` | — | Optional viewer id (sent as `X-User-Id` on license requests). |
|
|
240
|
+
| `debug` | `boolean` | `false` | Turn on hls.js's internal console logging: fragment/part scheduling, ABR switches, EME key sessions and license traffic. For diagnosing a playback problem with Oddin support; leave it off in production (it logs on every fragment, and the EME lines include license request/response metadata). hls.js engine only. `hlsConfig.debug` still wins if you set both. |
|
|
241
|
+
| `hlsConfig` | `Partial<HlsConfig>` | — | Escape hatch for advanced hls.js tuning. |
|
|
242
|
+
| `statsIntervalMs` | `number` | `2000` | How often `'stats'` is emitted. |
|
|
243
|
+
| `licenseRefreshMs` | `number` | `480000` (8 min) | Silent DRM license‑URL refresh interval for long sessions. `0` disables. |
|
|
244
|
+
| `poster` | `string` | — | Poster image shown before playback starts. |
|
|
245
|
+
| `startLevel` | `number` | `-1` | Initial rendition index (`-1` = auto). With auto, ABR starts on a conservative, sustainable rendition (~360p) and ramps up as bandwidth allows; see [Low‑latency & adaptive bitrate](#low-latency--adaptive-bitrate). |
|
|
246
|
+
| `maxBitrate` | `number` | — | Cap ABR to renditions at/below this bitrate (bps). |
|
|
247
|
+
| `liveLatencyTarget` | `number` | `2` | Target live‑edge latency in seconds (hls.js `liveSyncDuration`). The player derives a matching latency **ceiling** internally and catches up at 1.5×, so this is the only live‑latency knob you need. Lower = closer to live but more rebuffer‑prone. Live/LL only. |
|
|
248
|
+
| `snapToLiveOnRefocus` | `boolean` | `true` | Snap to the live edge when a backgrounded tab is refocused while far behind. Live/LL only. |
|
|
249
|
+
| `liveStateEvents` | `boolean` | `true` | Use the push-based **live-state SSE** channel for go-live and end-of-stream (see [Live state](#live-state-sse)). Graceful: falls back to polling if the endpoint is unreachable. Set `false` to disable. |
|
|
250
|
+
| `rejoinOnLive` | `boolean` | `true` | Rejoin automatically when a stream that ended comes back live. An ingest outage longer than the bridge's reconnect grace recreates the session under a **new manifest URL** (the old playlist freezes at HTTP 200), so the player keeps the live-state subscription open after `ended` (until `gone`) and re-arms on a pushed `live`. Set `false` to keep `ended` terminal. Needs `liveStateEvents`. |
|
|
251
|
+
| `eventsBaseUrl` | `string` | derived | Override the SSE endpoint base. Default: `baseUrl` with the host prefixed by `events.` (e.g. `feed.<d>` → `events.<d>`). |
|
|
252
|
+
|
|
253
|
+
### Low-latency & adaptive bitrate
|
|
254
254
|
|
|
255
255
|
The player ships tuned defaults for Oddin's low‑latency HLS, so you normally
|
|
256
256
|
don't need to set any of the live or ABR knobs:
|
|
257
257
|
|
|
258
|
-
- **Live latency
|
|
258
|
+
- **Live latency**: playback targets **~2 s** behind the live edge
|
|
259
259
|
(`liveLatencyTarget` → hls.js `liveSyncDuration`). It deliberately sits back
|
|
260
260
|
from the bleeding `PART‑HOLD‑BACK` edge, where the newest partial segments
|
|
261
261
|
aren't reliably published yet; chasing that edge causes constant rebuffering.
|
|
262
262
|
Set `liveLatencyTarget` lower for closer‑to‑live (more rebuffer‑prone) or
|
|
263
|
-
higher for steadier playback. _Lowered from 3 s
|
|
264
|
-
- **Drift ceiling
|
|
263
|
+
higher for steadier playback. _Lowered from 3 s; see the migration note below._
|
|
264
|
+
- **Drift ceiling**: after a stall, the player catches up at up to **1.5×** and,
|
|
265
265
|
as a backstop, force‑seeks back toward the target once latency exceeds an
|
|
266
266
|
internal **ceiling (~12 s, derived as `max(12, liveLatencyTarget + 9)`)**. This is what
|
|
267
267
|
stops latency from snowballing to tens of seconds after buffering. The ceiling
|
|
268
268
|
is internal and always kept above your `liveLatencyTarget`.
|
|
269
|
-
- **Over‑seek clamp
|
|
269
|
+
- **Over‑seek clamp**: the drift ceiling's counterpart on the _near_ side. A seek
|
|
270
270
|
that lands closer to live than `liveLatencyTarget` is pulled back onto it, so
|
|
271
271
|
dragging the scrub bar to its far right means **"go live"** rather than landing
|
|
272
|
-
on the bleeding edge
|
|
272
|
+
on the bleeding edge, where latency reads under a second and playback stutters
|
|
273
273
|
frame‑by‑frame on an empty forward buffer. hls.js can't recover from that state
|
|
274
274
|
on its own (its live‑sync only ever corrects being _behind_), so the stutter
|
|
275
|
-
would otherwise persist for the rest of the session. Live only
|
|
275
|
+
would otherwise persist for the rest of the session. Live only: VOD seeks are
|
|
276
276
|
untouched, and the clamp stands down while chasing is suspended on a stale
|
|
277
277
|
playlist (there is no live edge left to protect at that point). On the custom
|
|
278
278
|
control bar the bar's right end **is** the target, so the thumb reads full at
|
|
279
279
|
the live edge. Bring‑your‑own‑player (Mode B) owns its own seek UI and needs
|
|
280
280
|
the equivalent clamp itself.
|
|
281
|
-
- **Audio‑hole bridge
|
|
281
|
+
- **Audio‑hole bridge**: `maxBufferHole` is raised to **0.5 s** so the ~0.45 s
|
|
282
282
|
audio "remainder" gap at each segment boundary is bridged as a sub‑perceptible
|
|
283
283
|
hitch instead of a rebuffer.
|
|
284
|
-
- **Start quality
|
|
284
|
+
- **Start quality**: auto ABR seeds a **~1 Mbps** estimate so the first
|
|
285
285
|
rendition is one a modest link can sustain (**~360p**) and then **ramps up**
|
|
286
|
-
within a few seconds as measured bandwidth allows
|
|
286
|
+
within a few seconds as measured bandwidth allows, rather than grabbing a high
|
|
287
287
|
rung and rebuffering on a constrained link. This trades a slightly softer first
|
|
288
288
|
~2 s for a **stall‑free start** (the first‑seconds buffering fix). In addition,
|
|
289
289
|
the session is bounded to the player's pixel box, so a small/embedded player
|
|
290
|
-
won't fetch 1080p
|
|
290
|
+
won't fetch 1080p, but by an SDK‑managed, **debounced** size cap rather than
|
|
291
291
|
hls.js's own `capLevelToPlayerSize` poller: a size change only reaches ABR
|
|
292
292
|
once the box has been stable for **~2 s** (raise), and a shrink is applied
|
|
293
293
|
**lazily (~30 s)**, so a fullscreen enter/exit never forces a quality
|
|
294
|
-
switch
|
|
294
|
+
switch, and its ~1 s stall, mid‑transition (the higher rendition simply
|
|
295
295
|
downscales into the smaller box until the down‑cap lands). Passing
|
|
296
296
|
`hlsConfig.capLevelToPlayerSize: true` restores hls.js's immediate poller and
|
|
297
297
|
stands the SDK manager down. Pin a fixed rung with `startLevel`,
|
|
298
298
|
cap the top with `maxBitrate`, or raise the seed via
|
|
299
299
|
`hlsConfig.abrEwmaDefaultEstimate` if you want a sharper (higher‑risk) start.
|
|
300
300
|
|
|
301
|
-
All of these can be overridden through
|
|
302
|
-
|
|
301
|
+
All of these can be overridden through [`hlsConfig`](#playeroptions)
|
|
302
|
+
(e.g. `maxLiveSyncPlaybackRate`), but the defaults are tuned for Oddin's
|
|
303
303
|
streams and rarely need changing. Raw live‑sync overrides are normalized so they
|
|
304
304
|
can't crash the player: if you pass the count‑based `liveSyncDurationCount` /
|
|
305
305
|
`liveMaxLatencyDurationCount`, the seconds‑based defaults step aside (hls.js
|
|
@@ -307,12 +307,12 @@ rejects mixing the two); if you set `liveSyncDuration` alone, a matching ceiling
|
|
|
307
307
|
is derived above it.
|
|
308
308
|
|
|
309
309
|
> **Migration note:** the default `liveLatencyTarget` **dropped from 3 s to 2 s**.
|
|
310
|
-
> Nothing to change
|
|
310
|
+
> Nothing to change: the drift ceiling stays 12 s and the ABR reaction budgets
|
|
311
311
|
> are derived from the target, so they tighten with it automatically. Expect
|
|
312
312
|
> playback ~1 s closer to live. If you were relying on the old behaviour, pass
|
|
313
313
|
> `liveLatencyTarget: 3` explicitly.
|
|
314
314
|
|
|
315
|
-
> **Migration note:** `liveCatchUpRate` was **removed** as a public option
|
|
315
|
+
> **Migration note:** `liveCatchUpRate` was **removed** as a public option; the
|
|
316
316
|
> catch‑up rate (1.5×) is now managed internally alongside the drift ceiling. If
|
|
317
317
|
> you set it before, drop it; reach for `hlsConfig.maxLiveSyncPlaybackRate` only
|
|
318
318
|
> if you genuinely need to override the rate.
|
|
@@ -356,7 +356,7 @@ afterward (`play`/`pause`/etc. become no-ops).
|
|
|
356
356
|
Notes:
|
|
357
357
|
|
|
358
358
|
- `reload()` **rejects** (and emits `error`, setting state to `error`) if the
|
|
359
|
-
re-resolution fails
|
|
359
|
+
re-resolution fails, so `await` it or attach a `.catch`.
|
|
360
360
|
- `play()` resolves even when the browser blocks autoplay; rely on the `playing`
|
|
361
361
|
state / `error` event (not `play()`'s return value) to determine the outcome.
|
|
362
362
|
|
|
@@ -364,25 +364,25 @@ Notes:
|
|
|
364
364
|
|
|
365
365
|
### Player events
|
|
366
366
|
|
|
367
|
-
`on(event, cb)
|
|
368
|
-
|
|
369
|
-
| Event | Payload | Fires when
|
|
370
|
-
| ------------------ | --------------------------------- |
|
|
371
|
-
| `ready` | `void` | The manifest is parsed and the player is attached.
|
|
372
|
-
| `waiting` | [`WaitState`](#waitstate) | Armed and waiting for go‑live. On the polling path it fires per poll with the countdown; on the SSE arming path it fires once, just after `createPlayer` resolves (with `ttkMs` when the server sent a kickoff time).
|
|
373
|
-
| `playing` | `void` | Playback is progressing.
|
|
374
|
-
| `buffering` | `void` | The player is rebuffering.
|
|
375
|
-
| `paused` | `void` | Playback is paused.
|
|
376
|
-
| `ended` | `EndedReason \| undefined` | The live stream ended; playback stops and the last frame is kept. The payload says **why**: `'interrupted'` (the match is still running
|
|
377
|
-
| `error` | [`PlaybackError`](#playbackerror) | A fatal error occurred (playback stopped).
|
|
378
|
-
| `warning` | [`PlaybackError`](#playbackerror) | A non-fatal condition (e.g. FairPlay passthrough, a failed license refresh). Playback continues.
|
|
379
|
-
| `autoplayblocked` | `void` | The browser blocked autoplay (`NotAllowedError`). Show a tap-to-play / tap-to-unmute affordance, then call `player.play()` from the gesture.
|
|
380
|
-
| `qualitychange` | `number` | Active rendition changed (level index, `-1` = auto).
|
|
381
|
-
| `audiotrackchange` | `number` | Active audio track changed (track id).
|
|
382
|
-
| `texttrackchange` | `number` | Active text track changed (track id, `-1` = off).
|
|
383
|
-
| `pipchange` | `boolean` | Picture-in-Picture entered (`true`) or exited (`false`).
|
|
384
|
-
| `stats` | [`PlaybackStats`](#playbackstats) | Periodic stats sample (`statsIntervalMs`).
|
|
385
|
-
| `statechange` | [`PlayerState`](#playerstate) | Any state transition.
|
|
367
|
+
`on(event, cb)`: the payload type depends on the event ([`PlayerEventMap`](#playereventmap)):
|
|
368
|
+
|
|
369
|
+
| Event | Payload | Fires when |
|
|
370
|
+
| ------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
371
|
+
| `ready` | `void` | The manifest is parsed and the player is attached. |
|
|
372
|
+
| `waiting` | [`WaitState`](#waitstate) | Armed and waiting for go‑live. On the polling path it fires per poll with the countdown; on the SSE arming path it fires once, just after `createPlayer` resolves (with `ttkMs` when the server sent a kickoff time). |
|
|
373
|
+
| `playing` | `void` | Playback is progressing. |
|
|
374
|
+
| `buffering` | `void` | The player is rebuffering. |
|
|
375
|
+
| `paused` | `void` | Playback is paused. |
|
|
376
|
+
| `ended` | `EndedReason \| undefined` | The live stream ended; playback stops and the last frame is kept. The payload says **why**: `'interrupted'` (the match is still running, and its stream dropped, so show "reconnecting", **never** "ended"; the player rejoins by itself) or `'match_ended'` (a real end). `undefined` = the end came from the HLS staleness watchdog, or a server predating the field; treat it as a real end. Fires **again** with `'match_ended'` if an interruption later turns out to be the end, so don't latch on the first one. Also readable any time as `player.endedReason`. |
|
|
377
|
+
| `error` | [`PlaybackError`](#playbackerror) | A fatal error occurred (playback stopped). |
|
|
378
|
+
| `warning` | [`PlaybackError`](#playbackerror) | A non-fatal condition (e.g. FairPlay passthrough, a failed license refresh). Playback continues. |
|
|
379
|
+
| `autoplayblocked` | `void` | The browser blocked autoplay (`NotAllowedError`). Show a tap-to-play / tap-to-unmute affordance, then call `player.play()` from the gesture. |
|
|
380
|
+
| `qualitychange` | `number` | Active rendition changed (level index, `-1` = auto). |
|
|
381
|
+
| `audiotrackchange` | `number` | Active audio track changed (track id). |
|
|
382
|
+
| `texttrackchange` | `number` | Active text track changed (track id, `-1` = off). |
|
|
383
|
+
| `pipchange` | `boolean` | Picture-in-Picture entered (`true`) or exited (`false`). |
|
|
384
|
+
| `stats` | [`PlaybackStats`](#playbackstats) | Periodic stats sample (`statsIntervalMs`). |
|
|
385
|
+
| `statechange` | [`PlayerState`](#playerstate) | Any state transition. |
|
|
386
386
|
|
|
387
387
|
<a id="controls--theming"></a>
|
|
388
388
|
|
|
@@ -393,9 +393,9 @@ The managed player ships a built-in, fully **skinnable** control bar. By default
|
|
|
393
393
|
renders the Oddin-branded UI: a center play/overlay, a seek bar, play/pause,
|
|
394
394
|
volume, a live badge + go-live, quality / audio / subtitle menus,
|
|
395
395
|
Picture-in-Picture, fullscreen, a buffering spinner, and **branded status cards**
|
|
396
|
-
for the stopped/pre-play states
|
|
396
|
+
for the stopped/pre-play states: _ended_ ("This stream has ended"), _fatal
|
|
397
397
|
error_ (message + Retry when retryable), _pre-play_ (poster + play CTA), and
|
|
398
|
-
_armed-waiting_ ("Starting soon…")
|
|
398
|
+
_armed-waiting_ ("Starting soon…"), so a finished or failed stream never leaves
|
|
399
399
|
a frozen frame behind a dead play button. The cards pick up `theme.logoUrl` and
|
|
400
400
|
the theme colors; customise the copy with `endedMessage`/`interruptedMessage`/`errorMessage`, or set
|
|
401
401
|
`statusOverlays: false` to drive your own end/error UI from the player events.
|
|
@@ -405,7 +405,7 @@ fully keyboard- and screen-reader-accessible.
|
|
|
405
405
|
|
|
406
406
|
> **Fullscreen on iPhone:** iPhone Safari has no element Fullscreen API, so the
|
|
407
407
|
> fullscreen button there enters the **video-native** fullscreen
|
|
408
|
-
> (`webkitEnterFullscreen`)
|
|
408
|
+
> (`webkitEnterFullscreen`): the YouTube-style player with **native iOS
|
|
409
409
|
> controls**. The custom control bar, watermark, and overlays are not shown
|
|
410
410
|
> inside it; the viewer exits with the native Done button. Video-native
|
|
411
411
|
> fullscreen only exists once media is loaded, so on iPhone the button takes
|
|
@@ -423,7 +423,7 @@ type Controls = 'custom' | 'native' | 'none';
|
|
|
423
423
|
| `'native'` | The browser's built-in `<video controls>` UI. No wrapper element is added. |
|
|
424
424
|
| `'none'` | No controls. Drive the player entirely through its [API](#player) (build your own). |
|
|
425
425
|
|
|
426
|
-
Re-skin the custom bar in one of two ways
|
|
426
|
+
Re-skin the custom bar in one of two ways; both set the same CSS variables, so
|
|
427
427
|
they're interchangeable:
|
|
428
428
|
|
|
429
429
|
**1. Pass a `theme`** (merged over the Oddin defaults; omitted keys keep the brand value):
|
|
@@ -494,7 +494,7 @@ type PlayerState =
|
|
|
494
494
|
|
|
495
495
|
---
|
|
496
496
|
|
|
497
|
-
## Mode B
|
|
497
|
+
## Mode B: Bring your own player
|
|
498
498
|
|
|
499
499
|
The SDK resolves the stream and gives you DRM helpers; you own the `<video>` element
|
|
500
500
|
and the playback engine.
|
|
@@ -601,7 +601,7 @@ works everywhere the managed control bar's does. `enterVideoFullscreen` walks a
|
|
|
601
601
|
capability ladder: standard element fullscreen on `container` (pass your player
|
|
602
602
|
wrapper to keep your own controls visible; defaults to the video), the
|
|
603
603
|
`webkit`-prefixed variant (older iPad/macOS Safari), and finally
|
|
604
|
-
`video.webkitEnterFullscreen()
|
|
604
|
+
`video.webkitEnterFullscreen()`: **iPhone Safari has no element Fullscreen API
|
|
605
605
|
at all**, so the only fullscreen there is the video-native presentation with
|
|
606
606
|
native iOS controls (your DOM is not visible inside it, and it only takes
|
|
607
607
|
effect once media is loaded). Call it from a user gesture. It resolves `false`
|
|
@@ -609,13 +609,13 @@ when the platform has no mechanism at all (some webviews) so you can hide your
|
|
|
609
609
|
button; a denied request (permissions policy) rejects.
|
|
610
610
|
|
|
611
611
|
`exitVideoFullscreen` leaves whichever presentation is active (video-native
|
|
612
|
-
first
|
|
612
|
+
first; it reports only through the video, never the document), is safe to
|
|
613
613
|
call when not fullscreen, and is **scoped to your video's presentation**: an
|
|
614
614
|
unrelated element's fullscreen elsewhere on the page is left alone.
|
|
615
615
|
`videoFullscreenActive` tells you whether your video's presentation is
|
|
616
616
|
currently fullscreen. Pass the same `container` to all three. For fullscreen **state changes**, listen to `fullscreenchange` /
|
|
617
617
|
`webkitfullscreenchange` on the document and `webkitbeginfullscreen` /
|
|
618
|
-
`webkitendfullscreen` on the video
|
|
618
|
+
`webkitendfullscreen` on the video; the last two are the only signal for
|
|
619
619
|
iPhone's video-native fullscreen.
|
|
620
620
|
|
|
621
621
|
<a id="detectdrmsupport"></a>
|
|
@@ -643,16 +643,16 @@ request, bounded by a timeout. It never rejects.
|
|
|
643
643
|
|
|
644
644
|
Rules of use:
|
|
645
645
|
|
|
646
|
-
- **Call it lazily, cache the verdict yourself
|
|
646
|
+
- **Call it lazily, cache the verdict yourself**: only once you know the
|
|
647
647
|
content is DRM‑protected (`descriptor.drmEnabled`). The EME spec allows the
|
|
648
648
|
probe to trigger consent prompts or CDM downloads, and results are not
|
|
649
649
|
memoized; never run it on page load, and never probe for a clear stream.
|
|
650
650
|
- **Probe, don't UA‑sniff.** The verdict is the playability gate; use the UA
|
|
651
651
|
only to word the message.
|
|
652
|
-
- `insecure-context` means EME is unavailable by spec
|
|
652
|
+
- `insecure-context` means EME is unavailable by spec, typical when testing
|
|
653
653
|
over `http://<LAN‑IP>`; `localhost` is fine.
|
|
654
654
|
- `blocked-by-policy` means an embedding problem (an iframe missing
|
|
655
|
-
`allow="encrypted-media"`), not an unsupported browser
|
|
655
|
+
`allow="encrypted-media"`), not an unsupported browser, so surface it to the
|
|
656
656
|
integrator, not the viewer.
|
|
657
657
|
- `supported` is necessary, not sufficient: licensing, CORS onboarding and
|
|
658
658
|
packaging still apply.
|
|
@@ -671,7 +671,7 @@ function clearDeviceId(): void;
|
|
|
671
671
|
`X-Device-Id` on every DRM license request.
|
|
672
672
|
|
|
673
673
|
> **Privacy:** the device id is a stable, cross‑session identifier (personal data
|
|
674
|
-
> under GDPR/ePrivacy). Disclose it in your privacy policy and offer a reset
|
|
674
|
+
> under GDPR/ePrivacy). Disclose it in your privacy policy and offer a reset:
|
|
675
675
|
> `clearDeviceId()` removes the stored id so the next call generates a fresh one.
|
|
676
676
|
|
|
677
677
|
### `fetchCatalog`
|
|
@@ -681,7 +681,7 @@ function fetchCatalog(opts: FetchCatalogOptions): Promise<Catalog>;
|
|
|
681
681
|
```
|
|
682
682
|
|
|
683
683
|
Fetches the metadata catalog (tournaments → matches). **No** manifest or DRM data is
|
|
684
|
-
included
|
|
684
|
+
included; use [`resolveStream`](#resolvestream) for playback. Supports a conditional
|
|
685
685
|
refresh via ETag.
|
|
686
686
|
|
|
687
687
|
```ts
|
|
@@ -751,18 +751,18 @@ function deriveEventsBaseUrl(baseUrl: string): string | undefined;
|
|
|
751
751
|
|
|
752
752
|
**Push**-based live state over Server-Sent Events (`events.<domain>/v1/events/{urn}`).
|
|
753
753
|
The managed player uses this automatically (the `liveStateEvents` option) to go live
|
|
754
|
-
the instant a match starts
|
|
754
|
+
the instant a match starts, without polling `/v1/playback`, and to detect
|
|
755
755
|
end-of-stream instantly; BYO players can use it directly. `state` mirrors the
|
|
756
756
|
`/v1/playback` outcome ladder:
|
|
757
757
|
|
|
758
758
|
| `state` | meaning | maps to `/v1/playback` |
|
|
759
759
|
| ---------- | ----------------------------- | ---------------------- |
|
|
760
|
-
| `live` | serveable now
|
|
760
|
+
| `live` | serveable now, so play | `200 OK` |
|
|
761
761
|
| `upcoming` | scheduled, not serving yet | `425 Too Early` |
|
|
762
762
|
| `ended` | was live; media stopped | `503` |
|
|
763
763
|
| `gone` | ended past the catchup window | `410 Gone` |
|
|
764
764
|
|
|
765
|
-
An `ended` event also carries a `reason
|
|
765
|
+
An `ended` event also carries a `reason`: `'interrupted'` (the match is still
|
|
766
766
|
running; only its stream dropped, so a BYO player should show "reconnecting"
|
|
767
767
|
and expect play to resume) or `'match_ended'` (a real end). It is absent for
|
|
768
768
|
other states, and absent entirely from servers predating it, so read
|
|
@@ -774,7 +774,7 @@ listening after the first.
|
|
|
774
774
|
Uses `fetch` (not native `EventSource`, which can't set the `x-api-key` header),
|
|
775
775
|
reconnects with backoff, and replays the current state on connect. Every event is
|
|
776
776
|
also reported via `onAlive` (including heartbeats) so callers can treat the stream as
|
|
777
|
-
healthy. A subscription problem is **non-fatal
|
|
777
|
+
healthy. A subscription problem is **non-fatal**; it retries in the background.
|
|
778
778
|
|
|
779
779
|
```ts
|
|
780
780
|
import { subscribeLiveState, deriveEventsBaseUrl } from '@oddin-gg/havik-player';
|
|
@@ -815,7 +815,7 @@ Utility: returns `true` if an HLS playlist body contains low‑latency tags
|
|
|
815
815
|
|
|
816
816
|
---
|
|
817
817
|
|
|
818
|
-
## Mode C
|
|
818
|
+
## Mode C: Iframe embed
|
|
819
819
|
|
|
820
820
|
Embed a hosted player page and control it over `postMessage`. This is the lowest‑touch
|
|
821
821
|
integration and isolates playback in its own browsing context.
|
|
@@ -851,7 +851,7 @@ embed.setMuted(false);
|
|
|
851
851
|
| `src` | `string` | **Required.** URL of the hosted embed page. |
|
|
852
852
|
| `baseUrl` | `string` | **Required.** API base URL (threaded to the iframe). |
|
|
853
853
|
| `matchUrn` | `string` | **Required.** Match to play. |
|
|
854
|
-
| `apiKey` | `string` | **Dev only.** Appended as a query param. In production the embed page injects the key server‑side
|
|
854
|
+
| `apiKey` | `string` | **Dev only.** Appended as a query param. In production the embed page injects the key server‑side, so omit this. |
|
|
855
855
|
| `allowedFrameOrigin` | `string` | Origin of the embed page, used to verify its messages. Defaults to the origin of `src` (resolved against the current page when `src` is relative). |
|
|
856
856
|
| `sandbox` | `string` | iframe `sandbox` value. Default `allow-scripts allow-same-origin allow-presentation` (keeps EME + fullscreen, blocks top-frame navigation and popups). The iframe is also mounted with `referrerpolicy="no-referrer"`. |
|
|
857
857
|
| `muted` | `boolean` | Start muted. |
|
|
@@ -896,13 +896,13 @@ bootEmbed();
|
|
|
896
896
|
|
|
897
897
|
`bootEmbed()` reads its [`EmbedConfig`](#embedconfig) from `window.HAVIK_EMBED_CONFIG`
|
|
898
898
|
(server‑injected, preferred) or the URL query string, runs a managed player full‑bleed,
|
|
899
|
-
and speaks the postMessage protocol. **The api‑key is never accepted over postMessage
|
|
899
|
+
and speaks the postMessage protocol. **The api‑key is never accepted over postMessage**;
|
|
900
900
|
inject it server‑side in production.
|
|
901
901
|
|
|
902
902
|
#### `EmbedConfig`
|
|
903
903
|
|
|
904
904
|
Read by `bootEmbed()` in one of two **mutually-exclusive** modes: if
|
|
905
|
-
`window.HAVIK_EMBED_CONFIG` is present (production) it is the **only** source
|
|
905
|
+
`window.HAVIK_EMBED_CONFIG` is present (production) it is the **only** source; the
|
|
906
906
|
query string is ignored entirely, so a tampered `?baseUrl=` can't redirect the injected
|
|
907
907
|
api-key. Otherwise (dev/demo) config comes from the query string. The postMessage bridge
|
|
908
908
|
**fails closed**: with no `parentOrigin`, the embed neither accepts commands nor posts
|
|
@@ -927,7 +927,7 @@ events.
|
|
|
927
927
|
Every message is filtered with [`isOddinMessage`](#isoddinmessage) (anything not a
|
|
928
928
|
recognized `oddin:` message is ignored). The **host** verifies `event.origin` against the
|
|
929
929
|
embed page's origin on every message. The **iframe** verifies `event.origin` against its
|
|
930
|
-
configured `parentOrigin
|
|
930
|
+
configured `parentOrigin`, and `mountEmbed` always sets this, so the bridge is origin‑verified
|
|
931
931
|
end‑to‑end. If you self‑host the embed page and call `bootEmbed()` directly, **always set
|
|
932
932
|
`parentOrigin`** (see [`EmbedConfig`](#embedconfig)); without it the iframe accepts `oddin:`
|
|
933
933
|
messages from any origin and posts events with target origin `*`.
|
|
@@ -998,14 +998,14 @@ class PlaybackError extends Error {
|
|
|
998
998
|
| `TIMEOUT` | (last)¹ | `waitForLive` exceeded `timeoutMs`. | No |
|
|
999
999
|
| `ABORTED` | 0 | Cancelled via `AbortSignal`. | No |
|
|
1000
1000
|
|
|
1001
|
-
¹ `TIMEOUT.httpStatus` carries the status of the last error it was waiting on
|
|
1001
|
+
¹ `TIMEOUT.httpStatus` carries the status of the last error it was waiting on, typically
|
|
1002
1002
|
`425` while waiting for an upcoming match, `503` while an origin warms up, or `0` if the
|
|
1003
1003
|
timeout lands on a network failure.
|
|
1004
1004
|
|
|
1005
1005
|
² `DRM_CLIENT.httpStatus` carries the license/certificate HTTP status when one was
|
|
1006
1006
|
involved (e.g. a `5xx` after the SDK's license-retry budget is exhausted), else `0`
|
|
1007
1007
|
(no usable key system, CDM/EME error, request never got a response). A genuine license
|
|
1008
|
-
`401`/`403` surfaces as `UNAUTHORIZED`/`FORBIDDEN`, never as `DRM_CLIENT
|
|
1008
|
+
`401`/`403` surfaces as `UNAUTHORIZED`/`FORBIDDEN`, never as `DRM_CLIENT`, so branch on
|
|
1009
1009
|
the `code`, not on `httpStatus`, to tell a server rejection from a device that can't
|
|
1010
1010
|
play DRM. The built-in error card still offers Retry (it re-resolves and re-attaches,
|
|
1011
1011
|
which can recover a transient license failure); an unsupported device will simply fail
|
|
@@ -1148,7 +1148,7 @@ interface TextTrackInfo {
|
|
|
1148
1148
|
|
|
1149
1149
|
<a id="live-pickup-options"></a>
|
|
1150
1150
|
|
|
1151
|
-
### Live
|
|
1151
|
+
### Live-pickup options
|
|
1152
1152
|
|
|
1153
1153
|
#### `WaitForLive`
|
|
1154
1154
|
|
|
@@ -1158,22 +1158,22 @@ type WaitForLive = boolean | WaitForLiveOptions;
|
|
|
1158
1158
|
|
|
1159
1159
|
Set to `true` for sensible defaults, or pass options:
|
|
1160
1160
|
|
|
1161
|
-
| Option | Type | Default | Description
|
|
1162
|
-
| --------------- | ---------------------------- | --------- |
|
|
1163
|
-
| `timeoutMs` | `number` | unbounded | Overall budget before giving up with `TIMEOUT`.
|
|
1164
|
-
| `floorMs` | `number` | `1000` | Minimum delay between polls (also the server minimum).
|
|
1165
|
-
| `ceilMs` | `number` | `30000` | Maximum delay between polls **inside the imminent window** (see `imminentMs`). Far-from-kickoff polls are widened
|
|
1166
|
-
| `kickoffAt` | `number \| string` | — | Scheduled kickoff (epoch ms or ISO string). When given, the SDK widens long-tail polls automatically
|
|
1167
|
-
| `imminentMs` | `number` | `600000` | Time-to-kickoff threshold for "imminent" (collapses to the tight `[floorMs, ceilMs]` band).
|
|
1168
|
-
| `sanityFloorMs` | `number` | `300000` | Hard cap on any single sleep (so an early go-live / schedule change is caught within bounded latency). 5 min default.
|
|
1169
|
-
| `jitter` | `boolean` | `true` | Add up to +15% jitter (never polls faster than the server asks).
|
|
1170
|
-
| `onState` | `(state: WaitState) => void` | — | Called before each wait.
|
|
1161
|
+
| Option | Type | Default | Description |
|
|
1162
|
+
| --------------- | ---------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
1163
|
+
| `timeoutMs` | `number` | unbounded | Overall budget before giving up with `TIMEOUT`. |
|
|
1164
|
+
| `floorMs` | `number` | `1000` | Minimum delay between polls (also the server minimum). |
|
|
1165
|
+
| `ceilMs` | `number` | `30000` | Maximum delay between polls **inside the imminent window** (see `imminentMs`). Far-from-kickoff polls are widened; see below. |
|
|
1166
|
+
| `kickoffAt` | `number \| string` | — | Scheduled kickoff (epoch ms or ISO string). When given, the SDK widens long-tail polls automatically; see "Far-future polling". |
|
|
1167
|
+
| `imminentMs` | `number` | `600000` | Time-to-kickoff threshold for "imminent" (collapses to the tight `[floorMs, ceilMs]` band). |
|
|
1168
|
+
| `sanityFloorMs` | `number` | `300000` | Hard cap on any single sleep (so an early go-live / schedule change is caught within bounded latency). 5 min default. |
|
|
1169
|
+
| `jitter` | `boolean` | `true` | Add up to +15% jitter (never polls faster than the server asks). |
|
|
1170
|
+
| `onState` | `(state: WaitState) => void` | — | Called before each wait. |
|
|
1171
1171
|
|
|
1172
1172
|
##### Far-future polling
|
|
1173
1173
|
|
|
1174
1174
|
A viewer armed hours before kickoff would otherwise hammer `/v1/playback` every
|
|
1175
1175
|
30s for the entire long tail. When **`kickoffAt`** is set (or the server includes
|
|
1176
|
-
`liveStartsAt` on the 425 body
|
|
1176
|
+
`liveStartsAt` on the 425 body; see [Errors](#errors) `liveStartsAtMs`), the
|
|
1177
1177
|
poll cadence is widened to `min(ttk / 10, sanityFloorMs)` for the long tail and
|
|
1178
1178
|
collapses to the tight `[floorMs, ceilMs]` range only inside the imminent
|
|
1179
1179
|
window. Guards baked in: the server's `Retry-After` is always a **floor** (the
|
|
@@ -1198,11 +1198,11 @@ interface WaitState {
|
|
|
1198
1198
|
|
|
1199
1199
|
These exports are for advanced/custom integrations.
|
|
1200
1200
|
|
|
1201
|
-
- **`pollToLive(attempt, cfg, signal?)
|
|
1201
|
+
- **`pollToLive(attempt, cfg, signal?)`**: the generic retry primitive behind
|
|
1202
1202
|
`waitForLive`. Polls an arbitrary `() => Promise<T>`, retrying on retryable
|
|
1203
1203
|
`PlaybackError`s and honoring `retryAfterMs`.
|
|
1204
|
-
- **`isOddinMessage(data)
|
|
1205
|
-
- **`ODDIN_THEME`** / **`applyTheme(root, theme?)
|
|
1204
|
+
- **`isOddinMessage(data)`**: type guard for the postMessage protocol.
|
|
1205
|
+
- **`ODDIN_THEME`** / **`applyTheme(root, theme?)`**: the default control-bar skin
|
|
1206
1206
|
and the helper that writes a [`HavikTheme`](#haviktheme) onto an element as
|
|
1207
1207
|
`--havik-*` CSS variables. See [Controls & theming](#controls--theming).
|
|
1208
1208
|
|
|
@@ -1214,7 +1214,7 @@ These exports are for advanced/custom integrations.
|
|
|
1214
1214
|
|
|
1215
1215
|
## Onboarding & CORS
|
|
1216
1216
|
|
|
1217
|
-
The api‑key is **publishable
|
|
1217
|
+
The api‑key is **publishable**: it is designed to live in browser JavaScript. Its
|
|
1218
1218
|
protection is a **server‑side allow‑list**, so every origin you serve the player from
|
|
1219
1219
|
must be onboarded before it will work:
|
|
1220
1220
|
|
|
@@ -1227,7 +1227,7 @@ must be onboarded before it will work:
|
|
|
1227
1227
|
If you serve the player from a new domain, request it be added to the key's allow‑lists
|
|
1228
1228
|
first. Key revocation is not instantaneous (validation is cached for a few minutes).
|
|
1229
1229
|
|
|
1230
|
-
> **Never use a wildcard (`*`) origin allow‑list in production
|
|
1230
|
+
> **Never use a wildcard (`*`) origin allow‑list in production**: it removes the only
|
|
1231
1231
|
> guardrail on a publishable key.
|
|
1232
1232
|
|
|
1233
1233
|
### Service URL discovery
|
|
@@ -1241,7 +1241,7 @@ GET https://status-dev.oddin-video.gg/v1/endpoints.json
|
|
|
1241
1241
|
|
|
1242
1242
|
The document lists the current base URL per region and is served with
|
|
1243
1243
|
`Access-Control-Allow-Origin: *`, so any backend or build pipeline can consume it.
|
|
1244
|
-
If an endpoint moves
|
|
1244
|
+
If an endpoint moves, most likely in regions with distinct internet regulations,
|
|
1245
1245
|
the directory updates and your backend follows, with no app release or store
|
|
1246
1246
|
re‑review. Endpoint changes and live availability for every Havik service are
|
|
1247
1247
|
published at [status-dev.oddin-video.gg](https://status-dev.oddin-video.gg), the integration
|
|
@@ -1259,12 +1259,12 @@ directory. The production directory URL is provided at onboarding.
|
|
|
1259
1259
|
| `DRM_CLIENT` | The device/browser can't play this DRM (no usable key system, CDM/EME error), or license delivery failed without a server denial. Check the console for the EME/CDM detail; call `detectDrmSupport()` to pre‑flight and message the viewer. |
|
|
1260
1260
|
| `NOT_FOUND` (404) for a match you can see | The key isn't entitled to that tournament (404 is intentionally indistinguishable from "unknown"). |
|
|
1261
1261
|
| Player stays in `waiting` forever | The match hasn't gone live yet; the SDK is polling. Set `waitForLive.timeoutMs` to bound it. |
|
|
1262
|
-
| Autoplay doesn't start | Browser sound policy
|
|
1262
|
+
| Autoplay doesn't start | Browser sound policy: set `muted: true` for autoplay, or start playback from a user gesture. |
|
|
1263
1263
|
| Black video on Safari with DRM | Check the console for an EME/CDM error; verify the origin is on the DRM CORS allow‑list. On very old Safari the native fallback plays only OS‑trusted passthrough. |
|
|
1264
1264
|
| Black video in Chrome/Firefox/Edge **on iOS** | iOS has no Widevine CDM, and FairPlay availability outside Safari varies by WebKit build. Call `detectDrmSupport()`: on `no-cdm`, tell the viewer this browser can't play protected streams. |
|
|
1265
1265
|
| Black video, `levelHeight`/`droppedFrames` stuck | Check the browser console for an EME/CDM error and verify DRM CORS onboarding. |
|
|
1266
1266
|
|
|
1267
|
-
Include `PlaybackError.requestId` (when present) when contacting support
|
|
1267
|
+
Include `PlaybackError.requestId` (when present) when contacting support; it correlates
|
|
1268
1268
|
to server‑side logs.
|
|
1269
1269
|
|
|
1270
1270
|
---
|