@oddin-gg/havik-player 2.14.0 → 2.15.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/index.js CHANGED
@@ -1219,21 +1219,21 @@ function it(e, t) {
1219
1219
  function at() {
1220
1220
  return e.isSupported();
1221
1221
  }
1222
- var ot = 3, st = 5, ct = 3, lt = 3e4, ut = 12e3, dt = 3e3, ft = 6e3, pt = 5e3, mt = 4e3, ht = 2, gt = 5, _t = 2, vt = 2, yt = 4, bt = 4, xt = 2e3, St = 3e4, Ct = 2, wt = 9, Tt = .5, Et = 64;
1223
- function Dt(e) {
1222
+ var ot = 3, st = 5, ct = 3, lt = 3e4, ut = 12e3, dt = 3e3, ft = 6e3, pt = 3, mt = .5, ht = 1.2, gt = .25, _t = 5e3, vt = 4e3, yt = 2, bt = 5, xt = 2, St = 2, Ct = 4, wt = 4, Tt = 2e3, Et = 3e4, Dt = 2, Ot = 9, kt = .5, At = 64;
1223
+ function jt(e) {
1224
1224
  if (e.liveSyncDurationCount != null || e.liveMaxLatencyDurationCount != null) {
1225
1225
  delete e.liveSyncDuration, delete e.liveMaxLatencyDuration;
1226
1226
  return;
1227
1227
  }
1228
- e.liveSyncDuration ??= Ct;
1229
- let t = Math.max(12, e.liveSyncDuration + wt);
1230
- (e.liveMaxLatencyDuration == null || e.liveMaxLatencyDuration <= e.liveSyncDuration) && (e.liveMaxLatencyDuration = t), Ot(e, e.liveSyncDuration);
1228
+ e.liveSyncDuration ??= Dt;
1229
+ let t = Math.max(12, e.liveSyncDuration + Ot);
1230
+ (e.liveMaxLatencyDuration == null || e.liveMaxLatencyDuration <= e.liveSyncDuration) && (e.liveMaxLatencyDuration = t), Mt(e, e.liveSyncDuration);
1231
1231
  }
1232
- function Ot(e, t) {
1232
+ function Mt(e, t) {
1233
1233
  let n = (e, t, n) => Math.min(n, Math.max(t, e));
1234
1234
  e.maxStarvationDelay ??= n(t / 2, .5, 4), e.maxLoadingDelay ??= n(t, 1, 4), e.abrEwmaFastLive ??= n(t / 2, 1, 3);
1235
1235
  }
1236
- var kt = class {
1236
+ var Nt = class {
1237
1237
  name = "hls";
1238
1238
  hls = null;
1239
1239
  deps;
@@ -1289,6 +1289,9 @@ var kt = class {
1289
1289
  chaseSuspended = !1;
1290
1290
  savedLiveMaxLatency;
1291
1291
  savedMaxSyncRate;
1292
+ chaseLatency = -1;
1293
+ chaseTicks = 0;
1294
+ liveRetargets = 0;
1292
1295
  capObserver = null;
1293
1296
  capTimer = null;
1294
1297
  sizeCapLevel = null;
@@ -1333,11 +1336,11 @@ var kt = class {
1333
1336
  }
1334
1337
  this.loadSuspended = !0;
1335
1338
  }
1336
- }, pt));
1339
+ }, _t));
1337
1340
  }
