@oddin-gg/havik-player 2.15.2 → 2.15.4

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 = 3, mt = .5, ht = 1.1, 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) {
1222
+ var ot = 3, st = 5, ct = 3, lt = 3e4, ut = 12e3, dt = 3e3, ft = 6e3, pt = .5, mt = 3, ht = .5, gt = 1.1, _t = .25, vt = 5e3, yt = 4e3, bt = 2, xt = 5, St = 2, Ct = 2, wt = 4, Tt = 4, Et = 2e3, Dt = 3e4, Ot = .2, kt = 30, At = 2, jt = 2, Mt = 9, Nt = .5, Pt = 64;
1223
+ function Ft(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 ??= 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);
1228
+ e.liveSyncDuration ??= jt;
1229
+ let t = Math.max(12, e.liveSyncDuration + Mt);
1230
+ (e.liveMaxLatencyDuration == null || e.liveMaxLatencyDuration <= e.liveSyncDuration) && (e.liveMaxLatencyDuration = t), It(e, e.liveSyncDuration);
1231
1231
  }
1232
- function Mt(e, t) {
1232
+ function It(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 Nt = class {
1236
+ var Lt = class {
1237
1237
  name = "hls";
1238
1238
  hls = null;
1239
1239
  deps;
@@ -1290,12 +1290,22 @@ var Nt = class {
1290
1290
  savedLiveMaxLatency;
1291
1291
  savedMaxSyncRate;
1292
1292
  chaseLatency = -1;
1293
+ catchUpRate = 1;
1294
+ catchUpEngaged = !1;
1295
+ onTimeUpdate = null;
1293
1296
  chaseTicks = 0;
1294
1297
  liveRetargets = 0;
1295
1298
  capObserver = null;
1296
1299
  capTimer = null;
1297
1300
  sizeCapLevel = null;
1298
1301
  bitrateCapLevel = -1;
1302
+ fpsCapBitrate = null;
1303
+ fpsCapFailedHeight = null;
1304
+ fpsCapManaged = !0;
1305
+ fpsLastFrames = -1;
1306
+ fpsLastDropped = -1;
1307
+ fpsBadTicks = 0;
1308
+ fpsDownCaps = 0;
1299
1309
  sizeCapManaged = !0;
1300
1310
  dprMedia = null;
1301
1311
  onDprChange = null;
@@ -1322,7 +1332,7 @@ var Nt = class {
1322
1332
  t - r.currentTime > Math.max(n * 3, 8) && (r.currentTime = t);
1323
1333
  }, document.addEventListener("visibilitychange", this.onVisibility)), this.onSeeking = () => {
1324
1334
  this.clampOverSeek(), this.loadSuspended && (this.resumeLoading(), this.schedulePauseSuspend());
1325
- }, r.addEventListener("seeking", this.onSeeking), this.onPause = () => this.schedulePauseSuspend(), this.onPlay = () => this.resumeLoading(), r.addEventListener("pause", this.onPause), r.addEventListener("play", this.onPlay), this.sizeCapManaged && typeof ResizeObserver < "u" && (this.capObserver = new ResizeObserver(() => this.onMediaResize()), this.capObserver.observe(r), this.watchDprChanges());
1335
+ }, r.addEventListener("seeking", this.onSeeking), this.onTimeUpdate = () => this.updateCatchUpBand(), r.addEventListener("timeupdate", this.onTimeUpdate), this.onPause = () => this.schedulePauseSuspend(), this.onPlay = () => this.resumeLoading(), r.addEventListener("pause", this.onPause), r.addEventListener("play", this.onPlay), this.sizeCapManaged && typeof ResizeObserver < "u" && (this.capObserver = new ResizeObserver(() => this.onMediaResize()), this.capObserver.observe(r), this.watchDprChanges());
1326
1336
  }
1327
1337
  schedulePauseSuspend() {
1328
1338
  this.pauseSuspendTimer || this.loadSuspended || this.stopped || !this.loadStarted || (this.pauseSuspendTimer = setTimeout(() => {
@@ -1336,10 +1346,10 @@ var Nt = class {
1336
1346
  }
1337
1347
  this.loadSuspended = !0;
1338
1348
  }
1339
- }, _t));
1349
+ }, vt));
1340
1350
  }
