@oddin-gg/havik-player 2.19.0 → 2.20.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
@@ -198,7 +198,7 @@ function createPlayer(opts: CreatePlayerOptions): Promise<Player>;
198
198
 
199
199
  Resolves to a [`Player`](#player) as soon as the stream is attached (or armed, when
200
200
  `waitForLive` is set and the match is still upcoming). Terminal errors during the
201
- initial resolution (e.g. `NOT_FOUND`, `GONE`, `UNAUTHORIZED`) reject the promise; the
201
+ initial resolution (e.g. `NOT_FOUND`, `GEO_DENIED`, `GONE`, `UNAUTHORIZED`) reject the promise; the
202
202
  live wait itself never blocks the promise and is surfaced through `'waiting'` events.
203
203
 
204
204
  ```ts
@@ -450,8 +450,12 @@ Picture-in-Picture, fullscreen, a buffering spinner, and **branded status cards*
450
450
  for the stopped/pre-play states: _ended_ ("This stream has ended"), _fatal
451
451
  error_ (message + Retry when retryable), _pre-play_ (poster + play CTA), and
452
452
  _armed-waiting_ ("Starting soon…"), so a finished or failed stream never leaves
453
- a frozen frame behind a dead play button. The cards pick up `theme.logoUrl` and
454
- the theme colors; customise the copy with `endedMessage`/`interruptedMessage`/`errorMessage`, or set
453
+ a frozen frame behind a dead play button. A `GEO_DENIED` error (the match is
454
+ booked for your key but not playable in the viewer's country) gets its own copy,
455
+ "This match isn't available in your country (RU)", naming the country the server
456
+ saw, and no Retry button: a retry would be refused again. The cards pick up `theme.logoUrl` and
457
+ the theme colors; customise the copy with `endedMessage`/`interruptedMessage`/`errorMessage`
458
+ (an `errorMessage` replaces the `GEO_DENIED` copy too), or set
455
459
  `statusOverlays: false` to drive your own end/error UI from the player events.
456
460
  It auto-hides during playback, supports keyboard shortcuts (space/`k` play-pause,
457
461
  `m` mute, `f` fullscreen, `c` captions, `l` go-live, arrows seek/volume), and is
@@ -866,14 +870,14 @@ const watcher = watchStatus({
866
870
 
867
871
  #### `WatchStatusOptions`
868
872
 
869
- | Option | Type | Default | Description |
870
- | ------------ | --------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
871
- | `baseUrl` | `string` | — | **Required.** |
872
- | `matchUrn` | `string` | — | **Required.** |
873
- | `credential` | [`CredentialSource`](#credentialsource) | — | **Required.** |
874
- | `onChange` | `(status: MatchStatus) => void` | — | **Required.** Fired on first read, then on any status, teams or start change. |
875
- | `onError` | `(err: PlaybackError) => void` | — | Fired on a poll error; polling continues, except on `NOT_FOUND`, `GONE` or `INVALID_URN`: the watcher stops itself and this is its last call. |
876
- | `intervalMs` | `number` | `10000` | Poll interval (floored at `2000`). |
873
+ | Option | Type | Default | Description |
874
+ | ------------ | --------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
875
+ | `baseUrl` | `string` | — | **Required.** |
876
+ | `matchUrn` | `string` | — | **Required.** |
877
+ | `credential` | [`CredentialSource`](#credentialsource) | — | **Required.** |
878
+ | `onChange` | `(status: MatchStatus) => void` | — | **Required.** Fired on first read, then on any status, teams or start change. |
879
+ | `onError` | `(err: PlaybackError) => void` | — | Fired on a poll error; polling continues, except on `NOT_FOUND`, `GEO_DENIED`, `GONE` or `INVALID_URN`: the watcher stops itself and this is its last call. |
880
+ | `intervalMs` | `number` | `10000` | Poll interval (floored at `2000`). |
877
881
 
878
882
  <a id="live-state-sse"></a>
879
883
 
@@ -1103,7 +1107,7 @@ messages from any origin and posts events with target origin `*`.
1103
1107
  { type: 'oddin:tracks'; quality: QualityLevel[]; audio: AudioTrackInfo[]; text: TextTrackInfo[] }
1104
1108
  { type: 'oddin:qualitychange'; index } | { type: 'oddin:audiotrackchange'; id } | { type: 'oddin:texttrackchange'; id }
1105
1109
  { type: 'oddin:stats'; droppedFrames; latencySeconds?; bandwidthKbps?; levelHeight?; lowLatency?; isLive?; atLiveEdge? }
1106
- { type: 'oddin:error'; code: PlaybackErrorCode; message: string }
1110
+ { type: 'oddin:error'; code: PlaybackErrorCode; message: string; country?: string } // country: set with GEO_DENIED
1107
1111
  ```
