@oddin-gg/havik-player 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -126,6 +126,17 @@ export declare interface CreatePlayerOptions {
126
126
  endedMessage?: string;
127
127
  /** Message on the fatal-error card. Default: the error's own message. */
128
128
  errorMessage?: string;
129
+ /**
130
+ * Turn on hls.js's internal console logging — segment/part scheduling, ABR
131
+ * switches, and EME key-session + license traffic. Default: false.
132
+ *
133
+ * For diagnosing a playback problem with Oddin support; leave it off in
134
+ * production (it logs on every fragment, and the EME lines include license
135
+ * request/response metadata). hls.js engine only — a no-op on the native
136
+ * engine, which has no equivalent. Equivalent to `hlsConfig: { debug: true }`,
137
+ * which still wins if you set both.
138
+ */
139
+ debug?: boolean;
129
140
  /** Escape hatch for low-level hls.js tuning. */
130
141
  hlsConfig?: Partial<HlsConfig>;
131
142
  /** Stats sampling cadence. Default: 2000ms. */
package/dist/index.d.ts CHANGED
@@ -126,6 +126,17 @@ export declare interface CreatePlayerOptions {
126
126
  endedMessage?: string;
127
127
  /** Message on the fatal-error card. Default: the error's own message. */
128
128
  errorMessage?: string;
129
+ /**
130
+ * Turn on hls.js's internal console logging — segment/part scheduling, ABR
131
+ * switches, and EME key-session + license traffic. Default: false.
132
+ *
133
+ * For diagnosing a playback problem with Oddin support; leave it off in
134
+ * production (it logs on every fragment, and the EME lines include license
135
+ * request/response metadata). hls.js engine only — a no-op on the native
136
+ * engine, which has no equivalent. Equivalent to `hlsConfig: { debug: true }`,
137
+ * which still wins if you set both.
138
+ */
139
+ debug?: boolean;
129
140
  /** Escape hatch for low-level hls.js tuning. */
130
141
  hlsConfig?: Partial<HlsConfig>;
131
142
  /** Stats sampling cadence. Default: 2000ms. */
package/dist/index.js CHANGED
@@ -510,21 +510,21 @@ function I(e, t) {
510
510
  function L() {
511
511
  return e.isSupported();
512
512
  }
513
- var R = 3, z = 5, B = 3, V = 3e4, H = 12e3, U = 3e3, de = 6e3, fe = 3, pe = 9;
514
- function me(e) {
513
+ var R = 3, z = 5, B = 3, V = 3e4, H = 12e3, U = 3e3, de = 6e3, fe = 3, pe = 9, me = .5;
514
+ function he(e) {
515
515
  if (e.liveSyncDurationCount != null || e.liveMaxLatencyDurationCount != null) {
516
516
  delete e.liveSyncDuration, delete e.liveMaxLatencyDuration;
517
517
  return;
518
518
  }
519
519
  e.liveSyncDuration ??= fe;
520
520
  let t = Math.max(12, e.liveSyncDuration + pe);
521
- (e.liveMaxLatencyDuration == null || e.liveMaxLatencyDuration <= e.liveSyncDuration) && (e.liveMaxLatencyDuration = t), he(e, e.liveSyncDuration);
521
+ (e.liveMaxLatencyDuration == null || e.liveMaxLatencyDuration <= e.liveSyncDuration) && (e.liveMaxLatencyDuration = t), ge(e, e.liveSyncDuration);
522
522
  }
523
- function he(e, t) {
523
+ function ge(e, t) {
524
524
  let n = (e, t, n) => Math.min(n, Math.max(t, e));
525
525
  e.maxStarvationDelay ??= n(t / 2, .5, 4), e.maxLoadingDelay ??= n(t, 1, 4), e.abrEwmaFastLive ??= n(t / 2, 1, 3);
526
526
  }
527
- var ge = class {
527
+ var _e = class {
528
528
  name = "hls";
529
529
  hls = null;
530
530
  deps;
@@ -537,6 +537,7 @@ var ge = class {
537
537
  networkRetryTimer = null;
538
538
  licenseRetryTimer = null;
539
539
  onVisibility = null;
540
+ onSeeking = null;
540
541
  sawLive = !1;
541
542
  lastEndSN = -1;
542
543
  lastAdvanceMs = 0;
@@ -565,10 +566,19 @@ var ge = class {
565
566
  if (typeof t != "number") return;
566
567
  let n = typeof e.targetLatency == "number" && e.targetLatency > 0 ? e.targetLatency : 3;
567
568
  t - r.currentTime > Math.max(n * 3, 8) && (r.currentTime = t);
568
- }, document.addEventListener("visibilitychange", this.onVisibility));
569
+ }, document.addEventListener("visibilitychange", this.onVisibility)), this.onSeeking = () => this.clampOverSeek(), r.addEventListener("seeking", this.onSeeking);
570
+ }
571
+ clampOverSeek() {
572
+ let e = this.hls;
573
+ if (!e || this.chaseSuspended || (e.levels[e.currentLevel] ?? e.levels[e.loadLevel])?.details?.live !== !0) return;
574
+ let t = e.liveSyncPosition;
575
+ if (typeof t != "number" || !Number.isFinite(t)) return;
576
+ let { video: n } = this.deps;
577
+ n.currentTime <= t + me || (n.currentTime = t);
569
578
  }
570
579
  startHls() {
571
- let { video: t, descriptor: n, credential: r, deviceId: i, userId: a, lowLatency: o, hlsConfig: s, emit: c } = this.deps, l = n.drmEnabled && !!n.drm?.widevine?.licenseUrl, u = {
580
+ let { video: t, descriptor: n, credential: r, deviceId: i, userId: a, lowLatency: o, debug: s, hlsConfig: c, emit: l } = this.deps, u = n.drmEnabled && !!n.drm?.widevine?.licenseUrl, d = {
581
+ debug: s === !0,
572
582
  enableWorker: !o,
573
583
  lowLatencyMode: o,
574
584
  backBufferLength: 90,
@@ -576,7 +586,7 @@ var ge = class {
576
586
  maxLiveSyncPlaybackRate: 1.5,
577
587
  abrEwmaDefaultEstimate: 1e6,
578
588
  capLevelToPlayerSize: !0,
579
- ...l ? {
589
+ ...u ? {
580
590
  emeEnabled: !0,
581
591
  drmSystems: { "com.widevine.alpha": { licenseUrl: this.licenseUrl } },
582
592
  drmSystemOptions: {
@@ -589,33 +599,33 @@ var ge = class {
589
599
  e.open("POST", this.licenseUrl ?? t, !0), e.setRequestHeader("x-api-key", r.apiKey), e.setRequestHeader("X-Match-Urn", n.matchUrn), e.setRequestHeader("X-Device-Id", i), a && e.setRequestHeader("X-User-Id", a);
590
600
  }
591
601
  } : {},
592
- ...s
602
+ ...c
593
603
  };
594
- me(u);
595
- let d = new e(u);
596
- this.hls = d, d.on(e.Events.MANIFEST_PARSED, () => {
597
- this.mediaRecoveries = 0, this.networkRecoveries = 0, c({ type: "ready" });
598
- }), d.on(e.Events.FRAG_BUFFERED, () => {
604
+ he(d);
605
+ let f = new e(d);
606
+ this.hls = f, f.on(e.Events.MANIFEST_PARSED, () => {
607
+ this.mediaRecoveries = 0, this.networkRecoveries = 0, l({ type: "ready" });
608
+ }), f.on(e.Events.FRAG_BUFFERED, () => {
599
609
  (this.mediaRecoveries > 0 || this.networkRecoveries > 0 || this.licenseRecoveries > 0) && !this.recoveryResetTimer && (this.recoveryResetTimer = setTimeout(() => {
600
610
  this.mediaRecoveries = 0, this.networkRecoveries = 0, this.licenseRecoveries = 0, this.recoveryResetTimer = null;
601
611
  }, V));
602
- }), d.on(e.Events.LEVEL_LOADED, (e, t) => {
612
+ }), f.on(e.Events.LEVEL_LOADED, (e, t) => {
603
613
  if (this.detectedLowLatency) return;
604
614
  let n = t.details;
605
- (n?.partList?.length || (n?.partTarget ?? 0) > 0) && (this.detectedLowLatency = !0, c({
615
+ (n?.partList?.length || (n?.partTarget ?? 0) > 0) && (this.detectedLowLatency = !0, l({
606
616
  type: "lowLatencyDetected",
607
617
  value: !0
608
618
  }));
609
- }), d.on(e.Events.LEVEL_LOADED, (e, t) => this.trackEnd(t)), this.endWatchdog = setInterval(() => this.checkEndStale(), U), d.on(e.Events.LEVEL_SWITCHED, (e, t) => c({
619
+ }), f.on(e.Events.LEVEL_LOADED, (e, t) => this.trackEnd(t)), this.endWatchdog = setInterval(() => this.checkEndStale(), U), f.on(e.Events.LEVEL_SWITCHED, (e, t) => l({
610
620
  type: "qualitychange",
611
621
  index: t.level
612
- })), d.on(e.Events.AUDIO_TRACK_SWITCHED, (e, t) => c({
622
+ })), f.on(e.Events.AUDIO_TRACK_SWITCHED, (e, t) => l({
613
623
  type: "audiotrackchange",
614
624
  id: t.id
615
- })), d.on(e.Events.SUBTITLE_TRACK_SWITCH, (e, t) => c({
625
+ })), f.on(e.Events.SUBTITLE_TRACK_SWITCH, (e, t) => l({
616
626
  type: "texttrackchange",
617
627
  id: t.id
618
- })), d.on(e.Events.ERROR, (e, t) => this.onError(t)), d.loadSource(n.manifestUrl), d.attachMedia(t);
628
+ })), f.on(e.Events.ERROR, (e, t) => this.onError(t)), f.loadSource(n.manifestUrl), f.attachMedia(t);
619
629
  }
620
630
  getQualityLevels() {
621
631
  return this.hls ? this.hls.levels.map((e, t) => ({
@@ -731,7 +741,7 @@ var ge = class {
731
741
  } catch {}
732
742
  }
733
743
  destroy() {
734
- if (this.clearRecoveryReset(), this.endWatchdog &&= (clearInterval(this.endWatchdog), null), this.onVisibility && typeof document < "u" && (document.removeEventListener("visibilitychange", this.onVisibility), this.onVisibility = null), this.networkRetryTimer &&= (clearTimeout(this.networkRetryTimer), null), this.licenseRetryTimer &&= (clearTimeout(this.licenseRetryTimer), null), this.hls) {
744
+ if (this.clearRecoveryReset(), 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.networkRetryTimer &&= (clearTimeout(this.networkRetryTimer), null), this.licenseRetryTimer &&= (clearTimeout(this.licenseRetryTimer), null), this.hls) {
735
745
  try {
736
746
  this.hls.destroy();
737
747
  } catch (e) {
@@ -815,7 +825,7 @@ var ge = class {
815
825
  };
816
826
  //#endregion
817
827
  //#region src/core/engine/native.ts
818
- function _e(e) {
828
+ function ve(e) {
819
829
  return !!(e ?? (typeof document < "u" ? document.createElement("video") : void 0))?.canPlayType("application/vnd.apple.mpegurl");
820
830
  }
821
831
  var W = class {
@@ -946,15 +956,15 @@ function q(e, t) {
946
956
  }
947
957
  //#endregion
948
958
  //#region src/ui/styles.ts
949
- var J = "havik-player-styles", ve = "\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.havik-spinner{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\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.havik-vol{display:flex;align-items:center}\n.havik-vol__slider{width:0;opacity:0;transition:width .18s,opacity .18s;height:4px;cursor:pointer;\n appearance:none;-webkit-appearance:none;background:rgba(255,255,255,.3);border-radius:3px;margin:0 4px}\n.havik-vol:hover .havik-vol__slider,.havik-vol:focus-within .havik-vol__slider{width:70px;opacity:1}\n.havik-vol__slider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--havik-accent,#E1B600)}\n.havik-vol__slider::-moz-range-thumb{width:12px;height:12px;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";
950
- function ye(e = document) {
959
+ var J = "havik-player-styles", ye = "\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.havik-spinner{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\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.havik-vol{display:flex;align-items:center}\n.havik-vol__slider{width:0;opacity:0;transition:width .18s,opacity .18s;height:4px;cursor:pointer;\n appearance:none;-webkit-appearance:none;background:rgba(255,255,255,.3);border-radius:3px;margin:0 4px}\n.havik-vol:hover .havik-vol__slider,.havik-vol:focus-within .havik-vol__slider{width:70px;opacity:1}\n.havik-vol__slider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--havik-accent,#E1B600)}\n.havik-vol__slider::-moz-range-thumb{width:12px;height:12px;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";
960
+ function be(e = document) {
951
961
  if (e.getElementById(J)) return;
952
962
  let t = e.createElement("style");
953
- t.id = J, t.textContent = ve, e.head.appendChild(t);
963
+ t.id = J, t.textContent = ye, e.head.appendChild(t);
954
964
  }
955
965
  //#endregion
956
966
  //#region src/ui/icons.ts
957
- var be = {
967
+ var xe = {
958
968
  play: "<path d=\"M8 5v14l11-7z\"/>",
959
969
  pause: "<path d=\"M6 5h4v14H6zM14 5h4v14h-4z\"/>",
960
970
  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\"/>",
@@ -968,20 +978,20 @@ var be = {
968
978
  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\"/>"
969
979
  };
970
980
  function Y(e) {
971
- return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${be[e]}</svg>`;
981
+ return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${xe[e]}</svg>`;
972
982
  }
973
983
  //#endregion
974
984
  //#region src/ui/controls.ts
975
- var xe = 3e3, Se = /* @__PURE__ */ new Set([
985
+ var Se = 3e3, Ce = /* @__PURE__ */ new Set([
976
986
  "GONE",
977
987
  "NOT_FOUND",
978
988
  "INVALID_URN",
979
989
  "UNAUTHORIZED"
980
990
  ]);
981
- function Ce(e) {
982
- return new we(e);
991
+ function we(e) {
992
+ return new Te(e);
983
993
  }
984
- var we = class {
994
+ var Te = class {
985
995
  root;
986
996
  video;
987
997
  player;
@@ -1021,7 +1031,7 @@ var we = class {
1021
1031
  scrubbing = !1;
1022
1032
  offFns = [];
1023
1033
  constructor(e) {
1024
- this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? xe, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.errorMessage = e.errorMessage, ye(this.doc), 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");
1034
+ this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? Se, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.errorMessage = e.errorMessage, be(this.doc), 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");
1025
1035
  let t = (e, t, n) => {
1026
1036
  let r = this.doc.createElement(e);
1027
1037
  if (r.className = t, n) for (let [e, t] of Object.entries(n)) r.setAttribute(e, t);
@@ -1108,7 +1118,7 @@ var we = class {
1108
1118
  e.type = "button", e.className = "havik-status__btn havik-status__btn--play", e.setAttribute("aria-label", "Play"), e.innerHTML = Y("play"), e.addEventListener("click", () => void this.player.play()), t.append(e);
1109
1119
  } else {
1110
1120
  let n = this.doc.createElement("div");
1111
- if (n.className = "havik-status__msg", n.textContent = e === "ended" ? this.endedMessage : e === "waiting" ? "Starting soon…" : this.errorMessage ?? this.lastError?.message ?? "Playback unavailable", t.append(n), e === "error" && this.lastError && !Se.has(this.lastError.code)) {
1121
+ if (n.className = "havik-status__msg", n.textContent = e === "ended" ? this.endedMessage : e === "waiting" ? "Starting soon…" : this.errorMessage ?? this.lastError?.message ?? "Playback unavailable", t.append(n), e === "error" && this.lastError && !Ce.has(this.lastError.code)) {
1112
1122
  let e = this.doc.createElement("button");
1113
1123
  e.type = "button", e.className = "havik-status__btn", e.innerHTML = `${Y("retry")}<span>Retry</span>`, e.addEventListener("click", () => void this.player.retry()), t.append(e);
1114
1124
  }
@@ -1120,17 +1130,14 @@ var we = class {
1120
1130
  this.setIcon(this.muteBtn, t ? "volumeMute" : "volumeHigh"), this.muteBtn.setAttribute("aria-label", t ? "Unmute" : "Mute"), this.volSlider.value = String(t ? 0 : e.volume);
1121
1131
  }
1122
1132
  seekableRange() {
1123
- let e = this.video;
1124
- if (e.seekable && e.seekable.length > 0) {
1125
- let t = e.seekable.start(0), n = e.seekable.end(e.seekable.length - 1);
1126
- if (n > t) return {
1127
- start: t,
1128
- end: n
1129
- };
1130
- }
1131
- return Number.isFinite(e.duration) && e.duration > 0 ? {
1132
- start: 0,
1133
- end: e.duration
1133
+ let e = this.video, t, n;
1134
+ if (e.seekable && e.seekable.length > 0) t = e.seekable.start(0), n = e.seekable.end(e.seekable.length - 1);
1135
+ else if (Number.isFinite(e.duration) && e.duration > 0) t = 0, n = e.duration;
1136
+ else return null;
1137
+ let r = this.player.getStats();
1138
+ return r.isLive !== !1 && r.targetLatencySeconds && r.targetLatencySeconds > 0 && (n = Math.max(t, n - r.targetLatencySeconds)), n > t ? {
1139
+ start: t,
1140
+ end: n
1134
1141
  } : null;
1135
1142
  }
1136
1143
  syncProgress() {
@@ -1197,17 +1204,17 @@ var we = class {
1197
1204
  renderQualityMenu() {
1198
1205
  let e = this.player.getQualityLevels(), t = this.player.getCurrentQuality(), n = this.doc.createElement("div");
1199
1206
  n.className = "havik-menu__title", n.textContent = "Quality", this.menu.replaceChildren(n, this.menuItem("Auto", t === -1, () => this.player.setQuality("auto")));
1200
- for (let n of e) this.menu.append(this.menuItem(Te(n), t === n.index, () => this.player.setQuality(n.index)));
1207
+ for (let n of e) this.menu.append(this.menuItem(Ee(n), t === n.index, () => this.player.setQuality(n.index)));
1201
1208
  }
1202
1209
  renderAudioMenu() {
1203
1210
  let e = this.player.getAudioTracks(), t = this.doc.createElement("div");
1204
1211
  t.className = "havik-menu__title", t.textContent = "Audio", this.menu.replaceChildren(t);
1205
- for (let t of e) this.menu.append(this.menuItem(Ee(t), t.default, () => this.player.setAudioTrack(t.id)));
1212
+ for (let t of e) this.menu.append(this.menuItem(De(t), t.default, () => this.player.setAudioTrack(t.id)));
1206
1213
  }
1207
1214
  renderCcMenu() {
1208
1215
  let e = this.player.getTextTracks(), t = this.doc.createElement("div");
1209
1216
  t.className = "havik-menu__title", t.textContent = "Subtitles", this.menu.replaceChildren(t, this.menuItem("Off", !1, () => this.player.setTextTrack(-1)));
1210
- for (let t of e) this.menu.append(this.menuItem(De(t), !1, () => this.player.setTextTrack(t.id)));
1217
+ for (let t of e) this.menu.append(this.menuItem(Oe(t), !1, () => this.player.setTextTrack(t.id)));
1211
1218
  }
1212
1219
  onTracksChanged() {
1213
1220
  this.openMenu && this.renderMenu(this.openMenu);
@@ -1313,27 +1320,27 @@ function X(e) {
1313
1320
  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");
1314
1321
  return r > 0 ? `${r}:${i}:${a}` : `${i}:${a}`;
1315
1322
  }
1316
- function Te(e) {
1323
+ function Ee(e) {
1317
1324
  return e.height ? `${e.height}p` : `${Math.round(e.bitrate / 1e3)} kbps`;
1318
1325
  }
1319
- function Ee(e) {
1326
+ function De(e) {
1320
1327
  return e.name || e.lang || `Track ${e.id}`;
1321
1328
  }
1322
- function De(e) {
1329
+ function Oe(e) {
1323
1330
  return e.name || e.lang || `Track ${e.id}`;
1324
1331
  }
1325
1332
  //#endregion
1326
1333
  //#region src/managed/index.ts
1327
- var Oe = 6e3, ke = 45e3, Z = 2e3, Ae = 3, je = 2e3, Me = 480 * 1e3;
1334
+ var ke = 6e3, Z = 45e3, Ae = 2e3, je = 3, Me = 2e3, Ne = 480 * 1e3;
1328
1335
  async function Q(e) {
1329
1336
  if (!e.video || typeof e.video.play != "function") throw new t("INTERNAL", 0, "createPlayer: opts.video must be an HTMLVideoElement");
1330
1337
  if (!e.baseUrl) throw new t("INTERNAL", 0, "createPlayer: baseUrl is required");
1331
1338
  if (!e.matchUrn) throw new t("INVALID_URN", 0, "createPlayer: matchUrn is required");
1332
1339
  if (!e.credential) throw new t("INTERNAL", 0, "createPlayer: credential is required");
1333
- let n = new Ne(e);
1340
+ let n = new Pe(e);
1334
1341
  return await n.start(), n;
1335
1342
  }
1336
- var Ne = class {
1343
+ var Pe = class {
1337
1344
  opts;
1338
1345
  abort = new AbortController();
1339
1346
  listeners = {};
@@ -1412,13 +1419,13 @@ var Ne = class {
1412
1419
  !r && !s() && l();
1413
1420
  }, e);
1414
1421
  };
1415
- d(Oe), this.armingSub = w({
1422
+ d(ke), this.armingSub = w({
1416
1423
  eventsBaseUrl: n,
1417
1424
  matchUrn: this.opts.matchUrn,
1418
1425
  credential: this.opts.credential,
1419
- onAlive: () => d(ke),
1426
+ onAlive: () => d(Z),
1420
1427
  onState: (e) => {
1421
- r || s() || (e.state === "live" || e.state === "gone") && setTimeout(u, Math.random() * Z);
1428
+ r || s() || (e.state === "live" || e.state === "gone") && setTimeout(u, Math.random() * Ae);
1422
1429
  },
1423
1430
  onError: () => l()
1424
1431
  });
@@ -1459,12 +1466,13 @@ var Ne = class {
1459
1466
  userId: this.opts.userId,
1460
1467
  lowLatency: this.opts.lowLatency === void 0 || this.opts.lowLatency === "auto" || this.opts.lowLatency,
1461
1468
  snapToLiveOnRefocus: this.opts.snapToLiveOnRefocus,
1469
+ debug: this.opts.debug,
1462
1470
  hlsConfig: r,
1463
1471
  emit: (e) => this.onEngineEvent(e)
1464
1472
  }, a = this.opts.engine ?? "auto";
1465
1473
  if (a === "native") return new W(i);
1466
- if (a === "hls" || L()) return new ge(i);
1467
- if (_e(this.opts.video)) return new W(i);
1474
+ if (a === "hls" || L()) return new _e(i);
1475
+ if (ve(this.opts.video)) return new W(i);
1468
1476
  throw new t("INTERNAL", 0, "no supported HLS playback engine in this browser");
1469
1477
  }
1470
1478
  onEngineEvent(e) {
@@ -1509,7 +1517,7 @@ var Ne = class {
1509
1517
  r = 0, i = !1;
1510
1518
  },
1511
1519
  onError: () => {
1512
- this.destroyed || e !== this.loadGen || i || (r += 1, r >= Ae && (i = !0, this.emit("warning", new t("NETWORK", 0, `live-state events unreachable at ${n} 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.`))));
1520
+ this.destroyed || e !== this.loadGen || i || (r += 1, r >= je && (i = !0, this.emit("warning", new t("NETWORK", 0, `live-state events unreachable at ${n} 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.`))));
1513
1521
  }
1514
1522
  });
1515
1523
  }
@@ -1531,7 +1539,7 @@ var Ne = class {
1531
1539
  }
1532
1540
  t.controls = !1;
1533
1541
  let n = (t.ownerDocument ?? document).createElement("div"), r = t.parentNode;
1534
- r && r.insertBefore(n, t), n.appendChild(t), q(n, this.opts.theme), this.container = n, this.controlBar = Ce({
1542
+ r && r.insertBefore(n, t), n.appendChild(t), q(n, this.opts.theme), this.container = n, this.controlBar = we({
1535
1543
  root: n,
1536
1544
  video: t,
1537
1545
  player: this,
@@ -1554,7 +1562,7 @@ var Ne = class {
1554
1562
  }
1555
1563
  startStats() {
1556
1564
  this.stopStats();
1557
- let e = Math.max(500, this.opts.statsIntervalMs ?? je);
1565
+ let e = Math.max(500, this.opts.statsIntervalMs ?? Me);
1558
1566
  this.statsTimer = setInterval(() => {
1559
1567
  this.engine && this.emit("stats", this.engine.getStats());
1560
1568
  }, e);
@@ -1564,7 +1572,7 @@ var Ne = class {
1564
1572
  }
1565
1573
  scheduleLicenseRefresh() {
1566
1574
  this.stopRefresh();
1567
- let e = this.opts.licenseRefreshMs ?? Me;
1575
+ let e = this.opts.licenseRefreshMs ?? Ne;
1568
1576
  !e || !this._descriptor?.drmEnabled || (this.refreshTimer = setInterval(() => void this.refreshLicense(), e));
1569
1577
  }
1570
1578
  stopRefresh() {
@@ -1699,7 +1707,7 @@ function $(e) {
1699
1707
  }
1700
1708
  //#endregion
1701
1709
  //#region src/embed/host.ts
1702
- function Pe(e) {
1710
+ function Fe(e) {
1703
1711
  if (!e.container || typeof e.container.appendChild != "function") throw Error("mountEmbed: opts.container must be a DOM element");
1704
1712
  if (!e.src) throw Error("mountEmbed: opts.src is required");
1705
1713
  if (!e.baseUrl || !e.matchUrn) throw Error("mountEmbed: opts.baseUrl and opts.matchUrn are required");
@@ -1758,8 +1766,8 @@ function Pe(e) {
1758
1766
  }
1759
1767
  //#endregion
1760
1768
  //#region src/embed/page.ts
1761
- var Fe = /^[A-Za-z0-9:_.-]{1,128}$/;
1762
- function Ie() {
1769
+ var Ie = /^[A-Za-z0-9:_.-]{1,128}$/;
1770
+ function Le() {
1763
1771
  let e = window.HAVIK_EMBED_CONFIG;
1764
1772
  if (e) return {
1765
1773
  baseUrl: e.baseUrl ?? "",
@@ -1781,9 +1789,9 @@ function Ie() {
1781
1789
  waitForLive: !0
1782
1790
  };
1783
1791
  }
1784
- async function Le() {
1785
- let e = Ie();
1786
- ze();
1792
+ async function Re() {
1793
+ let e = Le();
1794
+ Be();
1787
1795
  let n = document.createElement("video");
1788
1796
  n.setAttribute("playsinline", ""), n.muted = e.muted ?? !0, n.style.cssText = "width:100%;height:100%;object-fit:contain;background:#000;aspect-ratio:auto", document.body.appendChild(n);
1789
1797
  let r = e.parentOrigin && e.parentOrigin !== "null" ? e.parentOrigin : null;
@@ -1838,7 +1846,7 @@ async function Le() {
1838
1846
  code: e.code,
1839
1847
  message: e.message
1840
1848
  })), e.on("autoplayblocked", () => {
1841
- i({ type: "oddin:autoplayblocked" }), Re(n, e);
1849
+ i({ type: "oddin:autoplayblocked" }), ze(n, e);
1842
1850
  });
1843
1851
  }, s = async (r) => {
1844
1852
  a?.destroy(), a = null;
@@ -1904,7 +1912,7 @@ async function Le() {
1904
1912
  a?.retry();
1905
1913
  break;
1906
1914
  case "oddin:load":
1907
- typeof e.matchUrn == "string" && Fe.test(e.matchUrn) && s(e.matchUrn);
1915
+ typeof e.matchUrn == "string" && Ie.test(e.matchUrn) && s(e.matchUrn);
1908
1916
  break;
1909
1917
  case "oddin:destroy":
1910
1918
  c();
@@ -1918,18 +1926,18 @@ async function Le() {
1918
1926
  protocol: 1
1919
1927
  });
1920
1928
  }
1921
- function Re(e, t) {
1929
+ function ze(e, t) {
1922
1930
  if (document.getElementById("havik-tap")) return;
1923
1931
  let n = document.createElement("button");
1924
1932
  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", () => {
1925
1933
  n.remove(), t.play();
1926
1934
  }), document.body.appendChild(n);
1927
1935
  }
1928
- function ze() {
1936
+ function Be() {
1929
1937
  let e = document.createElement("style");
1930
1938
  e.textContent = "html,body{margin:0;height:100%;background:#000;overflow:hidden}body{display:flex}", document.head.appendChild(e);
1931
1939
  }
1932
1940
  //#endregion
1933
- export { G as ODDIN_THEME, t as PlaybackError, q as applyTheme, Le as bootEmbed, k as clearDeviceId, Q as createPlayer, T as deriveEventsBaseUrl, oe as fetchCatalog, ce as fetchTournament, ue as flattenMatches, O as getDeviceId, N as isLowLatencyManifest, $ as isOddinMessage, j as licenseRequestHeaders, Pe as mountEmbed, y as pollToLive, x as resolveStream, w as subscribeLiveState, C as watchStatus };
1941
+ export { G as ODDIN_THEME, t as PlaybackError, q as applyTheme, Re as bootEmbed, k as clearDeviceId, Q as createPlayer, T as deriveEventsBaseUrl, oe as fetchCatalog, ce as fetchTournament, ue as flattenMatches, O as getDeviceId, N as isLowLatencyManifest, $ as isOddinMessage, j as licenseRequestHeaders, Fe as mountEmbed, y as pollToLive, x as resolveStream, w as subscribeLiveState, C as watchStatus };
1934
1942
 
1935
1943
  //# sourceMappingURL=index.js.map