1341
1351
  resumeLoading() {
1342
- if (this.pauseSuspendTimer &&= (clearTimeout(this.pauseSuspendTimer), null), this.resetChaseWindow(), this.loadSuspended && (this.loadSuspended = !1, !this.stopped && this.hls)) {
1352
+ if (this.pauseSuspendTimer &&= (clearTimeout(this.pauseSuspendTimer), null), this.resetChaseWindow(), this.resetFrameDropWindow(), this.loadSuspended && (this.loadSuspended = !1, !this.stopped && this.hls)) {
1343
1353
  this.lastAdvanceMs = Date.now(), this.lastFragLoadingMs = 0, this.resetStarvationWindow(), this.resetChaseWindow(), this.clearKick();
1344
1354
  try {
1345
1355
  this.hls.startLoad(-1);
@@ -1368,11 +1378,11 @@ var Nt = class {
1368
1378
  let t = e.liveSyncPosition;
1369
1379
  if (typeof t != "number" || !Number.isFinite(t)) return;
1370
1380
  let { video: n } = this.deps;
1371
- n.currentTime <= t + kt || (n.currentTime = t);
1381
+ n.currentTime <= t + Nt || (n.currentTime = t);
1372
1382
  }
1373
1383
  startHls() {
1374
1384
  let { video: t, descriptor: n, credential: r, deviceId: i, userId: a, lowLatency: o, debug: s, hlsConfig: c, emit: l } = this.deps;
1375
- this.clearKick(), this.resetStarvationWindow(), this.resetChaseWindow(), this.lastFragLoadingMs = 0, this.lastEndSN = -1, this.lastAdvanceMs = 0;
1385
+ this.clearKick(), this.resetStarvationWindow(), this.resetChaseWindow(), this.resetFrameDropWindow(), this.lastFragLoadingMs = 0, this.lastEndSN = -1, this.lastAdvanceMs = 0;
1376
1386
  let u = n.drmEnabled && !!(n.drm?.widevine?.licenseUrl || n.drm?.fairplay?.licenseUrl), d = {
1377
1387
  debug: s === !0,
1378
1388
  enableWorker: !o,
@@ -1415,7 +1425,7 @@ var Nt = class {
1415
1425
  }), t = tt(e);
1416
1426
  t && (this.prefetch = e, d.fLoader = t);
1417
1427
  }
1418
- jt(d), this.sizeCapManaged = d.capLevelToPlayerSize !== !0, this.loadStarted = d.autoStartLoad !== !1;
1428
+ Ft(d), this.catchUpRate = typeof d.maxLiveSyncPlaybackRate == "number" ? d.maxLiveSyncPlaybackRate : 1, this.catchUpEngaged = !1, this.catchUpRate > 1 && (d.maxLiveSyncPlaybackRate = 1), this.sizeCapManaged = d.capLevelToPlayerSize !== !0, this.fpsCapManaged = !(c && "capLevelOnFPSDrop" in c), this.loadStarted = d.autoStartLoad !== !1;
1419
1429
  let f = new e(d);
1420
1430
  if (this.hls = f, f.on(e.Events.MANIFEST_PARSED, () => {
1421
1431
  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" });
@@ -1430,7 +1440,7 @@ var Nt = class {
1430
1440
  let n = t?.frag?.stats, r = n?.loaded;
1431
1441
  typeof r == "number" && (this.bytesLoaded += r), !this.reportedFirstBytes && typeof r == "number" && r > 0 && (this.reportedFirstBytes = !0, l({ type: "firstBytes" }));
1432
1442
  let i = n?.loading;
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());
1443
+ 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 > Pt && this.requestTimesMs.shift());
1434
1444
  let a = t?.frag?.url;
1435
1445
  if (a) {
1436
1446
  let e = H(a);
@@ -1454,7 +1464,7 @@ var Nt = class {
1454
1464
  f.on(e.Events.KEY_LOADED, () => {
1455
1465
  u && !this.reportedDrmReady && (this.reportedDrmReady = !0, l({ type: "drmReady" }));
1456
1466
  }), this.endWatchdog && clearInterval(this.endWatchdog), this.endWatchdog = setInterval(() => {
1457
- this.checkEndStale(), this.checkStarvation(), this.checkFutileChase();
1467
+ this.checkEndStale(), this.checkStarvation(), this.updateCatchUpBand(), this.checkFutileChase(), this.checkFrameDrops();
1458
1468
  }, dt), f.on(e.Events.LEVEL_SWITCHED, (e, t) => {
1459
1469
  this.renditionSwitches += 1;
1460
1470
  let n = this.lastLevelIndex >= 0 ? f.levels[this.lastLevelIndex] : void 0, r = f.levels[t.level];
@@ -1519,7 +1529,7 @@ var Nt = class {
1519
1529
  return;
1520
1530
  }
1521
1531
  this.capTimer && clearTimeout(this.capTimer);
1522
- let t = e > this.sizeCapLevel ? Tt : Et;
1532
+ let t = e > this.sizeCapLevel ? Et : Dt;
1523
1533
  this.capTimer = setTimeout(() => {
1524
1534
  this.capTimer = null, this.computeSizeCapTarget() === e ? this.applySizeCap() : this.onMediaResize();
1525
1535
  }, t);
@@ -1555,8 +1565,12 @@ var Nt = class {
1555
1565
  pushCap() {
1556
1566
  let e = this.hls;
1557
1567
  if (!e) return;
1558
- let t = this.sizeCapLevel ?? -1, n = this.bitrateCapLevel;
1559
- e.autoLevelCapping = t < 0 ? n : n < 0 ? t : Math.min(t, n);
1568
+ let t = [
1569
+ this.sizeCapLevel ?? -1,
1570
+ this.bitrateCapLevel,
1571
+ this.fpsCapLevel(e)
1572
+ ].filter((e) => e >= 0);
1573
+ e.autoLevelCapping = t.length ? Math.min(...t) : -1;
1560
1574
  }
1561
1575
  getAudioTracks() {
1562
1576
  return this.hls ? this.hls.audioTracks.map((e) => ({
@@ -1596,6 +1610,7 @@ var Nt = class {
1596
1610
  playlistBytesLoaded: this.playlistBytesLoaded,
1597
1611
  renditionSwitches: this.renditionSwitches,
1598
1612
  liveRetargets: this.liveRetargets,
1613
+ fpsDownCaps: this.fpsDownCaps,
1599
1614
  licenseTimeMs: this.licenseTimeMs,
1600
1615
  upshifts: this.upshifts,
1601
1616
  downshifts: this.downshifts,
@@ -1650,7 +1665,7 @@ var Nt = class {
1650
1665
  this.resetStarvationWindow();
1651
1666
  return;
1652
1667
  }
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;
1668
+ let n = Date.now(), r = this.lastAdvanceMs !== 0 && n - this.lastAdvanceMs < ft, i = this.lastFragLoadingMs !== 0 && n - this.lastFragLoadingMs > yt, a = t.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !t.paused, { totalFrames: o, dropped: s } = U(t), c = o - s, l = c !== this.starvedFrames;
1654
1669
  if (this.starvedFrames = c, !a || l) {
1655
1670
  (t.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA || l) && this.clearKick(), this.resetStarvationWindow();
1656
1671
  return;
@@ -1659,12 +1674,12 @@ var Nt = class {
1659
1674
  this.resetStarvationWindow();
1660
1675
  return;
1661
1676
  }
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)) {
1677
+ 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 > wt && this.recentMainFrags.shift(), this.clearRecoveryReset(), this.kickPending && this.kickTicks >= St && (this.clearKick(), this.starvationRebuilds < Ct)) {
1663
1678
  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");
1664
1679
  return;
1665
1680
  }
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))) {
1681
+ let u = i && this.starvedTicks >= bt, d = !i && this.deadTicks >= wt && this.recentMainFrags.reduce((e, t) => e + t, 0) >= Tt;
1682
+ if ((u || d) && (this.resetStarvationWindow(), !(this.starvationKicks >= xt))) {
1668
1683
  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");
1669
1684
  try {
1670
1685
  e.stopLoad(), e.startLoad(-1);
@@ -1673,6 +1688,49 @@ var Nt = class {
1673
1688
  }
1674
1689
  }
1675
1690
  }
1691
+ updateCatchUpBand() {
1692
+ let e = this.hls;
1693
+ if (!e || this.catchUpRate <= 1 || this.chaseSuspended || this.loadSuspended) return;
1694
+ let t = e.latency, n = e.targetLatency;
1695
+ if (typeof t != "number" || typeof n != "number" || !Number.isFinite(t)) return;
1696
+ let r = t - n;
1697
+ !this.catchUpEngaged && r > pt ? this.catchUpEngaged = !0 : this.catchUpEngaged && r <= pt && this.deps.video.playbackRate === 1 && (this.catchUpEngaged = !1);
1698
+ let i = this.catchUpEngaged ? this.catchUpRate : 1;
1699
+ e.config.maxLiveSyncPlaybackRate !== i && (e.config.maxLiveSyncPlaybackRate = i);
1700
+ }
1701
+ fpsCapLevel(e) {
1702
+ if (this.fpsCapBitrate == null) return -1;
1703
+ let t = this.fpsCapFailedHeight, n = -1;
1704
+ return e.levels.forEach((e, r) => {
1705
+ let i = t == null || !(e.height > 0) || e.height < t;
1706
+ e.bitrate <= this.fpsCapBitrate && i && (n = r);
1707
+ }), n < 0 ? 0 : n;
1708
+ }
1709
+ resetFrameDropWindow() {
1710
+ this.fpsLastFrames = -1, this.fpsLastDropped = -1, this.fpsBadTicks = 0;
1711
+ }
1712
+ checkFrameDrops() {
1713
+ let e = this.hls, { video: t } = this.deps;
1714
+ if (!e || !this.fpsCapManaged || this.loadSuspended || t.paused || !e.autoLevelEnabled || typeof document < "u" && document.hidden) {
1715
+ this.resetFrameDropWindow();
1716
+ return;
1717
+ }
1718
+ let { totalFrames: n, dropped: r } = U(t), i = this.fpsLastFrames < 0, a = n - this.fpsLastFrames, o = r - this.fpsLastDropped;
1719
+ if (this.fpsLastFrames = n, this.fpsLastDropped = r, i || a < kt || o < 0) {
1720
+ this.fpsBadTicks = 0;
1721
+ return;
1722
+ }
1723
+ if (o <= Ot * a) {
1724
+ this.fpsBadTicks = 0;
1725
+ return;
1726
+ }
1727
+ if (this.fpsBadTicks += 1, this.fpsBadTicks < At) return;
1728
+ this.fpsBadTicks = 0;
1729
+ let s = e.levels, c = s[e.currentLevel];
1730
+ if (!c) return;
1731
+ let l = s.filter((e) => e.height > 0 && e.height < c.height), u = (l.length ? l : s.filter((e) => e.bitrate < c.bitrate)).map((e) => e.bitrate).sort((e, t) => t - e)[0];
1732
+ u != null && (this.fpsCapBitrate != null && u >= this.fpsCapBitrate || (this.fpsCapBitrate = u, l.length && (this.fpsCapFailedHeight = c.height), this.fpsDownCaps += 1, console.warn(`havik-player: dropping ${Math.round(100 * o / a)}% of frames at ${c.height}p — capping quality below it`), this.pushCap()));
1733
+ }
1676
1734
  resetChaseWindow() {
1677
1735
  this.chaseLatency = -1, this.chaseTicks = 0;
1678
1736
  }
@@ -1683,24 +1741,27 @@ var Nt = class {
1683
1741
  return;
1684
1742
  }
1685
1743
  let n = e.latency, r = t.playbackRate, i = e.config.maxLiveSyncPlaybackRate;
1686
- if (!(!t.paused && typeof i == "number" && i > 1 && r >= Math.min(ht, i) && r <= i && typeof n == "number" && Number.isFinite(n))) {
1744
+ if (!(!t.paused && typeof i == "number" && i > 1 && r >= Math.min(gt, i) && r <= i && typeof n == "number" && Number.isFinite(n))) {
1687
1745
  this.resetChaseWindow();
1688
1746
  return;
1689
1747
  }
1690
- if (this.chaseLatency < 0 || this.chaseLatency - n >= mt) {
1748
+ if (this.chaseLatency < 0 || this.chaseLatency - n >= ht) {
1691
1749
  this.chaseLatency = n, this.chaseTicks = 0;
1692
1750
  return;
1693
1751
  }
1694
- if (this.chaseTicks += 1, this.chaseTicks < pt) return;
1752
+ if (this.chaseTicks += 1, this.chaseTicks < mt) return;
1695
1753
  this.resetChaseWindow();
1696
1754
  let a = typeof e.config.liveMaxLatencyDuration == "number" ? e.config.liveMaxLatencyDuration : e.maxLatency;
1697
1755
  if (!(a > 1)) return;
1698
1756
  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);
1757
+ s < o + _t || (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
1758
  }
1701
1759
  suspendChasing() {
1702
1760
  let e = this.hls;
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);
1761
+ if (!e) return;
1762
+ this.chaseSuspended = !0, this.savedLiveMaxLatency = e.config.liveMaxLatencyDuration, this.savedMaxSyncRate = e.config.maxLiveSyncPlaybackRate, e.config.liveMaxLatencyDuration = 2 ** 53 - 1, e.config.maxLiveSyncPlaybackRate = 1;
1763
+ let { video: t } = this.deps, n = this.savedMaxSyncRate;
1764
+ t.playbackRate > 1 && typeof n == "number" && t.playbackRate <= n && (t.playbackRate = 1);
1704
1765
  }
1705
1766
  resumeChasing() {
1706
1767
  this.chaseSuspended = !1;
@@ -1723,7 +1784,7 @@ var Nt = class {
1723
1784
  } catch {}
1724
1785
  }
1725
1786
  destroy() {
1726
- if (this.prefetch?.destroy(), this.prefetch = null, this.clearRecoveryReset(), this.capTimer &&= (clearTimeout(this.capTimer), null), this.capObserver &&= (this.capObserver.disconnect(), null), this.unwatchDprChanges(), this.endWatchdog &&= (clearInterval(this.endWatchdog), null), this.onVisibility && typeof document < "u" && (document.removeEventListener("visibilitychange", this.onVisibility), this.onVisibility = null), this.onSeeking &&= (this.deps.video.removeEventListener("seeking", this.onSeeking), null), this.onPause &&= (this.deps.video.removeEventListener("pause", this.onPause), null), this.onPlay &&= (this.deps.video.removeEventListener("play", this.onPlay), null), this.pauseSuspendTimer &&= (clearTimeout(this.pauseSuspendTimer), null), this.networkRetryTimer &&= (clearTimeout(this.networkRetryTimer), null), this.licenseRetryTimer &&= (clearTimeout(this.licenseRetryTimer), null), this.hls) {
1787
+ if (this.prefetch?.destroy(), this.prefetch = null, this.clearRecoveryReset(), this.capTimer &&= (clearTimeout(this.capTimer), null), this.capObserver &&= (this.capObserver.disconnect(), null), this.unwatchDprChanges(), this.endWatchdog &&= (clearInterval(this.endWatchdog), null), this.onVisibility && typeof document < "u" && (document.removeEventListener("visibilitychange", this.onVisibility), this.onVisibility = null), this.onSeeking &&= (this.deps.video.removeEventListener("seeking", this.onSeeking), null), this.onTimeUpdate &&= (this.deps.video.removeEventListener("timeupdate", this.onTimeUpdate), null), this.onPause &&= (this.deps.video.removeEventListener("pause", this.onPause), null), this.onPlay &&= (this.deps.video.removeEventListener("play", this.onPlay), null), this.pauseSuspendTimer &&= (clearTimeout(this.pauseSuspendTimer), null), this.networkRetryTimer &&= (clearTimeout(this.networkRetryTimer), null), this.licenseRetryTimer &&= (clearTimeout(this.licenseRetryTimer), null), this.hls) {
1727
1788
  try {
1728
1789
  this.hls.destroy();
1729
1790
  } catch (e) {
@@ -1811,19 +1872,19 @@ var Nt = class {
1811
1872
  let r = e.response?.code ?? 0, a = e.details ?? e.type, o = e.reason ?? e.error?.message ?? "";
1812
1873
  return new n(t === "DRM" ? i(r) : r === 401 ? "UNAUTHORIZED" : r === 403 ? "FORBIDDEN" : "INTERNAL", r, `${t} error: ${a}${o ? ` (${o})` : ""}`);
1813
1874
  }
1814
- }, Pt = 15e3, Ft = 6e5, It = 3e5, Lt = 100, q = 1, Rt = 1e3, zt = 3e4, Bt = 1e3, Vt = {
1875
+ }, Rt = 15e3, zt = 6e5, Bt = 3e5, Vt = 100, q = 1, Ht = 1e3, Ut = 3e4, Wt = 1e3, Gt = {
1815
1876
  inline: "PRESENTATION_INLINE",
1816
1877
  fullscreen: "PRESENTATION_FULLSCREEN",
1817
1878
  pip: "PRESENTATION_PIP"
1818
- }, Ht = {
1879
+ }, Kt = {
1819
1880
  widevine: "KEY_SYSTEM_WIDEVINE",
1820
1881
  fairplay: "KEY_SYSTEM_FAIRPLAY",
1821
1882
  none: "KEY_SYSTEM_NONE"
1822
1883
  };
1823
- function Ut(e, t) {
1884
+ function qt(e, t) {
1824
1885
  return e.length === 0 ? 0 : e[Math.min(e.length, Math.max(1, Math.ceil(t / 100 * e.length))) - 1] ?? 0;
1825
1886
  }
1826
- var Wt = /* @__PURE__ */ new Set([
1887
+ var Jt = /* @__PURE__ */ new Set([
1827
1888
  "INVALID_URN",
1828
1889
  "NOT_FOUND",
1829
1890
  "GONE",
@@ -1843,7 +1904,7 @@ var Wt = /* @__PURE__ */ new Set([
1843
1904
  ended: "PLAYER_STATE_ENDED",
1844
1905
  error: "PLAYER_STATE_ERROR"
1845
1906
  };
1846
- function Gt(e) {
1907
+ function Yt(e) {
1847
1908
  try {
1848
1909
  let t = new URL(e), [n, ...r] = t.hostname.split(".");
1849
1910
  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);
@@ -1851,8 +1912,8 @@ function Gt(e) {
1851
1912
  return;
1852
1913
  }
1853
1914
  }
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) {
1915
+ var Xt = (e, t) => e.replace(/\/$/, "").toLowerCase() === t.replace(/\/$/, "").toLowerCase(), Zt = /* @__PURE__ */ new Set(), Y = /* @__PURE__ */ new Set(), X = /* @__PURE__ */ new Set(), Qt = (e) => `this page's Content-Security-Policy blocks QoE beacons. Add ${e} to connect-src; until then none are sent.`;
1916
+ function $t(e) {
1856
1917
  if (e.disposition !== "enforce" || e.effectiveDirective !== "connect-src") return;
1857
1918
  let t;
1858
1919
  try {
@@ -1861,19 +1922,19 @@ function Yt(e) {
1861
1922
  } catch {
1862
1923
  return;
1863
1924
  }
1864
- if (qt.has(t) && !Y.has(t)) {
1865
- Y.add(t), g(Jt(new URL(t).origin));
1925
+ if (Zt.has(t) && !Y.has(t)) {
1926
+ Y.add(t), g(Qt(new URL(t).origin));
1866
1927
  for (let e of X) e.noteCspBlocked();
1867
1928
  }
1868
1929
  }
1869
- function Xt(e, t) {
1930
+ function en(e, t) {
1870
1931
  return g(t, e), { disabledReason: t };
1871
1932
  }
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 };
1933
+ function tn(e, t, n) {
1934
+ if (n) return t && !Xt(t, n) && g(`analytics.endpoint "${t}" ignored; the server named "${n}".`), { endpoint: n };
1874
1935
  if (t) return { endpoint: t };
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);
1936
+ if (t !== void 0) return en("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).");
1937
+ let r = Yt(e);
1877
1938
  if (r) return { endpoint: r };
1878
1939
  let i;
1879
1940
  try {
@@ -1881,9 +1942,9 @@ function Zt(e, t, n) {
1881
1942
  } catch {
1882
1943
  i = e;
1883
1944
  }
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).`);
1945
+ return en(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).`);
1885
1946
  }
1886
- var Qt = class {
1947
+ var nn = class {
1887
1948
  opts;
1888
1949
  url;
1889
1950
  cspKey;
@@ -1965,8 +2026,8 @@ var Qt = class {
1965
2026
  constructor(e) {
1966
2027
  this.opts = {
1967
2028
  ...e,
1968
- heartbeatMs: e.heartbeatMs ?? Pt,
1969
- idleMs: e.idleMs ?? Ft
2029
+ heartbeatMs: e.heartbeatMs ?? Rt,
2030
+ idleMs: e.idleMs ?? zt
1970
2031
  }, this.url = `${e.endpoint.replace(/\/$/, "")}/v1/beacons`;
1971
2032
  try {
1972
2033
  let e = new URL(this.url);
@@ -1983,11 +2044,11 @@ var Qt = class {
1983
2044
  sdkVersion: this.opts.sdkVersion,
1984
2045
  ...this.opts.environment
1985
2046
  };
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());
2047
+ this.pendingStart = e, this.enqueue({ sessionStart: e }), this.firstFlushTimer = setTimeout(() => this.flush(), Ht), 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 && Zt.add(this.cspKey), document.addEventListener("securitypolicyviolation", $t), X.add(this), this.noteCspBlocked(), this.onVisibility());
1987
2048
  }
1988
2049
  checkSuspension(e) {
1989
2050
  if (this.endedFlag || !this.opts.idleMs) return !1;
1990
- let t = Math.max(this.opts.idleMs, It);
2051
+ let t = Math.max(this.opts.idleMs, Bt);
1991
2052
  return this.lastTickMs > 0 && e - this.lastTickMs >= t ? (this.endIdle(this.lastTickMs), !0) : (this.lastTickMs = e, !1);
1992
2053
  }
1993
2054
  syncIdle(e) {
@@ -2010,12 +2071,12 @@ var Qt = class {
2010
2071
  let a = {
2011
2072
  fromState: J[e] ?? "PLAYER_STATE_UNSPECIFIED",
2012
2073
  toState: J[t] ?? "PLAYER_STATE_UNSPECIFIED",
2013
- presentation: Vt[this.presentation]
2074
+ presentation: Gt[this.presentation]
2014
2075
  }, o = this.causeOf(e, t, i);
2015
2076
  o && (a.cause = o), n > 0 && (a.positionMs = Math.round(n)), r && (a.renditionHeight = r), this.enqueue({ stateChange: a });
2016
2077
  }
2017
2078
  seekInduced(e) {
2018
- return this.lastSeekMs >= 0 && e - this.lastSeekMs < Bt;
2079
+ return this.lastSeekMs >= 0 && e - this.lastSeekMs < Wt;
2019
2080
  }
2020
2081
  causeOf(e, t, n) {
2021
2082
  if (t === "error") return "TRANSITION_CAUSE_ERROR";
@@ -2089,7 +2150,7 @@ var Qt = class {
2089
2150
  let r = n ?? "";
2090
2151
  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";
2091
2152
  if (e === "DRM_CLIENT") return "ERROR_STAGE_DRM";
2092
- if (Wt.has(e)) return "ERROR_STAGE_RESOLVE";
2153
+ if (Jt.has(e)) return "ERROR_STAGE_RESOLVE";
2093
2154
  let i = t ?? "";
2094
2155
  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";
2095
2156
  }
@@ -2136,14 +2197,14 @@ var Qt = class {
2136
2197
  if (this.lastRequestCount = g, _ > 0) {
2137
2198
  o.requestCountDelta = _;
2138
2199
  let e = t.requestTimesMs ?? [], n = e.slice(Math.max(0, e.length - _)).sort((e, t) => e - t);
2139
- n.length > 0 && (o.requestTimeP50Ms = Ut(n, 50), o.requestTimeP95Ms = Ut(n, 95));
2200
+ n.length > 0 && (o.requestTimeP50Ms = qt(n, 50), o.requestTimeP95Ms = qt(n, 95));
2140
2201
  }
2141
2202
  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();
2142
2203
  }
2143
2204
  maybeEmitMediaInfo(e) {
2144
2205
  if (!(e.videoCodec ?? e.keySystem ?? e.ladderHeights)) return;
2145
2206
  let t = {};
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);
2207
+ e.videoCodec && (t.videoCodec = e.videoCodec), e.audioCodec && (t.audioCodec = e.audioCodec), e.keySystem && (t.keySystem = Kt[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);
2147
2208
  let n = JSON.stringify(t);
2148
2209
  n !== this.mediaInfoSignature && (this.mediaInfoSignature = n, this.enqueue({ mediaInfo: t }));
2149
2210
  }
@@ -2163,7 +2224,7 @@ var Qt = class {
2163
2224
  let c = r?.positionMs ?? 0;
2164
2225
  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));
2165
2226
  let l = e !== "END_REASON_ENDED";
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);
2227
+ l && this.lastStallMs > 0 && n - this.lastStallMs <= Ut && (i.abandonAfterStallMs = Math.max(q, Math.round(n - this.lastStallMs))), l && this.lastErrorMs > 0 && n - this.lastErrorMs <= Ut && (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);
2167
2228
  let u = r?.decodedFrames ?? this.lastDecoded;
2168
2229
  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");
2169
2230
  }
@@ -2183,13 +2244,13 @@ var Qt = class {
2183
2244
  };
2184
2245
  }
2185
2246
  noteCspBlocked() {
2186
- this.cspBlockedNoted || this.endedFlag || !this.cspBlocked() || (this.cspBlockedNoted = !0, this.opts.onBlocked?.(Jt(new URL(this.url).origin)));
2247
+ this.cspBlockedNoted || this.endedFlag || !this.cspBlocked() || (this.cspBlockedNoted = !0, this.opts.onBlocked?.(Qt(new URL(this.url).origin)));
2187
2248
  }
2188
2249
  cspBlocked() {
2189
2250
  return !!this.cspKey && Y.has(this.cspKey);
2190
2251
  }
2191
2252
  enqueue(e) {
2192
- if (!this.cspBlocked()) for (this.pending.push(this.wrap(e)); this.pending.length > Lt;) {
2253
+ if (!this.cspBlocked()) for (this.pending.push(this.wrap(e)); this.pending.length > Vt;) {
2193
2254
  let e = this.pending.findIndex((e) => e.heartbeat || e.stateChange);
2194
2255
  this.pending.splice(e >= 0 ? e : 0, 1);
2195
2256
  }
@@ -2216,7 +2277,7 @@ var Qt = class {
2216
2277
  };
2217
2278
  //#endregion
2218
2279
  //#region src/core/environment.ts
2219
- function $t() {
2280
+ function rn() {
2220
2281
  if (typeof window > "u") return { integration: "INTEGRATION_KIND_SDK" };
2221
2282
  let e;
2222
2283
  try {
@@ -2241,14 +2302,14 @@ function $t() {
2241
2302
  integration: "INTEGRATION_KIND_IFRAME"
2242
2303
  };
2243
2304
  }
2244
- function en() {
2305
+ function an() {
2245
2306
  if (typeof navigator > "u") return "DEVICE_CLASS_OTHER";
2246
2307
  let e = navigator.userAgentData, t = e?.platform ?? "";
2247
2308
  if (/tv|tizen|webos/i.test(t)) return "DEVICE_CLASS_TV";
2248
2309
  let n = typeof screen < "u" ? Math.min(screen.width, screen.height) : 0;
2249
2310
  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";
2250
2311
  }
2251
- function tn() {
2312
+ function on() {
2252
2313
  let e = typeof navigator < "u" ? navigator.connection : void 0;
2253
2314
  if (!e) return { connectionType: "CONNECTION_TYPE_UNKNOWN" };
2254
2315
  let t = {
@@ -2267,17 +2328,17 @@ function tn() {
2267
2328
  }
2268
2329
  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;
2269
2330
  }
2270
- function nn(e) {
2271
- let t = $t(), n = {
2272
- deviceClass: en(),
2331
+ function sn(e) {
2332
+ let t = rn(), n = {
2333
+ deviceClass: an(),
2273
2334
  integration: t.integration,
2274
- ...tn()
2335
+ ...on()
2275
2336
  };
2276
2337
  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);
2277
2338
  let r = e.getBoundingClientRect?.();
2278
2339
  return r && r.width > 0 && (n.viewportWidth = Math.round(r.width), n.viewportHeight = Math.round(r.height)), n;
2279
2340
  }
2280
- async function rn() {
2341
+ async function cn() {
2281
2342
  let e = typeof navigator < "u" ? navigator.userAgentData : void 0;
2282
2343
  if (!e?.getHighEntropyValues) return {};
2283
2344
  try {
@@ -2289,7 +2350,7 @@ async function rn() {
2289
2350
  }
2290
2351
  //#endregion
2291
2352
  //#region src/core/version.ts
2292
- var an = "2.15.2", on = {
2353
+ var ln = "2.15.4", un = {
2293
2354
  accent: "#E1B600",
2294
2355
  accentText: "#1B1C23",
2295
2356
  background: "#0e0f13",
@@ -2300,7 +2361,7 @@ var an = "2.15.2", on = {
2300
2361
  border: "rgba(255, 255, 255, 0.1)",
2301
2362
  radius: "8px",
2302
2363
  fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
2303
- }, sn = {
2364
+ }, dn = {
2304
2365
  accent: "--havik-accent",
2305
2366
  accentText: "--havik-accent-text",
2306
2367
  background: "--havik-bg",
@@ -2312,26 +2373,26 @@ var an = "2.15.2", on = {
2312
2373
  radius: "--havik-radius",
2313
2374
  fontFamily: "--havik-font"
2314
2375
  };
2315
- function cn(e, t) {
2376
+ function fn(e, t) {
2316
2377
  let n = {
2317
- ...on,
2378
+ ...un,
2318
2379
  ...t
2319
2380
  };
2320
- for (let t of Object.keys(sn)) e.style.setProperty(sn[t], n[t]);
2381
+ for (let t of Object.keys(dn)) e.style.setProperty(dn[t], n[t]);
2321
2382
  n.logoUrl && e.style.setProperty("--havik-logo", `url("${n.logoUrl}")`);
2322
2383
  }
2323
2384
  //#endregion
2324
2385
  //#region src/ui/styles.ts
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;
2386
+ var pn = "havik-player-styles", mn = "\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", Z = /* @__PURE__ */ new WeakMap();
2387
+ function hn(e = document, t) {
2388
+ let n = e.querySelector(`style#${pn}`);
2389
+ if (n && (!t || Z.get(n) === t)) return;
2329
2390
  let r = e.createElement("style");
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);
2391
+ r.id = pn, t && (r.nonce = t, r.setAttribute("nonce", t)), r.textContent = mn, Z.set(r, t), n ? n.replaceWith(r) : e.head.appendChild(r);
2331
2392
  }
2332
2393
  //#endregion
2333
2394
  //#region src/ui/icons.ts
2334
- var pn = {
2395
+ var gn = {
2335
2396
  play: "<path d=\"M8 5v14l11-7z\"/>",
2336
2397
  pause: "<path d=\"M6 5h4v14H6zM14 5h4v14h-4z\"/>",
2337
2398
  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\"/>",
@@ -2344,21 +2405,21 @@ var pn = {
2344
2405
  liveDot: "<circle cx=\"12\" cy=\"12\" r=\"6\"/>",
2345
2406
  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\"/>"
2346
2407
  };
2347
- function Z(e) {
2348
- return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${pn[e]}</svg>`;
2408
+ function Q(e) {
2409
+ return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${gn[e]}</svg>`;
2349
2410
  }
2350
2411
  //#endregion
2351
2412
  //#region src/ui/controls.ts
2352
- var mn = 3e3, hn = 250, gn = /* @__PURE__ */ new Set([
2413
+ var _n = 3e3, vn = 250, yn = /* @__PURE__ */ new Set([
2353
2414
  "GONE",
2354
2415
  "NOT_FOUND",
2355
2416
  "INVALID_URN",
2356
2417
  "UNAUTHORIZED"
2357
2418
  ]);
2358
- function _n(e) {
2359
- return new vn(e);
2419
+ function bn(e) {
2420
+ return new xn(e);
2360
2421
  }
2361
- var vn = class {
2422
+ var xn = class {
2362
2423
  root;
2363
2424
  video;
2364
2425
  player;
@@ -2404,7 +2465,7 @@ var vn = class {
2404
2465
  scrubbing = !1;
2405
2466
  offFns = [];
2406
2467
  constructor(e) {
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");
2468
+ this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? _n, 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, hn(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");
2408
2469
  let t = (e, t, n) => {
2409
2470
  let r = this.doc.createElement(e);
2410
2471
  if (r.className = t, n) for (let [e, t] of Object.entries(n)) r.setAttribute(e, t);
@@ -2415,12 +2476,12 @@ var vn = class {
2415
2476
  "aria-label": r,
2416
2477
  title: r
2417
2478
  });
2418
- return i.innerHTML = Z(n), i;
2479
+ return i.innerHTML = Q(n), i;
2419
2480
  };
2420
2481
  this.overlay = t("div", "havik-overlay"), this.center = t("button", "havik-center", {
2421
2482
  type: "button",
2422
2483
  "aria-label": "Play"
2423
- }), this.center.innerHTML = Z("play"), this.spinner = t("div", "havik-spinner"), this.spinner.style.display = "none", this.overlay.append(this.center, this.spinner), this.gradient = t("div", "havik-gradient"), this.bar = t("div", "havik-bar"), this.seek = t("div", "havik-seek", {
2484
+ }), this.center.innerHTML = Q("play"), this.spinner = t("div", "havik-spinner"), this.spinner.style.display = "none", this.overlay.append(this.center, this.spinner), this.gradient = t("div", "havik-gradient"), this.bar = t("div", "havik-bar"), this.seek = t("div", "havik-seek", {
2424
2485
  role: "slider",
2425
2486
  "aria-label": "Seek",
2426
2487
  "aria-valuemin": "0",
@@ -2438,7 +2499,7 @@ var vn = class {
2438
2499
  "aria-label": "Volume"
2439
2500
  });
2440
2501
  let a = t("div", "havik-vol");
2441
- a.append(this.muteBtn, this.volSlider), this.live = t("div", "havik-live"), this.live.innerHTML = Z("liveDot");
2502
+ a.append(this.muteBtn, this.volSlider), this.live = t("div", "havik-live"), this.live.innerHTML = Q("liveDot");
2442
2503
  let o = this.doc.createElement("span");
2443
2504
  o.textContent = "LIVE", this.liveGo = t("button", "havik-live__go", { type: "button" }), this.liveGo.textContent = "Go live", this.live.append(o, this.liveGo), this.time = t("div", "havik-time"), this.time.textContent = "0:00", this.time.style.display = "none";
2444
2505
  let s = t("div", "havik-spacer");
@@ -2490,7 +2551,7 @@ var vn = class {
2490
2551
  let t = this.surfaceWasHidden;
2491
2552
  this.tapTimer = setTimeout(() => {
2492
2553
  this.tapTimer = void 0, this.toggleBar(t);
2493
- }, hn);
2554
+ }, vn);
2494
2555
  }
2495
2556
  toggleBar(e) {
2496
2557
  e ? this.poke() : this.hideIfIdle();
@@ -2522,12 +2583,12 @@ var vn = class {
2522
2583
  }
2523
2584
  if (e === "preplay") {
2524
2585
  let e = this.doc.createElement("button");
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);
2586
+ e.type = "button", e.className = "havik-status__btn havik-status__btn--play", e.setAttribute("aria-label", "Play"), e.innerHTML = Q("play"), e.addEventListener("click", () => void this.player.play()), n.append(e);
2526
2587
  } else {
2527
2588
  let r = this.doc.createElement("div");
2528
- if (r.className = "havik-status__msg", r.textContent = t ?? "", n.append(r), e === "error" && this.lastError && !gn.has(this.lastError.code)) {
2589
+ if (r.className = "havik-status__msg", r.textContent = t ?? "", n.append(r), e === "error" && this.lastError && !yn.has(this.lastError.code)) {
2529
2590
  let e = this.doc.createElement("button");
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);
2591
+ e.type = "button", e.className = "havik-status__btn", e.innerHTML = `${Q("retry")}<span>Retry</span>`, e.addEventListener("click", () => void this.player.retry()), n.append(e);
2531
2592
  }
2532
2593
  }
2533
2594
  this.root.append(n), this.statusBox = n, this.show();
@@ -2566,7 +2627,7 @@ var vn = class {
2566
2627
  let n = r.end(r.length - 1), i = Math.min(1, Math.max(0, (n - e.start) / t));
2567
2628
  this.seekBuffered.style.width = `${(i * 100).toFixed(2)}%`;
2568
2629
  }
2569
- !this.player.getStats().isLive && Number.isFinite(this.video.duration) && (this.time.textContent = `${yn(this.video.currentTime)} / ${yn(this.video.duration)}`);
2630
+ !this.player.getStats().isLive && Number.isFinite(this.video.duration) && (this.time.textContent = `${Sn(this.video.currentTime)} / ${Sn(this.video.duration)}`);
2570
2631
  }
2571
2632
  syncLive() {
2572
2633
  let e = this.player.getStats(), t = e.isLive !== !1;
@@ -2616,17 +2677,17 @@ var vn = class {
2616
2677
  renderQualityMenu() {
2617
2678
  let e = this.player.getQualityLevels(), t = this.player.getCurrentQuality(), n = this.doc.createElement("div");
2618
2679
  n.className = "havik-menu__title", n.textContent = "Quality", this.menu.replaceChildren(n, this.menuItem("Auto", t === -1, () => this.player.setQuality("auto")));
2619
- for (let n of e) this.menu.append(this.menuItem(bn(n), t === n.index, () => this.player.setQuality(n.index)));
2680
+ for (let n of e) this.menu.append(this.menuItem(Cn(n), t === n.index, () => this.player.setQuality(n.index)));
2620
2681
  }
2621
2682
  renderAudioMenu() {
2622
2683
  let e = this.player.getAudioTracks(), t = this.doc.createElement("div");
2623
2684
  t.className = "havik-menu__title", t.textContent = "Audio", this.menu.replaceChildren(t);
2624
- for (let t of e) this.menu.append(this.menuItem(xn(t), t.default, () => this.player.setAudioTrack(t.id)));
2685
+ for (let t of e) this.menu.append(this.menuItem(wn(t), t.default, () => this.player.setAudioTrack(t.id)));
2625
2686
  }
2626
2687
  renderCcMenu() {
2627
2688
  let e = this.player.getTextTracks(), t = this.doc.createElement("div");
2628
2689
  t.className = "havik-menu__title", t.textContent = "Subtitles", this.menu.replaceChildren(t, this.menuItem("Off", !1, () => this.player.setTextTrack(-1)));
2629
- for (let t of e) this.menu.append(this.menuItem(Sn(t), !1, () => this.player.setTextTrack(t.id)));
2690
+ for (let t of e) this.menu.append(this.menuItem(Tn(t), !1, () => this.player.setTextTrack(t.id)));
2630
2691
  }
2631
2692
  onTracksChanged() {
2632
2693
  this.openMenu && this.renderMenu(this.openMenu);
@@ -2710,7 +2771,7 @@ var vn = class {
2710
2771
  }
2711
2772
  }
2712
2773
  setIcon(e, t) {
2713
- e.innerHTML = Z(t);
2774
+ e.innerHTML = Q(t);
2714
2775
  }
2715
2776
  destroy() {
2716
2777
  this.hideTimer && clearTimeout(this.hideTimer), this.tapTimer && clearTimeout(this.tapTimer);
@@ -2727,41 +2788,41 @@ var vn = class {
2727
2788
  this.root.classList.remove("havik-player", "havik-hide", "havik-has-status");
2728
2789
  }
2729
2790
  };
2730
- function yn(e) {
2791
+ function Sn(e) {
2731
2792
  (!Number.isFinite(e) || e < 0) && (e = 0);
2732
2793
  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");
2733
2794
  return r > 0 ? `${r}:${i}:${a}` : `${i}:${a}`;
2734
2795
  }
2735
- function bn(e) {
2796
+ function Cn(e) {
2736
2797
  return e.height ? `${e.height}p` : `${Math.round(e.bitrate / 1e3)} kbps`;
2737
2798
  }
2738
- function xn(e) {
2799
+ function wn(e) {
2739
2800
  return e.name || e.lang || `Track ${e.id}`;
2740
2801
  }
2741
- function Sn(e) {
2802
+ function Tn(e) {
2742
2803
  return e.name || e.lang || `Track ${e.id}`;
2743
2804
  }
2744
2805
  //#endregion
2745
2806
  //#region src/managed/index.ts
2746
- var Cn = 6e3, Q = 45e3, wn = 2e3, Tn = 3, En = 2e3, Dn = 48e4;
2747
- function On(e) {
2807
+ var En = 6e3, Dn = 45e3, On = 2e3, kn = 3, An = 2e3, jn = 48e4;
2808
+ function Mn(e) {
2748
2809
  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)})`);
2749
2810
  if (e.env !== void 0) return B(e.env);
2750
2811
  if (!e.baseUrl) throw new n("INTERNAL", 0, "createPlayer: env or baseUrl is required (env: 'integration' | 'production')");
2751
2812
  return e.baseUrl;
2752
2813
  }
2753
- async function kn(e) {
2814
+ async function Nn(e) {
2754
2815
  if (!e.video || typeof e.video.play != "function") throw new n("INTERNAL", 0, "createPlayer: opts.video must be an HTMLVideoElement");
2755
- let t = On(e);
2816
+ let t = Mn(e);
2756
2817
  if (!e.matchUrn) throw new n("INVALID_URN", 0, "createPlayer: matchUrn is required");
2757
2818
  if (!e.credential) throw new n("INTERNAL", 0, "createPlayer: credential is required");
2758
- let r = new An({
2819
+ let r = new Pn({
2759
2820
  ...e,
2760
2821
  baseUrl: t
2761
2822
  });
2762
2823
  return await r.start(), r;
2763
2824
  }
2764
- var An = class {
2825
+ var Pn = class {
2765
2826
  opts;
2766
2827
  abort = new AbortController();
2767
2828
  listeners = {};
@@ -2849,7 +2910,7 @@ var An = class {
2849
2910
  setTimeout(() => {
2850
2911
  a || this.destroyed || e !== this.loadGen || this.emit("waiting", {
2851
2912
  phase: i ? "unavailable" : "tooEarly",
2852
- retryInMs: Q,
2913
+ retryInMs: Dn,
2853
2914
  attempt: 1,
2854
2915
  elapsedMs: 0,
2855
2916
  ...!i && f !== void 0 && f > Date.now() ? { ttkMs: f - Date.now() } : {}
@@ -2888,13 +2949,13 @@ var An = class {
2888
2949
  !a && !p() && h();
2889
2950
  }, e);
2890
2951
  };
2891
- _(Cn), this.armingSub = T({
2952
+ _(En), this.armingSub = T({
2892
2953
  eventsBaseUrl: t,
2893
2954
  matchUrn: this.opts.matchUrn,
2894
2955
  credential: this.opts.credential,
2895
- onAlive: () => _(Q),
2956
+ onAlive: () => _(Dn),
2896
2957
  onState: (e) => {
2897
- a || p() || (e.state === "live" || e.state === "gone") && setTimeout(g, Math.random() * wn);
2958
+ a || p() || (e.state === "live" || e.state === "gone") && setTimeout(g, Math.random() * On);
2898
2959
  },
2899
2960
  onError: () => h()
2900
2961
  });
@@ -2939,19 +3000,19 @@ var An = class {
2939
3000
  if (this.analyticsSession?.noteDestroyed(), this.analyticsSession = null, this.detachMediaListeners?.(), this.opts.analytics === !1) return;
2940
3001
  let i = e.analytics?.sid;
2941
3002
  if (!i) return;
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);
3003
+ let a = typeof this.opts.analytics == "object" ? this.opts.analytics.endpoint : void 0, { endpoint: o, disabledReason: s } = tn(this.opts.baseUrl, a, e.analytics?.endpoint);
2943
3004
  if (!o) {
2944
3005
  s && setTimeout(() => {
2945
3006
  this.destroyed || r !== this.loadGen || this.emit("warning", new n("INTERNAL", 0, s));
2946
3007
  }, 0);
2947
3008
  return;
2948
3009
  }
2949
- let c = this.opts.video, l = new Qt({
3010
+ let c = this.opts.video, l = new nn({
2950
3011
  endpoint: o,
2951
3012
  sid: i,
2952
3013
  player: t.name === "native" ? "PLAYER_SAFARI_NATIVE" : "PLAYER_HLSJS",
2953
- sdkVersion: an,
2954
- environment: nn(c),
3014
+ sdkVersion: ln,
3015
+ environment: sn(c),
2955
3016
  idleMs: typeof this.opts.analytics == "object" ? this.opts.analytics.idleMs : void 0,
2956
3017
  onIdle: () => this.onAnalyticsIdle(t),
2957
3018
  onBlocked: (e) => setTimeout(() => {
@@ -2972,7 +3033,7 @@ var An = class {
2972
3033
  let u = () => l.noteSeek(), d = () => l.noteVolume(c.muted, c.volume);
2973
3034
  c.addEventListener("seeking", u), c.addEventListener("volumechange", d), this.detachMediaListeners = () => {
2974
3035
  c.removeEventListener("seeking", u), c.removeEventListener("volumechange", d);
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");
3036
+ }, l.start(), l.seedVolume(c.muted, c.volume), l.notePresentation(this.currentPresentation()), cn().then((e) => l.noteEnvironment(e)), this.opts.autoplay === !1 && l.noteAutoplay("AUTOPLAY_RESULT_NOT_ATTEMPTED");
2976
3037
  }
2977
3038
  currentPresentation() {
2978
3039
  let e = this.opts.video;
@@ -2998,9 +3059,9 @@ var An = class {
2998
3059
  emit: (e) => this.onEngineEvent(e)
2999
3060
  }, o = this.opts.engine ?? "auto";
3000
3061
  if (o === "native") return new K(a);
3001
- if (o === "hls") return new Nt(a);
3062
+ if (o === "hls") return new Lt(a);
3002
3063
  if (e.drmEnabled && e.drm?.fairplay?.licenseUrl && e.drm.fairplay.certificateUrl && W(this.opts.video) && G()) return new K(a);
3003
- if (at()) return new Nt(a);
3064
+ if (at()) return new Lt(a);
3004
3065
  if (W(this.opts.video)) return new K(a);
3005
3066
  throw new n("INTERNAL", 0, "no supported HLS playback engine in this browser");
3006
3067
  }
@@ -3053,7 +3114,7 @@ var An = class {
3053
3114
  r = 0, i = !1;
3054
3115
  },
3055
3116
  onError: () => {
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.`))));
3117
+ this.destroyed || e !== this.loadGen || i || this.endedHandled || (r += 1, r >= kn && (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.`))));
3057
3118
  }
3058
3119
  });
3059
3120
  }
@@ -3075,7 +3136,7 @@ var An = class {
3075
3136
  scheduleRejoin(e) {
3076
3137
  this.rejoinEnabled() && !this.rejoinTimer && (this.rejoinTimer = setTimeout(() => {
3077
3138
  this.rejoinTimer = null, !(this.destroyed || e !== this.loadGen || this._state !== "ended") && this.rejoin();
3078
- }, Math.random() * wn));
3139
+ }, Math.random() * On));
3079
3140
  }
3080
3141
  cancelRejoin() {
3081
3142
  this.rejoinTimer &&= (clearTimeout(this.rejoinTimer), null);
@@ -3099,7 +3160,7 @@ var An = class {
3099
3160
  }
3100
3161
  t.controls = !1;
3101
3162
  let n = (t.ownerDocument ?? document).createElement("div"), r = t.parentNode;
3102
- r && r.insertBefore(n, t), n.appendChild(t), cn(n, this.opts.theme), this.container = n, this.controlBar = _n({
3163
+ r && r.insertBefore(n, t), n.appendChild(t), fn(n, this.opts.theme), this.container = n, this.controlBar = bn({
3103
3164
  root: n,
3104
3165
  video: t,
3105
3166
  player: this,
@@ -3130,7 +3191,7 @@ var An = class {
3130
3191
  }
3131
3192
  startStats() {
3132
3193
  this.stopStats();
3133
- let e = Math.max(500, this.opts.statsIntervalMs ?? En);
3194
+ let e = Math.max(500, this.opts.statsIntervalMs ?? An);
3134
3195
  this.statsTimer = setInterval(() => {
3135
3196
  if (!this.engine) return;
3136
3197
  let e = this.engine.getStats();
@@ -3142,7 +3203,7 @@ var An = class {
3142
3203
  }
3143
3204
  scheduleLicenseRefresh() {
3144
3205
  this.stopRefresh();
3145
- let e = this.opts.licenseRefreshMs ?? Dn;
3206
+ let e = this.opts.licenseRefreshMs ?? jn;
3146
3207
  e && this._descriptor?.drmEnabled && (this.refreshTimer = setInterval(() => void this.refreshLicense(), e));
3147
3208
  }
3148
3209
  stopRefresh() {
@@ -3297,7 +3358,7 @@ function $(e) {
3297
3358
  }
3298
3359
  //#endregion
3299
3360
  //#region src/embed/host.ts
3300
- function jn(e) {
3361
+ function Fn(e) {
3301
3362
  if (!e.container || typeof e.container.appendChild != "function") throw Error("mountEmbed: opts.container must be a DOM element");
3302
3363
  if (!e.src) throw Error("mountEmbed: opts.src is required");
3303
3364
  if (!e.baseUrl || !e.matchUrn) throw Error("mountEmbed: opts.baseUrl and opts.matchUrn are required");
@@ -3356,15 +3417,15 @@ function jn(e) {
3356
3417
  }
3357
3418
  //#endregion
3358
3419
  //#region src/embed/page.ts
3359
- var Mn = /^[A-Za-z0-9:_.-]{1,128}$/;
3360
- function Nn(e) {
3420
+ var In = /^[A-Za-z0-9:_.-]{1,128}$/;
3421
+ function Ln(e) {
3361
3422
  if (typeof e == "boolean") return e;
3362
3423
  if (typeof e == "object" && e) {
3363
3424
  let t = e.endpoint;
3364
3425
  return typeof t == "string" ? { endpoint: t } : {};
3365
3426
  }
3366
3427
  }
3367
- function Pn() {
3428
+ function Rn() {
3368
3429
  let e = window.HAVIK_EMBED_CONFIG;
3369
3430
  if (e) return {
3370
3431
  baseUrl: e.baseUrl ?? "",
@@ -3374,7 +3435,7 @@ function Pn() {
3374
3435
  autoplay: e.autoplay ?? !0,
3375
3436
  muted: e.muted ?? !0,
3376
3437
  waitForLive: e.waitForLive ?? !0,
3377
- analytics: Nn(e.analytics)
3438
+ analytics: Ln(e.analytics)
3378
3439
  };
3379
3440
  let t = new URLSearchParams(location.search);
3380
3441
  return {
@@ -3387,9 +3448,9 @@ function Pn() {
3387
3448
  waitForLive: !0
3388
3449
  };
3389
3450
  }
3390
- async function Fn() {
3391
- let e = Pn();
3392
- Ln();
3451
+ async function zn() {
3452
+ let e = Rn();
3453
+ Vn();
3393
3454
  let t = document.createElement("video");
3394
3455
  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);
3395
3456
  let r = e.parentOrigin && e.parentOrigin !== "null" ? e.parentOrigin : null;
@@ -3444,12 +3505,12 @@ async function Fn() {
3444
3505
  code: e.code,
3445
3506
  message: e.message
3446
3507
  })), e.on("autoplayblocked", () => {
3447
- i({ type: "oddin:autoplayblocked" }), In(t, e);
3508
+ i({ type: "oddin:autoplayblocked" }), Bn(t, e);
3448
3509
  });
3449
3510
  }, s = async (r) => {
3450
3511
  a?.destroy(), a = null;
3451
3512
  try {
3452
- let n = await kn({
3513
+ let n = await Nn({
3453
3514
  video: t,
3454
3515
  baseUrl: e.baseUrl,
3455
3516
  matchUrn: r,
@@ -3511,7 +3572,7 @@ async function Fn() {
3511
3572
  a?.retry();
3512
3573
  break;
3513
3574
  case "oddin:load":
3514
- typeof e.matchUrn == "string" && Mn.test(e.matchUrn) && s(e.matchUrn);
3575
+ typeof e.matchUrn == "string" && In.test(e.matchUrn) && s(e.matchUrn);
3515
3576
  break;
3516
3577
  case "oddin:destroy": c();
3517
3578
  }
@@ -3523,18 +3584,18 @@ async function Fn() {
3523
3584
  protocol: 1
3524
3585
  });
3525
3586
  }
3526
- function In(e, t) {
3587
+ function Bn(e, t) {
3527
3588
  if (document.getElementById("havik-tap")) return;
3528
3589
  let n = document.createElement("button");
3529
3590
  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", () => {
3530
3591
  n.remove(), t.play();
3531
3592
  }), document.body.appendChild(n);
3532
3593
  }
3533
- function Ln() {
3594
+ function Vn() {
3534
3595
  let e = document.createElement("style");
3535
3596
  e.textContent = "html,body{margin:0;height:100%;background:#000;overflow:hidden}body{display:flex}", document.head.appendChild(e);
3536
3597
  }
3537
3598
  //#endregion
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 };
3599
+ export { un as ODDIN_THEME, n as PlaybackError, fn as applyTheme, B as baseUrlForEnv, zn as bootEmbed, Te as clearDeviceId, Nn 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, Fn as mountEmbed, de as pollToLive, y as resolveStream, T as subscribeLiveState, S as videoFullscreenActive, Ce as watchStatus };
3539
3600
 
3540
3601
  //# sourceMappingURL=index.js.map