@oddin-gg/havik-player 1.8.4 → 1.10.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.js CHANGED
@@ -171,7 +171,7 @@ async function v(e) {
171
171
  signal: e.signal
172
172
  });
173
173
  if (!n.ok) throw await ae(n);
174
- return x(await n.json(), e.matchUrn);
174
+ return se(await n.json(), e.matchUrn);
175
175
  }
176
176
  function y(e) {
177
177
  let t = () => v(e);
@@ -201,7 +201,8 @@ var oe = /* @__PURE__ */ new Set([
201
201
  "drm",
202
202
  "serverTime",
203
203
  "liveStartsAt",
204
- "expiresAt"
204
+ "expiresAt",
205
+ "analytics"
205
206
  ]);
206
207
  function b(e, n) {
207
208
  let r;
@@ -213,7 +214,7 @@ function b(e, n) {
213
214
  if (r.protocol !== "https:" && r.protocol !== "http:") throw new t("INTERNAL", 0, `playback ${e} has a disallowed scheme: ${r.protocol}`);
214
215
  return n;
215
216
  }
216
- function x(e, n) {
217
+ function se(e, n) {
217
218
  if (!e.manifestUrl) throw new t("INTERNAL", 0, "playback response is missing manifestUrl");
218
219
  let r = e.drmEnabled ?? !!e.drm, i = {
219
220
  matchUrn: e.matchUrn ?? n,
@@ -222,7 +223,7 @@ function x(e, n) {
222
223
  manifestUrl: b("manifestUrl", e.manifestUrl),
223
224
  serverTime: e.serverTime ?? ""
224
225
  };
225
- if (e.liveStartsAt && (i.liveStartsAt = e.liveStartsAt), r && e.drm) {
226
+ if (e.liveStartsAt && (i.liveStartsAt = e.liveStartsAt), typeof e.analytics?.sid == "string" && e.analytics.sid && (i.analytics = { sid: e.analytics.sid }), r && e.drm) {
226
227
  let t = {};
227
228
  e.drm.widevine?.licenseUrl && (t.widevine = { licenseUrl: b("widevine licenseUrl", e.drm.widevine.licenseUrl) }), e.drm.fairplay?.licenseUrl && e.drm.fairplay.certificateUrl && (t.fairplay = {
228
229
  licenseUrl: b("fairplay licenseUrl", e.drm.fairplay.licenseUrl),
@@ -235,7 +236,7 @@ function x(e, n) {
235
236
  }
236
237
  //#endregion
237
238
  //#region src/core/catalog.ts
238
- async function S(e) {
239
+ async function x(e) {
239
240
  let r = new URLSearchParams();
240
241
  e.status && r.set("status", Array.isArray(e.status) ? e.status.join(",") : e.status), e.sport && r.set("sport", e.sport);
241
242
  let i = r.toString(), a = o(e.baseUrl, "/v1/catalog" + (i ? `?${i}` : "")), s = new Headers();
@@ -252,12 +253,12 @@ async function S(e) {
252
253
  };
253
254
  if (!l.ok) throw new t(n(l.status), l.status, `catalog request failed (HTTP ${l.status})`);
254
255
  return {
255
- tournaments: se(await l.json()),
256
+ tournaments: S(await l.json()),
256
257
  etag: l.headers.get("etag") ?? void 0,
257
258
  notModified: !1
258
259
  };
259
260
  }
260
- function se(e) {
261
+ function S(e) {
261
262
  return (e.tournaments ?? []).map((e) => {
262
263
  let t = e.urn ?? "", n = e.name ?? "", r = e.sport ?? "";
263
264
  return {
@@ -480,11 +481,11 @@ function me(e, t, n = {}) {
480
481
  }
481
482
  //#endregion
482
483
  //#region src/core/drm/support.ts
483
- var he = 5e3, O = "video/mp4; codecs=\"avc1.42E01E\"", k = "audio/mp4; codecs=\"mp4a.40.2\"", A = {
484
+ var he = 5e3, O = "video/mp4; codecs=\"avc1.42E01E\"", k = "audio/mp4; codecs=\"mp4a.40.2\"", ge = {
484
485
  widevine: ["com.widevine.alpha"],
485
486
  fairplay: ["com.apple.fps"]
486
487
  };
487
- function j(e) {
488
+ function _e(e) {
488
489
  return e === "fairplay" ? [{
489
490
  initDataTypes: [
490
491
  "sinf",
@@ -515,7 +516,7 @@ function j(e) {
515
516
  }]
516
517
  }];
517
518
  }
518
- function M(e, t) {
519
+ function A(e, t) {
519
520
  return new Promise((n, r) => {
520
521
  let i = setTimeout(() => r(/* @__PURE__ */ Error("probe-timeout")), t);
521
522
  e.then((e) => {
@@ -525,7 +526,7 @@ function M(e, t) {
525
526
  });
526
527
  });
527
528
  }
528
- async function N(e = {}) {
529
+ async function j(e = {}) {
529
530
  let t = e.systems ?? ["widevine", "fairplay"], n = e.timeoutMs ?? he, r = {};
530
531
  if (typeof window < "u" && window.isSecureContext === !1) return {
531
532
  verdict: "insecure-context",
@@ -538,7 +539,7 @@ async function N(e = {}) {
538
539
  };
539
540
  let a = !1, o = !1, s = Date.now() + n, c = () => Math.max(1, s - Date.now());
540
541
  for (let e of t) {
541
- let t = A[e];
542
+ let t = ge[e];
542
543
  if (!t) {
543
544
  r[e] = "unknown system";
544
545
  continue;
@@ -549,7 +550,7 @@ async function N(e = {}) {
549
550
  break;
550
551
  }
551
552
  try {
552
- return await M((await M(i(n, j(e)), c())).createMediaKeys(), c()), r[e] = `ok (${n})`, {
553
+ return await A((await A(i(n, _e(e)), c())).createMediaKeys(), c()), r[e] = `ok (${n})`, {
553
554
  verdict: "supported",
554
555
  system: e,
555
556
  detail: r
@@ -573,17 +574,17 @@ async function N(e = {}) {
573
574
  }
574
575
  //#endregion
575
576
  //#region src/core/manifest.ts
576
- var P = /^#EXT-X-(PART-INF|PART|PRELOAD-HINT)[:\s]/m;
577
- function F(e) {
578
- return P.test(e);
577
+ var M = /^#EXT-X-(PART-INF|PART|PRELOAD-HINT)[:\s]/m;
578
+ function N(e) {
579
+ return M.test(e);
579
580
  }
580
581
  //#endregion
581
582
  //#region src/core/stats.ts
582
- var I = { droppedFrames: 0 };
583
- function L(e) {
583
+ var P = { droppedFrames: 0 };
584
+ function F(e) {
584
585
  return e.getVideoPlaybackQuality?.()?.droppedVideoFrames ?? 0;
585
586
  }
586
- function R(e, t) {
587
+ function I(e, t) {
587
588
  let n = [
588
589
  ["playing", () => t("playing")],
589
590
  ["waiting", () => t("buffering")],
@@ -600,7 +601,7 @@ function R(e, t) {
600
601
  }
601
602
  //#endregion
602
603
  //#region src/core/engine/native.ts
603
- function ge(e) {
604
+ function ve(e) {
604
605
  try {
605
606
  let t = new TextDecoder("utf-8").decode(new Uint8Array(e));
606
607
  return /^skd:\/\/[0-9a-fA-F-]{36}:([0-9a-fA-F]{32})$/.exec(t)?.[1];
@@ -608,15 +609,15 @@ function ge(e) {
608
609
  return;
609
610
  }
610
611
  }
611
- function z(e) {
612
+ function L(e) {
612
613
  return !!(e ?? (typeof document < "u" ? document.createElement("video") : void 0))?.canPlayType("application/vnd.apple.mpegurl");
613
614
  }
614
- function B() {
615
+ function R() {
615
616
  if (typeof window > "u") return !1;
616
617
  let e = "WebKitMediaKeys" in window || typeof HTMLMediaElement < "u" && "webkitSetMediaKeys" in HTMLMediaElement.prototype, t = typeof navigator < "u" && /Apple/i.test(navigator.vendor ?? "");
617
618
  return e || t;
618
619
  }
619
- var V = class {
620
+ var z = class {
620
621
  name = "native";
621
622
  deps;
622
623
  onLoaded;
@@ -646,7 +647,7 @@ var V = class {
646
647
  type: "error",
647
648
  error: new t("INTERNAL", 0, "native HLS playback error"),
648
649
  fatal: !0
649
- }), e.addEventListener("loadedmetadata", this.onLoaded, { once: !0 }), e.addEventListener("error", this.onError), e.src = n.manifestUrl, e.load();
650
+ }), e.addEventListener("loadedmetadata", this.onLoaded, { once: !0 }), e.addEventListener("error", this.onError), e.src = ye(n.manifestUrl, n.analytics?.sid), e.load();
650
651
  }
651
652
  armFairPlay() {
652
653
  let { video: e, descriptor: n, credential: r, deviceId: i, userId: a, emit: o } = this.deps, s = n.drm?.fairplay;
@@ -659,7 +660,7 @@ var V = class {
659
660
  });
660
661
  this.onEncrypted = (t) => {
661
662
  let o = t;
662
- !o.initData || this.fairplayArmed || (this.fairplayArmed = !0, this.fairplayKeyIV = ge(o.initData), (async () => {
663
+ !o.initData || this.fairplayArmed || (this.fairplayArmed = !0, this.fairplayKeyIV = ve(o.initData), (async () => {
663
664
  try {
664
665
  let t = await (await navigator.requestMediaKeySystemAccess("com.apple.fps", [{
665
666
  initDataTypes: [o.initDataType],
@@ -751,7 +752,7 @@ var V = class {
751
752
  e.seekable.length > 0 && (e.currentTime = e.seekable.end(e.seekable.length - 1));
752
753
  }
753
754
  getStats() {
754
- let e = this.deps.video, t = { droppedFrames: L(e) };
755
+ let e = this.deps.video, t = { droppedFrames: F(e) };
755
756
  return e.duration === Infinity && (t.isLive = !0, e.seekable.length > 0 && (t.atLiveEdge = e.currentTime >= e.seekable.end(e.seekable.length - 1) - 2)), t;
756
757
  }
757
758
  stop() {
@@ -771,31 +772,38 @@ var V = class {
771
772
  } catch {}
772
773
  }
773
774
  };
775
+ function ye(e, t) {
776
+ return t ? `${e}${e.includes("?") ? "&" : "?"}CMCD=${encodeURIComponent(`sid="${t}"`)}` : e;
777
+ }
774
778
  //#endregion
775
779
  //#region src/core/engine/hls.ts
776
- function _e() {
780
+ function be() {
777
781
  return e.isSupported();
778
782
  }
779
- var ve = 3, ye = 5, be = 3, xe = 3e4, Se = 12e3, Ce = 3e3, H = 6e3, we = 4e3, Te = 2, Ee = 5, De = 2, Oe = 9, ke = .5;
780
- function Ae(e) {
783
+ var xe = 3, Se = 5, Ce = 3, we = 3e4, Te = 12e3, Ee = 3e3, B = 6e3, De = 4e3, Oe = 2, ke = 5, Ae = 2, je = 9, Me = .5;
784
+ function Ne(e) {
781
785
  if (e.liveSyncDurationCount != null || e.liveMaxLatencyDurationCount != null) {
782
786
  delete e.liveSyncDuration, delete e.liveMaxLatencyDuration;
783
787
  return;
784
788
  }
785
- e.liveSyncDuration ??= De;
786
- let t = Math.max(12, e.liveSyncDuration + Oe);
787
- (e.liveMaxLatencyDuration == null || e.liveMaxLatencyDuration <= e.liveSyncDuration) && (e.liveMaxLatencyDuration = t), je(e, e.liveSyncDuration);
789
+ e.liveSyncDuration ??= Ae;
790
+ let t = Math.max(12, e.liveSyncDuration + je);
791
+ (e.liveMaxLatencyDuration == null || e.liveMaxLatencyDuration <= e.liveSyncDuration) && (e.liveMaxLatencyDuration = t), Pe(e, e.liveSyncDuration);
788
792
  }
789
- function je(e, t) {
793
+ function Pe(e, t) {
790
794
  let n = (e, t, n) => Math.min(n, Math.max(t, e));
791
795
  e.maxStarvationDelay ??= n(t / 2, .5, 4), e.maxLoadingDelay ??= n(t, 1, 4), e.abrEwmaFastLive ??= n(t / 2, 1, 3);
792
796
  }
793
- var U = class {
797
+ var V = class {
794
798
  name = "hls";
795
799
  hls = null;
796
800
  deps;
797
801
  licenseUrl;
798
802
  detectedLowLatency = !1;
803
+ bytesLoaded = 0;
804
+ playlistBytesLoaded = 0;
805
+ renditionSwitches = 0;
806
+ licenseTimeMs = 0;
799
807
  mediaRecoveries = 0;
800
808
  networkRecoveries = 0;
801
809
  licenseRecoveries = 0;
@@ -819,7 +827,7 @@ var U = class {
819
827
  this.deps = e, this.licenseUrl = e.descriptor.drm?.widevine?.licenseUrl;
820
828
  }
821
829
  async load() {
822
- let { descriptor: e, snapToLiveOnRefocus: n, video: r, emit: i } = this.deps, a = z(r) && B(), o = !!e.drm?.widevine?.licenseUrl && !a;
830
+ let { descriptor: e, snapToLiveOnRefocus: n, video: r, emit: i } = this.deps, a = L(r) && R(), o = !!e.drm?.widevine?.licenseUrl && !a;
823
831
  if (e.drmEnabled && !o) {
824
832
  i({
825
833
  type: "error",
@@ -843,7 +851,7 @@ var U = class {
843
851
  let t = e.liveSyncPosition;
844
852
  if (typeof t != "number" || !Number.isFinite(t)) return;
845
853
  let { video: n } = this.deps;
846
- n.currentTime <= t + ke || (n.currentTime = t);
854
+ n.currentTime <= t + Me || (n.currentTime = t);
847
855
  }
848
856
  startHls() {
849
857
  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 || n.drm?.fairplay?.licenseUrl), d = {
@@ -866,33 +874,48 @@ var U = class {
866
874
  },
867
875
  licenseXhrSetup: (e, t) => {
868
876
  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);
877
+ let o = performance.now();
878
+ e.addEventListener("loadend", () => {
879
+ this.licenseTimeMs = Math.max(1, Math.round(performance.now() - o));
880
+ });
869
881
  }
870
882
  } : {},
883
+ ...n.analytics?.sid ? { cmcd: {
884
+ sessionId: n.analytics.sid,
885
+ contentId: n.matchUrn,
886
+ useHeaders: !1
887
+ } } : {},
871
888
  ...c
872
889
  };
873
- Ae(d);
890
+ Ne(d);
874
891
  let f = new e(d);
875
892
  this.hls = f, f.on(e.Events.MANIFEST_PARSED, () => {
876
893
  this.mediaRecoveries = 0, this.networkRecoveries = 0, l({ type: "ready" });
877
894
  }), f.on(e.Events.FRAG_BUFFERED, (e, t) => {
878
895
  t?.frag?.type === "main" && (this.mediaRecoveries > 0 || this.networkRecoveries > 0 || this.licenseRecoveries > 0 || this.starvationKicks > 0) && !this.recoveryResetTimer && (this.recoveryResetTimer = setTimeout(() => {
879
896
  this.mediaRecoveries = 0, this.networkRecoveries = 0, this.licenseRecoveries = 0, this.starvationKicks = 0, this.recoveryResetTimer = null;
880
- }, xe));
897
+ }, we));
881
898
  }), f.on(e.Events.FRAG_LOADING, (e, t) => {
882
899
  t?.frag?.type === "main" && (this.lastFragLoadingMs = Date.now());
900
+ }), f.on(e.Events.FRAG_LOADED, (e, t) => {
901
+ let n = t?.frag?.stats?.loaded;
902
+ typeof n == "number" && (this.bytesLoaded += n);
883
903
  }), f.on(e.Events.LEVEL_LOADED, (e, t) => {
884
- if (this.detectedLowLatency) return;
885
- let n = t.details;
886
- (n?.partList?.length || (n?.partTarget ?? 0) > 0) && (this.detectedLowLatency = !0, l({
904
+ let n = t?.stats?.loaded;
905
+ if (typeof n == "number" && (this.playlistBytesLoaded += n), this.detectedLowLatency) return;
906
+ let r = t.details;
907
+ (r?.partList?.length || (r?.partTarget ?? 0) > 0) && (this.detectedLowLatency = !0, l({
887
908
  type: "lowLatencyDetected",
888
909
  value: !0
889
910
  }));
890
911
  }), f.on(e.Events.LEVEL_LOADED, (e, t) => this.trackEnd(t)), this.endWatchdog = setInterval(() => {
891
912
  this.checkEndStale(), this.checkStarvation();
892
- }, Ce), f.on(e.Events.LEVEL_SWITCHED, (e, t) => l({
893
- type: "qualitychange",
894
- index: t.level
895
- })), f.on(e.Events.AUDIO_TRACK_SWITCHED, (e, t) => l({
913
+ }, Ee), f.on(e.Events.LEVEL_SWITCHED, (e, t) => {
914
+ this.renditionSwitches += 1, l({
915
+ type: "qualitychange",
916
+ index: t.level
917
+ });
918
+ }), f.on(e.Events.AUDIO_TRACK_SWITCHED, (e, t) => l({
896
919
  type: "audiotrackchange",
897
920
  id: t.id
898
921
  })), f.on(e.Events.SUBTITLE_TRACK_SWITCH, (e, t) => l({
@@ -958,13 +981,17 @@ var U = class {
958
981
  }
959
982
  getStats() {
960
983
  let e = this.hls, t = {
961
- droppedFrames: L(this.deps.video),
962
- lowLatency: this.detectedLowLatency
984
+ droppedFrames: F(this.deps.video),
985
+ lowLatency: this.detectedLowLatency,
986
+ bytesLoaded: this.bytesLoaded,
987
+ playlistBytesLoaded: this.playlistBytesLoaded,
988
+ renditionSwitches: this.renditionSwitches,
989
+ licenseTimeMs: this.licenseTimeMs
963
990
  };
964
991
  if (e) {
965
992
  typeof e.latency == "number" && (t.latencySeconds = e.latency), typeof e.targetLatency == "number" && e.targetLatency > 0 && (t.targetLatencySeconds = e.targetLatency), typeof e.bandwidthEstimate == "number" && (t.bandwidthKbps = Math.round(e.bandwidthEstimate / 1e3));
966
993
  let n = e.currentLevel >= 0 ? e.levels[e.currentLevel] : void 0;
967
- n && (t.levelHeight = n.height), typeof n?.details?.live == "boolean" && (t.isLive = n.details.live, typeof e.liveSyncPosition == "number" && (t.atLiveEdge = this.deps.video.currentTime >= e.liveSyncPosition - 1.5));
994
+ n && (t.levelHeight = n.height), n && typeof n.bitrate == "number" && (t.renditionBitrateKbps = Math.round(n.bitrate / 1e3)), typeof n?.details?.live == "boolean" && (t.isLive = n.details.live, typeof e.liveSyncPosition == "number" && (t.atLiveEdge = this.deps.video.currentTime >= e.liveSyncPosition - 1.5));
968
995
  }
969
996
  return t;
970
997
  }
@@ -987,7 +1014,7 @@ var U = class {
987
1014
  return;
988
1015
  }
989
1016
  let e = Date.now() - this.lastAdvanceMs;
990
- !this.chaseSuspended && e > H && this.suspendChasing(), e > Se && this.emitEnded("stalled");
1017
+ !this.chaseSuspended && e > B && this.suspendChasing(), e > Te && this.emitEnded("stalled");
991
1018
  }
992
1019
  checkStarvation() {
993
1020
  let e = this.hls, { video: t } = this.deps;
@@ -996,12 +1023,12 @@ var U = class {
996
1023
  this.starvedTicks = 0;
997
1024
  return;
998
1025
  }
999
- let n = this.lastAdvanceMs !== 0 && Date.now() - this.lastAdvanceMs < H, r = this.lastFragLoadingMs !== 0 && Date.now() - this.lastFragLoadingMs > we;
1026
+ let n = this.lastAdvanceMs !== 0 && Date.now() - this.lastAdvanceMs < B, r = this.lastFragLoadingMs !== 0 && Date.now() - this.lastFragLoadingMs > De;
1000
1027
  if (!(t.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !t.paused && n && r)) {
1001
1028
  this.starvedTicks = 0;
1002
1029
  return;
1003
1030
  }
1004
- if (!(++this.starvedTicks < Te) && (this.starvedTicks = 0, !(this.starvationKicks >= Ee))) {
1031
+ if (!(++this.starvedTicks < Oe) && (this.starvedTicks = 0, !(this.starvationKicks >= ke))) {
1005
1032
  this.starvationKicks += 1, console.warn("havik-player: video starved while the live playlist advances (wedged hls.js fragment pipeline) — restarting load at the live edge");
1006
1033
  try {
1007
1034
  e.stopLoad(), e.startLoad(-1);
@@ -1050,7 +1077,7 @@ var U = class {
1050
1077
  scheduleLicenseRetry(t) {
1051
1078
  if (!(t.details === e.ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED || t.details === e.ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED)) return !1;
1052
1079
  let n = t.response?.code ?? 0;
1053
- if (!(n === 0 || n === 408 || n === 429 || n >= 500) || this.licenseRecoveries >= be) return !1;
1080
+ if (!(n === 0 || n === 408 || n === 429 || n >= 500) || this.licenseRecoveries >= Ce) return !1;
1054
1081
  this.licenseRecoveries += 1;
1055
1082
  let r = Math.min(8e3, 500 * 2 ** (this.licenseRecoveries - 1));
1056
1083
  return this.licenseRetryTimer = setTimeout(() => {
@@ -1078,7 +1105,7 @@ var U = class {
1078
1105
  return;
1079
1106
  }
1080
1107
  if (t.type === e.ErrorTypes.NETWORK_ERROR) {
1081
- if (this.networkRecoveries < ye) {
1108
+ if (this.networkRecoveries < Se) {
1082
1109
  this.networkRecoveries += 1;
1083
1110
  let e = Math.min(8e3, 1e3 * 2 ** (this.networkRecoveries - 1));
1084
1111
  this.networkRetryTimer = setTimeout(() => {
@@ -1094,7 +1121,7 @@ var U = class {
1094
1121
  return;
1095
1122
  }
1096
1123
  if (t.type === e.ErrorTypes.MEDIA_ERROR) {
1097
- if (this.mediaRecoveries < ve) {
1124
+ if (this.mediaRecoveries < xe) {
1098
1125
  this.mediaRecoveries += 1, n.recoverMediaError();
1099
1126
  return;
1100
1127
  }
@@ -1116,7 +1143,172 @@ var U = class {
1116
1143
  let r = e.response?.code ?? 0, i = e.details ?? e.type, a = e.reason ?? e.error?.message ?? "";
1117
1144
  return new t(r === 401 ? "UNAUTHORIZED" : r === 403 || n === "DRM" ? "FORBIDDEN" : "INTERNAL", r, `${n} error: ${i}${a ? ` (${a})` : ""}`);
1118
1145
  }
1119
- }, W = {
1146
+ }, Fe = 15e3, Ie = 100, H = 1, U = {
1147
+ idle: "PLAYER_STATE_IDLE",
1148
+ waiting: "PLAYER_STATE_WAITING",
1149
+ loading: "PLAYER_STATE_LOADING",
1150
+ playing: "PLAYER_STATE_PLAYING",
1151
+ buffering: "PLAYER_STATE_BUFFERING",
1152
+ paused: "PLAYER_STATE_PAUSED",
1153
+ ended: "PLAYER_STATE_ENDED",
1154
+ error: "PLAYER_STATE_ERROR"
1155
+ };
1156
+ function Le(e) {
1157
+ try {
1158
+ let t = new URL(e), [n, ...r] = t.hostname.split(".");
1159
+ 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);
1160
+ } catch {
1161
+ return;
1162
+ }
1163
+ }
1164
+ var Re = class {
1165
+ opts;
1166
+ url;
1167
+ now;
1168
+ seq = 0;
1169
+ pending = [];
1170
+ timer = null;
1171
+ endedFlag = !1;
1172
+ lastBytes = 0;
1173
+ lastPlaylistBytes = 0;
1174
+ lastDropped = 0;
1175
+ lastSwitches = 0;
1176
+ playingSinceMs = null;
1177
+ hiddenSinceMs = null;
1178
+ playingTotalMs = 0;
1179
+ hiddenTotalMs = 0;
1180
+ lastHbPlayingMs = 0;
1181
+ lastHbHiddenMs = 0;
1182
+ rebufferCount = 0;
1183
+ rebufferTotalMs = 0;
1184
+ bufferingSinceMs = null;
1185
+ errorCount = 0;
1186
+ seekCount = 0;
1187
+ pauseCount = 0;
1188
+ bornMs;
1189
+ startupTimeMs = 0;
1190
+ manifestLoadMs = 0;
1191
+ firstFragLoadMs = 0;
1192
+ onPagehide = () => this.end("END_REASON_PAGEHIDE");
1193
+ onVisibility = () => {
1194
+ typeof document > "u" || (document.visibilityState === "hidden" ? this.hiddenSinceMs === null && (this.hiddenSinceMs = this.now()) : this.hiddenSinceMs !== null && (this.hiddenTotalMs += this.now() - this.hiddenSinceMs, this.hiddenSinceMs = null));
1195
+ };
1196
+ constructor(e) {
1197
+ this.opts = {
1198
+ ...e,
1199
+ heartbeatMs: e.heartbeatMs ?? Fe
1200
+ }, this.url = `${e.endpoint.replace(/\/$/, "")}/v1/beacons`, this.now = e.now ?? (() => Date.now()), this.bornMs = this.now();
1201
+ }
1202
+ start() {
1203
+ this.endedFlag || (this.enqueue({ sessionStart: {
1204
+ player: this.opts.player,
1205
+ sdkVersion: this.opts.sdkVersion
1206
+ } }), this.flush(), this.timer = setInterval(() => this.heartbeat(), this.opts.heartbeatMs), typeof window < "u" && window.addEventListener("pagehide", this.onPagehide), typeof document < "u" && (document.addEventListener("visibilitychange", this.onVisibility), this.onVisibility()));
1207
+ }
1208
+ noteStateChange(e, t, n, r) {
1209
+ if (this.endedFlag || e === t) return;
1210
+ let i = this.now();
1211
+ t === "playing" ? (this.playingSinceMs === null && (this.playingSinceMs = i), this.startupTimeMs === 0 && (this.startupTimeMs = Math.max(H, i - this.bornMs))) : this.playingSinceMs !== null && (this.playingTotalMs += i - this.playingSinceMs, this.playingSinceMs = null), t === "buffering" && e === "playing" ? (this.rebufferCount += 1, this.bufferingSinceMs = i) : e === "buffering" && this.bufferingSinceMs !== null && (this.rebufferTotalMs += i - this.bufferingSinceMs, this.bufferingSinceMs = null), t === "paused" && (this.pauseCount += 1);
1212
+ let a = {
1213
+ fromState: U[e] ?? "PLAYER_STATE_UNSPECIFIED",
1214
+ toState: U[t] ?? "PLAYER_STATE_UNSPECIFIED"
1215
+ };
1216
+ n > 0 && (a.positionMs = Math.round(n)), r && (a.renditionHeight = r), this.enqueue({ stateChange: a });
1217
+ }
1218
+ noteReady() {
1219
+ this.manifestLoadMs === 0 && (this.manifestLoadMs = Math.max(H, this.now() - this.bornMs));
1220
+ }
1221
+ noteFirstBytes() {
1222
+ this.firstFragLoadMs === 0 && (this.firstFragLoadMs = Math.max(H, this.now() - this.bornMs));
1223
+ }
1224
+ noteSeek() {
1225
+ this.endedFlag || (this.seekCount += 1);
1226
+ }
1227
+ noteError(e, t, n, r, i) {
1228
+ if (this.endedFlag) return;
1229
+ this.errorCount += 1;
1230
+ let a = { code: e };
1231
+ t && (a.detail = t), n && (a.fatal = !0), r > 0 && (a.httpStatus = r), i > 0 && (a.positionMs = Math.round(i)), this.enqueue({ error: a }), this.flush();
1232
+ }
1233
+ noteEnded() {
1234
+ this.end("END_REASON_ENDED");
1235
+ }
1236
+ noteDestroyed() {
1237
+ this.end("END_REASON_STOPPED");
1238
+ }
1239
+ noteFatal() {
1240
+ this.end("END_REASON_FATAL_ERROR");
1241
+ }
1242
+ heartbeat() {
1243
+ if (this.endedFlag) return;
1244
+ let e = this.opts.sample(), t = this.now(), n = this.playingTotalMs;
1245
+ this.playingSinceMs !== null && (n += t - this.playingSinceMs);
1246
+ let r = this.hiddenTotalMs;
1247
+ this.hiddenSinceMs !== null && (r += t - this.hiddenSinceMs);
1248
+ let i = {}, a = e.bytesLoaded ?? 0;
1249
+ a > this.lastBytes && (i.bytesDownloadedDelta = a - this.lastBytes), this.lastBytes = a;
1250
+ let o = e.playlistBytesLoaded ?? 0;
1251
+ o > this.lastPlaylistBytes && (i.playlistBytesDelta = o - this.lastPlaylistBytes), this.lastPlaylistBytes = o;
1252
+ let s = e.droppedFrames ?? 0;
1253
+ s > this.lastDropped && (i.droppedFramesDelta = s - this.lastDropped), this.lastDropped = s;
1254
+ let c = e.renditionSwitches ?? 0;
1255
+ c > this.lastSwitches && (i.renditionSwitchesDelta = c - this.lastSwitches), this.lastSwitches = c, n > this.lastHbPlayingMs && (i.playingMsDelta = Math.round(n - this.lastHbPlayingMs)), this.lastHbPlayingMs = n, r > this.lastHbHiddenMs && (i.hiddenMsDelta = Math.round(r - this.lastHbHiddenMs)), this.lastHbHiddenMs = r, typeof e.latencySeconds == "number" && (i.latencyMs = Math.round(e.latencySeconds * 1e3)), typeof e.targetLatencySeconds == "number" && (i.targetLatencyMs = Math.round(e.targetLatencySeconds * 1e3)), typeof e.bandwidthKbps == "number" && (i.bandwidthEstKbps = e.bandwidthKbps), typeof e.levelHeight == "number" && (i.renditionHeight = e.levelHeight), typeof e.renditionBitrateKbps == "number" && (i.renditionBitrateKbps = e.renditionBitrateKbps), e.atLiveEdge === !0 && (i.atLiveEdge = !0);
1256
+ let l = this.bufferedMsFrom(e);
1257
+ l > 0 && (i.bufferedMs = l), i.playerState = U[e.state] ?? "PLAYER_STATE_UNSPECIFIED", this.enqueue({ heartbeat: i }), this.flush(), a > 0 && this.noteFirstBytes();
1258
+ }
1259
+ bufferedMsFrom(e) {
1260
+ let t = e;
1261
+ return typeof t.bufferedMs == "number" ? Math.round(t.bufferedMs) : 0;
1262
+ }
1263
+ end(e) {
1264
+ if (this.endedFlag) return;
1265
+ this.endedFlag = !0;
1266
+ let t = this.now();
1267
+ this.playingSinceMs !== null && (this.playingTotalMs += t - this.playingSinceMs, this.playingSinceMs = null), this.hiddenSinceMs !== null && (this.hiddenTotalMs += t - this.hiddenSinceMs, this.hiddenSinceMs = null), this.bufferingSinceMs !== null && (this.rebufferTotalMs += t - this.bufferingSinceMs, this.bufferingSinceMs = null), this.timer &&= (clearInterval(this.timer), null), typeof window < "u" && window.removeEventListener("pagehide", this.onPagehide), typeof document < "u" && document.removeEventListener("visibilitychange", this.onVisibility);
1268
+ let n = this.safeSample(), r = { reason: e }, i = n?.bytesLoaded ?? this.lastBytes;
1269
+ i > 0 && (r.bytesDownloadedTotal = i), this.playingTotalMs > 0 && (r.watchTimeMs = Math.round(this.playingTotalMs)), this.rebufferCount > 0 && (r.rebufferCount = this.rebufferCount), this.rebufferTotalMs > 0 && (r.rebufferTimeMs = Math.round(this.rebufferTotalMs)), this.startupTimeMs > 0 && (r.startupTimeMs = Math.round(this.startupTimeMs)), this.manifestLoadMs > 0 && (r.manifestLoadMs = Math.round(this.manifestLoadMs)), this.firstFragLoadMs > 0 && (r.firstFragLoadMs = Math.round(this.firstFragLoadMs));
1270
+ let a = n?.licenseTimeMs ?? 0;
1271
+ a > 0 && (r.licenseTimeMs = Math.round(a));
1272
+ let o = n?.droppedFrames ?? this.lastDropped;
1273
+ o > 0 && (r.droppedFramesTotal = o), this.errorCount > 0 && (r.errorCount = this.errorCount), this.hiddenTotalMs > 0 && (r.hiddenTimeMs = Math.round(this.hiddenTotalMs)), this.seekCount > 0 && (r.seekCount = this.seekCount), this.pauseCount > 0 && (r.pauseCount = this.pauseCount), this.pending.push(this.wrap({ sessionEnd: r })), this.flush(e === "END_REASON_PAGEHIDE");
1274
+ }
1275
+ safeSample() {
1276
+ try {
1277
+ return this.opts.sample();
1278
+ } catch {
1279
+ return null;
1280
+ }
1281
+ }
1282
+ wrap(e) {
1283
+ return this.seq += 1, {
1284
+ sid: this.opts.sid,
1285
+ clientTime: new Date(this.now()).toISOString(),
1286
+ seq: this.seq,
1287
+ ...e
1288
+ };
1289
+ }
1290
+ enqueue(e) {
1291
+ for (this.pending.push(this.wrap(e)); this.pending.length > Ie;) {
1292
+ let e = this.pending.findIndex((e) => e.heartbeat || e.stateChange);
1293
+ this.pending.splice(e >= 0 ? e : 0, 1);
1294
+ }
1295
+ }
1296
+ flush(e = !1) {
1297
+ if (this.pending.length === 0) return;
1298
+ let t = JSON.stringify({ beacons: this.pending });
1299
+ if (this.pending = [], e) {
1300
+ let e = this.opts.sendBeacon ?? (typeof navigator < "u" && navigator.sendBeacon ? navigator.sendBeacon.bind(navigator) : void 0);
1301
+ if (e && e(this.url, t)) return;
1302
+ }
1303
+ let n = this.opts.fetchFn ?? (typeof fetch < "u" ? fetch : void 0);
1304
+ n && n(this.url, {
1305
+ method: "POST",
1306
+ headers: { "content-type": "application/json" },
1307
+ body: t,
1308
+ keepalive: !0
1309
+ }).catch(() => {});
1310
+ }
1311
+ }, ze = "1.10.0", W = {
1120
1312
  accent: "#E1B600",
1121
1313
  accentText: "#1B1C23",
1122
1314
  background: "#0e0f13",
@@ -1149,15 +1341,15 @@ function K(e, t) {
1149
1341
  }
1150
1342
  //#endregion
1151
1343
  //#region src/ui/styles.ts
1152
- var q = "havik-player-styles", Me = "\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/* 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";
1153
- function Ne(e = document) {
1344
+ var q = "havik-player-styles", Be = "\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/* 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";
1345
+ function Ve(e = document) {
1154
1346
  if (e.getElementById(q)) return;
1155
1347
  let t = e.createElement("style");
1156
- t.id = q, t.textContent = Me, e.head.appendChild(t);
1348
+ t.id = q, t.textContent = Be, e.head.appendChild(t);
1157
1349
  }
1158
1350
  //#endregion
1159
1351
  //#region src/ui/icons.ts
1160
- var Pe = {
1352
+ var He = {
1161
1353
  play: "<path d=\"M8 5v14l11-7z\"/>",
1162
1354
  pause: "<path d=\"M6 5h4v14H6zM14 5h4v14h-4z\"/>",
1163
1355
  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\"/>",
@@ -1171,20 +1363,20 @@ var Pe = {
1171
1363
  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\"/>"
1172
1364
  };
1173
1365
  function J(e) {
1174
- return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${Pe[e]}</svg>`;
1366
+ return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${He[e]}</svg>`;
1175
1367
  }
1176
1368
  //#endregion
1177
1369
  //#region src/ui/controls.ts
1178
- var Fe = 3e3, Ie = /* @__PURE__ */ new Set([
1370
+ var Ue = 3e3, We = /* @__PURE__ */ new Set([
1179
1371
  "GONE",
1180
1372
  "NOT_FOUND",
1181
1373
  "INVALID_URN",
1182
1374
  "UNAUTHORIZED"
1183
1375
  ]);
1184
- function Le(e) {
1185
- return new Re(e);
1376
+ function Ge(e) {
1377
+ return new Ke(e);
1186
1378
  }
1187
- var Re = class {
1379
+ var Ke = class {
1188
1380
  root;
1189
1381
  video;
1190
1382
  player;
@@ -1224,7 +1416,7 @@ var Re = class {
1224
1416
  scrubbing = !1;
1225
1417
  offFns = [];
1226
1418
  constructor(e) {
1227
- this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? Fe, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.errorMessage = e.errorMessage, Ne(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");
1419
+ this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? Ue, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.errorMessage = e.errorMessage, Ve(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");
1228
1420
  let t = (e, t, n) => {
1229
1421
  let r = this.doc.createElement(e);
1230
1422
  if (r.className = t, n) for (let [e, t] of Object.entries(n)) r.setAttribute(e, t);
@@ -1311,7 +1503,7 @@ var Re = class {
1311
1503
  e.type = "button", e.className = "havik-status__btn havik-status__btn--play", e.setAttribute("aria-label", "Play"), e.innerHTML = J("play"), e.addEventListener("click", () => void this.player.play()), t.append(e);
1312
1504
  } else {
1313
1505
  let n = this.doc.createElement("div");
1314
- 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 && !Ie.has(this.lastError.code)) {
1506
+ 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 && !We.has(this.lastError.code)) {
1315
1507
  let e = this.doc.createElement("button");
1316
1508
  e.type = "button", e.className = "havik-status__btn", e.innerHTML = `${J("retry")}<span>Retry</span>`, e.addEventListener("click", () => void this.player.retry()), t.append(e);
1317
1509
  }
@@ -1402,17 +1594,17 @@ var Re = class {
1402
1594
  renderQualityMenu() {
1403
1595
  let e = this.player.getQualityLevels(), t = this.player.getCurrentQuality(), n = this.doc.createElement("div");
1404
1596
  n.className = "havik-menu__title", n.textContent = "Quality", this.menu.replaceChildren(n, this.menuItem("Auto", t === -1, () => this.player.setQuality("auto")));
1405
- for (let n of e) this.menu.append(this.menuItem(ze(n), t === n.index, () => this.player.setQuality(n.index)));
1597
+ for (let n of e) this.menu.append(this.menuItem(qe(n), t === n.index, () => this.player.setQuality(n.index)));
1406
1598
  }
1407
1599
  renderAudioMenu() {
1408
1600
  let e = this.player.getAudioTracks(), t = this.doc.createElement("div");
1409
1601
  t.className = "havik-menu__title", t.textContent = "Audio", this.menu.replaceChildren(t);
1410
- for (let t of e) this.menu.append(this.menuItem(Be(t), t.default, () => this.player.setAudioTrack(t.id)));
1602
+ for (let t of e) this.menu.append(this.menuItem(Je(t), t.default, () => this.player.setAudioTrack(t.id)));
1411
1603
  }
1412
1604
  renderCcMenu() {
1413
1605
  let e = this.player.getTextTracks(), t = this.doc.createElement("div");
1414
1606
  t.className = "havik-menu__title", t.textContent = "Subtitles", this.menu.replaceChildren(t, this.menuItem("Off", !1, () => this.player.setTextTrack(-1)));
1415
- for (let t of e) this.menu.append(this.menuItem(Ve(t), !1, () => this.player.setTextTrack(t.id)));
1607
+ for (let t of e) this.menu.append(this.menuItem(Ye(t), !1, () => this.player.setTextTrack(t.id)));
1416
1608
  }
1417
1609
  onTracksChanged() {
1418
1610
  this.openMenu && this.renderMenu(this.openMenu);
@@ -1518,27 +1710,27 @@ function Y(e) {
1518
1710
  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");
1519
1711
  return r > 0 ? `${r}:${i}:${a}` : `${i}:${a}`;
1520
1712
  }
1521
- function ze(e) {
1713
+ function qe(e) {
1522
1714
  return e.height ? `${e.height}p` : `${Math.round(e.bitrate / 1e3)} kbps`;
1523
1715
  }
1524
- function Be(e) {
1716
+ function Je(e) {
1525
1717
  return e.name || e.lang || `Track ${e.id}`;
1526
1718
  }
1527
- function Ve(e) {
1719
+ function Ye(e) {
1528
1720
  return e.name || e.lang || `Track ${e.id}`;
1529
1721
  }
1530
1722
  //#endregion
1531
1723
  //#region src/managed/index.ts
1532
- var He = 6e3, X = 45e3, Ue = 2e3, Z = 3, We = 2e3, Ge = 480 * 1e3;
1724
+ var Xe = 6e3, X = 45e3, Z = 2e3, Ze = 3, Qe = 2e3, $e = 480 * 1e3;
1533
1725
  async function Q(e) {
1534
1726
  if (!e.video || typeof e.video.play != "function") throw new t("INTERNAL", 0, "createPlayer: opts.video must be an HTMLVideoElement");
1535
1727
  if (!e.baseUrl) throw new t("INTERNAL", 0, "createPlayer: baseUrl is required");
1536
1728
  if (!e.matchUrn) throw new t("INVALID_URN", 0, "createPlayer: matchUrn is required");
1537
1729
  if (!e.credential) throw new t("INTERNAL", 0, "createPlayer: credential is required");
1538
- let n = new Ke(e);
1730
+ let n = new et(e);
1539
1731
  return await n.start(), n;
1540
1732
  }
1541
- var Ke = class {
1733
+ var et = class {
1542
1734
  opts;
1543
1735
  abort = new AbortController();
1544
1736
  listeners = {};
@@ -1559,6 +1751,8 @@ var Ke = class {
1559
1751
  armSseTimer = null;
1560
1752
  armFallbackCtrl = null;
1561
1753
  _lastError = null;
1754
+ analyticsSession = null;
1755
+ detachSeekListener;
1562
1756
  constructor(e) {
1563
1757
  this.opts = e;
1564
1758
  }
@@ -1648,13 +1842,13 @@ var Ke = class {
1648
1842
  !a && !f() && m();
1649
1843
  }, e);
1650
1844
  };
1651
- g(He), this.armingSub = C({
1845
+ g(Xe), this.armingSub = C({
1652
1846
  eventsBaseUrl: n,
1653
1847
  matchUrn: this.opts.matchUrn,
1654
1848
  credential: this.opts.credential,
1655
1849
  onAlive: () => g(X),
1656
1850
  onState: (e) => {
1657
- a || f() || (e.state === "live" || e.state === "gone") && setTimeout(h, Math.random() * Ue);
1851
+ a || f() || (e.state === "live" || e.state === "gone") && setTimeout(h, Math.random() * Z);
1658
1852
  },
1659
1853
  onError: () => m()
1660
1854
  });
@@ -1677,7 +1871,7 @@ var Ke = class {
1677
1871
  let n = await a(this.opts.credential);
1678
1872
  if (this.destroyed || t !== this.loadGen) return;
1679
1873
  this.setState("loading");
1680
- let r = this.createEngine(e, n), i = R(this.opts.video, (e) => {
1874
+ let r = this.createEngine(e, n), i = I(this.opts.video, (e) => {
1681
1875
  this._state === "ended" || this._state === "error" || this.setState(e);
1682
1876
  });
1683
1877
  if (await r.load(), this.destroyed || t !== this.loadGen) {
@@ -1688,7 +1882,32 @@ var Ke = class {
1688
1882
  i(), r.destroy();
1689
1883
  return;
1690
1884
  }
1691
- this.engine = r, this.detachMedia = i, this.opts.maxBitrate != null && r.setMaxBitrate(this.opts.maxBitrate), this.startStats(), this.scheduleLicenseRefresh(), this.startLiveState(t);
1885
+ this.engine = r, this.detachMedia = i, this.opts.maxBitrate != null && r.setMaxBitrate(this.opts.maxBitrate), this.startStats(), this.scheduleLicenseRefresh(), this.startLiveState(t), this.startAnalytics(e, r);
1886
+ }
1887
+ startAnalytics(e, t) {
1888
+ if (this.analyticsSession?.noteDestroyed(), this.analyticsSession = null, this.detachSeekListener?.(), this.opts.analytics === !1) return;
1889
+ let n = e.analytics?.sid;
1890
+ if (!n) return;
1891
+ let r = (typeof this.opts.analytics == "object" ? this.opts.analytics.endpoint : void 0) ?? Le(this.opts.baseUrl);
1892
+ if (!r) return;
1893
+ let i = this.opts.video, a = new Re({
1894
+ endpoint: r,
1895
+ sid: n,
1896
+ player: t.name === "native" ? "PLAYER_SAFARI_NATIVE" : "PLAYER_HLSJS",
1897
+ sdkVersion: ze,
1898
+ sample: () => {
1899
+ let e = this.engine?.getStats() ?? P, t = i.buffered.length ? Math.max(0, i.buffered.end(i.buffered.length - 1) - i.currentTime) : 0;
1900
+ return {
1901
+ ...e,
1902
+ bufferedMs: Math.round(t * 1e3),
1903
+ state: this._state,
1904
+ positionMs: Math.round(i.currentTime * 1e3)
1905
+ };
1906
+ }
1907
+ });
1908
+ this.analyticsSession = a;
1909
+ let o = () => a.noteSeek();
1910
+ i.addEventListener("seeking", o), this.detachSeekListener = () => i.removeEventListener("seeking", o), a.start();
1692
1911
  }
1693
1912
  createEngine(e, n) {
1694
1913
  let r = { ...this.opts.hlsConfig };
@@ -1705,17 +1924,17 @@ var Ke = class {
1705
1924
  hlsConfig: r,
1706
1925
  emit: (e) => this.onEngineEvent(e)
1707
1926
  }, a = this.opts.engine ?? "auto";
1708
- if (a === "native") return new V(i);
1709
- if (a === "hls") return new U(i);
1710
- if (e.drmEnabled && e.drm?.fairplay?.licenseUrl && e.drm.fairplay.certificateUrl && z(this.opts.video) && B()) return new V(i);
1711
- if (_e()) return new U(i);
1712
- if (z(this.opts.video)) return new V(i);
1927
+ if (a === "native") return new z(i);
1928
+ if (a === "hls") return new V(i);
1929
+ if (e.drmEnabled && e.drm?.fairplay?.licenseUrl && e.drm.fairplay.certificateUrl && L(this.opts.video) && R()) return new z(i);
1930
+ if (be()) return new V(i);
1931
+ if (L(this.opts.video)) return new z(i);
1713
1932
  throw new t("INTERNAL", 0, "no supported HLS playback engine in this browser");
1714
1933
  }
1715
1934
  onEngineEvent(e) {
1716
1935
  if (!this.destroyed) switch (e.type) {
1717
1936
  case "ready":
1718
- this.emit("ready"), this.opts.autoplay !== !1 && this.play();
1937
+ this.analyticsSession?.noteReady(), this.emit("ready"), this.opts.autoplay !== !1 && this.play();
1719
1938
  break;
1720
1939
  case "lowLatencyDetected":
1721
1940
  this._descriptor && (this._descriptor.isLowLatency = e.value);
@@ -1733,7 +1952,7 @@ var Ke = class {
1733
1952
  this.onStreamEnded();
1734
1953
  break;
1735
1954
  case "error":
1736
- e.fatal ? this.fail(e.error) : this.emit("warning", e.error);
1955
+ this.analyticsSession?.noteError(e.error.code, e.error.message, e.fatal, e.error.httpStatus ?? 0, Math.round(this.opts.video.currentTime * 1e3)), e.fatal ? this.fail(e.error) : this.emit("warning", e.error);
1737
1956
  break;
1738
1957
  }
1739
1958
  }
@@ -1754,7 +1973,7 @@ var Ke = class {
1754
1973
  r = 0, i = !1;
1755
1974
  },
1756
1975
  onError: () => {
1757
- this.destroyed || e !== this.loadGen || i || (r += 1, r >= Z && (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.`))));
1976
+ this.destroyed || e !== this.loadGen || i || (r += 1, r >= Ze && (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.`))));
1758
1977
  }
1759
1978
  });
1760
1979
  }
@@ -1776,7 +1995,7 @@ var Ke = class {
1776
1995
  }
1777
1996
  t.controls = !1;
1778
1997
  let n = (t.ownerDocument ?? document).createElement("div"), r = t.parentNode;
1779
- r && r.insertBefore(n, t), n.appendChild(t), K(n, this.opts.theme), this.container = n, this.controlBar = Le({
1998
+ r && r.insertBefore(n, t), n.appendChild(t), K(n, this.opts.theme), this.container = n, this.controlBar = Ge({
1780
1999
  root: n,
1781
2000
  video: t,
1782
2001
  player: this,
@@ -1799,9 +2018,11 @@ var Ke = class {
1799
2018
  }
1800
2019
  startStats() {
1801
2020
  this.stopStats();
1802
- let e = Math.max(500, this.opts.statsIntervalMs ?? We);
2021
+ let e = Math.max(500, this.opts.statsIntervalMs ?? Qe);
1803
2022
  this.statsTimer = setInterval(() => {
1804
- this.engine && this.emit("stats", this.engine.getStats());
2023
+ if (!this.engine) return;
2024
+ let e = this.engine.getStats();
2025
+ (e.bytesLoaded ?? 0) > 0 && this.analyticsSession?.noteFirstBytes(), this.emit("stats", e);
1805
2026
  }, e);
1806
2027
  }
1807
2028
  stopStats() {
@@ -1809,7 +2030,7 @@ var Ke = class {
1809
2030
  }
1810
2031
  scheduleLicenseRefresh() {
1811
2032
  this.stopRefresh();
1812
- let e = this.opts.licenseRefreshMs ?? Ge;
2033
+ let e = this.opts.licenseRefreshMs ?? $e;
1813
2034
  !e || !this._descriptor?.drmEnabled || (this.refreshTimer = setInterval(() => void this.refreshLicense(), e));
1814
2035
  }
1815
2036
  stopRefresh() {
@@ -1846,7 +2067,7 @@ var Ke = class {
1846
2067
  this.destroyed || (this.opts.video.volume = Math.min(1, Math.max(0, e)));
1847
2068
  }
1848
2069
  getStats() {
1849
- return this.engine?.getStats() ?? I;
2070
+ return this.engine?.getStats() ?? P;
1850
2071
  }
1851
2072
  getQualityLevels() {
1852
2073
  return this.engine?.getQualityLevels() ?? [];
@@ -1907,16 +2128,22 @@ var Ke = class {
1907
2128
  return n.add(t), () => n.delete(t);
1908
2129
  }
1909
2130
  destroy() {
1910
- this.destroyed || (this.destroyed = !0, this.loadGen += 1, this.abort.abort(), this.stopStats(), this.stopRefresh(), this.unmountControls(), this.closeArming(), this.closeLiveState(), this.detachMedia?.(), this.detachPip?.(), this.engine?.destroy(), this.engine = null, this.setState("idle"), this.clearListeners());
2131
+ this.destroyed || (this.destroyed = !0, this.loadGen += 1, this.abort.abort(), this.stopStats(), this.stopRefresh(), this.unmountControls(), this.closeArming(), this.closeLiveState(), this.detachMedia?.(), this.detachPip?.(), this.analyticsSession?.noteDestroyed(), this.analyticsSession = null, this.detachSeekListener?.(), this.engine?.destroy(), this.engine = null, this.setState("idle"), this.clearListeners());
1911
2132
  }
1912
2133
  clearListeners() {
1913
2134
  for (let e of Object.keys(this.listeners)) this.listeners[e]?.clear();
1914
2135
  }
1915
2136
  fail(e) {
1916
- this._lastError = e, this.setState("error"), this.emit("error", e);
2137
+ this._lastError = e, this.setState("error"), this.analyticsSession?.noteFatal(), this.emit("error", e);
1917
2138
  }
1918
2139
  setState(e) {
1919
- if (e !== this._state) switch (this._state = e, this.emit("statechange", e), e) {
2140
+ if (e === this._state) return;
2141
+ let t = this._state;
2142
+ if (this._state = e, this.analyticsSession) {
2143
+ let n = this.opts.video;
2144
+ this.analyticsSession.noteStateChange(t, e, Math.round(n.currentTime * 1e3), this.engine?.getStats().levelHeight), e === "ended" && this.analyticsSession.noteEnded();
2145
+ }
2146
+ switch (this.emit("statechange", e), e) {
1920
2147
  case "playing":
1921
2148
  this.emit("playing");
1922
2149
  break;
@@ -1947,7 +2174,7 @@ function $(e) {
1947
2174
  }
1948
2175
  //#endregion
1949
2176
  //#region src/embed/host.ts
1950
- function qe(e) {
2177
+ function tt(e) {
1951
2178
  if (!e.container || typeof e.container.appendChild != "function") throw Error("mountEmbed: opts.container must be a DOM element");
1952
2179
  if (!e.src) throw Error("mountEmbed: opts.src is required");
1953
2180
  if (!e.baseUrl || !e.matchUrn) throw Error("mountEmbed: opts.baseUrl and opts.matchUrn are required");
@@ -2006,8 +2233,8 @@ function qe(e) {
2006
2233
  }
2007
2234
  //#endregion
2008
2235
  //#region src/embed/page.ts
2009
- var Je = /^[A-Za-z0-9:_.-]{1,128}$/;
2010
- function Ye() {
2236
+ var nt = /^[A-Za-z0-9:_.-]{1,128}$/;
2237
+ function rt() {
2011
2238
  let e = window.HAVIK_EMBED_CONFIG;
2012
2239
  if (e) return {
2013
2240
  baseUrl: e.baseUrl ?? "",
@@ -2029,9 +2256,9 @@ function Ye() {
2029
2256
  waitForLive: !0
2030
2257
  };
2031
2258
  }
2032
- async function Xe() {
2033
- let e = Ye();
2034
- Qe();
2259
+ async function it() {
2260
+ let e = rt();
2261
+ ot();
2035
2262
  let n = document.createElement("video");
2036
2263
  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);
2037
2264
  let r = e.parentOrigin && e.parentOrigin !== "null" ? e.parentOrigin : null;
@@ -2086,7 +2313,7 @@ async function Xe() {
2086
2313
  code: e.code,
2087
2314
  message: e.message
2088
2315
  })), e.on("autoplayblocked", () => {
2089
- i({ type: "oddin:autoplayblocked" }), Ze(n, e);
2316
+ i({ type: "oddin:autoplayblocked" }), at(n, e);
2090
2317
  });
2091
2318
  }, s = async (r) => {
2092
2319
  a?.destroy(), a = null;
@@ -2152,7 +2379,7 @@ async function Xe() {
2152
2379
  a?.retry();
2153
2380
  break;
2154
2381
  case "oddin:load":
2155
- typeof e.matchUrn == "string" && Je.test(e.matchUrn) && s(e.matchUrn);
2382
+ typeof e.matchUrn == "string" && nt.test(e.matchUrn) && s(e.matchUrn);
2156
2383
  break;
2157
2384
  case "oddin:destroy":
2158
2385
  c();
@@ -2166,18 +2393,18 @@ async function Xe() {
2166
2393
  protocol: 1
2167
2394
  });
2168
2395
  }
2169
- function Ze(e, t) {
2396
+ function at(e, t) {
2170
2397
  if (document.getElementById("havik-tap")) return;
2171
2398
  let n = document.createElement("button");
2172
2399
  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", () => {
2173
2400
  n.remove(), t.play();
2174
2401
  }), document.body.appendChild(n);
2175
2402
  }
2176
- function Qe() {
2403
+ function ot() {
2177
2404
  let e = document.createElement("style");
2178
2405
  e.textContent = "html,body{margin:0;height:100%;background:#000;overflow:hidden}body{display:flex}", document.head.appendChild(e);
2179
2406
  }
2180
2407
  //#endregion
2181
- export { W as ODDIN_THEME, t as PlaybackError, K as applyTheme, Xe as bootEmbed, fe as clearDeviceId, Q as createPlayer, w as deriveEventsBaseUrl, N as detectDrmSupport, S as fetchCatalog, ce as fetchTournament, ue as flattenMatches, D as getDeviceId, F as isLowLatencyManifest, $ as isOddinMessage, me as licenseRequestHeaders, qe as mountEmbed, _ as pollToLive, y as resolveStream, C as subscribeLiveState, de as watchStatus };
2408
+ export { W as ODDIN_THEME, t as PlaybackError, K as applyTheme, it as bootEmbed, fe as clearDeviceId, Q as createPlayer, w as deriveEventsBaseUrl, j as detectDrmSupport, x as fetchCatalog, ce as fetchTournament, ue as flattenMatches, D as getDeviceId, N as isLowLatencyManifest, $ as isOddinMessage, me as licenseRequestHeaders, tt as mountEmbed, _ as pollToLive, y as resolveStream, C as subscribeLiveState, de as watchStatus };
2182
2409
 
2183
2410
  //# sourceMappingURL=index.js.map