@oddin-gg/havik-player 2.17.0 → 2.19.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
@@ -249,6 +249,7 @@ player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
249
249
  | `maxBitrate` | `number` | — | Cap ABR to renditions at/below this bitrate (bps). |
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
+ | `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). |
252
253
  | `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. |
253
254
  | `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. |
254
255
  | `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`. |
@@ -305,7 +306,14 @@ don't need to set any of the live or ABR knobs:
305
306
  rendition is one a modest link can sustain (**~360p**) and then **ramps up**
306
307
  within a few seconds as measured bandwidth allows, rather than grabbing a high
307
308
  rung and rebuffering on a constrained link. This trades a slightly softer first
308
- ~2 s for a **stall‑free start** (the first‑seconds buffering fix). In addition,
309
+ ~2 s for a **stall‑free start** (the first‑seconds buffering fix). A device
310
+ that has **measured a fast line in the last 3 days** starts from that
311
+ measurement instead (`rememberBandwidth`, on by default: the estimate after
312
+ ~10 s of playback, stored per connection type and reused at 80%), so a
313
+ returning viewer on a fast line starts near full quality without a 360p jump,
314
+ and a device that measured a slow line starts from that lower figure (also at
315
+ 80%, never below 0.3 Mbps). A first visit or a device with no usable record
316
+ keeps the ~1 Mbps start. In addition,
309
317
  the session is bounded to the player's pixel box, so a small/embedded player
310
318
  won't fetch 1080p, but by an SDK‑managed, **debounced** size cap rather than
311
319
  hls.js's own `capLevelToPlayerSize` poller: a size change only reaches ABR
@@ -386,6 +394,7 @@ interface Player {
386
394
  exitPictureInPicture(): Promise<void>;
387
395
  enterFullscreen(): Promise<boolean>; // element FS on the player container; iPhone → video-native. false = no mechanism
388
396
  exitFullscreen(): Promise<void>; // element or video-native; safe when not fullscreen
397
+ fullscreenSupported(): boolean; // false = permissions policy blocks it (iframe without allow=fullscreen) or no mechanism; optimistic before media loads on iPhone
389
398
  retry(): Promise<void>; // recover from an error (re-resolve + re-attach)
390
399
  reload(): Promise<void>; // re‑resolve playback (refreshes the license URL) + re‑attach
391
400
  on<E extends PlayerEvent>(event: E, cb: (payload: PlayerEventMap[E]) => void): () => void;
@@ -460,6 +469,17 @@ stream.
460
469
  > effect **after playback has started** (a tap on the pre-play poster is a
461
470
  > no-op there). Everywhere else (desktop, Android, iPad) the button uses
462
471
  > element fullscreen and keeps the branded controls.
472
+ >
473
+ > **Where fullscreen cannot work, the button is not shown** and the `f`
474
+ > shortcut is inert, the same gate the Picture-in-Picture button has: a
475
+ > cross-origin iframe whose `allow` list omits `fullscreen` (the browser
476
+ > reports `document.fullscreenEnabled === false`), or a webview with no
477
+ > fullscreen mechanism at all. So an operator who must block fullscreen on an
478
+ > embed drops it from the iframe's `allow` attribute and needs no custom
479
+ > control bar. The decision is re-made on the video's `loadedmetadata`, which
480
+ > is when iPhone first reports whether its video-native presentation exists,
481
+ > so on iPhone the button is still shown (and inert, as above) until playback
482
+ > has started.
463
483
 
464
484
  ```ts
465
485
  type Controls = 'custom' | 'native' | 'none';
@@ -689,6 +709,7 @@ hls.attachMedia(videoElement);
689
709
  function enterVideoFullscreen(video: HTMLVideoElement, container?: HTMLElement): Promise<boolean>;
690
710
  function exitVideoFullscreen(video: HTMLVideoElement, container?: HTMLElement): Promise<void>;
