@oddin-gg/havik-player 1.17.0 → 1.18.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/docs/API.md CHANGED
@@ -219,35 +219,36 @@ 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
- | `errorMessage` | `string` | the error's message | Message on the fatal-error card (otherwise the error's own message). |
235
- | `waitForLive` | [`WaitForLive`](#waitforlive) | — | Arm on an upcoming match and auto‑attach at go‑live. |
236
- | `lowLatency` | `'auto' \| boolean` | `'auto'` | `'auto'` lets the player decide from the manifest. |
237
- | `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. |
238
- | `userId` | `string` | — | Optional viewer id (sent as `X-User-Id` on license requests). |
239
- | `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. |
240
- | `hlsConfig` | `Partial<HlsConfig>` | — | Escape hatch for advanced hls.js tuning. |
241
- | `statsIntervalMs` | `number` | `2000` | How often `'stats'` is emitted. |
242
- | `licenseRefreshMs` | `number` | `480000` (8 min) | Silent DRM license‑URL refresh interval for long sessions. `0` disables. |
243
- | `poster` | `string` | — | Poster image shown before playback starts. |
244
- | `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). |
245
- | `maxBitrate` | `number` | — | Cap ABR to renditions at/below this bitrate (bps). |
246
- | `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. |
247
- | `snapToLiveOnRefocus` | `boolean` | `true` | Snap to the live edge when a backgrounded tab is refocused while far behind. Live/LL only. |
248
- | `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. |
249
- | `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`. |
250
- | `eventsBaseUrl` | `string` | derived | Override the SSE endpoint base. Default: `baseUrl` with the host prefixed by `events.` (e.g. `feed.<d>` → `events.<d>`). |
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>`). |
251
252
 
252
253
  ### Low‑latency & adaptive bitrate
253
254
 
@@ -365,23 +366,23 @@ Notes:
365
366
 
366
367
  `on(event, cb)` — the payload type depends on the event ([`PlayerEventMap`](#playereventmap)):
367
368
 
368
- | Event | Payload | Fires when |
369
- | ------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
370
- | `ready` | `void` | The manifest is parsed and the player is attached. |
371
- | `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). |
372
- | `playing` | `void` | Playback is progressing. |
373
- | `buffering` | `void` | The player is rebuffering. |
374
- | `paused` | `void` | Playback is paused. |
375
- | `ended` | `void` | The live stream ended. Playback is stopped and the last frame is kept. |
376
- | `error` | [`PlaybackError`](#playbackerror) | A fatal error occurred (playback stopped). |
377
- | `warning` | [`PlaybackError`](#playbackerror) | A non-fatal condition (e.g. FairPlay passthrough, a failed license refresh). Playback continues. |
378
- | `autoplayblocked` | `void` | The browser blocked autoplay (`NotAllowedError`). Show a tap-to-play / tap-to-unmute affordance, then call `player.play()` from the gesture. |
379
- | `qualitychange` | `number` | Active rendition changed (level index, `-1` = auto). |
380
- | `audiotrackchange` | `number` | Active audio track changed (track id). |
381
- | `texttrackchange` | `number` | Active text track changed (track id, `-1` = off). |
382
- | `pipchange` | `boolean` | Picture-in-Picture entered (`true`) or exited (`false`). |
383
- | `stats` | [`PlaybackStats`](#playbackstats) | Periodic stats sample (`statsIntervalMs`). |
384
- | `statechange` | [`PlayerState`](#playerstate) | Any state transition. |
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 — 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. |
385
386
 
386
387
  <a id="controls--theming"></a>
387
388
 
@@ -396,7 +397,7 @@ for the stopped/pre-play states — _ended_ ("This stream has ended"), _fatal
396
397
  error_ (message + Retry when retryable), _pre-play_ (poster + play CTA), and
397
398
  _armed-waiting_ ("Starting soon…") — so a finished or failed stream never leaves
398
399
  a frozen frame behind a dead play button. The cards pick up `theme.logoUrl` and
399
- the theme colors; customise the copy with `endedMessage`/`errorMessage`, or set
400
+ the theme colors; customise the copy with `endedMessage`/`interruptedMessage`/`errorMessage`, or set
400
401
  `statusOverlays: false` to drive your own end/error UI from the player events.
401
402
  It auto-hides during playback, supports keyboard shortcuts (space/`k` play-pause,
402
403
  `m` mute, `f` fullscreen, `c` captions, `l` go-live, arrows seek/volume), and is
@@ -761,6 +762,15 @@ end-of-stream instantly; BYO players can use it directly. `state` mirrors the
761
762
  | `ended` | was live; media stopped | `503` |
762
763
  | `gone` | ended past the catchup window | `410 Gone` |
763
764
 
765
+ An `ended` event also carries a `reason` — `'interrupted'` (the match is still
766
+ running; only its stream dropped, so a BYO player should show "reconnecting"
767
+ and expect play to resume) or `'match_ended'` (a real end). It is absent for
768
+ other states, and absent entirely from servers predating it, so read
769
+ `undefined` as "assume a real end". A stream that is interrupted and whose
770
+ match later finishes sends a **second** `ended` event with the reason moved
771
+ `interrupted` → `match_ended`; the state never changes, so don't stop
772
+ listening after the first.
773
+
764
774
  Uses `fetch` (not native `EventSource`, which can't set the `x-api-key` header),
765
775
  reconnects with backoff, and replays the current state on connect. Every event is
766
776
  also reported via `onAlive` (including heartbeats) so callers can treat the stream as
package/embed/index.html CHANGED
@@ -23,7 +23,7 @@
23
23
  apiKey, parentOrigin). When injected, the URL query string is IGNORED for
24
24
  security; the api-key must NOT travel in the query in production.
25
25
  -->
26
- <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@1.17.0/dist/havik-player.global.js" integrity="sha384-OX+N8AnO+WaVi6AnRhVIs7vYm8RugKMxdmtkdu9zwlpyIcAuXCbt16XqnccW49h2" crossorigin="anonymous"></script>
26
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@1.18.0/dist/havik-player.global.js" integrity="sha384-x5N52M0d5W79TEJT7U7VHQ2CKuumXoxQfUXVwlztUIB2PS0mUcQaOEeJPZdrTa1k" crossorigin="anonymous"></script>
27
27
  <script>
28
28
  HavikPlayer.bootEmbed();
29
29
  </script>
@@ -12,7 +12,7 @@
12
12
  shipped in each npm release is pinned to that release with Subresource
13
13
  Integrity, injected at publish time (scripts/inject-sri.mjs). In
14
14
  production, always use a pinned + SRI'd tag like the released copy. -->
15
- <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@1.17.0/dist/havik-player.global.js" integrity="sha384-OX+N8AnO+WaVi6AnRhVIs7vYm8RugKMxdmtkdu9zwlpyIcAuXCbt16XqnccW49h2" crossorigin="anonymous"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@1.18.0/dist/havik-player.global.js" integrity="sha384-x5N52M0d5W79TEJT7U7VHQ2CKuumXoxQfUXVwlztUIB2PS0mUcQaOEeJPZdrTa1k" crossorigin="anonymous"></script>
16
16
  <script>
17
17
  (async () => {
18
18
  const player = await HavikPlayer.createPlayer({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddin-gg/havik-player",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Havik HTML5 player SDK — LL-HLS + DRM playback for Oddin live streams (managed, bring-your-own-player, and iframe-embed modes).",
5
5
  "type": "module",
6
6
  "sideEffects": false,