1338
1341
  resumeLoading() {
1339
- if (this.pauseSuspendTimer &&= (clearTimeout(this.pauseSuspendTimer), null), this.loadSuspended && (this.loadSuspended = !1, !this.stopped && this.hls)) {
1340
- this.lastAdvanceMs = Date.now(), this.lastFragLoadingMs = 0, this.resetStarvationWindow(), this.clearKick();
1342
+ if (this.pauseSuspendTimer &&= (clearTimeout(this.pauseSuspendTimer), null), this.resetChaseWindow(), this.loadSuspended && (this.loadSuspended = !1, !this.stopped && this.hls)) {
1343
+ this.lastAdvanceMs = Date.now(), this.lastFragLoadingMs = 0, this.resetStarvationWindow(), this.resetChaseWindow(), this.clearKick();
1341
1344
  try {
1342
1345
  this.hls.startLoad(-1);
1343
1346
  } catch {}
@@ -1365,11 +1368,11 @@ var kt = class {
1365
1368
  let t = e.liveSyncPosition;
1366
1369
  if (typeof t != "number" || !Number.isFinite(t)) return;
1367
1370
  let { video: n } = this.deps;
1368
- n.currentTime <= t + Tt || (n.currentTime = t);
1371
+ n.currentTime <= t + kt || (n.currentTime = t);
1369
1372
  }
1370
1373
  startHls() {
1371
1374
  let { video: t, descriptor: n, credential: r, deviceId: i, userId: a, lowLatency: o, debug: s, hlsConfig: c, emit: l } = this.deps;
1372
- this.clearKick(), this.resetStarvationWindow(), this.lastFragLoadingMs = 0, this.lastEndSN = -1, this.lastAdvanceMs = 0;
1375
+ this.clearKick(), this.resetStarvationWindow(), this.resetChaseWindow(), this.lastFragLoadingMs = 0, this.lastEndSN = -1, this.lastAdvanceMs = 0;
1373
1376
  let u = n.drmEnabled && !!(n.drm?.widevine?.licenseUrl || n.drm?.fairplay?.licenseUrl), d = {
1374
1377
  debug: s === !0,
1375
1378
  enableWorker: !o,
@@ -1412,7 +1415,7 @@ var kt = class {
1412
1415
  }), t = tt(e);
1413
1416
  t && (this.prefetch = e, d.fLoader = t);
1414
1417
  }
1415
- Dt(d), this.sizeCapManaged = d.capLevelToPlayerSize !== !0, this.loadStarted = d.autoStartLoad !== !1;
1418
+ jt(d), this.sizeCapManaged = d.capLevelToPlayerSize !== !0, this.loadStarted = d.autoStartLoad !== !1;
1416
1419
  let f = new e(d);
1417
1420
  if (this.hls = f, f.on(e.Events.MANIFEST_PARSED, () => {
1418
1421
  this.mediaRecoveries = 0, this.networkRecoveries = 0, u && !this.deps.hlsConfig?.requestMediaKeySystemAccessFunc && this.dropLevelsWidevineCannotPlay(f), this.measureLadder(f), this.applySizeCap(), this.loadSuspended = !1, t.paused && this.schedulePauseSuspend(), l({ type: "ready" });
@@ -1427,7 +1430,7 @@ var kt = class {
1427
1430
  let n = t?.frag?.stats, r = n?.loaded;
1428
1431
  typeof r == "number" && (this.bytesLoaded += r), !this.reportedFirstBytes && typeof r == "number" && r > 0 && (this.reportedFirstBytes = !0, l({ type: "firstBytes" }));
1429
1432
  let i = n?.loading;
1430
- i && i.end > 0 && i.start > 0 && (this.requestCount += 1, this.requestTimesMs.push(Math.max(0, Math.round(i.end - i.start))), this.requestTimesMs.length > Et && this.requestTimesMs.shift());
1433
+ i && i.end > 0 && i.start > 0 && (this.requestCount += 1, this.requestTimesMs.push(Math.max(0, Math.round(i.end - i.start))), this.requestTimesMs.length > At && this.requestTimesMs.shift());
1431
1434
  let a = t?.frag?.url;
1432
1435
  if (a) {
1433
1436
  let e = H(a);
@@ -1451,7 +1454,7 @@ var kt = class {
1451
1454
  f.on(e.Events.KEY_LOADED, () => {
1452
1455
  u && !this.reportedDrmReady && (this.reportedDrmReady = !0, l({ type: "drmReady" }));
1453
1456
  }), this.endWatchdog && clearInterval(this.endWatchdog), this.endWatchdog = setInterval(() => {
1454
- this.checkEndStale(), this.checkStarvation();
1457
+ this.checkEndStale(), this.checkStarvation(), this.checkFutileChase();
1455
1458
  }, dt), f.on(e.Events.LEVEL_SWITCHED, (e, t) => {
1456
1459
  this.renditionSwitches += 1;
1457
1460
  let n = this.lastLevelIndex >= 0 ? f.levels[this.lastLevelIndex] : void 0, r = f.levels[t.level];
@@ -1516,7 +1519,7 @@ var kt = class {
1516
1519
  return;
1517
1520
  }
1518
1521
  this.capTimer && clearTimeout(this.capTimer);
1519
- let t = e > this.sizeCapLevel ? xt : St;
1522
+ let t = e > this.sizeCapLevel ? Tt : Et;
1520
1523
  this.capTimer = setTimeout(() => {
1521
1524
  this.capTimer = null, this.computeSizeCapTarget() === e ? this.applySizeCap() : this.onMediaResize();
1522
1525
  }, t);
@@ -1592,6 +1595,7 @@ var kt = class {
1592
1595
  bytesLoaded: this.bytesLoaded,
1593
1596
  playlistBytesLoaded: this.playlistBytesLoaded,
1594
1597
  renditionSwitches: this.renditionSwitches,
1598
+ liveRetargets: this.liveRetargets,
1595
1599
  licenseTimeMs: this.licenseTimeMs,
1596
1600
  upshifts: this.upshifts,
1597
1601
  downshifts: this.downshifts,
@@ -1646,7 +1650,7 @@ var kt = class {
1646
1650
  this.resetStarvationWindow();
1647
1651
  return;
1648
1652
  }
1649
- let n = Date.now(), r = this.lastAdvanceMs !== 0 && n - this.lastAdvanceMs < ft, i = this.lastFragLoadingMs !== 0 && n - this.lastFragLoadingMs > mt, a = t.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !t.paused, { totalFrames: o, dropped: s } = U(t), c = o - s, l = c !== this.starvedFrames;
1653
+ let n = Date.now(), r = this.lastAdvanceMs !== 0 && n - this.lastAdvanceMs < ft, i = this.lastFragLoadingMs !== 0 && n - this.lastFragLoadingMs > vt, a = t.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !t.paused, { totalFrames: o, dropped: s } = U(t), c = o - s, l = c !== this.starvedFrames;
1650
1654
  if (this.starvedFrames = c, !a || l) {
1651
1655
  (t.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA || l) && this.clearKick(), this.resetStarvationWindow();
1652
1656
  return;
@@ -1655,12 +1659,12 @@ var kt = class {
1655
1659
  this.resetStarvationWindow();
1656
1660
  return;
1657
1661
  }
1658
- if (this.kickPending && (this.kickKind === "dead" || i) && (this.kickTicks += 1), this.starvedTicks = i ? this.starvedTicks + 1 : 0, this.deadTicks += 1, this.recentMainFrags.push(this.mainFragsSinceTick), this.mainFragsSinceTick = 0, this.recentMainFrags.length > yt && this.recentMainFrags.shift(), this.clearRecoveryReset(), this.kickPending && this.kickTicks >= _t && (this.clearKick(), this.starvationRebuilds < vt)) {
1662
+ if (this.kickPending && (this.kickKind === "dead" || i) && (this.kickTicks += 1), this.starvedTicks = i ? this.starvedTicks + 1 : 0, this.deadTicks += 1, this.recentMainFrags.push(this.mainFragsSinceTick), this.mainFragsSinceTick = 0, this.recentMainFrags.length > Ct && this.recentMainFrags.shift(), this.clearRecoveryReset(), this.kickPending && this.kickTicks >= xt && (this.clearKick(), this.starvationRebuilds < St)) {
1659
1663
  this.starvationRebuilds += 1, this.resetStarvationWindow(), this.clearRecoveryReset(), console.warn("havik-player: restarting the load did not resume video (the wedge is in hls.js state that only a rebuild clears) — rebuilding the player"), this.rebuildInstance("starvation rebuild");
1660
1664
  return;
1661
1665
  }
1662
- let u = i && this.starvedTicks >= ht, d = !i && this.deadTicks >= yt && this.recentMainFrags.reduce((e, t) => e + t, 0) >= bt;
1663
- if ((u || d) && (this.resetStarvationWindow(), !(this.starvationKicks >= gt))) {
1666
+ let u = i && this.starvedTicks >= yt, d = !i && this.deadTicks >= Ct && this.recentMainFrags.reduce((e, t) => e + t, 0) >= wt;
1667
+ if ((u || d) && (this.resetStarvationWindow(), !(this.starvationKicks >= bt))) {
1664
1668
  this.starvationKicks += 1, this.kickPending = !0, this.kickTicks = 0, this.kickKind = d ? "dead" : "quiet", this.clearRecoveryReset(), console.warn(d ? "havik-player: video starved while hls.js keeps loading media it never plays (dead pipeline at the live edge) — restarting the load" : "havik-player: video starved while the live playlist advances (wedged hls.js fragment pipeline) — restarting the load");
1665
1669
  try {
1666
1670
  e.stopLoad(), e.startLoad(-1);
@@ -1669,6 +1673,31 @@ var kt = class {
1669
1673
  }
1670
1674
  }
1671
1675
  }
1676
+ resetChaseWindow() {
1677
+ this.chaseLatency = -1, this.chaseTicks = 0;
1678
+ }
1679
+ checkFutileChase() {
1680
+ let e = this.hls, { video: t } = this.deps;
1681
+ if (!e || this.endedEmitted || !this.sawLive || this.chaseSuspended || this.loadSuspended || typeof document < "u" && document.hidden) {
1682
+ this.resetChaseWindow();
1683
+ return;
1684
+ }
1685
+ let n = e.latency, r = t.playbackRate, i = e.config.maxLiveSyncPlaybackRate;
1686
+ if (!(!t.paused && r >= ht && typeof i == "number" && i > 1 && r <= i && typeof n == "number" && Number.isFinite(n))) {
1687
+ this.resetChaseWindow();
1688
+ return;
1689
+ }
1690
+ if (this.chaseLatency < 0 || this.chaseLatency - n >= mt) {
1691
+ this.chaseLatency = n, this.chaseTicks = 0;
1692
+ return;
1693
+ }
1694
+ if (this.chaseTicks += 1, this.chaseTicks < pt) return;
1695
+ this.resetChaseWindow();
1696
+ let a = typeof e.config.liveMaxLatencyDuration == "number" ? e.config.liveMaxLatencyDuration : e.maxLatency;
1697
+ if (!(a > 1)) return;
1698
+ let o = typeof e.targetLatency == "number" ? e.targetLatency : 0, s = Math.min(n, a - 1);
1699
+ s < o + gt || (console.warn(`havik-player: catch-up at ${r}x not gaining on the live edge (latency ${n.toFixed(1)}s); holding at ${s.toFixed(1)}s`), this.liveRetargets += 1, e.targetLatency = s);
1700
+ }
1672
1701
  suspendChasing() {
1673
1702
  let e = this.hls;
1674
1703
  e && (this.chaseSuspended = !0, this.savedLiveMaxLatency = e.config.liveMaxLatencyDuration, this.savedMaxSyncRate = e.config.maxLiveSyncPlaybackRate, e.config.liveMaxLatencyDuration = 2 ** 53 - 1, e.config.maxLiveSyncPlaybackRate = 1);
@@ -1782,19 +1811,19 @@ var kt = class {
1782
1811
  let r = e.response?.code ?? 0, a = e.details ?? e.type, o = e.reason ?? e.error?.message ?? "";
1783
1812
  return new n(t === "DRM" ? i(r) : r === 401 ? "UNAUTHORIZED" : r === 403 ? "FORBIDDEN" : "INTERNAL", r, `${t} error: ${a}${o ? ` (${o})` : ""}`);
1784
1813
  }
1785
- }, At = 15e3, jt = 6e5, Mt = 3e5, Nt = 100, q = 1, Pt = 1e3, Ft = 3e4, It = 1e3, Lt = {
1814
+ }, Pt = 15e3, Ft = 6e5, It = 3e5, Lt = 100, q = 1, Rt = 1e3, zt = 3e4, Bt = 1e3, Vt = {
1786
1815
  inline: "PRESENTATION_INLINE",
1787
1816
  fullscreen: "PRESENTATION_FULLSCREEN",
1788
1817
  pip: "PRESENTATION_PIP"
1789
- }, Rt = {
1818
+ }, Ht = {
1790
1819
  widevine: "KEY_SYSTEM_WIDEVINE",
1791
1820
  fairplay: "KEY_SYSTEM_FAIRPLAY",
1792
1821
  none: "KEY_SYSTEM_NONE"
1793
1822
  };
1794
- function zt(e, t) {
1823
+ function Ut(e, t) {
1795
1824
  return e.length === 0 ? 0 : e[Math.min(e.length, Math.max(1, Math.ceil(t / 100 * e.length))) - 1] ?? 0;
1796
1825
  }
1797
- var Bt = /* @__PURE__ */ new Set([
1826
+ var Wt = /* @__PURE__ */ new Set([
1798
1827
  "INVALID_URN",
1799
1828
  "NOT_FOUND",
1800
1829
  "GONE",
@@ -1814,7 +1843,7 @@ var Bt = /* @__PURE__ */ new Set([
1814
1843
  ended: "PLAYER_STATE_ENDED",
1815
1844
  error: "PLAYER_STATE_ERROR"
1816
1845
  };
1817
- function Vt(e) {
1846
+ function Gt(e) {
1818
1847
  try {
1819
1848
  let t = new URL(e), [n, ...r] = t.hostname.split(".");
1820
1849
  return !n || r.length === 0 || n !== "feed" && !n.startsWith("feed-") ? void 0 : (t.hostname = [n.replace(/^feed/, "beacons"), ...r].join("."), t.pathname = "", t.search = "", t.origin);
@@ -1822,8 +1851,8 @@ function Vt(e) {
1822
1851
  return;
1823
1852
  }
1824
1853
  }
1825
- var Ht = (e, t) => e.replace(/\/$/, "").toLowerCase() === t.replace(/\/$/, "").toLowerCase(), Ut = /* @__PURE__ */ new Set(), Y = /* @__PURE__ */ new Set(), X = /* @__PURE__ */ new Set(), Wt = (e) => `this page's Content-Security-Policy blocks QoE beacons. Add ${e} to connect-src; until then none are sent.`;
1826
- function Gt(e) {
1854
+ var Kt = (e, t) => e.replace(/\/$/, "").toLowerCase() === t.replace(/\/$/, "").toLowerCase(), qt = /* @__PURE__ */ new Set(), Y = /* @__PURE__ */ new Set(), X = /* @__PURE__ */ new Set(), Jt = (e) => `this page's Content-Security-Policy blocks QoE beacons. Add ${e} to connect-src; until then none are sent.`;
1855
+ function Yt(e) {
1827
1856
  if (e.disposition !== "enforce" || e.effectiveDirective !== "connect-src") return;
1828
1857
  let t;
1829
1858
  try {
@@ -1832,19 +1861,19 @@ function Gt(e) {
1832
1861
  } catch {
1833
1862
  return;
1834
1863
  }
1835
- if (Ut.has(t) && !Y.has(t)) {
1836
- Y.add(t), g(Wt(new URL(t).origin));
1864
+ if (qt.has(t) && !Y.has(t)) {
1865
+ Y.add(t), g(Jt(new URL(t).origin));
1837
1866
  for (let e of X) e.noteCspBlocked();
1838
1867
  }
1839
1868
  }
1840
- function Kt(e, t) {
1869
+ function Xt(e, t) {
1841
1870
  return g(t, e), { disabledReason: t };
1842
1871
  }
1843
- function qt(e, t, n) {
1844
- if (n) return t && !Ht(t, n) && g(`analytics.endpoint "${t}" ignored; the server named "${n}".`), { endpoint: n };
1872
+ function Zt(e, t, n) {
1873
+ if (n) return t && !Kt(t, n) && g(`analytics.endpoint "${t}" ignored; the server named "${n}".`), { endpoint: n };
1845
1874
  if (t) return { endpoint: t };
1846
- if (t !== void 0) return Kt("empty-endpoint", "analytics is disabled: no QoE beacons will be sent. analytics.endpoint was supplied but empty. Pass a real beacons origin, analytics: { endpoint: 'https://beacons[-dev].<domain>' }, or omit the option to derive it from baseUrl (or analytics: false to opt out).");
1847
- let r = Vt(e);
1875
+ if (t !== void 0) return Xt("empty-endpoint", "analytics is disabled: no QoE beacons will be sent. analytics.endpoint was supplied but empty. Pass a real beacons origin, analytics: { endpoint: 'https://beacons[-dev].<domain>' }, or omit the option to derive it from baseUrl (or analytics: false to opt out).");
1876
+ let r = Gt(e);
1848
1877
  if (r) return { endpoint: r };
1849
1878
  let i;
1850
1879
  try {
@@ -1852,9 +1881,9 @@ function qt(e, t, n) {
1852
1881
  } catch {
1853
1882
  i = e;
1854
1883
  }
1855
- return Kt(i, `analytics is disabled: no QoE beacons will be sent. The beacons endpoint cannot be derived from the baseUrl host "${i}" — the convention is feed[-dev].<domain> → beacons[-dev].<domain>. Pass an explicit endpoint, analytics: { endpoint: 'https://beacons[-dev].<domain>' }, to createPlayer (or analytics: false to opt out deliberately).`);
1884
+ return Xt(i, `analytics is disabled: no QoE beacons will be sent. The beacons endpoint cannot be derived from the baseUrl host "${i}" — the convention is feed[-dev].<domain> → beacons[-dev].<domain>. Pass an explicit endpoint, analytics: { endpoint: 'https://beacons[-dev].<domain>' }, to createPlayer (or analytics: false to opt out deliberately).`);
1856
1885
  }
1857
- var Jt = class {
1886
+ var Qt = class {
1858
1887
  opts;
1859
1888
  url;
1860
1889
  cspKey;
@@ -1936,8 +1965,8 @@ var Jt = class {
1936
1965
  constructor(e) {
1937
1966
  this.opts = {
1938
1967
  ...e,
1939
- heartbeatMs: e.heartbeatMs ?? At,
1940
- idleMs: e.idleMs ?? jt
1968
+ heartbeatMs: e.heartbeatMs ?? Pt,
1969
+ idleMs: e.idleMs ?? Ft
1941
1970
  }, this.url = `${e.endpoint.replace(/\/$/, "")}/v1/beacons`;
1942
1971
  try {
1943
1972
  let e = new URL(this.url);
@@ -1954,11 +1983,11 @@ var Jt = class {
1954
1983
  sdkVersion: this.opts.sdkVersion,
1955
1984
  ...this.opts.environment
1956
1985
  };
1957
- this.pendingStart = e, this.enqueue({ sessionStart: e }), this.firstFlushTimer = setTimeout(() => this.flush(), Pt), this.lastTickMs = this.now(), this.timer = setInterval(() => this.heartbeat(), this.opts.heartbeatMs), typeof window < "u" && window.addEventListener("pagehide", this.onPagehide), typeof document < "u" && (document.addEventListener("visibilitychange", this.onVisibility), document.addEventListener("freeze", this.onFreeze), document.addEventListener("resume", this.onResume), this.cspKey && Ut.add(this.cspKey), document.addEventListener("securitypolicyviolation", Gt), X.add(this), this.noteCspBlocked(), this.onVisibility());
1986
+ this.pendingStart = e, this.enqueue({ sessionStart: e }), this.firstFlushTimer = setTimeout(() => this.flush(), Rt), this.lastTickMs = this.now(), this.timer = setInterval(() => this.heartbeat(), this.opts.heartbeatMs), typeof window < "u" && window.addEventListener("pagehide", this.onPagehide), typeof document < "u" && (document.addEventListener("visibilitychange", this.onVisibility), document.addEventListener("freeze", this.onFreeze), document.addEventListener("resume", this.onResume), this.cspKey && qt.add(this.cspKey), document.addEventListener("securitypolicyviolation", Yt), X.add(this), this.noteCspBlocked(), this.onVisibility());
1958
1987
  }
1959
1988
  checkSuspension(e) {
1960
1989
  if (this.endedFlag || !this.opts.idleMs) return !1;
1961
- let t = Math.max(this.opts.idleMs, Mt);
1990
+ let t = Math.max(this.opts.idleMs, It);
1962
1991
  return this.lastTickMs > 0 && e - this.lastTickMs >= t ? (this.endIdle(this.lastTickMs), !0) : (this.lastTickMs = e, !1);
1963
1992
  }
1964
1993
  syncIdle(e) {
@@ -1981,12 +2010,12 @@ var Jt = class {
1981
2010
  let a = {
1982
2011
  fromState: J[e] ?? "PLAYER_STATE_UNSPECIFIED",
1983
2012
  toState: J[t] ?? "PLAYER_STATE_UNSPECIFIED",
1984
- presentation: Lt[this.presentation]
2013
+ presentation: Vt[this.presentation]
1985
2014
  }, o = this.causeOf(e, t, i);
1986
2015
  o && (a.cause = o), n > 0 && (a.positionMs = Math.round(n)), r && (a.renditionHeight = r), this.enqueue({ stateChange: a });
1987
2016
  }
1988
2017
  seekInduced(e) {
1989
- return this.lastSeekMs >= 0 && e - this.lastSeekMs < It;
2018
+ return this.lastSeekMs >= 0 && e - this.lastSeekMs < Bt;
1990
2019
  }
1991
2020
  causeOf(e, t, n) {
1992
2021
  if (t === "error") return "TRANSITION_CAUSE_ERROR";
@@ -2060,7 +2089,7 @@ var Jt = class {
2060
2089
  let r = n ?? "";
2061
2090
  if (r !== "") return /^fragLoad/i.test(r) ? this.sawFirstBytes ? "ERROR_STAGE_SEGMENT" : "ERROR_STAGE_FIRST_SEGMENT" : /^(manifest|level|audioTrack|subtitleTrack)/i.test(r) ? "ERROR_STAGE_MANIFEST" : /key|licen[cs]e/i.test(r) ? "ERROR_STAGE_DRM" : /parsing|append|buffer|remux|decrypt|media/i.test(r) ? "ERROR_STAGE_DECODE" : "ERROR_STAGE_OTHER";
2062
2091
  if (e === "DRM_CLIENT") return "ERROR_STAGE_DRM";
2063
- if (Bt.has(e)) return "ERROR_STAGE_RESOLVE";
2092
+ if (Wt.has(e)) return "ERROR_STAGE_RESOLVE";
2064
2093
  let i = t ?? "";
2065
2094
  return /frag|segment/i.test(i) ? this.sawFirstBytes ? "ERROR_STAGE_SEGMENT" : "ERROR_STAGE_FIRST_SEGMENT" : /manifest|level|playlist/i.test(i) ? "ERROR_STAGE_MANIFEST" : /key|licen[cs]e/i.test(i) ? "ERROR_STAGE_DRM" : /buffer|decode|media|append/i.test(i) ? "ERROR_STAGE_DECODE" : "ERROR_STAGE_OTHER";
2066
2095
  }
@@ -2107,14 +2136,14 @@ var Jt = class {
2107
2136
  if (this.lastRequestCount = g, _ > 0) {
2108
2137
  o.requestCountDelta = _;
2109
2138
  let e = t.requestTimesMs ?? [], n = e.slice(Math.max(0, e.length - _)).sort((e, t) => e - t);
2110
- n.length > 0 && (o.requestTimeP50Ms = zt(n, 50), o.requestTimeP95Ms = zt(n, 95));
2139
+ n.length > 0 && (o.requestTimeP50Ms = Ut(n, 50), o.requestTimeP95Ms = Ut(n, 95));
2111
2140
  }
2112
2141
  typeof t.latencySeconds == "number" && typeof t.targetLatencySeconds == "number" && (o.driftMs = Math.round((t.latencySeconds - t.targetLatencySeconds) * 1e3)), typeof t.playbackRate == "number" && t.playbackRate > 0 && (o.playbackRate = t.playbackRate), t.qualityPinned === !0 && (o.qualityPinned = !0), t.cdnHost && (o.cdnHost = t.cdnHost), this.maybeEmitMediaInfo(t), this.enqueue({ heartbeat: o }), this.flush(), s > 0 && this.noteFirstBytes();
2113
2142
  }
2114
2143
  maybeEmitMediaInfo(e) {
2115
2144
  if (!(e.videoCodec ?? e.keySystem ?? e.ladderHeights)) return;
2116
2145
  let t = {};
2117
- e.videoCodec && (t.videoCodec = e.videoCodec), e.audioCodec && (t.audioCodec = e.audioCodec), e.keySystem && (t.keySystem = Rt[e.keySystem]), e.drmSecurityLevel && (t.drmSecurityLevel = e.drmSecurityLevel), e.ladderHeights?.length && (t.ladderHeights = e.ladderHeights), e.ladderTopBitrateKbps && (t.ladderTopBitrateKbps = e.ladderTopBitrateKbps), e.lowLatency && (t.lowLatency = !0);
2146
+ e.videoCodec && (t.videoCodec = e.videoCodec), e.audioCodec && (t.audioCodec = e.audioCodec), e.keySystem && (t.keySystem = Ht[e.keySystem]), e.drmSecurityLevel && (t.drmSecurityLevel = e.drmSecurityLevel), e.ladderHeights?.length && (t.ladderHeights = e.ladderHeights), e.ladderTopBitrateKbps && (t.ladderTopBitrateKbps = e.ladderTopBitrateKbps), e.lowLatency && (t.lowLatency = !0);
2118
2147
  let n = JSON.stringify(t);
2119
2148
  n !== this.mediaInfoSignature && (this.mediaInfoSignature = n, this.enqueue({ mediaInfo: t }));
2120
2149
  }
@@ -2134,7 +2163,7 @@ var Jt = class {
2134
2163
  let c = r?.positionMs ?? 0;
2135
2164
  c > 0 && (i.exitPositionMs = Math.round(c)), this.activeTotalMs > 0 && (i.activeWatchTimeMs = Math.round(this.activeTotalMs)), this.mutedTotalMs > 0 && (i.mutedTimeMs = Math.round(this.mutedTotalMs));
2136
2165
  let l = e !== "END_REASON_ENDED";
2137
- l && this.lastStallMs > 0 && n - this.lastStallMs <= Ft && (i.abandonAfterStallMs = Math.max(q, Math.round(n - this.lastStallMs))), l && this.lastErrorMs > 0 && n - this.lastErrorMs <= Ft && (i.abandonAfterErrorMs = Math.max(q, Math.round(n - this.lastErrorMs))), this.fullscreenTotalMs > 0 && (i.fullscreenTimeMs = Math.round(this.fullscreenTotalMs)), this.pipTotalMs > 0 && (i.pipTimeMs = Math.round(this.pipTotalMs)), this.volumeChanges > 0 && (i.volumeChanges = this.volumeChanges), this.muteChanges > 0 && (i.muteChanges = this.muteChanges);
2166
+ l && this.lastStallMs > 0 && n - this.lastStallMs <= zt && (i.abandonAfterStallMs = Math.max(q, Math.round(n - this.lastStallMs))), l && this.lastErrorMs > 0 && n - this.lastErrorMs <= zt && (i.abandonAfterErrorMs = Math.max(q, Math.round(n - this.lastErrorMs))), this.fullscreenTotalMs > 0 && (i.fullscreenTimeMs = Math.round(this.fullscreenTotalMs)), this.pipTotalMs > 0 && (i.pipTimeMs = Math.round(this.pipTotalMs)), this.volumeChanges > 0 && (i.volumeChanges = this.volumeChanges), this.muteChanges > 0 && (i.muteChanges = this.muteChanges);
2138
2167
  let u = r?.decodedFrames ?? this.lastDecoded;
2139
2168
  u > 0 && (i.decodedFramesTotal = u), e === "END_REASON_STOPPED" && this.startupTimeMs === 0 && this.lastErrorStage === null && (e = "END_REASON_ABORTED", i.reason = e), this.startupTimeMs === 0 && e !== "END_REASON_ABORTED" && (i.failedStage = this.lastErrorStage ?? (this.manifestLoadMs === 0 ? "ERROR_STAGE_MANIFEST" : this.firstFragLoadMs === 0 ? "ERROR_STAGE_FIRST_SEGMENT" : "ERROR_STAGE_OTHER")), this.pending.push(this.wrap({ sessionEnd: i })), this.flush(e === "END_REASON_PAGEHIDE");
2140
2169
  }
@@ -2154,13 +2183,13 @@ var Jt = class {
2154
2183
  };
2155
2184
  }
2156
2185
  noteCspBlocked() {
2157
- this.cspBlockedNoted || this.endedFlag || !this.cspBlocked() || (this.cspBlockedNoted = !0, this.opts.onBlocked?.(Wt(new URL(this.url).origin)));
2186
+ this.cspBlockedNoted || this.endedFlag || !this.cspBlocked() || (this.cspBlockedNoted = !0, this.opts.onBlocked?.(Jt(new URL(this.url).origin)));
2158
2187
  }
2159
2188
  cspBlocked() {
2160
2189
  return !!this.cspKey && Y.has(this.cspKey);
2161
2190
  }
2162
2191
  enqueue(e) {
2163
- if (!this.cspBlocked()) for (this.pending.push(this.wrap(e)); this.pending.length > Nt;) {
2192
+ if (!this.cspBlocked()) for (this.pending.push(this.wrap(e)); this.pending.length > Lt;) {
2164
2193
  let e = this.pending.findIndex((e) => e.heartbeat || e.stateChange);
2165
2194
  this.pending.splice(e >= 0 ? e : 0, 1);
2166
2195
  }
@@ -2187,7 +2216,7 @@ var Jt = class {
2187
2216
  };
2188
2217
  //#endregion
2189
2218
  //#region src/core/environment.ts
2190
- function Yt() {
2219
+ function $t() {
2191
2220
  if (typeof window > "u") return { integration: "INTEGRATION_KIND_SDK" };
2192
2221
  let e;
2193
2222
  try {
@@ -2212,14 +2241,14 @@ function Yt() {
2212
2241
  integration: "INTEGRATION_KIND_IFRAME"
2213
2242
  };
2214
2243
  }
2215
- function Xt() {
2244
+ function en() {
2216
2245
  if (typeof navigator > "u") return "DEVICE_CLASS_OTHER";
2217
2246
  let e = navigator.userAgentData, t = e?.platform ?? "";
2218
2247
  if (/tv|tizen|webos/i.test(t)) return "DEVICE_CLASS_TV";
2219
2248
  let n = typeof screen < "u" ? Math.min(screen.width, screen.height) : 0;
2220
2249
  return e?.mobile ?? (typeof navigator.maxTouchPoints == "number" && navigator.maxTouchPoints > 1 && n > 0 && n < 900) ? n >= 600 ? "DEVICE_CLASS_TABLET" : "DEVICE_CLASS_PHONE" : e || n >= 900 || /mac|win|linux|chrome os/i.test(t) ? "DEVICE_CLASS_DESKTOP" : "DEVICE_CLASS_OTHER";
2221
2250
  }
2222
- function Zt() {
2251
+ function tn() {
2223
2252
  let e = typeof navigator < "u" ? navigator.connection : void 0;
2224
2253
  if (!e) return { connectionType: "CONNECTION_TYPE_UNKNOWN" };
2225
2254
  let t = {
@@ -2238,17 +2267,17 @@ function Zt() {
2238
2267
  }
2239
2268
  return typeof e.downlink == "number" && e.downlink > 0 && (i.downlinkKbps = Math.round(e.downlink * 1e3)), typeof e.rtt == "number" && e.rtt > 0 && (i.rttMs = Math.round(e.rtt)), e.saveData && (i.saveData = !0), i;
2240
2269
  }
2241
- function Qt(e) {
2242
- let t = Yt(), n = {
2243
- deviceClass: Xt(),
2270
+ function nn(e) {
2271
+ let t = $t(), n = {
2272
+ deviceClass: en(),
2244
2273
  integration: t.integration,
2245
- ...Zt()
2274
+ ...tn()
2246
2275
  };
2247
2276
  t.host && (n.host = t.host), typeof screen < "u" && screen.width > 0 && (n.screenWidth = screen.width, n.screenHeight = screen.height), typeof window < "u" && window.devicePixelRatio > 0 && (n.devicePixelRatio = Math.round(window.devicePixelRatio * 100) / 100);
2248
2277
  let r = e.getBoundingClientRect?.();
2249
2278
  return r && r.width > 0 && (n.viewportWidth = Math.round(r.width), n.viewportHeight = Math.round(r.height)), n;
2250
2279
  }
2251
- async function $t() {
2280
+ async function rn() {
2252
2281
  let e = typeof navigator < "u" ? navigator.userAgentData : void 0;
2253
2282
  if (!e?.getHighEntropyValues) return {};
2254
2283
  try {
@@ -2260,7 +2289,7 @@ async function $t() {
2260
2289
  }
2261
2290
  //#endregion
2262
2291
  //#region src/core/version.ts
2263
- var en = "2.14.0", tn = {
2292
+ var an = "2.15.1", on = {
2264
2293
  accent: "#E1B600",
2265
2294
  accentText: "#1B1C23",
2266
2295
  background: "#0e0f13",
@@ -2271,7 +2300,7 @@ var en = "2.14.0", tn = {
2271
2300
  border: "rgba(255, 255, 255, 0.1)",
2272
2301
  radius: "8px",
2273
2302
  fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
2274
- }, nn = {
2303
+ }, sn = {
2275
2304
  accent: "--havik-accent",
2276
2305
  accentText: "--havik-accent-text",
2277
2306
  background: "--havik-bg",
@@ -2283,26 +2312,26 @@ var en = "2.14.0", tn = {
2283
2312
  radius: "--havik-radius",
2284
2313
  fontFamily: "--havik-font"
2285
2314
  };
2286
- function rn(e, t) {
2315
+ function cn(e, t) {
2287
2316
  let n = {
2288
- ...tn,
2317
+ ...on,
2289
2318
  ...t
2290
2319
  };
2291
- for (let t of Object.keys(nn)) e.style.setProperty(nn[t], n[t]);
2320
+ for (let t of Object.keys(sn)) e.style.setProperty(sn[t], n[t]);
2292
2321
  n.logoUrl && e.style.setProperty("--havik-logo", `url("${n.logoUrl}")`);
2293
2322
  }
2294
2323
  //#endregion
2295
2324
  //#region src/ui/styles.ts
2296
- var an = "havik-player-styles", on = "\n.havik-player{position:relative;display:block;width:100%;background:var(--havik-bg,#0e0f13);\n color:var(--havik-text,#fff);font-family:var(--havik-font,system-ui,sans-serif);\n overflow:hidden;line-height:1.4;-webkit-tap-highlight-color:transparent}\n.havik-player video{display:block;width:100%;height:auto;aspect-ratio:16/9;\n object-fit:contain;background:#000;outline:none}\n.havik-player:focus{outline:none}\n.havik-player *{box-sizing:border-box}\n\n.havik-player:fullscreen,.havik-player:-webkit-full-screen{display:flex;align-items:center;\n justify-content:center;width:100vw;height:100vh;aspect-ratio:auto}\n.havik-player:fullscreen video,.havik-player:-webkit-full-screen video{width:100%;height:100%;\n aspect-ratio:auto;object-fit:contain}\n\n.havik-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;cursor:pointer}\n.havik-gradient{position:absolute;left:0;right:0;bottom:0;height:120px;pointer-events:none;\n background:linear-gradient(to top,rgba(0,0,0,.78),transparent);\n opacity:1;transition:opacity .25s}\n.havik-player.havik-hide .havik-gradient,\n.havik-player.havik-hide .havik-bar{opacity:0;pointer-events:none}\n.havik-player.havik-hide{cursor:none}\n\n.havik-center{width:74px;height:74px;border-radius:50%;border:0;cursor:pointer;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);\n display:flex;align-items:center;justify-content:center;\n box-shadow:0 6px 24px rgba(0,0,0,.45);transition:transform .15s,opacity .15s}\n.havik-center:hover{transform:scale(1.06)}\n.havik-center svg{width:38px;height:38px;margin-left:2px}\n.havik-center.havik-gone{opacity:0;pointer-events:none;transform:scale(.8)}\n\n/* Absolutely centred, NOT a flex sibling of .havik-center: the hidden center\n button (.havik-gone) still occupies its 74px in the row, which pushed an\n in-flow spinner 37px right of centre. */\n.havik-spinner{position:absolute;inset:0;margin:auto;width:56px;height:56px;border-radius:50%;\n border:4px solid rgba(255,255,255,.2);border-top-color:var(--havik-accent,#E1B600);\n animation:havik-spin .8s linear infinite}\n@keyframes havik-spin{to{transform:rotate(360deg)}}\n\n/* Status card — branded full-bleed overlay for the stopped/pre-play states\n (ended / fatal error / pre-play / armed-waiting). */\n.havik-status{position:absolute;inset:0;display:flex;flex-direction:column;gap:16px;\n align-items:center;justify-content:center;background:rgba(14,15,19,.82);text-align:center;padding:24px}\n.havik-status__logo{width:128px;height:46px;background:var(--havik-logo) center/contain no-repeat;opacity:.95}\n.havik-status__msg{color:var(--havik-text,#fff);font-size:15px;font-weight:600;max-width:480px}\n.havik-status__btn{display:inline-flex;align-items:center;gap:8px;border:0;cursor:pointer;\n padding:10px 18px;border-radius:var(--havik-radius,8px);font-weight:700;font-size:14px;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);transition:transform .15s}\n.havik-status__btn:hover{transform:scale(1.04)}\n.havik-status__btn svg{width:18px;height:18px}\n/* Pre-play: lighter scrim so the poster shows through, and a round play CTA. */\n.havik-status--preplay{background:rgba(14,15,19,.4)}\n.havik-status--preplay .havik-status__btn--play{width:74px;height:74px;border-radius:50%;padding:0;\n justify-content:center;box-shadow:0 6px 24px rgba(0,0,0,.45)}\n.havik-status--preplay .havik-status__btn--play svg{width:38px;height:38px;margin-left:3px}\n\n.havik-watermark{position:absolute;top:12px;right:14px;height:22px;opacity:.85;pointer-events:none;\n content:'';background:var(--havik-logo) center/contain no-repeat;width:90px}\n/* A status card carries its own logo — the corner watermark would duplicate it. */\n.havik-player.havik-has-status .havik-watermark{display:none}\n\n.havik-bar{position:absolute;left:0;right:0;bottom:0;padding:0 12px 8px;\n opacity:1;transition:opacity .25s;z-index:2}\n.havik-seek{position:relative;height:16px;display:flex;align-items:center;cursor:pointer;margin-bottom:2px}\n.havik-seek__track{position:relative;width:100%;height:4px;border-radius:3px;background:rgba(255,255,255,.25)}\n.havik-seek__buffered{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:rgba(255,255,255,.4)}\n.havik-seek__played{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-seek__thumb{position:absolute;top:50%;width:13px;height:13px;border-radius:50%;\n background:var(--havik-accent,#E1B600);transform:translate(-50%,-50%) scale(0);transition:transform .1s}\n.havik-seek:hover .havik-seek__thumb{transform:translate(-50%,-50%) scale(1)}\n\n.havik-controls{display:flex;align-items:center;gap:6px;height:40px}\n.havik-spacer{flex:1}\n.havik-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;border-radius:var(--havik-radius,8px);\n padding:0;transition:background .12s,color .12s}\n.havik-btn:hover{background:rgba(255,255,255,.12)}\n.havik-btn:focus-visible{outline:2px solid var(--havik-accent,#E1B600);outline-offset:1px}\n.havik-btn svg{width:22px;height:22px}\n\n/* The volume level is filled in accent up to the thumb, like the seek bar's\n played span — a native range input paints no \"elapsed\" side, which left the\n level readable only from the dot's position. --havik-vol (0..1) is set by\n syncVolume(); the track width and thumb size live here as variables because\n the WebKit fill has to compute the thumb centre from both. */\n.havik-vol{display:flex;align-items:center}\n.havik-vol__slider{--havik-vol-w:70px;--havik-vol-thumb:12px;--havik-vol:1;\n --havik-vol-pos:calc(var(--havik-vol-thumb) / 2 + var(--havik-vol) * (var(--havik-vol-w) - var(--havik-vol-thumb)));\n width:0;opacity:0;transition:width .18s,opacity .18s;height:4px;cursor:pointer;\n appearance:none;-webkit-appearance:none;background:transparent;border-radius:3px;margin:0 4px}\n.havik-vol:hover .havik-vol__slider,.havik-vol:focus-within .havik-vol__slider{width:var(--havik-vol-w);opacity:1}\n/* WebKit/Blink have no progress pseudo-element: paint the fill as a hard-stop\n gradient that ends at the thumb centre. */\n.havik-vol__slider::-webkit-slider-runnable-track{height:4px;border-radius:3px;\n background:linear-gradient(to right,var(--havik-accent,#E1B600) var(--havik-vol-pos),\n rgba(255,255,255,.3) var(--havik-vol-pos))}\n/* Firefox fills natively, and tracks the thumb exactly. */\n.havik-vol__slider::-moz-range-track{height:4px;border-radius:3px;background:rgba(255,255,255,.3)}\n.havik-vol__slider::-moz-range-progress{height:4px;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-vol__slider::-webkit-slider-thumb{-webkit-appearance:none;width:var(--havik-vol-thumb);\n height:var(--havik-vol-thumb);border-radius:50%;background:var(--havik-accent,#E1B600);\n margin-top:calc((4px - var(--havik-vol-thumb)) / 2)}\n.havik-vol__slider::-moz-range-thumb{width:var(--havik-vol-thumb);height:var(--havik-vol-thumb);\n border:0;border-radius:50%;background:var(--havik-accent,#E1B600)}\n\n.havik-time{font-size:12px;color:var(--havik-text-muted,#9aa0ad);\n font-variant-numeric:tabular-nums;padding:0 6px;white-space:nowrap}\n.havik-live{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;\n letter-spacing:.4px;padding:0 8px;color:var(--havik-text,#fff)}\n.havik-live svg{width:9px;height:9px;color:var(--havik-text-muted,#9aa0ad)}\n.havik-live.is-live svg{color:var(--havik-accent,#E1B600)}\n.havik-live__go{margin-left:2px;border:0;cursor:pointer;background:transparent;color:var(--havik-text-muted,#9aa0ad);\n font:inherit;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 6px;border-radius:6px}\n.havik-live.is-live .havik-live__go{display:none}\n.havik-live__go:hover{color:var(--havik-accent,#E1B600)}\n\n.havik-menu{position:absolute;bottom:54px;right:12px;min-width:170px;max-height:230px;overflow:auto;\n background:var(--havik-surface,#1B1C23);border:1px solid var(--havik-border,rgba(255,255,255,.1));\n border-radius:var(--havik-radius,8px);box-shadow:0 10px 30px rgba(0,0,0,.5);padding:6px;z-index:3}\n.havik-menu[hidden]{display:none}\n.havik-menu__title{font-size:11px;text-transform:uppercase;letter-spacing:.6px;\n color:var(--havik-text-muted,#9aa0ad);padding:6px 8px 4px}\n.havik-menu__item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;font:inherit;font-size:13px;\n text-align:left;padding:7px 8px;border-radius:6px}\n.havik-menu__item:hover{background:var(--havik-surface-muted,#2a2c36)}\n.havik-menu__item[aria-checked=\"true\"]{color:var(--havik-accent,#E1B600);font-weight:600}\n.havik-menu__item[aria-checked=\"true\"]::after{content:'✓'}\n", sn = /* @__PURE__ */ new WeakMap();
2297
- function cn(e = document, t) {
2298
- let n = e.querySelector(`style#${an}`);
2299
- if (n && (!t || sn.get(n) === t)) return;
2325
+ var ln = "havik-player-styles", un = "\n.havik-player{position:relative;display:block;width:100%;background:var(--havik-bg,#0e0f13);\n color:var(--havik-text,#fff);font-family:var(--havik-font,system-ui,sans-serif);\n overflow:hidden;line-height:1.4;-webkit-tap-highlight-color:transparent}\n.havik-player video{display:block;width:100%;height:auto;aspect-ratio:16/9;\n object-fit:contain;background:#000;outline:none}\n.havik-player:focus{outline:none}\n.havik-player *{box-sizing:border-box}\n\n.havik-player:fullscreen,.havik-player:-webkit-full-screen{display:flex;align-items:center;\n justify-content:center;width:100vw;height:100vh;aspect-ratio:auto}\n.havik-player:fullscreen video,.havik-player:-webkit-full-screen video{width:100%;height:100%;\n aspect-ratio:auto;object-fit:contain}\n\n.havik-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;cursor:pointer;\n touch-action:manipulation}\n.havik-gradient{position:absolute;left:0;right:0;bottom:0;height:120px;pointer-events:none;\n background:linear-gradient(to top,rgba(0,0,0,.78),transparent);\n opacity:1;transition:opacity .25s}\n.havik-player.havik-hide .havik-gradient,\n.havik-player.havik-hide .havik-bar{opacity:0;pointer-events:none}\n.havik-player.havik-hide{cursor:none}\n\n.havik-center{width:74px;height:74px;border-radius:50%;border:0;cursor:pointer;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);\n display:flex;align-items:center;justify-content:center;\n box-shadow:0 6px 24px rgba(0,0,0,.45);transition:transform .15s,opacity .15s}\n.havik-center:hover{transform:scale(1.06)}\n.havik-center svg{width:38px;height:38px;margin-left:2px}\n.havik-center.havik-gone{opacity:0;pointer-events:none;transform:scale(.8)}\n\n/* Absolutely centred, NOT a flex sibling of .havik-center: the hidden center\n button (.havik-gone) still occupies its 74px in the row, which pushed an\n in-flow spinner 37px right of centre. */\n.havik-spinner{position:absolute;inset:0;margin:auto;width:56px;height:56px;border-radius:50%;\n border:4px solid rgba(255,255,255,.2);border-top-color:var(--havik-accent,#E1B600);\n animation:havik-spin .8s linear infinite}\n@keyframes havik-spin{to{transform:rotate(360deg)}}\n\n/* Status card — branded full-bleed overlay for the stopped/pre-play states\n (ended / fatal error / pre-play / armed-waiting). */\n.havik-status{position:absolute;inset:0;display:flex;flex-direction:column;gap:16px;\n align-items:center;justify-content:center;background:rgba(14,15,19,.82);text-align:center;padding:24px}\n.havik-status__logo{width:128px;height:46px;background:var(--havik-logo) center/contain no-repeat;opacity:.95}\n.havik-status__msg{color:var(--havik-text,#fff);font-size:15px;font-weight:600;max-width:480px}\n.havik-status__btn{display:inline-flex;align-items:center;gap:8px;border:0;cursor:pointer;\n padding:10px 18px;border-radius:var(--havik-radius,8px);font-weight:700;font-size:14px;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);transition:transform .15s}\n.havik-status__btn:hover{transform:scale(1.04)}\n.havik-status__btn svg{width:18px;height:18px}\n/* Pre-play: lighter scrim so the poster shows through, and a round play CTA. */\n.havik-status--preplay{background:rgba(14,15,19,.4)}\n.havik-status--preplay .havik-status__btn--play{width:74px;height:74px;border-radius:50%;padding:0;\n justify-content:center;box-shadow:0 6px 24px rgba(0,0,0,.45)}\n.havik-status--preplay .havik-status__btn--play svg{width:38px;height:38px;margin-left:3px}\n\n.havik-watermark{position:absolute;top:12px;right:14px;height:22px;opacity:.85;pointer-events:none;\n content:'';background:var(--havik-logo) center/contain no-repeat;width:90px}\n/* A status card carries its own logo — the corner watermark would duplicate it. */\n.havik-player.havik-has-status .havik-watermark{display:none}\n\n.havik-bar{position:absolute;left:0;right:0;bottom:0;padding:0 12px 8px;\n opacity:1;transition:opacity .25s;z-index:2}\n.havik-seek{position:relative;height:16px;display:flex;align-items:center;cursor:pointer;margin-bottom:2px}\n.havik-seek__track{position:relative;width:100%;height:4px;border-radius:3px;background:rgba(255,255,255,.25)}\n.havik-seek__buffered{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:rgba(255,255,255,.4)}\n.havik-seek__played{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-seek__thumb{position:absolute;top:50%;width:13px;height:13px;border-radius:50%;\n background:var(--havik-accent,#E1B600);transform:translate(-50%,-50%) scale(0);transition:transform .1s}\n.havik-seek:hover .havik-seek__thumb{transform:translate(-50%,-50%) scale(1)}\n\n.havik-controls{display:flex;align-items:center;gap:6px;height:40px}\n.havik-spacer{flex:1}\n.havik-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;border-radius:var(--havik-radius,8px);\n padding:0;transition:background .12s,color .12s}\n.havik-btn:hover{background:rgba(255,255,255,.12)}\n.havik-btn:focus-visible{outline:2px solid var(--havik-accent,#E1B600);outline-offset:1px}\n.havik-btn svg{width:22px;height:22px}\n\n/* The volume level is filled in accent up to the thumb, like the seek bar's\n played span — a native range input paints no \"elapsed\" side, which left the\n level readable only from the dot's position. --havik-vol (0..1) is set by\n syncVolume(); the track width and thumb size live here as variables because\n the WebKit fill has to compute the thumb centre from both. */\n.havik-vol{display:flex;align-items:center}\n.havik-vol__slider{--havik-vol-w:70px;--havik-vol-thumb:12px;--havik-vol:1;\n --havik-vol-pos:calc(var(--havik-vol-thumb) / 2 + var(--havik-vol) * (var(--havik-vol-w) - var(--havik-vol-thumb)));\n width:0;opacity:0;transition:width .18s,opacity .18s;height:4px;cursor:pointer;\n appearance:none;-webkit-appearance:none;background:transparent;border-radius:3px;margin:0 4px}\n.havik-vol:hover .havik-vol__slider,.havik-vol:focus-within .havik-vol__slider{width:var(--havik-vol-w);opacity:1}\n/* WebKit/Blink have no progress pseudo-element: paint the fill as a hard-stop\n gradient that ends at the thumb centre. */\n.havik-vol__slider::-webkit-slider-runnable-track{height:4px;border-radius:3px;\n background:linear-gradient(to right,var(--havik-accent,#E1B600) var(--havik-vol-pos),\n rgba(255,255,255,.3) var(--havik-vol-pos))}\n/* Firefox fills natively, and tracks the thumb exactly. */\n.havik-vol__slider::-moz-range-track{height:4px;border-radius:3px;background:rgba(255,255,255,.3)}\n.havik-vol__slider::-moz-range-progress{height:4px;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-vol__slider::-webkit-slider-thumb{-webkit-appearance:none;width:var(--havik-vol-thumb);\n height:var(--havik-vol-thumb);border-radius:50%;background:var(--havik-accent,#E1B600);\n margin-top:calc((4px - var(--havik-vol-thumb)) / 2)}\n.havik-vol__slider::-moz-range-thumb{width:var(--havik-vol-thumb);height:var(--havik-vol-thumb);\n border:0;border-radius:50%;background:var(--havik-accent,#E1B600)}\n\n.havik-time{font-size:12px;color:var(--havik-text-muted,#9aa0ad);\n font-variant-numeric:tabular-nums;padding:0 6px;white-space:nowrap}\n.havik-live{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;\n letter-spacing:.4px;padding:0 8px;color:var(--havik-text,#fff)}\n.havik-live svg{width:9px;height:9px;color:var(--havik-text-muted,#9aa0ad)}\n.havik-live.is-live svg{color:var(--havik-accent,#E1B600)}\n.havik-live__go{margin-left:2px;border:0;cursor:pointer;background:transparent;color:var(--havik-text-muted,#9aa0ad);\n font:inherit;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 6px;border-radius:6px}\n.havik-live.is-live .havik-live__go{display:none}\n.havik-live__go:hover{color:var(--havik-accent,#E1B600)}\n\n.havik-menu{position:absolute;bottom:54px;right:12px;min-width:170px;max-height:230px;overflow:auto;\n background:var(--havik-surface,#1B1C23);border:1px solid var(--havik-border,rgba(255,255,255,.1));\n border-radius:var(--havik-radius,8px);box-shadow:0 10px 30px rgba(0,0,0,.5);padding:6px;z-index:3}\n.havik-menu[hidden]{display:none}\n.havik-menu__title{font-size:11px;text-transform:uppercase;letter-spacing:.6px;\n color:var(--havik-text-muted,#9aa0ad);padding:6px 8px 4px}\n.havik-menu__item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;font:inherit;font-size:13px;\n text-align:left;padding:7px 8px;border-radius:6px}\n.havik-menu__item:hover{background:var(--havik-surface-muted,#2a2c36)}\n.havik-menu__item[aria-checked=\"true\"]{color:var(--havik-accent,#E1B600);font-weight:600}\n.havik-menu__item[aria-checked=\"true\"]::after{content:'✓'}\n", dn = /* @__PURE__ */ new WeakMap();
2326
+ function fn(e = document, t) {
2327
+ let n = e.querySelector(`style#${ln}`);
2328
+ if (n && (!t || dn.get(n) === t)) return;
2300
2329
  let r = e.createElement("style");
2301
- r.id = an, t && (r.nonce = t, r.setAttribute("nonce", t)), r.textContent = on, sn.set(r, t), n ? n.replaceWith(r) : e.head.appendChild(r);
2330
+ r.id = ln, t && (r.nonce = t, r.setAttribute("nonce", t)), r.textContent = un, dn.set(r, t), n ? n.replaceWith(r) : e.head.appendChild(r);
2302
2331
  }
2303
2332
  //#endregion
2304
2333
  //#region src/ui/icons.ts
2305
- var ln = {
2334
+ var pn = {
2306
2335
  play: "<path d=\"M8 5v14l11-7z\"/>",
2307
2336
  pause: "<path d=\"M6 5h4v14H6zM14 5h4v14h-4z\"/>",
2308
2337
  volumeHigh: "<path d=\"M3 10v4h4l5 5V5L7 10H3zm13.5 2a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4zm-2.5-9v2.06A7 7 0 0 1 14 19v2a9 9 0 0 0 0-18z\"/>",
@@ -2316,20 +2345,20 @@ var ln = {
2316
2345
  retry: "<path d=\"M17.65 6.35A8 8 0 1 0 19.73 13h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z\"/>"
2317
2346
  };
2318
2347
  function Z(e) {
2319
- return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${ln[e]}</svg>`;
2348
+ return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${pn[e]}</svg>`;
2320
2349
  }
2321
2350
  //#endregion
2322
2351
  //#region src/ui/controls.ts
2323
- var un = 3e3, dn = /* @__PURE__ */ new Set([
2352
+ var mn = 3e3, hn = 250, gn = /* @__PURE__ */ new Set([
2324
2353
  "GONE",
2325
2354
  "NOT_FOUND",
2326
2355
  "INVALID_URN",
2327
2356
  "UNAUTHORIZED"
2328
2357
  ]);
2329
- function fn(e) {
2330
- return new pn(e);
2358
+ function _n(e) {
2359
+ return new vn(e);
2331
2360
  }
2332
- var pn = class {
2361
+ var vn = class {
2333
2362
  root;
2334
2363
  video;
2335
2364
  player;
@@ -2367,11 +2396,15 @@ var pn = class {
2367
2396
  interruptedMessage;
2368
2397
  errorMessage;
2369
2398
  hideTimer;
2399
+ tapTimer;
2400
+ surfacePointerType = "";
2401
+ surfaceWasHidden = !1;
2402
+ pointerClosedMenu = !1;
2370
2403
  openMenu = null;
2371
2404
  scrubbing = !1;
2372
2405
  offFns = [];
2373
2406
  constructor(e) {
2374
- this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? un, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.interruptedMessage = e.interruptedMessage ?? "Reconnecting — please stand by", this.errorMessage = e.errorMessage, cn(this.doc, e.cspNonce), this.root.classList.add("havik-player"), this.root.hasAttribute("tabindex") || (this.root.tabIndex = 0), this.root.setAttribute("role", "region"), this.root.setAttribute("aria-label", "Video player");
2407
+ this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? mn, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.interruptedMessage = e.interruptedMessage ?? "Reconnecting — please stand by", this.errorMessage = e.errorMessage, fn(this.doc, e.cspNonce), this.root.classList.add("havik-player"), this.root.hasAttribute("tabindex") || (this.root.tabIndex = 0), this.root.setAttribute("role", "region"), this.root.setAttribute("aria-label", "Video player");
2375
2408
  let t = (e, t, n) => {
2376
2409
  let r = this.doc.createElement(e);
2377
2410
  if (r.className = t, n) for (let [e, t] of Object.entries(n)) r.setAttribute(e, t);
@@ -2420,18 +2453,48 @@ var pn = class {
2420
2453
  }
2421
2454
  wire() {
2422
2455
  let e = this.video;
2423
- this.on(this.playBtn, "click", () => this.togglePlay()), this.on(this.center, "click", () => this.togglePlay()), this.on(this.overlay, "click", (e) => {
2424
- e.target === this.overlay && this.togglePlay();
2456
+ this.on(this.playBtn, "click", () => this.togglePlay()), this.on(this.center, "click", () => this.togglePlay()), this.on(this.overlay, "pointerdown", (e) => {
2457
+ this.surfacePointerType = e.pointerType ?? "", this.surfaceWasHidden = this.root.classList.contains("havik-hide");
2458
+ }), this.on(this.overlay, "click", (e) => {
2459
+ e.target === this.overlay && this.onSurfaceClick();
2425
2460
  }), this.on(this.muteBtn, "click", () => this.player.setMuted(!e.muted)), this.on(this.volSlider, "input", () => {
2426
2461
  let t = Number(this.volSlider.value);
2427
2462
  this.setVolumeFill(t), this.player.setVolume(t), t > 0 && e.muted && this.player.setMuted(!1);
2428
2463
  }), this.on(this.liveGo, "click", () => this.player.seekToLive()), this.on(this.pipBtn, "click", () => void this.togglePip()), this.on(this.fsBtn, "click", () => void this.toggleFullscreen()), this.on(this.ccBtn, "click", () => this.toggleMenu("cc")), this.on(this.audioBtn, "click", () => this.toggleMenu("audio")), this.on(this.qualityBtn, "click", () => this.toggleMenu("quality")), this.on(this.seek, "pointerdown", (e) => this.onSeekDown(e)), this.on(this.seek, "keydown", (e) => this.onSeekKey(e)), this.on(e, "play", () => this.syncPlayPause()), this.on(e, "pause", () => this.syncPlayPause()), this.on(e, "volumechange", () => this.syncVolume()), this.on(e, "timeupdate", () => this.syncProgress()), this.on(e, "progress", () => this.syncProgress()), this.on(e, "durationchange", () => this.syncProgress()), this.offFns.push(this.player.on("statechange", () => this.syncState())), this.offFns.push(this.player.on("ended", () => this.syncState())), this.offFns.push(this.player.on("ready", () => this.refreshTracks())), this.offFns.push(this.player.on("qualitychange", () => this.onTracksChanged())), this.offFns.push(this.player.on("audiotrackchange", () => this.onTracksChanged())), this.offFns.push(this.player.on("texttrackchange", () => this.onTracksChanged())), this.offFns.push(this.player.on("pipchange", (e) => this.syncPip(e))), this.offFns.push(this.player.on("stats", () => this.syncLive())), this.offFns.push(this.player.on("error", (e) => {
2429
2464
  this.lastError = e, this.statusKind = null, this.syncState();
2430
- })), this.on(this.root, "pointermove", () => this.poke()), this.on(this.root, "pointerleave", () => this.scheduleHide(0)), this.on(this.root, "focusin", () => this.poke()), this.on(this.root, "keydown", (e) => this.onKey(e)), this.on(this.doc, "pointerdown", (e) => this.onDocPointer(e), { capture: !0 }), this.on(this.doc, "fullscreenchange", () => this.syncFullscreen()), this.on(this.doc, "webkitfullscreenchange", () => this.syncFullscreen()), this.on(e, "webkitbeginfullscreen", () => this.syncFullscreen()), this.on(e, "webkitendfullscreen", () => this.syncFullscreen()), this.syncState();
2465
+ }));
2466
+ let t = (e) => e.pointerType !== "touch";
2467
+ this.on(this.root, "pointermove", (e) => {
2468
+ t(e) && this.poke();
2469
+ }), this.on(this.root, "pointerleave", (e) => {
2470
+ t(e) && this.scheduleHide(0);
2471
+ }), this.on(this.root, "focusin", () => this.poke()), this.on(this.root, "keydown", (e) => this.onKey(e)), this.on(this.doc, "pointerdown", (e) => this.onDocPointer(e), { capture: !0 }), this.on(this.doc, "fullscreenchange", () => this.syncFullscreen()), this.on(this.doc, "webkitfullscreenchange", () => this.syncFullscreen()), this.on(e, "webkitbeginfullscreen", () => this.syncFullscreen()), this.on(e, "webkitendfullscreen", () => this.syncFullscreen()), this.syncState();
2431
2472
  }
2432
2473
  togglePlay() {
2433
2474
  this.video.paused || this.video.ended ? this.player.play() : this.player.pause();
2434
2475
  }
2476
+ onSurfaceClick() {
2477
+ let e = this.surfacePointerType === "touch";
2478
+ if (this.surfacePointerType = "", !e) {
2479
+ this.tapTimer && clearTimeout(this.tapTimer), this.tapTimer = void 0, this.togglePlay();
2480
+ return;
2481
+ }
2482
+ if (this.pointerClosedMenu) {
2483
+ this.tapTimer && clearTimeout(this.tapTimer), this.tapTimer = void 0, this.poke();
2484
+ return;
2485
+ }
2486
+ if (this.tapTimer) {
2487
+ clearTimeout(this.tapTimer), this.tapTimer = void 0, this.togglePlay();
2488
+ return;
2489
+ }
2490
+ let t = this.surfaceWasHidden;
2491
+ this.tapTimer = setTimeout(() => {
2492
+ this.tapTimer = void 0, this.toggleBar(t);
2493
+ }, hn);
2494
+ }
2495
+ toggleBar(e) {
2496
+ e ? this.poke() : this.hideIfIdle();
2497
+ }
2435
2498
  syncPlayPause() {
2436
2499
  let e = !this.video.paused && !this.video.ended;
2437
2500
  this.setIcon(this.playBtn, e ? "pause" : "play"), this.playBtn.setAttribute("aria-label", e ? "Pause" : "Play"), this.setIcon(this.center, e ? "pause" : "play"), this.center.classList.toggle("havik-gone", e || !!this.statusBox), e ? this.scheduleHide() : this.show();
@@ -2462,7 +2525,7 @@ var pn = class {
2462
2525
  e.type = "button", e.className = "havik-status__btn havik-status__btn--play", e.setAttribute("aria-label", "Play"), e.innerHTML = Z("play"), e.addEventListener("click", () => void this.player.play()), n.append(e);
2463
2526
  } else {
2464
2527
  let r = this.doc.createElement("div");
2465
- if (r.className = "havik-status__msg", r.textContent = t ?? "", n.append(r), e === "error" && this.lastError && !dn.has(this.lastError.code)) {
2528
+ if (r.className = "havik-status__msg", r.textContent = t ?? "", n.append(r), e === "error" && this.lastError && !gn.has(this.lastError.code)) {
2466
2529
  let e = this.doc.createElement("button");
2467
2530
  e.type = "button", e.className = "havik-status__btn", e.innerHTML = `${Z("retry")}<span>Retry</span>`, e.addEventListener("click", () => void this.player.retry()), n.append(e);
2468
2531
  }
@@ -2503,7 +2566,7 @@ var pn = class {
2503
2566
  let n = r.end(r.length - 1), i = Math.min(1, Math.max(0, (n - e.start) / t));
2504
2567
  this.seekBuffered.style.width = `${(i * 100).toFixed(2)}%`;
2505
2568
  }
2506
- !this.player.getStats().isLive && Number.isFinite(this.video.duration) && (this.time.textContent = `${mn(this.video.currentTime)} / ${mn(this.video.duration)}`);
2569
+ !this.player.getStats().isLive && Number.isFinite(this.video.duration) && (this.time.textContent = `${yn(this.video.currentTime)} / ${yn(this.video.duration)}`);
2507
2570
  }
2508
2571
  syncLive() {
2509
2572
  let e = this.player.getStats(), t = e.isLive !== !1;
@@ -2553,17 +2616,17 @@ var pn = class {
2553
2616
  renderQualityMenu() {
2554
2617
  let e = this.player.getQualityLevels(), t = this.player.getCurrentQuality(), n = this.doc.createElement("div");
2555
2618
  n.className = "havik-menu__title", n.textContent = "Quality", this.menu.replaceChildren(n, this.menuItem("Auto", t === -1, () => this.player.setQuality("auto")));
2556
- for (let n of e) this.menu.append(this.menuItem(hn(n), t === n.index, () => this.player.setQuality(n.index)));
2619
+ for (let n of e) this.menu.append(this.menuItem(bn(n), t === n.index, () => this.player.setQuality(n.index)));
2557
2620
  }
2558
2621
  renderAudioMenu() {
2559
2622
  let e = this.player.getAudioTracks(), t = this.doc.createElement("div");
2560
2623
  t.className = "havik-menu__title", t.textContent = "Audio", this.menu.replaceChildren(t);
2561
- for (let t of e) this.menu.append(this.menuItem(gn(t), t.default, () => this.player.setAudioTrack(t.id)));
2624
+ for (let t of e) this.menu.append(this.menuItem(xn(t), t.default, () => this.player.setAudioTrack(t.id)));
2562
2625
  }
2563
2626
  renderCcMenu() {
2564
2627
  let e = this.player.getTextTracks(), t = this.doc.createElement("div");
2565
2628
  t.className = "havik-menu__title", t.textContent = "Subtitles", this.menu.replaceChildren(t, this.menuItem("Off", !1, () => this.player.setTextTrack(-1)));
2566
- for (let t of e) this.menu.append(this.menuItem(_n(t), !1, () => this.player.setTextTrack(t.id)));
2629
+ for (let t of e) this.menu.append(this.menuItem(Sn(t), !1, () => this.player.setTextTrack(t.id)));
2567
2630
  }
2568
2631
  onTracksChanged() {
2569
2632
  this.openMenu && this.renderMenu(this.openMenu);
@@ -2597,16 +2660,17 @@ var pn = class {
2597
2660
  this.root.classList.remove("havik-hide");
2598
2661
  }
2599
2662
  scheduleHide(e = this.autoHideMs) {
2600
- this.hideTimer && clearTimeout(this.hideTimer), this.hideTimer = setTimeout(() => {
2601
- !this.video.paused && !this.video.ended && !this.openMenu && !this.statusBox && this.root.classList.add("havik-hide");
2602
- }, e);
2663
+ this.hideTimer && clearTimeout(this.hideTimer), this.hideTimer = setTimeout(() => this.hideIfIdle(), e);
2664
+ }
2665
+ hideIfIdle() {
2666
+ !this.video.paused && !this.video.ended && !this.openMenu && !this.statusBox && this.root.classList.add("havik-hide");
2603
2667
  }
2604
2668
  onDocPointer(e) {
2605
- this.openMenu && !this.menu.contains(e.target) && ([
2669
+ this.pointerClosedMenu = !1, this.openMenu && !this.menu.contains(e.target) && ([
2606
2670
  this.qualityBtn,
2607
2671
  this.audioBtn,
2608
2672
  this.ccBtn
2609
- ].some((t) => t.contains(e.target)) || this.closeMenu());
2673
+ ].some((t) => t.contains(e.target)) || (this.closeMenu(), this.pointerClosedMenu = !0));
2610
2674
  }
2611
2675
  onKey(e) {
2612
2676
  let t = e.target;
@@ -2649,7 +2713,7 @@ var pn = class {
2649
2713
  e.innerHTML = Z(t);
2650
2714
  }
2651
2715
  destroy() {
2652
- this.hideTimer && clearTimeout(this.hideTimer);
2716
+ this.hideTimer && clearTimeout(this.hideTimer), this.tapTimer && clearTimeout(this.tapTimer);
2653
2717
  for (let e of this.offFns) try {
2654
2718
  e();
2655
2719
  } catch {}
@@ -2663,41 +2727,41 @@ var pn = class {
2663
2727
  this.root.classList.remove("havik-player", "havik-hide", "havik-has-status");
2664
2728
  }
2665
2729
  };
2666
- function mn(e) {
2730
+ function yn(e) {
2667
2731
  (!Number.isFinite(e) || e < 0) && (e = 0);
2668
2732
  let t = Math.floor(e % 60), n = Math.floor(e / 60 % 60), r = Math.floor(e / 3600), i = r > 0 ? String(n).padStart(2, "0") : String(n), a = String(t).padStart(2, "0");
2669
2733
  return r > 0 ? `${r}:${i}:${a}` : `${i}:${a}`;
2670
2734
  }
2671
- function hn(e) {
2735
+ function bn(e) {
2672
2736
  return e.height ? `${e.height}p` : `${Math.round(e.bitrate / 1e3)} kbps`;
2673
2737
  }
2674
- function gn(e) {
2738
+ function xn(e) {
2675
2739
  return e.name || e.lang || `Track ${e.id}`;
2676
2740
  }
2677
- function _n(e) {
2741
+ function Sn(e) {
2678
2742
  return e.name || e.lang || `Track ${e.id}`;
2679
2743
  }
2680
2744
  //#endregion
2681
2745
  //#region src/managed/index.ts
2682
- var vn = 6e3, yn = 45e3, Q = 2e3, bn = 3, xn = 2e3, Sn = 48e4;
2683
- function Cn(e) {
2746
+ var Cn = 6e3, Q = 45e3, wn = 2e3, Tn = 3, En = 2e3, Dn = 48e4;
2747
+ function On(e) {
2684
2748
  if (e.env !== void 0 && e.baseUrl !== void 0) throw new n("INTERNAL", 0, `createPlayer: pass env OR baseUrl, not both (got env=${JSON.stringify(e.env)} and baseUrl=${JSON.stringify(e.baseUrl)})`);
2685
2749
  if (e.env !== void 0) return B(e.env);
2686
2750
  if (!e.baseUrl) throw new n("INTERNAL", 0, "createPlayer: env or baseUrl is required (env: 'integration' | 'production')");
2687
2751
  return e.baseUrl;
2688
2752
  }
2689
- async function wn(e) {
2753
+ async function kn(e) {
2690
2754
  if (!e.video || typeof e.video.play != "function") throw new n("INTERNAL", 0, "createPlayer: opts.video must be an HTMLVideoElement");
2691
- let t = Cn(e);
2755
+ let t = On(e);
2692
2756
  if (!e.matchUrn) throw new n("INVALID_URN", 0, "createPlayer: matchUrn is required");
2693
2757
  if (!e.credential) throw new n("INTERNAL", 0, "createPlayer: credential is required");
2694
- let r = new Tn({
2758
+ let r = new An({
2695
2759
  ...e,
2696
2760
  baseUrl: t
2697
2761
  });
2698
2762
  return await r.start(), r;
2699
2763
  }
2700
- var Tn = class {
2764
+ var An = class {
2701
2765
  opts;
2702
2766
  abort = new AbortController();
2703
2767
  listeners = {};
@@ -2785,7 +2849,7 @@ var Tn = class {
2785
2849
  setTimeout(() => {
2786
2850
  a || this.destroyed || e !== this.loadGen || this.emit("waiting", {
2787
2851
  phase: i ? "unavailable" : "tooEarly",
2788
- retryInMs: yn,
2852
+ retryInMs: Q,
2789
2853
  attempt: 1,
2790
2854
  elapsedMs: 0,
2791
2855
  ...!i && f !== void 0 && f > Date.now() ? { ttkMs: f - Date.now() } : {}
@@ -2824,13 +2888,13 @@ var Tn = class {
2824
2888
  !a && !p() && h();
2825
2889
  }, e);
2826
2890
  };
2827
- _(vn), this.armingSub = T({
2891
+ _(Cn), this.armingSub = T({
2828
2892
  eventsBaseUrl: t,
2829
2893
  matchUrn: this.opts.matchUrn,
2830
2894
  credential: this.opts.credential,
2831
- onAlive: () => _(yn),
2895
+ onAlive: () => _(Q),
2832
2896
  onState: (e) => {
2833
- a || p() || (e.state === "live" || e.state === "gone") && setTimeout(g, Math.random() * Q);
2897
+ a || p() || (e.state === "live" || e.state === "gone") && setTimeout(g, Math.random() * wn);
2834
2898
  },
2835
2899
  onError: () => h()
2836
2900
  });
@@ -2875,19 +2939,19 @@ var Tn = class {
2875
2939
  if (this.analyticsSession?.noteDestroyed(), this.analyticsSession = null, this.detachMediaListeners?.(), this.opts.analytics === !1) return;
2876
2940
  let i = e.analytics?.sid;
2877
2941
  if (!i) return;
2878
- let a = typeof this.opts.analytics == "object" ? this.opts.analytics.endpoint : void 0, { endpoint: o, disabledReason: s } = qt(this.opts.baseUrl, a, e.analytics?.endpoint);
2942
+ let a = typeof this.opts.analytics == "object" ? this.opts.analytics.endpoint : void 0, { endpoint: o, disabledReason: s } = Zt(this.opts.baseUrl, a, e.analytics?.endpoint);
2879
2943
  if (!o) {
2880
2944
  s && setTimeout(() => {
2881
2945
  this.destroyed || r !== this.loadGen || this.emit("warning", new n("INTERNAL", 0, s));
2882
2946
  }, 0);
2883
2947
  return;
2884
2948
  }
2885
- let c = this.opts.video, l = new Jt({
2949
+ let c = this.opts.video, l = new Qt({
2886
2950
  endpoint: o,
2887
2951
  sid: i,
2888
2952
  player: t.name === "native" ? "PLAYER_SAFARI_NATIVE" : "PLAYER_HLSJS",
2889
- sdkVersion: en,
2890
- environment: Qt(c),
2953
+ sdkVersion: an,
2954
+ environment: nn(c),
2891
2955
  idleMs: typeof this.opts.analytics == "object" ? this.opts.analytics.idleMs : void 0,
2892
2956
  onIdle: () => this.onAnalyticsIdle(t),
2893
2957
  onBlocked: (e) => setTimeout(() => {
@@ -2908,7 +2972,7 @@ var Tn = class {
2908
2972
  let u = () => l.noteSeek(), d = () => l.noteVolume(c.muted, c.volume);
2909
2973
  c.addEventListener("seeking", u), c.addEventListener("volumechange", d), this.detachMediaListeners = () => {
2910
2974
  c.removeEventListener("seeking", u), c.removeEventListener("volumechange", d);
2911
- }, l.start(), l.seedVolume(c.muted, c.volume), l.notePresentation(this.currentPresentation()), $t().then((e) => l.noteEnvironment(e)), this.opts.autoplay === !1 && l.noteAutoplay("AUTOPLAY_RESULT_NOT_ATTEMPTED");
2975
+ }, l.start(), l.seedVolume(c.muted, c.volume), l.notePresentation(this.currentPresentation()), rn().then((e) => l.noteEnvironment(e)), this.opts.autoplay === !1 && l.noteAutoplay("AUTOPLAY_RESULT_NOT_ATTEMPTED");
2912
2976
  }
2913
2977
  currentPresentation() {
2914
2978
  let e = this.opts.video;
@@ -2934,9 +2998,9 @@ var Tn = class {
2934
2998
  emit: (e) => this.onEngineEvent(e)
2935
2999
  }, o = this.opts.engine ?? "auto";
2936
3000
  if (o === "native") return new K(a);
2937
- if (o === "hls") return new kt(a);
3001
+ if (o === "hls") return new Nt(a);
2938
3002
  if (e.drmEnabled && e.drm?.fairplay?.licenseUrl && e.drm.fairplay.certificateUrl && W(this.opts.video) && G()) return new K(a);
2939
- if (at()) return new kt(a);
3003
+ if (at()) return new Nt(a);
2940
3004
  if (W(this.opts.video)) return new K(a);
2941
3005
  throw new n("INTERNAL", 0, "no supported HLS playback engine in this browser");
2942
3006
  }
@@ -2989,7 +3053,7 @@ var Tn = class {
2989
3053
  r = 0, i = !1;
2990
3054
  },
2991
3055
  onError: () => {
2992
- this.destroyed || e !== this.loadGen || i || this.endedHandled || (r += 1, r >= bn && (i = !0, this.emit("warning", new n("NETWORK", 0, `live-state events unreachable at ${t} after ${r} attempts — end-of-stream detection degraded to the HLS staleness watchdog. Check the events host, the api-key's allowed origins, and CSP connect-src.`))));
3056
+ this.destroyed || e !== this.loadGen || i || this.endedHandled || (r += 1, r >= Tn && (i = !0, this.emit("warning", new n("NETWORK", 0, `live-state events unreachable at ${t} after ${r} attempts — end-of-stream detection degraded to the HLS staleness watchdog. Check the events host, the api-key's allowed origins, and CSP connect-src.`))));
2993
3057
  }
2994
3058
  });
2995
3059
  }
@@ -3011,7 +3075,7 @@ var Tn = class {
3011
3075
  scheduleRejoin(e) {
3012
3076
  this.rejoinEnabled() && !this.rejoinTimer && (this.rejoinTimer = setTimeout(() => {
3013
3077
  this.rejoinTimer = null, !(this.destroyed || e !== this.loadGen || this._state !== "ended") && this.rejoin();
3014
- }, Math.random() * Q));
3078
+ }, Math.random() * wn));
3015
3079
  }
3016
3080
  cancelRejoin() {
3017
3081
  this.rejoinTimer &&= (clearTimeout(this.rejoinTimer), null);
@@ -3035,7 +3099,7 @@ var Tn = class {
3035
3099
  }
3036
3100
  t.controls = !1;
3037
3101
  let n = (t.ownerDocument ?? document).createElement("div"), r = t.parentNode;
3038
- r && r.insertBefore(n, t), n.appendChild(t), rn(n, this.opts.theme), this.container = n, this.controlBar = fn({
3102
+ r && r.insertBefore(n, t), n.appendChild(t), cn(n, this.opts.theme), this.container = n, this.controlBar = _n({
3039
3103
  root: n,
3040
3104
  video: t,
3041
3105
  player: this,
@@ -3066,7 +3130,7 @@ var Tn = class {
3066
3130
  }
3067
3131
  startStats() {
3068
3132
  this.stopStats();
3069
- let e = Math.max(500, this.opts.statsIntervalMs ?? xn);
3133
+ let e = Math.max(500, this.opts.statsIntervalMs ?? En);
3070
3134
  this.statsTimer = setInterval(() => {
3071
3135
  if (!this.engine) return;
3072
3136
  let e = this.engine.getStats();
@@ -3078,7 +3142,7 @@ var Tn = class {
3078
3142
  }
3079
3143
  scheduleLicenseRefresh() {
3080
3144
  this.stopRefresh();
3081
- let e = this.opts.licenseRefreshMs ?? Sn;
3145
+ let e = this.opts.licenseRefreshMs ?? Dn;
3082
3146
  e && this._descriptor?.drmEnabled && (this.refreshTimer = setInterval(() => void this.refreshLicense(), e));
3083
3147
  }
3084
3148
  stopRefresh() {
@@ -3233,7 +3297,7 @@ function $(e) {
3233
3297
  }
3234
3298
  //#endregion
3235
3299
  //#region src/embed/host.ts
3236
- function En(e) {
3300
+ function jn(e) {
3237
3301
  if (!e.container || typeof e.container.appendChild != "function") throw Error("mountEmbed: opts.container must be a DOM element");
3238
3302
  if (!e.src) throw Error("mountEmbed: opts.src is required");
3239
3303
  if (!e.baseUrl || !e.matchUrn) throw Error("mountEmbed: opts.baseUrl and opts.matchUrn are required");
@@ -3292,15 +3356,15 @@ function En(e) {
3292
3356
  }
3293
3357
  //#endregion
3294
3358
  //#region src/embed/page.ts
3295
- var Dn = /^[A-Za-z0-9:_.-]{1,128}$/;
3296
- function On(e) {
3359
+ var Mn = /^[A-Za-z0-9:_.-]{1,128}$/;
3360
+ function Nn(e) {
3297
3361
  if (typeof e == "boolean") return e;
3298
3362
  if (typeof e == "object" && e) {
3299
3363
  let t = e.endpoint;
3300
3364
  return typeof t == "string" ? { endpoint: t } : {};
3301
3365
  }
3302
3366
  }
3303
- function kn() {
3367
+ function Pn() {
3304
3368
  let e = window.HAVIK_EMBED_CONFIG;
3305
3369
  if (e) return {
3306
3370
  baseUrl: e.baseUrl ?? "",
@@ -3310,7 +3374,7 @@ function kn() {
3310
3374
  autoplay: e.autoplay ?? !0,
3311
3375
  muted: e.muted ?? !0,
3312
3376
  waitForLive: e.waitForLive ?? !0,
3313
- analytics: On(e.analytics)
3377
+ analytics: Nn(e.analytics)
3314
3378
  };
3315
3379
  let t = new URLSearchParams(location.search);
3316
3380
  return {
@@ -3323,9 +3387,9 @@ function kn() {
3323
3387
  waitForLive: !0
3324
3388
  };
3325
3389
  }
3326
- async function An() {
3327
- let e = kn();
3328
- Mn();
3390
+ async function Fn() {
3391
+ let e = Pn();
3392
+ Ln();
3329
3393
  let t = document.createElement("video");
3330
3394
  t.setAttribute("playsinline", ""), t.muted = e.muted ?? !0, t.style.cssText = "width:100%;height:100%;object-fit:contain;background:#000;aspect-ratio:auto", document.body.appendChild(t);
3331
3395
  let r = e.parentOrigin && e.parentOrigin !== "null" ? e.parentOrigin : null;
@@ -3380,12 +3444,12 @@ async function An() {
3380
3444
  code: e.code,
3381
3445
  message: e.message
3382
3446
  })), e.on("autoplayblocked", () => {
3383
- i({ type: "oddin:autoplayblocked" }), jn(t, e);
3447
+ i({ type: "oddin:autoplayblocked" }), In(t, e);
3384
3448
  });
3385
3449
  }, s = async (r) => {
3386
3450
  a?.destroy(), a = null;
3387
3451
  try {
3388
- let n = await wn({
3452
+ let n = await kn({
3389
3453
  video: t,
3390
3454
  baseUrl: e.baseUrl,
3391
3455
  matchUrn: r,
@@ -3447,7 +3511,7 @@ async function An() {
3447
3511
  a?.retry();
3448
3512
  break;
3449
3513
  case "oddin:load":
3450
- typeof e.matchUrn == "string" && Dn.test(e.matchUrn) && s(e.matchUrn);
3514
+ typeof e.matchUrn == "string" && Mn.test(e.matchUrn) && s(e.matchUrn);
3451
3515
  break;
3452
3516
  case "oddin:destroy": c();
3453
3517
  }
@@ -3459,18 +3523,18 @@ async function An() {
3459
3523
  protocol: 1
3460
3524
  });
3461
3525
  }
3462
- function jn(e, t) {
3526
+ function In(e, t) {
3463
3527
  if (document.getElementById("havik-tap")) return;
3464
3528
  let n = document.createElement("button");
3465
3529
  n.id = "havik-tap", n.textContent = "▶ Tap to play", n.style.cssText = "position:fixed;inset:0;width:100%;height:100%;border:0;background:rgba(0,0,0,.55);color:#fff;font:600 18px system-ui,sans-serif;cursor:pointer;z-index:9", n.addEventListener("click", () => {
3466
3530
  n.remove(), t.play();
3467
3531
  }), document.body.appendChild(n);
3468
3532
  }
3469
- function Mn() {
3533
+ function Ln() {
3470
3534
  let e = document.createElement("style");
3471
3535
  e.textContent = "html,body{margin:0;height:100%;background:#000;overflow:hidden}body{display:flex}", document.head.appendChild(e);
3472
3536
  }
3473
3537
  //#endregion
3474
- export { tn as ODDIN_THEME, n as PlaybackError, rn as applyTheme, B as baseUrlForEnv, An as bootEmbed, Te as clearDeviceId, wn as createPlayer, E as deriveEventsBaseUrl, He as detectDrmSupport, C as enterVideoFullscreen, w as exitVideoFullscreen, _e as fetchCatalog, ye as fetchTournament, xe as flattenMatches, k as getDeviceId, We as isLowLatencyManifest, $ as isOddinMessage, De as licenseRequestHeaders, En as mountEmbed, de as pollToLive, y as resolveStream, T as subscribeLiveState, S as videoFullscreenActive, Ce as watchStatus };
3538
+ export { on as ODDIN_THEME, n as PlaybackError, cn as applyTheme, B as baseUrlForEnv, Fn as bootEmbed, Te as clearDeviceId, kn as createPlayer, E as deriveEventsBaseUrl, He as detectDrmSupport, C as enterVideoFullscreen, w as exitVideoFullscreen, _e as fetchCatalog, ye as fetchTournament, xe as flattenMatches, k as getDeviceId, We as isLowLatencyManifest, $ as isOddinMessage, De as licenseRequestHeaders, jn as mountEmbed, de as pollToLive, y as resolveStream, T as subscribeLiveState, S as videoFullscreenActive, Ce as watchStatus };
3475
3539
 
3476
3540
  //# sourceMappingURL=index.js.map