1108
1112
 
1109
1113
  The `oddin:ready` event carries the embed page's `protocol` version; `mountEmbed` warns on a
@@ -1128,25 +1132,27 @@ class PlaybackError extends Error {
1128
1132
  readonly httpStatus: number; // 0 for NETWORK / ABORTED; see the table notes for TIMEOUT / DRM_CLIENT
1129
1133
  readonly retryAfterMs?: number; // set for TOO_EARLY / RATE_LIMITED / UNAVAILABLE
1130
1134
  readonly liveStartsAtMs?: number; // epoch ms; set for TOO_EARLY when the server included `liveStartsAt`
1135
+ readonly country?: string; // upper-case ISO 3166-1 alpha-2; set for GEO_DENIED
1131
1136
  readonly requestId?: string; // server correlation id, when present
1132
1137
  }
1133
1138
  ```
1134
1139
 
1135
- | `code` | HTTP | Meaning | Retryable |
1136
- | -------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
1137
- | `INVALID_URN` | 400 | Malformed match URN. | No |
1138
- | `NOT_FOUND` | 404 | Unknown match **or** not entitled (indistinguishable by design). | No |
1139
- | `GONE` | 410 | Ended and past the catch‑up window. | No |
1140
- | `TOO_EARLY` | 425 | Upcoming; not live yet. | Yes (via `waitForLive`) |
1141
- | `UNAVAILABLE` | 503 | Should be live; origin warming up. | Yes |
1142
- | `UNAUTHORIZED` | 401 | Bad/absent api‑key. | No |
1143
- | `FORBIDDEN` | 403 | Origin not allowed, or DRM signature rejected (a real server 403). | No |
1144
- | `DRM_CLIENT` | (lic.)² | Client‑side DRM failure: no key system usable on this platform, a CDM/EME error, or license delivery failed without a server denial. | Sometimes² |
1145
- | `RATE_LIMITED` | 429 | Per‑client request limit. | Back off |
1146
- | `INTERNAL` | 5xx | Unexpected server/SDK error. | No |
1147
- | `NETWORK` | 0 | The network request failed (offline, CORS, DNS). | Yes (in the live‑poll loop) |
1148
- | `TIMEOUT` | (last)¹ | `waitForLive` exceeded `timeoutMs`. | No |
1149
- | `ABORTED` | 0 | Cancelled via `AbortSignal`. | No |
1140
+ | `code` | HTTP | Meaning | Retryable |
1141
+ | -------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
1142
+ | `INVALID_URN` | 400 | Malformed match URN. | No |
1143
+ | `NOT_FOUND` | 404 | Unknown match **or** not entitled (indistinguishable by design). | No |
1144
+ | `GEO_DENIED` | 451 | The match is booked for your key, but a grant's country list excludes the viewer's country; `country` names it. A server without the 451 answers `404 NOT_FOUND` for the same case. | No |
1145
+ | `GONE` | 410 | Ended and past the catch‑up window. | No |
1146
+ | `TOO_EARLY` | 425 | Upcoming; not live yet. | Yes (via `waitForLive`) |
1147
+ | `UNAVAILABLE` | 503 | Should be live; origin warming up. | Yes |
1148
+ | `UNAUTHORIZED` | 401 | Bad/absent api‑key. | No |
1149
+ | `FORBIDDEN` | 403 | Origin not allowed, or DRM signature rejected (a real server 403). | No |
1150
+ | `DRM_CLIENT` | (lic.)² | Client‑side DRM failure: no key system usable on this platform, a CDM/EME error, or license delivery failed without a server denial. | Sometimes² |
1151
+ | `RATE_LIMITED` | 429 | Per‑client request limit. | Back off |
1152
+ | `INTERNAL` | 5xx | Unexpected server/SDK error. | No |
1153
+ | `NETWORK` | 0 | The network request failed (offline, CORS, DNS). | Yes (in the live‑poll loop) |
1154
+ | `TIMEOUT` | (last)¹ | `waitForLive` exceeded `timeoutMs`. | No |
1155
+ | `ABORTED` | 0 | Cancelled via `AbortSignal`. | No |
1150
1156
 
1151
1157
  ¹ `TIMEOUT.httpStatus` carries the status of the last error it was waiting on, typically
1152
1158
  `425` while waiting for an upcoming match, `503` while an origin warms up, or `0` if the
@@ -1181,6 +1187,7 @@ try {
1181
1187
  await resolveStream({ baseUrl, matchUrn, credential });
1182
1188
  } catch (e) {
1183
1189
  if (e instanceof PlaybackError && e.code === 'NOT_FOUND') showUnavailable();
1190
+ if (e instanceof PlaybackError && e.code === 'GEO_DENIED') showBlockedIn(e.country); // e.g. "RU"
1184
1191
  }
1185
1192
  ```
