@oddin-gg/havik-player 2.27.2 → 2.28.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/dist/havik-player.global.js +3 -3
- 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 +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +314 -230
- package/dist/index.js.map +1 -1
- package/docs/API.md +57 -23
- package/embed/index.html +1 -1
- package/examples/vanilla.html +1 -1
- package/package.json +1 -1
package/docs/API.md
CHANGED
|
@@ -253,6 +253,7 @@ player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
|
|
|
253
253
|
| `adaptiveHoldBackCeiling` | `number` | `3.5` | Highest live‑sync target the adaptive hold‑back may step to, in seconds. Kept a second under the drift ceiling; at or below `liveLatencyTarget` it disables the stepping. |
|
|
254
254
|
| `stallCap` | `boolean` | `true` | hls.js engine: after a playback stall (not in the first 10 s after playback starts or resumes, not while seeking, not with media still buffered ahead, not a wait at the live edge for a part not yet published), cap quality one rendition below the one that stalled, and give one rendition back per 3 minutes of playback without another stall (paused time does not count); a stall while capped steps down again or restarts the 3 minutes. Without it, ABR climbs straight back onto a rendition the connection just failed to carry: on measured production sessions, 55–65 % of rebuffer time was such a repeat within 3 minutes. Counted in `stallDownCaps`; `capReason` is `'stall'` while it is the binding ceiling. Never applies to a quality the viewer pinned; off when `hlsConfig.capLevelToPlayerSize` or `capLevelOnFPSDrop: true` hands capping to hls.js. |
|
|
255
255
|
| `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. |
|
|
256
|
+
| `stuckReload` | `boolean` | `true` | Native engine (Safari, iPhone, iPad), live only: when the playhead has not moved and the buffered media under it has not grown for 8 s, with the page visible, the element playing and the browser online, the player reloads the stream at the live edge. WebKit's native HLS has a freeze it never leaves by itself: on production iPhone Safari over 8 days to 2026-11-04 there were 2.31 such freezes per watch-hour (hls.js on Android, on the same CDN slots and in the same countries: 0.35), not one recovered on its own, and viewers paused or left after a median 24 s. At most twice per 30 s of healthy playback; a slow start, a seek, a pause, a hidden tab and an offline browser never count. Each reload counts in `stuckReloads`. |
|
|
256
257
|
| `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). |
|
|
257
258
|
| `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`, **earned per session on LL-HLS** (its parts download within half a part duration even at that rung's bitrate, the bandwidth estimate is twice the rung and stays above 1.5×, and no stall in the last 2 minutes; a stall after the first 10 s of playback drops the rung at once; streams without parts get the rung as before), `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. |
|
|
258
259
|
| `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. |
|
|
@@ -369,6 +370,26 @@ is derived above it.
|
|
|
369
370
|
`hlsConfig.capLevelOnFPSDrop: false` turns frame‑drop capping off; `true` runs
|
|
370
371
|
hls.js's own controller instead, whose cap a later player‑size or `maxBitrate`
|
|
371
372
|
recompute can overwrite.
|
|
373
|
+
- **A frozen picture on Safari is reloaded.** The native engine (Safari,
|
|
374
|
+
iPhone, iPad) watches the playhead and the buffered media under it: when
|
|
375
|
+
neither has moved for 8 s while the page is visible, the element is playing
|
|
376
|
+
and the browser is online, the stream is reloaded at the live edge. WebKit's
|
|
377
|
+
native HLS has a freeze it never leaves by itself: the playhead stops, the
|
|
378
|
+
buffer stops growing, nothing decodes and no error is raised. On production
|
|
379
|
+
iPhone Safari (8 days to 2026-11-04) that happened 2.31 times per watch-hour,
|
|
380
|
+
none recovered, and viewers paused or left after a median 24 s. Twice per
|
|
381
|
+
30 s of healthy playback; a slow start, a seek, a pause, a hidden tab and an
|
|
382
|
+
offline browser never count. Counted in `stuckReloads`; `stuckReload: false`
|
|
383
|
+
turns it off.
|
|
384
|
+
- **A buffer hole hls.js cannot nudge across jumps to live.** When hls.js gives
|
|
385
|
+
up seeking the playhead over a hole in the buffer (its fatal
|
|
386
|
+
`bufferSeekOverHole`, or a `bufferStalledError` after its nudge budget) on a
|
|
387
|
+
live stream, the SDK restarts loading at the live edge, which is past the
|
|
388
|
+
hole, with the media element, the buffer and the DRM key sessions still in
|
|
389
|
+
place. Twice per 30 s of healthy playback, out of the same three-recovery
|
|
390
|
+
budget the MediaSource rebuild uses; after that, and on VOD, the
|
|
391
|
+
MediaSource is rebuilt as before, and the fourth consecutive failure is the
|
|
392
|
+
fatal error it always was.
|
|
372
393
|
|
|
373
394
|
- **Futile catch‑up is stopped.** hls.js speeds playback up (to 1.1×) whenever
|
|
374
395
|
latency sits above the target, and only lowers its ambition after a counted
|
|
@@ -470,26 +491,26 @@ Notes:
|
|
|
470
491
|
|
|
471
492
|
`on(event, cb)`: the payload type depends on the event ([`PlayerEventMap`](#playereventmap)):
|
|
472
493
|
|
|
473
|
-
| Event | Payload | Fires when
|
|
474
|
-
| --------------------- | --------------------------------------- |
|
|
475
|
-
| `ready` | `void` | The manifest is parsed and the player is attached.
|
|
476
|
-
| `waiting` | [`WaitState`](#waitstate) | Armed and waiting for go‑live. On the polling path it fires per poll with the countdown; on the SSE arming path it fires once, just after `createPlayer` resolves (with `ttkMs` when the server sent a kickoff time).
|
|
477
|
-
| `playing` | `void` | Playback is progressing.
|
|
478
|
-
| `buffering` | `void` | The player is rebuffering.
|
|
479
|
-
| `paused` | `void` | Playback is paused.
|
|
480
|
-
| `ended` | `EndedReason \| undefined` | The live stream ended; playback stops and the last frame is kept. The payload says **why**: `'interrupted'` (the match is still running, and its stream dropped, so show "reconnecting", **never** "ended"; the player rejoins by itself) or `'match_ended'` (a real end). `undefined` = the end came from the staleness watchdog (either engine: a live playlist that stopped advancing for 12 s), or a server predating the field; treat it as a real end. Fires **again** with `'match_ended'` if an interruption later turns out to be the end, so don't latch on the first one. Also readable any time as `player.endedReason`. |
|
|
481
|
-
| `error` | [`PlaybackError`](#playbackerror) | A fatal error occurred (playback stopped).
|
|
482
|
-
| `warning` | [`PlaybackError`](#playbackerror) | A non-fatal condition (e.g. FairPlay passthrough, a failed license refresh, **QoE beacons disabled because the endpoint could not be derived** — see [`analytics`](#createplayeroptions) — or **QoE beacons refused by the page's Content-Security-Policy**, naming the origin to add to `connect-src`). Playback continues.
|
|
483
|
-
| `autoplayblocked` | `void` | The browser blocked autoplay (`NotAllowedError`). Show a tap-to-play / tap-to-unmute affordance, then call `player.play()` from the gesture.
|
|
484
|
-
| `qualitychange` | `number` | Active rendition changed (level index, `-1` = auto).
|
|
485
|
-
| `audiotrackchange` | `number` | Active audio track changed (track id).
|
|
486
|
-
| `texttrackchange` | `number` | Active text track changed (track id, `-1` = off).
|
|
487
|
-
| `pipchange` | `boolean` | Picture-in-Picture entered (`true`) or exited (`false`).
|
|
488
|
-
| `latencytargetchange` | [`LiveTargetChange`](#livetargetchange) | The SDK moved the live‑sync target itself (hls.js engine): the adaptive hold‑back stepped it up for slow part fetches or down for fast ones, a high browser‑reported RTT started the session a step up, or a futile catch‑up parked it where playback is. Never for your own `liveLatencyTarget`.
|
|
489
|
-
| `stats` | [`PlaybackStats`](#playbackstats) | Periodic stats sample (`statsIntervalMs`).
|
|
490
|
-
| `statechange` | [`PlayerState`](#playerstate) | Any state transition.
|
|
491
|
-
| `offline` | `void` | The browser reported it lost its network (the window's `offline` event). `state` is unchanged; `player.offline` is now `true`. See [Connectivity](#connectivity).
|
|
492
|
-
| `online` | `void` | The browser reported its network is back (the window's `online` event). The player resumes on its own: a parked hls.js loader restarts at once, and a fatal error that happened while offline (or any `NETWORK` error) is retried, the same re-resolve as `retry()`.
|
|
494
|
+
| Event | Payload | Fires when |
|
|
495
|
+
| --------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
496
|
+
| `ready` | `void` | The manifest is parsed and the player is attached. |
|
|
497
|
+
| `waiting` | [`WaitState`](#waitstate) | Armed and waiting for go‑live. On the polling path it fires per poll with the countdown; on the SSE arming path it fires once, just after `createPlayer` resolves (with `ttkMs` when the server sent a kickoff time). |
|
|
498
|
+
| `playing` | `void` | Playback is progressing. |
|
|
499
|
+
| `buffering` | `void` | The player is rebuffering. |
|
|
500
|
+
| `paused` | `void` | Playback is paused. |
|
|
501
|
+
| `ended` | `EndedReason \| undefined` | The live stream ended; playback stops and the last frame is kept. The payload says **why**: `'interrupted'` (the match is still running, and its stream dropped, so show "reconnecting", **never** "ended"; the player rejoins by itself) or `'match_ended'` (a real end). `undefined` = the end came from the staleness watchdog (either engine: a live playlist that stopped advancing for 12 s), or a server predating the field; treat it as a real end. While the live-state push is fresh (a frame within the last 60 s) and last said `live`, a watchdog end is not delivered: the player fires a `warning` (`UNAVAILABLE`, "reloading instead of ending") and reloads the stream instead, at most twice per 10 minutes; the third ends as before. Fires **again** with `'match_ended'` if an interruption later turns out to be the end, so don't latch on the first one. Also readable any time as `player.endedReason`. |
|
|
502
|
+
| `error` | [`PlaybackError`](#playbackerror) | A fatal error occurred (playback stopped). |
|
|
503
|
+
| `warning` | [`PlaybackError`](#playbackerror) | A non-fatal condition (e.g. FairPlay passthrough, a failed license refresh, **QoE beacons disabled because the endpoint could not be derived** — see [`analytics`](#createplayeroptions) — or **QoE beacons refused by the page's Content-Security-Policy**, naming the origin to add to `connect-src`). Playback continues. |
|
|
504
|
+
| `autoplayblocked` | `void` | The browser blocked autoplay (`NotAllowedError`). Show a tap-to-play / tap-to-unmute affordance, then call `player.play()` from the gesture. |
|
|
505
|
+
| `qualitychange` | `number` | Active rendition changed (level index, `-1` = auto). |
|
|
506
|
+
| `audiotrackchange` | `number` | Active audio track changed (track id). |
|
|
507
|
+
| `texttrackchange` | `number` | Active text track changed (track id, `-1` = off). |
|
|
508
|
+
| `pipchange` | `boolean` | Picture-in-Picture entered (`true`) or exited (`false`). |
|
|
509
|
+
| `latencytargetchange` | [`LiveTargetChange`](#livetargetchange) | The SDK moved the live‑sync target itself (hls.js engine): the adaptive hold‑back stepped it up for slow part fetches or down for fast ones, a high browser‑reported RTT started the session a step up, or a futile catch‑up parked it where playback is. Never for your own `liveLatencyTarget`. |
|
|
510
|
+
| `stats` | [`PlaybackStats`](#playbackstats) | Periodic stats sample (`statsIntervalMs`). |
|
|
511
|
+
| `statechange` | [`PlayerState`](#playerstate) | Any state transition. |
|
|
512
|
+
| `offline` | `void` | The browser reported it lost its network (the window's `offline` event). `state` is unchanged; `player.offline` is now `true`. See [Connectivity](#connectivity). |
|
|
513
|
+
| `online` | `void` | The browser reported its network is back (the window's `online` event). The player resumes on its own: a parked hls.js loader restarts at once, and a fatal error that happened while offline (or any `NETWORK` error) is retried, the same re-resolve as `retry()`. |
|
|
493
514
|
|
|
494
515
|
<a id="connectivity"></a>
|
|
495
516
|
|
|
@@ -553,9 +574,13 @@ booked for your key but not playable in the viewer's country) gets its own copy,
|
|
|
553
574
|
"This match isn't available in your country (RU)", naming the country the server
|
|
554
575
|
saw, and no Retry button: a retry would be refused again. A `DRM_DEVICE_REVOKED` error
|
|
555
576
|
(the license server refuses this device's DRM client) likewise gets "This device can't
|
|
556
|
-
play protected video" and no Retry.
|
|
577
|
+
play protected video" and no Retry. A `DRM_CLIENT` error whose `detail` is
|
|
578
|
+
`keySystemNoAccess` (the browser has no usable Widevine) gets "This browser can't play
|
|
579
|
+
protected video" with the way out named, and one whose `detail` is
|
|
580
|
+
`keySystemStatusOutputRestricted` (no HDCP on the display path) gets "Protected video
|
|
581
|
+
can't be shown on this display"; both keep Retry. The cards pick up `theme.logoUrl` and
|
|
557
582
|
the theme colors; customise the copy with `endedMessage`/`interruptedMessage`/`errorMessage`/`offlineMessage`
|
|
558
|
-
(an `errorMessage` replaces the `GEO_DENIED` and `
|
|
583
|
+
(an `errorMessage` replaces the `GEO_DENIED`, `DRM_DEVICE_REVOKED` and `DRM_CLIENT` copy too), or set
|
|
559
584
|
`statusOverlays: false` to drive your own end/error UI from the player events.
|
|
560
585
|
It auto-hides during playback, supports keyboard shortcuts (space/`k` play-pause,
|
|
561
586
|
`m` mute, `f` fullscreen, `c` captions, `l` go-live, arrows seek/volume), and is
|
|
@@ -1243,6 +1268,7 @@ class PlaybackError extends Error {
|
|
|
1243
1268
|
readonly country?: string; // upper-case ISO 3166-1 alpha-2; set for GEO_DENIED
|
|
1244
1269
|
readonly serverCode?: string; // the server's own code, when the body carried one (e.g. 'cdm_revoked', 'license_refused')
|
|
1245
1270
|
readonly requestId?: string; // server correlation id, when present
|
|
1271
|
+
readonly detail?: string; // the engine's own name for the failure (an hls.js ErrorDetails value), when it supplied one
|
|
1246
1272
|
}
|
|
1247
1273
|
```
|
|
1248
1274
|
|
|
@@ -1275,7 +1301,14 @@ involved (e.g. a `5xx` after the SDK's license-retry budget is exhausted), else
|
|
|
1275
1301
|
the `code`, not on `httpStatus`, to tell a server rejection from a device that can't
|
|
1276
1302
|
play DRM. The built-in error card still offers Retry (it re-resolves and re-attaches,
|
|
1277
1303
|
which can recover a transient license failure); an unsupported device will simply fail
|
|
1278
|
-
the same way again.
|
|
1304
|
+
the same way again. `detail` carries hls.js's own name for the failure: on
|
|
1305
|
+
`keySystemNoAccess` (the browser has no usable Widevine: a Chromium build without the
|
|
1306
|
+
CDM, a WebView, DRM switched off) the card says "This browser can't play protected
|
|
1307
|
+
video" and names the site setting and the browsers that work (Safari only when the stream
|
|
1308
|
+
carries FairPlay, since the hls.js engine is Widevine-only), and on `keySystemStatusOutputRestricted` (the display path
|
|
1309
|
+
refuses the key, no HDCP) it says "Protected video can't be shown on this display". Both
|
|
1310
|
+
keep Retry: Firefox asks the viewer to allow DRM on the first attempt, and a changed
|
|
1311
|
+
display is a retry away.
|
|
1279
1312
|
|
|
1280
1313
|
³ `DRM_DEVICE_REVOKED` is read off the body of a license `403`: the DRM proxy answers
|
|
1281
1314
|
`{"error":"cdm_revoked"}` when the license server refuses the device's DRM client
|
|
@@ -1464,6 +1497,7 @@ interface PlaybackStats {
|
|
|
1464
1497
|
fpsDownCaps?: number; // times quality was capped because the device dropped frames (hls.js engine)
|
|
1465
1498
|
maxLevelHeight?: number; // highest rendition ABR may pick right now; absent when uncapped (hls.js engine)
|
|
1466
1499
|
stallDownCaps?: number; // times a playback stall stepped the quality ceiling down (hls.js engine)
|
|
1500
|
+
stuckReloads?: number; // times a frozen playhead with a buffer that stopped growing was reloaded at live (native engine)
|
|
1467
1501
|
capReason?: 'size' | 'bitrate' | 'fps' | 'stall'; // which SDK cap set maxLevelHeight: player pixel box, maxBitrate, frame drops, recent stall
|
|
1468
1502
|
}
|
|
1469
1503
|
```
|
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.28.1/dist/havik-player.global.js" integrity="sha384-MsDlqFBRNSeEkb89GR3AJ7jOau99rUmmttHqnHzmad+L9+HpYWywrL4YuFIPn+4o" 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.28.1/dist/havik-player.global.js" integrity="sha384-MsDlqFBRNSeEkb89GR3AJ7jOau99rUmmttHqnHzmad+L9+HpYWywrL4YuFIPn+4o" 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.28.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,
|