691
711
  function videoFullscreenActive(video: HTMLVideoElement, container?: HTMLElement): boolean;
712
+ function fullscreenSupported(video: HTMLVideoElement, container?: HTMLElement): boolean;
692
713
  ```
693
714
 
694
715
  Fullscreen with the platform quirks handled, so your own fullscreen button
@@ -708,7 +729,13 @@ first; it reports only through the video, never the document), is safe to
708
729
  call when not fullscreen, and is **scoped to your video's presentation**: an
709
730
  unrelated element's fullscreen elsewhere on the page is left alone.
710
731
  `videoFullscreenActive` tells you whether your video's presentation is
711
- currently fullscreen. Pass the same `container` to all three. For fullscreen **state changes**, listen to `fullscreenchange` /
732
+ currently fullscreen. `fullscreenSupported` is the pre-flight for your button:
733
+ `false` when a request is known to fail: the element API is there but the
734
+ document reports `fullscreenEnabled === false` (permissions policy; an iframe
735
+ without `fullscreen` in `allow`), or media is loaded and the platform has no
736
+ mechanism at all. It stays `true` before media loads where no element API
737
+ exists (iPhone cannot say yet), so re-check it on `loadedmetadata`. Pass the
738
+ same `container` to all four. For fullscreen **state changes**, listen to `fullscreenchange` /
712
739
  `webkitfullscreenchange` on the document and `webkitbeginfullscreen` /
713
740
  `webkitendfullscreen` on the video; the last two are the only signal for
714
741
  iPhone's video-native fullscreen.
@@ -769,6 +796,18 @@ function clearDeviceId(): void;
769
796
  > under GDPR/ePrivacy). Disclose it in your privacy policy and offer a reset:
770
797
  > `clearDeviceId()` removes the stored id so the next call generates a fresh one.
771
798
 
799
+ ### `clearRememberedBandwidth`
800
+
801
+ ```ts
802
+ function clearRememberedBandwidth(): void;
803
+ ```
804
+
805
+ Forgets the bandwidth the player remembered for this device (the
806
+ `rememberBandwidth` option, `localStorage` key `havik.player.bandwidth`). The next
807
+ session starts from the conservative ~1 Mbps estimate again. The record holds
808
+ only an estimated speed and a timestamp per connection type; offer it beside
809
+ `clearDeviceId()` in a "reset player data" control.
810
+
772
811
  ### `fetchCatalog`
773
812
 
774
813
  ```ts
package/docs/STABILITY.md CHANGED
@@ -13,7 +13,7 @@ breaking change → major. **Pin a version in production** (npm and the CDN URL)
13
13
 
14
14
  - `createPlayer` + the `Player` interface (Mode A)
15
15
  - `resolveStream`, `fetchCatalog`, `flattenMatches`, `watchStatus`,
16
- `licenseRequestHeaders`, `getDeviceId`, `clearDeviceId`, `isLowLatencyManifest` (Mode B)
16
+ `licenseRequestHeaders`, `getDeviceId`, `clearDeviceId`, `clearRememberedBandwidth`, `isLowLatencyManifest` (Mode B)
17
17
  - `mountEmbed` / `bootEmbed` + `EmbedHandle` / the `oddin:*` postMessage protocol (Mode C)
18
18
  - `PlaybackError` and all exported `type` definitions
19
19
 
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.17.0/dist/havik-player.global.js" integrity="sha384-iRLq+VhRDTaRt6z5NvY8V9X979b3eHBEZCYTwi4+6ycDHiaZvYLH0B02LhDJ7DoL" crossorigin="anonymous"></script>
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>
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.17.0/dist/havik-player.global.js" integrity="sha384-iRLq+VhRDTaRt6z5NvY8V9X979b3eHBEZCYTwi4+6ycDHiaZvYLH0B02LhDJ7DoL" crossorigin="anonymous"></script>
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>
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.17.0",
3
+ "version": "2.19.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,