@oddin-gg/havik-player 2.20.0 → 2.21.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
@@ -453,9 +453,11 @@ _armed-waiting_ ("Starting soon…"), so a finished or failed stream never leave
453
453
  a frozen frame behind a dead play button. A `GEO_DENIED` error (the match is
454
454
  booked for your key but not playable in the viewer's country) gets its own copy,
455
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
456
+ saw, and no Retry button: a retry would be refused again. A `DRM_DEVICE_REVOKED` error
457
+ (the license server refuses this device's DRM client) likewise gets "This device can't
458
+ play protected video" and no Retry. The cards pick up `theme.logoUrl` and
457
459
  the theme colors; customise the copy with `endedMessage`/`interruptedMessage`/`errorMessage`
458
- (an `errorMessage` replaces the `GEO_DENIED` copy too), or set
460
+ (an `errorMessage` replaces the `GEO_DENIED` and `DRM_DEVICE_REVOKED` copy too), or set
459
461
  `statusOverlays: false` to drive your own end/error UI from the player events.
460
462
  It auto-hides during playback, supports keyboard shortcuts (space/`k` play-pause,
461
463
  `m` mute, `f` fullscreen, `c` captions, `l` go-live, arrows seek/volume), and is
@@ -1133,26 +1135,28 @@ class PlaybackError extends Error {
1133
1135
  readonly retryAfterMs?: number; // set for TOO_EARLY / RATE_LIMITED / UNAVAILABLE
1134
1136
  readonly liveStartsAtMs?: number; // epoch ms; set for TOO_EARLY when the server included `liveStartsAt`
1135
1137
  readonly country?: string; // upper-case ISO 3166-1 alpha-2; set for GEO_DENIED
1138
+ readonly serverCode?: string; // the server's own code, when the body carried one (e.g. 'cdm_revoked', 'license_refused')
1136
1139
  readonly requestId?: string; // server correlation id, when present
1137
1140
  }
1138
1141
  ```
1139
1142
 
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 |
1143
+ | `code` | HTTP | Meaning | Retryable |
1144
+ | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
1145
+ | `INVALID_URN` | 400 | Malformed match URN. | No |
1146
+ | `NOT_FOUND` | 404 | Unknown match **or** not entitled (indistinguishable by design). | No |
1147
+ | `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 |
1148
+ | `GONE` | 410 | Ended and past the catch‑up window. | No |
1149
+ | `TOO_EARLY` | 425 | Upcoming; not live yet. | Yes (via `waitForLive`) |
1150
+ | `UNAVAILABLE` | 503 | Should be live; origin warming up. | Yes |
1151
+ | `UNAUTHORIZED` | 401 | Bad/absent api‑key. | No |
1152
+ | `FORBIDDEN` | 403 | Origin not allowed, DRM signature rejected, or a license refused for a reason the server did not name (`serverCode: 'license_refused'`); a real server 403. | No⁴ |
1153
+ | `DRM_DEVICE_REVOKED` | 403 | The license server refuses this device's DRM client outright (a revoked Widevine certificate); the license 403 body carries `cdm_revoked`. The device is the problem, not the match, the key or the network. | No³ |
1154
+ | `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² |
1155
+ | `RATE_LIMITED` | 429 | Per‑client request limit. | Back off |
1156
+ | `INTERNAL` | 5xx | Unexpected server/SDK error. | No |
1157
+ | `NETWORK` | 0 | The network request failed (offline, CORS, DNS). | Yes (in the live‑poll loop) |
1158
+ | `TIMEOUT` | (last)¹ | `waitForLive` exceeded `timeoutMs`. | No |
1159
+ | `ABORTED` | 0 | Cancelled via `AbortSignal`. | No |
1156
1160
 
1157
1161
  ¹ `TIMEOUT.httpStatus` carries the status of the last error it was waiting on, typically
1158
1162
  `425` while waiting for an upcoming match, `503` while an origin warms up, or `0` if the
@@ -1167,6 +1171,24 @@ play DRM. The built-in error card still offers Retry (it re-resolves and re-atta
1167
1171
  which can recover a transient license failure); an unsupported device will simply fail
1168
1172
  the same way again.
1169
1173
 
1174
+ ³ `DRM_DEVICE_REVOKED` is read off the body of a license `403`: the DRM proxy answers
1175
+ `{"error":"cdm_revoked"}` when the license server refuses the device's DRM client
1176
+ (Widevine in practice; the FairPlay path reads the same body). Any other license `403`
1177
+ body (`license_refused`, a WAF page, nothing) stays `FORBIDDEN`; `serverCode` carries the
1178
+ body's code either way. It is terminal for that device: the built-in error card shows
1179
+ "This device can't play protected video" with no Retry, and the lapsed-URL recovery below
1180
+ is not spent on it. On the hls.js engine the body is read off the XHR hls.js attaches to
1181
+ its error; a caller-supplied `hlsConfig.licenseXhrSetup` does not change that. Tell the
1182
+ viewer to use another device or an up-to-date browser; there is nothing to fix on your side.
1183
+
1184
+ ⁴ "Retryable" is whether a retry can succeed, and a `FORBIDDEN` request answered the
1185
+ same way will not. The built-in error card still offers Retry on `FORBIDDEN` (as it does on
1186
+ `DRM_CLIENT`), because one license `403` shape is recoverable: a signed URL whose `exp`
1187
+ lapsed is refreshed by the re-resolve Retry performs (the lapsed-URL handling below). A
1188
+ `403` with `serverCode: 'license_refused'` keeps that card unchanged; only
1189
+ `DRM_DEVICE_REVOKED` drops the Retry button, since the server named a cause no retry
1190
+ changes.
1191
+
1170
1192
  A lapsed signed license URL is handled before it reaches you. The `licenseRefreshMs`
1171
1193
  timer cannot run while the browser has the tab frozen (a long-hidden tab, a machine
1172
1194
  that slept), so the URL the player holds can carry an `exp` that has already passed.
@@ -1470,19 +1492,20 @@ whose `baseUrl` you serve.
1470
1492
 
1471
1493
  ## Troubleshooting
1472
1494
 
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. |
1495
+ | Symptom | Likely cause |
1496
+ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1497
+ | `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). |
1498
+ | `UNAUTHORIZED` (401) | Wrong/expired api‑key, or wrong `baseUrl` environment (test vs prod key). |
1499
+ | `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. With `serverCode: 'license_refused'`, the license server refused the request for a reason it did not name; include `requestId` when asking support. |
1500
+ | `DRM_DEVICE_REVOKED` (403) on license requests | The license server refuses this device's DRM client (a revoked Widevine certificate, typically an old browser build or a modified device). Nothing on your side or ours changes it; the viewer needs another device or an up-to-date browser. The built-in card says so and offers no Retry. |
1501
+ | `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. |
1502
+ | `NOT_FOUND` (404) for a match you can see | The key isn't entitled to that tournament (404 is intentionally indistinguishable from "unknown"). |
1503
+ | `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. |
1504
+ | Player stays in `waiting` forever | The match hasn't gone live yet; the SDK is polling. Set `waitForLive.timeoutMs` to bound it. |
1505
+ | Autoplay doesn't start | Browser sound policy: set `muted: true` for autoplay, or start playback from a user gesture. |
1506
+ | 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. |
1507
+ | 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. |
1508
+ | Black video, `levelHeight`/`droppedFrames` stuck | Check the browser console for an EME/CDM error and verify DRM CORS onboarding. |
1486
1509
 
1487
1510
  Include `PlaybackError.requestId` (when present) when contacting support; it correlates
1488
1511
  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.20.0/dist/havik-player.global.js" integrity="sha384-PPVgkwJKWlmuj52+pDqQWp+UmqBJk608YuskZya+/ESHrelRqOEfgzcXn3glsZv4" crossorigin="anonymous"></script>
31
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.21.0/dist/havik-player.global.js" integrity="sha384-KGq+XypxWv0FVoUTM6e/yelF8vOuAmx2dJbIj3E/Ee2E6Ky8bTHorPnRapaKqfY+" 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.20.0/dist/havik-player.global.js" integrity="sha384-PPVgkwJKWlmuj52+pDqQWp+UmqBJk608YuskZya+/ESHrelRqOEfgzcXn3glsZv4" crossorigin="anonymous"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.21.0/dist/havik-player.global.js" integrity="sha384-KGq+XypxWv0FVoUTM6e/yelF8vOuAmx2dJbIj3E/Ee2E6Ky8bTHorPnRapaKqfY+" 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.20.0",
3
+ "version": "2.21.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,