@oddin-gg/havik-player 2.10.0 → 2.11.1

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
 
@@ -1113,7 +1117,7 @@ type CredentialSource = Credential | (() => Credential | Promise<Credential>);
1113
1117
 
1114
1118
  <a id="catalogmatch"></a>
1115
1119
 
1116
- #### `Catalog`, `CatalogTournament`, `CatalogMatch`
1120
+ #### `Catalog`, `CatalogTournament`, `CatalogMatch`, `CatalogTeam`
1117
1121
 
1118
1122
  ```ts
1119
1123
  interface Catalog {
@@ -1141,9 +1145,29 @@ interface CatalogMatch {
1141
1145
  tournamentUrn: string;
1142
1146
  tournamentName: string;
1143
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;
1144
1163
  }
1145
1164
  ```
1146
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
+
1147
1171
  #### `MatchStatus`, `MatchLiveStatus`
1148
1172
 
1149
1173
  `pending` is "kickoff has passed, the stream is being packaged": not playable
@@ -1159,6 +1183,9 @@ interface MatchStatus {
1159
1183
  // The server's status string verbatim — see CatalogMatch.statusRaw.
1160
1184
  statusRaw?: string;
1161
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[];
1162
1189
  }
1163
1190
  ```
1164
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.10.0/dist/havik-player.global.js" integrity="sha384-8JeA3Kgt3Hq/rl4dUJlndmtAcw1oQPn17zgjHRxbvfe57Q1/0x1/lwp27RimmlIy" crossorigin="anonymous"></script>
31
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.11.1/dist/havik-player.global.js" integrity="sha384-Obf3G9MSlv0HGCIg8N1q+sE5paXAcmESENBeaJshLhGlQnjO4nqg94zUkDoOvN0y" 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.10.0/dist/havik-player.global.js" integrity="sha384-8JeA3Kgt3Hq/rl4dUJlndmtAcw1oQPn17zgjHRxbvfe57Q1/0x1/lwp27RimmlIy" crossorigin="anonymous"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.11.1/dist/havik-player.global.js" integrity="sha384-Obf3G9MSlv0HGCIg8N1q+sE5paXAcmESENBeaJshLhGlQnjO4nqg94zUkDoOvN0y" 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.10.0",
3
+ "version": "2.11.1",
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"