@oddin-gg/havik-player 2.20.0 → 2.22.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/README.md +12 -11
- package/dist/havik-player.global.js +18 -18
- package/dist/havik-player.global.js.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +430 -374
- package/dist/index.js.map +1 -1
- package/docs/API.md +65 -34
- package/embed/index.html +1 -1
- package/examples/vanilla.html +1 -1
- package/package.json +1 -1
package/docs/API.md
CHANGED
|
@@ -250,6 +250,7 @@ player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
|
|
|
250
250
|
| `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.1×, so this is the only live‑latency knob you need. Lower = closer to live but more rebuffer‑prone. Live/LL only. |
|
|
251
251
|
| `snapToLiveOnRefocus` | `boolean` | `true` | Snap to the live edge when a backgrounded tab is refocused: on hls.js while far behind; on the native player (iPhone, iPad, FairPlay) on every return after 2 s or more away and on every back/forward‑cache restore, 3 s behind the edge once the first playlist refresh after the return moves it (up to 6 s wait; the edge on return is still the one from before leaving), which also redraws a picture iOS left black. Live/LL only. |
|
|
252
252
|
| `rememberBandwidth` | `boolean` | `true` | hls.js engine: start from the bandwidth this device measured in a recent session (`localStorage` key `havik.player.bandwidth`, per connection type, up to 3 days old, reused at 80%) instead of the conservative ~1 Mbps start; a slow record starts from that lower figure (0.3 Mbps floor). A first visit, a stale record or blocked storage keeps the conservative start; an explicit `hlsConfig.abrEwmaDefaultEstimate` or `startLevel` wins. Reset with [`clearRememberedBandwidth()`](#clearrememberedbandwidth). |
|
|
253
|
+
| `sizeCapHeadroom` | `number` | unset | hls.js engine: rendition rungs ABR may pick above the one that merely covers the player's pixel box. Unset: `1` on displays below 2× `devicePixelRatio`, `0` at 2× and above. A 960 px box at 1x covers with 960×540; one rung of headroom plays 720p downscaled instead, which looks sharper for the same box; phones and 2× screens keep the cover-only cap (their covering rung already matches their device pixels; a rung above it mostly costs data). An explicit value applies on every display: `0` = strict cover-only cap, `Infinity` = no size cap. Never past the top rung; `maxBitrate` and the frame-drop cap still apply. `hlsConfig.capLevelToPlayerSize: true` caps to the covering rung with no headroom. |
|
|
253
254
|
| `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. |
|
|
254
255
|
| `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. On a return from the background (5 s or more away, or a back/forward-cache restore) the subscription is reopened so the server replays the current state: a page suspended through the end of a match does not sit buffering on a socket that will never deliver it. Set `false` to disable. |
|
|
255
256
|
| `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`. |
|
|
@@ -314,15 +315,22 @@ don't need to set any of the live or ABR knobs:
|
|
|
314
315
|
and a device that measured a slow line starts from that lower figure (also at
|
|
315
316
|
80%, never below 0.3 Mbps). A first visit or a device with no usable record
|
|
316
317
|
keeps the ~1 Mbps start. In addition,
|
|
317
|
-
the session is bounded to the player's pixel box
|
|
318
|
-
|
|
318
|
+
the session is bounded to the player's pixel box **plus one rung of
|
|
319
|
+
headroom** on displays below 2× `devicePixelRatio` (`sizeCapHeadroom`; set
|
|
320
|
+
it explicitly to apply a value on every display), so a small/embedded player won't fetch 1080p, while a 960 px
|
|
321
|
+
desktop embed gets 720p downscaled rather than 540p at 1:1 (a lower encode
|
|
322
|
+
shown pixel‑for‑pixel looks softer than a higher one scaled down; measured on
|
|
323
|
+
production, Windows viewers sat at ≤540p 73% of the time with bandwidth to
|
|
324
|
+
spare). Phones and 2× screens keep the cover‑only cap. The cap is
|
|
325
|
+
SDK‑managed and **debounced** rather than
|
|
319
326
|
hls.js's own `capLevelToPlayerSize` poller: a size change only reaches ABR
|
|
320
327
|
once the box has been stable for **~2 s** (raise), and a shrink is applied
|
|
321
328
|
**lazily (~30 s)**, so a fullscreen enter/exit never forces a quality
|
|
322
329
|
switch, and its ~1 s stall, mid‑transition (the higher rendition simply
|
|
323
330
|
downscales into the smaller box until the down‑cap lands). Passing
|
|
324
331
|
`hlsConfig.capLevelToPlayerSize: true` restores hls.js's immediate poller and
|
|
325
|
-
stands the SDK manager down
|
|
332
|
+
stands the SDK manager down, including the headroom: the poller caps to the
|
|
333
|
+
covering rung only. The ceiling in force and which limit set it
|
|
326
334
|
are reported in every `stats` event (`maxLevelHeight`, `capReason`), so a
|
|
327
335
|
player stuck at 540p in a 758 px box can say so. Pin a fixed rung with `startLevel`,
|
|
328
336
|
cap the top with `maxBitrate`, or raise the seed via
|
|
@@ -453,9 +461,11 @@ _armed-waiting_ ("Starting soon…"), so a finished or failed stream never leave
|
|
|
453
461
|
a frozen frame behind a dead play button. A `GEO_DENIED` error (the match is
|
|
454
462
|
booked for your key but not playable in the viewer's country) gets its own copy,
|
|
455
463
|
"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.
|
|
464
|
+
saw, and no Retry button: a retry would be refused again. A `DRM_DEVICE_REVOKED` error
|
|
465
|
+
(the license server refuses this device's DRM client) likewise gets "This device can't
|
|
466
|
+
play protected video" and no Retry. The cards pick up `theme.logoUrl` and
|
|
457
467
|
the theme colors; customise the copy with `endedMessage`/`interruptedMessage`/`errorMessage`
|
|
458
|
-
(an `errorMessage` replaces the `GEO_DENIED` copy too), or set
|
|
468
|
+
(an `errorMessage` replaces the `GEO_DENIED` and `DRM_DEVICE_REVOKED` copy too), or set
|
|
459
469
|
`statusOverlays: false` to drive your own end/error UI from the player events.
|
|
460
470
|
It auto-hides during playback, supports keyboard shortcuts (space/`k` play-pause,
|
|
461
471
|
`m` mute, `f` fullscreen, `c` captions, `l` go-live, arrows seek/volume), and is
|
|
@@ -1133,26 +1143,28 @@ class PlaybackError extends Error {
|
|
|
1133
1143
|
readonly retryAfterMs?: number; // set for TOO_EARLY / RATE_LIMITED / UNAVAILABLE
|
|
1134
1144
|
readonly liveStartsAtMs?: number; // epoch ms; set for TOO_EARLY when the server included `liveStartsAt`
|
|
1135
1145
|
readonly country?: string; // upper-case ISO 3166-1 alpha-2; set for GEO_DENIED
|
|
1146
|
+
readonly serverCode?: string; // the server's own code, when the body carried one (e.g. 'cdm_revoked', 'license_refused')
|
|
1136
1147
|
readonly requestId?: string; // server correlation id, when present
|
|
1137
1148
|
}
|
|
1138
1149
|
```
|
|
1139
1150
|
|
|
1140
|
-
| `code`
|
|
1141
|
-
|
|
|
1142
|
-
| `INVALID_URN`
|
|
1143
|
-
| `NOT_FOUND`
|
|
1144
|
-
| `GEO_DENIED`
|
|
1145
|
-
| `GONE`
|
|
1146
|
-
| `TOO_EARLY`
|
|
1147
|
-
| `UNAVAILABLE`
|
|
1148
|
-
| `UNAUTHORIZED`
|
|
1149
|
-
| `FORBIDDEN`
|
|
1150
|
-
| `
|
|
1151
|
-
| `
|
|
1152
|
-
| `
|
|
1153
|
-
| `
|
|
1154
|
-
| `
|
|
1155
|
-
| `
|
|
1151
|
+
| `code` | HTTP | Meaning | Retryable |
|
|
1152
|
+
| -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
|
|
1153
|
+
| `INVALID_URN` | 400 | Malformed match URN. | No |
|
|
1154
|
+
| `NOT_FOUND` | 404 | Unknown match **or** not entitled (indistinguishable by design). | No |
|
|
1155
|
+
| `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 |
|
|
1156
|
+
| `GONE` | 410 | Ended and past the catch‑up window. | No |
|
|
1157
|
+
| `TOO_EARLY` | 425 | Upcoming; not live yet. | Yes (via `waitForLive`) |
|
|
1158
|
+
| `UNAVAILABLE` | 503 | Should be live; origin warming up. | Yes |
|
|
1159
|
+
| `UNAUTHORIZED` | 401 | Bad/absent api‑key. | No |
|
|
1160
|
+
| `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⁴ |
|
|
1161
|
+
| `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³ |
|
|
1162
|
+
| `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² |
|
|
1163
|
+
| `RATE_LIMITED` | 429 | Per‑client request limit. | Back off |
|
|
1164
|
+
| `INTERNAL` | 5xx | Unexpected server/SDK error. | No |
|
|
1165
|
+
| `NETWORK` | 0 | The network request failed (offline, CORS, DNS). | Yes (in the live‑poll loop) |
|
|
1166
|
+
| `TIMEOUT` | (last)¹ | `waitForLive` exceeded `timeoutMs`. | No |
|
|
1167
|
+
| `ABORTED` | 0 | Cancelled via `AbortSignal`. | No |
|
|
1156
1168
|
|
|
1157
1169
|
¹ `TIMEOUT.httpStatus` carries the status of the last error it was waiting on, typically
|
|
1158
1170
|
`425` while waiting for an upcoming match, `503` while an origin warms up, or `0` if the
|
|
@@ -1167,6 +1179,24 @@ play DRM. The built-in error card still offers Retry (it re-resolves and re-atta
|
|
|
1167
1179
|
which can recover a transient license failure); an unsupported device will simply fail
|
|
1168
1180
|
the same way again.
|
|
1169
1181
|
|
|
1182
|
+
³ `DRM_DEVICE_REVOKED` is read off the body of a license `403`: the DRM proxy answers
|
|
1183
|
+
`{"error":"cdm_revoked"}` when the license server refuses the device's DRM client
|
|
1184
|
+
(Widevine in practice; the FairPlay path reads the same body). Any other license `403`
|
|
1185
|
+
body (`license_refused`, a WAF page, nothing) stays `FORBIDDEN`; `serverCode` carries the
|
|
1186
|
+
body's code either way. It is terminal for that device: the built-in error card shows
|
|
1187
|
+
"This device can't play protected video" with no Retry, and the lapsed-URL recovery below
|
|
1188
|
+
is not spent on it. On the hls.js engine the body is read off the XHR hls.js attaches to
|
|
1189
|
+
its error; a caller-supplied `hlsConfig.licenseXhrSetup` does not change that. Tell the
|
|
1190
|
+
viewer to use another device or an up-to-date browser; there is nothing to fix on your side.
|
|
1191
|
+
|
|
1192
|
+
⁴ "Retryable" is whether a retry can succeed, and a `FORBIDDEN` request answered the
|
|
1193
|
+
same way will not. The built-in error card still offers Retry on `FORBIDDEN` (as it does on
|
|
1194
|
+
`DRM_CLIENT`), because one license `403` shape is recoverable: a signed URL whose `exp`
|
|
1195
|
+
lapsed is refreshed by the re-resolve Retry performs (the lapsed-URL handling below). A
|
|
1196
|
+
`403` with `serverCode: 'license_refused'` keeps that card unchanged; only
|
|
1197
|
+
`DRM_DEVICE_REVOKED` drops the Retry button, since the server named a cause no retry
|
|
1198
|
+
changes.
|
|
1199
|
+
|
|
1170
1200
|
A lapsed signed license URL is handled before it reaches you. The `licenseRefreshMs`
|
|
1171
1201
|
timer cannot run while the browser has the tab frozen (a long-hidden tab, a machine
|
|
1172
1202
|
that slept), so the URL the player holds can carry an `exp` that has already passed.
|
|
@@ -1470,19 +1500,20 @@ whose `baseUrl` you serve.
|
|
|
1470
1500
|
|
|
1471
1501
|
## Troubleshooting
|
|
1472
1502
|
|
|
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
|
-
| `
|
|
1479
|
-
| `
|
|
1480
|
-
| `
|
|
1481
|
-
|
|
|
1482
|
-
|
|
|
1483
|
-
|
|
|
1484
|
-
| Black video
|
|
1485
|
-
| Black video
|
|
1503
|
+
| Symptom | Likely cause |
|
|
1504
|
+
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1505
|
+
| `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). |
|
|
1506
|
+
| `UNAUTHORIZED` (401) | Wrong/expired api‑key, or wrong `baseUrl` environment (test vs prod key). |
|
|
1507
|
+
| `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. |
|
|
1508
|
+
| `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. |
|
|
1509
|
+
| `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. |
|
|
1510
|
+
| `NOT_FOUND` (404) for a match you can see | The key isn't entitled to that tournament (404 is intentionally indistinguishable from "unknown"). |
|
|
1511
|
+
| `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. |
|
|
1512
|
+
| Player stays in `waiting` forever | The match hasn't gone live yet; the SDK is polling. Set `waitForLive.timeoutMs` to bound it. |
|
|
1513
|
+
| Autoplay doesn't start | Browser sound policy: set `muted: true` for autoplay, or start playback from a user gesture. |
|
|
1514
|
+
| 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. |
|
|
1515
|
+
| 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. |
|
|
1516
|
+
| Black video, `levelHeight`/`droppedFrames` stuck | Check the browser console for an EME/CDM error and verify DRM CORS onboarding. |
|
|
1486
1517
|
|
|
1487
1518
|
Include `PlaybackError.requestId` (when present) when contacting support; it correlates
|
|
1488
1519
|
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.
|
|
31
|
+
<script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.22.0/dist/havik-player.global.js" integrity="sha384-Du3mmsR/KRsxQK7fr2QKzQVXHnmnP5Jt2RzfQklbA2fNAffKypMA7MJDil1KbNYi" crossorigin="anonymous"></script>
|
|
32
32
|
<script>
|
|
33
33
|
HavikPlayer.bootEmbed();
|
|
34
34
|
</script>
|
package/examples/vanilla.html
CHANGED
|
@@ -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.
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@2.22.0/dist/havik-player.global.js" integrity="sha384-Du3mmsR/KRsxQK7fr2QKzQVXHnmnP5Jt2RzfQklbA2fNAffKypMA7MJDil1KbNYi" 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.
|
|
3
|
+
"version": "2.22.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,
|