@oddin-gg/havik-player 2.9.3 → 2.11.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
@@ -247,6 +247,7 @@ player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
247
247
  | `maxBitrate` | `number` | — | Cap ABR to renditions at/below this bitrate (bps). |
248
248
  | `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. |
249
249
  | `snapToLiveOnRefocus` | `boolean` | `true` | Snap to the live edge when a backgrounded tab is refocused while far behind. Live/LL only. |
250
+ | `mediaDuringKey` | `boolean` | `true` | DRM cold start: download the first init segment and parts while the first key is still being acquired, instead of after it. Bytes reach hls.js only when it asks for them; only the network time moves off the critical path (~0.5 s on Widevine LL-HLS). Ignored on clear streams and when `hlsConfig.fLoader` is set. Pass `false` to opt out. |
250
251
  | `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. |
251
252
  | `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`. |
252
253
  | `analytics` | `boolean \| { endpoint?: string }` | `true` | QoE beacons. Active only when the playback response carries a server-minted `sid`. The beacons host is derived from `baseUrl` by hostname convention (`feed[-dev].<domain>` → `beacons[-dev].<domain>`). **If `baseUrl` is not a `feed` host nothing derives and beacons are disabled** — the SDK then logs one `console.warn` naming the host and emits a non-fatal [`warning`](#player-events) event carrying the same sentence. Recovery: pass `{ endpoint: 'https://beacons[-dev].<domain>' }`. Pass `false` to opt out deliberately (no warning). |
@@ -276,10 +277,11 @@ don't need to set any of the live or ABR knobs:
276
277
  on its own (its live‑sync only ever corrects being _behind_), so the stutter
277
278
  would otherwise persist for the rest of the session. Live only: VOD seeks are
278
279
  untouched, and the clamp stands down while chasing is suspended on a stale
279
- playlist (there is no live edge left to protect at that point). On the custom
280
- control bar the bar's right end **is** the target, so the thumb reads full at
281
- the live edge. Bring‑your‑own‑player (Mode B) owns its own seek UI and needs
282
- the equivalent clamp itself.
280
+ playlist (there is no live edge left to protect at that point). The custom
281
+ control bar hides its seek bar on a live stream (the Go‑live button and
282
+ `seekToLive()` are the way back to the edge); on VOD the bar covers the full
283
+ duration. Bring‑your‑own‑player (Mode B) owns its own seek UI and needs the
284
+ equivalent clamp itself.
283
285
  - **Audio‑hole bridge**: `maxBufferHole` is raised to **0.5 s** so the ~0.45 s
284
286
  audio "remainder" gap at each segment boundary is bridged as a sub‑perceptible
285
287
  hitch instead of a rebuffer.
@@ -392,7 +394,8 @@ Notes:
392
394
 
393
395
  The managed player ships a built-in, fully **skinnable** control bar. By default
394
396
  (`controls: 'custom'`) it wraps your `<video>` in a `.havik-player` container and
395
- renders the Oddin-branded UI: a center play/overlay, a seek bar, play/pause,
397
+ renders the Oddin-branded UI: a center play/overlay, a seek bar (VOD only; a
398
+ live stream shows none, see below), play/pause,
396
399
  volume, a live badge + go-live, quality / audio / subtitle menus,
397
400
  Picture-in-Picture, fullscreen, a buffering spinner, and **branded status cards**
398
401
  for the stopped/pre-play states: _ended_ ("This stream has ended"), _fatal
@@ -740,8 +743,9 @@ if (next.notModified) {
740
743
  function watchStatus(opts: WatchStatusOptions): StatusWatcher;
741
744
  ```
742
745
 
743
- Polls a single match's live status and fires `onChange` on transitions (and once on
744
- first read). Returns a `StatusWatcher` with `stop()`. For a **push** channel (no
746
+ Polls a single match's live status and fires `onChange` once on first read and then
747
+ on every change of the status, the teams (a logo resolved later) or the planned
748
+ start; unchanged polls stay silent. Returns a `StatusWatcher` with `stop()`. For a **push** channel (no
745
749
  polling) use [`subscribeLiveState`](#subscribelivestate) instead; `watchStatus` is the
746
750
  zero-dependency polling alternative when the SSE endpoint isn't available.
747
751
 
@@ -759,14 +763,14 @@ const watcher = watchStatus({
759
763
 
760
764
  #### `WatchStatusOptions`
761
765
 
762
- | Option | Type | Default | Description |
763
- | ------------ | --------------------------------------- | ------- | ------------------------------------------ |
764
- | `baseUrl` | `string` | — | **Required.** |
765
- | `matchUrn` | `string` | — | **Required.** |
766
- | `credential` | [`CredentialSource`](#credentialsource) | — | **Required.** |
767
- | `onChange` | `(status: MatchStatus) => void` | — | **Required.** Fired on status transitions. |
768
- | `onError` | `(err: PlaybackError) => void` | — | Fired on a poll error; polling continues. |
769
- | `intervalMs` | `number` | `10000` | Poll interval (floored at `2000`). |
766
+ | Option | Type | Default | Description |
767
+ | ------------ | --------------------------------------- | ------- | ----------------------------------------------------------------------------- |
768
+ | `baseUrl` | `string` | — | **Required.** |
769
+ | `matchUrn` | `string` | — | **Required.** |
770
+ | `credential` | [`CredentialSource`](#credentialsource) | — | **Required.** |
771
+ | `onChange` | `(status: MatchStatus) => void` | — | **Required.** Fired on first read, then on any status, teams or start change. |
772
+ | `onError` | `(err: PlaybackError) => void` | — | Fired on a poll error; polling continues. |
773
+ | `intervalMs` | `number` | `10000` | Poll interval (floored at `2000`). |
770
774
 
771
775
  <a id="live-state-sse"></a>
772
776
 
@@ -783,12 +787,13 @@ the instant a match starts, without polling `/v1/playback`, and to detect
783
787
  end-of-stream instantly; BYO players can use it directly. `state` mirrors the
784
788
  `/v1/playback` outcome ladder:
785
789
 
786
- | `state` | meaning | maps to `/v1/playback` |
787
- | ---------- | ----------------------------- | ---------------------- |
788
- | `live` | serveable now, so play | `200 OK` |
789
- | `upcoming` | scheduled, not serving yet | `425 Too Early` |
790
- | `ended` | was live; media stopped | `503` |
791
- | `gone` | ended past the catchup window | `410 Gone` |
790
+ | `state` | meaning | maps to `/v1/playback` |
791
+ | ---------- | -------------------------------------------------------------------------------- | ------------------------------------- |
792
+ | `live` | serveable now, so play | `200 OK` |
793
+ | `upcoming` | scheduled, not serving yet | `425 Too Early` (with `liveStartsAt`) |
794
+ | `pending` | kickoff passed, stream being packaged — `live` follows within seconds to minutes | `425 Too Early` (no `liveStartsAt`) |
795
+ | `ended` | was live; media stopped | `503` |
796
+ | `gone` | ended past the catchup window | `410 Gone` |
792
797
 
793
798
  An `ended` event also carries a `reason`: `'interrupted'` (the match is still
794
799
  running; only its stream dropped, so a BYO player should show "reconnecting"
@@ -1112,7 +1117,7 @@ type CredentialSource = Credential | (() => Credential | Promise<Credential>);
1112
1117
 
1113
1118
  <a id="catalogmatch"></a>
1114
1119
 
1115
- #### `Catalog`, `CatalogTournament`, `CatalogMatch`
1120
+ #### `Catalog`, `CatalogTournament`, `CatalogMatch`, `CatalogTeam`
1116
1121
 
1117
1122
  ```ts
1118
1123
  interface Catalog {
@@ -1140,13 +1145,37 @@ interface CatalogMatch {
1140
1145
  tournamentUrn: string;
1141
1146
  tournamentName: string;
1142
1147
  sport: string;
1148
+ // The two sides, home first. Absent when no usable team survives parsing:
1149
+ // the server sent no `teams` (an older havik-streams), or every entry
1150
+ // lacked both a URN and a name. A team with a URN but no name IS kept.
1151
+ // `matchName` stays the display string; this is its structured form.
1152
+ teams?: CatalogTeam[];
1153
+ }
1154
+ interface CatalogTeam {
1155
+ urn: string; // competitor URN, e.g. "od:competitor:26406"; "" if the server sent a name only
1156
+ name: string;
1157
+ isHome: boolean;
1158
+ // An https image URL for the team logo, present only when the deployment
1159
+ // resolves logos and this team has one (most do). Can still fail to load —
1160
+ // always render a fallback for a missing or broken image. Logos are ~150 px
1161
+ // on the long side. Allow cdn.oddin.gg (and cdn.runestone.gg) in img-src.
1162
+ logoUrl?: string;
1143
1163
  }
1144
1164
  ```
1145
1165
 
1166
+ Parsing is tolerant: a non-array `teams` is ignored, entries with neither a
1167
+ URN nor a name are dropped, `isHome` is `true` only for a literal `true`, and a
1168
+ `logoUrl` that is not an absolute `https:` URL is dropped (an `http:` image is
1169
+ mixed content on an https page).
1170
+
1146
1171
  #### `MatchStatus`, `MatchLiveStatus`
1147
1172
 
1173
+ `pending` is "kickoff has passed, the stream is being packaged": not playable
1174
+ yet, `live` expected within seconds to minutes. Unknown values still collapse to
1175
+ `upcoming` with the verbatim value in `statusRaw`.
1176
+
1148
1177
  ```ts
1149
- type MatchLiveStatus = 'upcoming' | 'live' | 'ended';
1178
+ type MatchLiveStatus = 'upcoming' | 'pending' | 'live' | 'ended';
1150
1179
  interface MatchStatus {
1151
1180
  matchUrn: string;
1152
1181
  matchName?: string;
@@ -1154,6 +1183,9 @@ interface MatchStatus {
1154
1183
  // The server's status string verbatim — see CatalogMatch.statusRaw.
1155
1184
  statusRaw?: string;
1156
1185
  datePlannedStart?: string;
1186
+ // The teams as of this report (see CatalogTeam). watchStatus fires again
1187
+ // when they change, so a logo resolved later is re-reported.
1188
+ teams?: CatalogTeam[];
1157
1189
  }
1158
1190
  ```
1159
1191
 
package/embed/index.html CHANGED
@@ -28,7 +28,7 @@
28
28
  beacons endpoint can be derived and QoE analytics is silently off.
29
29
  `analytics` is honoured from the injected config only, never the query.
30
30
  -->
31
- <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.9.3/dist/havik-player.global.js" integrity="sha384-ucOZXRA7PYaCtY+Oj86b+BxFAiYADRwamwq1UvdjnJjmdF0uc4k6BZN08hCMDEG9" crossorigin="anonymous"></script>
31
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.11.0/dist/havik-player.global.js" integrity="sha384-xQ2Ld/ji10d1Gd8JkmSFDHZpq08KH7ZcUI5CVNoSMmVhPs1tQ/xXf2JJBzIfDqxp" crossorigin="anonymous"></script>
32
32
  <script>
33
33
  HavikPlayer.bootEmbed();
34
34
  </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@2.9.3/dist/havik-player.global.js" integrity="sha384-ucOZXRA7PYaCtY+Oj86b+BxFAiYADRwamwq1UvdjnJjmdF0uc4k6BZN08hCMDEG9" crossorigin="anonymous"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.11.0/dist/havik-player.global.js" integrity="sha384-xQ2Ld/ji10d1Gd8JkmSFDHZpq08KH7ZcUI5CVNoSMmVhPs1tQ/xXf2JJBzIfDqxp" 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": "2.9.3",
3
+ "version": "2.11.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,
@@ -77,6 +77,9 @@
77
77
  "sbom": "npx --yes @cyclonedx/cyclonedx-npm@2 --omit dev --output-file sbom.json",
78
78
  "prepack": "npm run build",
79
79
  "test:localqoe": "playwright test -c localqoe/playwright.config.ts",
80
+ "test:liveedge": "playwright test -c localqoe/liveedge.config.ts",
81
+ "test:drmlive": "playwright test -c localqoe/drmlive.config.ts",
82
+ "localqoe:pick-live": "node localqoe/drmlive-pick-live.mjs",
80
83
  "localqoe:media": "bash localqoe/make-media.sh && bash localqoe/make-live-media.sh",
81
84
  "test:blackhole": "npm run build:global && playwright test -c blackhole/playwright.config.ts",
82
85
  "blackhole:serve": "npm run build:global && node blackhole/server.mjs"