1186
1193
 
@@ -1463,18 +1470,19 @@ whose `baseUrl` you serve.
1463
1470
 
1464
1471
  ## Troubleshooting
1465
1472
 
1466
- | Symptom | Likely cause |
1467
- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1468
- | `NETWORK` error on every call; preflight fails in devtools | Your origin isn't on the key's allowed‑origins / DRM CORS allow‑list. See [Onboarding](#onboarding--cors). |
1469
- | `UNAUTHORIZED` (401) | Wrong/expired api‑key, or wrong `baseUrl` environment (test vs prod key). |
1470
- | `FORBIDDEN` (403) on license requests | The license URL was modified, or a different api‑key was used for the license than for playback. POST the URL verbatim with the same key. |
1471
- | `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. |
1472
- | `NOT_FOUND` (404) for a match you can see | The key isn't entitled to that tournament (404 is intentionally indistinguishable from "unknown"). |
1473
- | Player stays in `waiting` forever | The match hasn't gone live yet; the SDK is polling. Set `waitForLive.timeoutMs` to bound it. |
1474
- | Autoplay doesn't start | Browser sound policy: set `muted: true` for autoplay, or start playback from a user gesture. |
1475
- | 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. |
1476
- | 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. |
1477
- | Black video, `levelHeight`/`droppedFrames` stuck | Check the browser console for an EME/CDM error and verify DRM CORS onboarding. |
1473
+ | Symptom | Likely cause |
1474
+ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1475
+ | `NETWORK` error on every call; preflight fails in devtools | Your origin isn't on the key's allowed‑origins / DRM CORS allow‑list. See [Onboarding](#onboarding--cors). |
1476
+ | `UNAUTHORIZED` (401) | Wrong/expired api‑key, or wrong `baseUrl` environment (test vs prod key). |
1477
+ | `FORBIDDEN` (403) on license requests | The license URL was modified, or a different api‑key was used for the license than for playback. POST the URL verbatim with the same key. |
1478
+ | `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. |
1479
+ | `NOT_FOUND` (404) for a match you can see | The key isn't entitled to that tournament (404 is intentionally indistinguishable from "unknown"). |
1480
+ | `GEO_DENIED` (451) for a match you can see | The key's grant for that match carries a country list that excludes the viewer's country (`e.country`). The viewer sees a card naming the country; the listings never show such a match to that viewer, so this only happens on a direct URN. |
1481
+ | Player stays in `waiting` forever | The match hasn't gone live yet; the SDK is polling. Set `waitForLive.timeoutMs` to bound it. |
1482
+ | Autoplay doesn't start | Browser sound policy: set `muted: true` for autoplay, or start playback from a user gesture. |
1483
+ | 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. |
1484
+ | 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. |
1485
+ | Black video, `levelHeight`/`droppedFrames` stuck | Check the browser console for an EME/CDM error and verify DRM CORS onboarding. |
1478
1486
 
1479
1487
  Include `PlaybackError.requestId` (when present) when contacting support; it correlates
1480
1488
  to server‑side logs.
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.19.0/dist/havik-player.global.js" integrity="sha384-RWHZWgsgSqUcFMISDll7/ApdE2YOBYQ9C0xvHZo3OtY8AuU9OoSL5kOA3G29w/bg" crossorigin="anonymous"></script>
31
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.20.0/dist/havik-player.global.js" integrity="sha384-PPVgkwJKWlmuj52+pDqQWp+UmqBJk608YuskZya+/ESHrelRqOEfgzcXn3glsZv4" 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.19.0/dist/havik-player.global.js" integrity="sha384-RWHZWgsgSqUcFMISDll7/ApdE2YOBYQ9C0xvHZo3OtY8AuU9OoSL5kOA3G29w/bg" crossorigin="anonymous"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.20.0/dist/havik-player.global.js" integrity="sha384-PPVgkwJKWlmuj52+pDqQWp+UmqBJk608YuskZya+/ESHrelRqOEfgzcXn3glsZv4" 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.19.0",
3
+ "version": "2.20.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,