@oddin-gg/havik-player 2.16.4 → 2.16.5

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
@@ -1964,19 +1964,34 @@ var Wt = class {
1964
1964
  let r = e.response?.code ?? 0, a = e.details ?? e.type, o = e.reason ?? e.error?.message ?? "";
1965
1965
  return new n(t === "DRM" ? i(r) : r === 401 ? "UNAUTHORIZED" : r === 403 ? "FORBIDDEN" : "INTERNAL", r, `${t} error: ${a}${o ? ` (${o})` : ""}`);
1966
1966
  }
1967
- }, Gt = 15e3, Kt = 6e5, qt = 3e5, Jt = 100, q = 1, Yt = 1e3, Xt = 3e4, Zt = 1e3, Qt = {
1967
+ };
1968
+ //#endregion
1969
+ //#region src/core/drm/signedUrl.ts
1970
+ function Gt(e, t = Date.now()) {
1971
+ if (!e) return !1;
1972
+ let n;
1973
+ try {
1974
+ n = new URL(e).searchParams.get("exp");
1975
+ } catch {
1976
+ return !1;
1977
+ }
1978
+ return !n || !/^\d+$/.test(n) ? !1 : Number(n) * 1e3 <= t;
1979
+ }
1980
+ //#endregion
1981
+ //#region src/core/analytics.ts
1982
+ var Kt = 15e3, qt = 6e5, Jt = 3e5, Yt = 100, q = 1, Xt = 1e3, Zt = 3e4, Qt = 1e3, $t = {
1968
1983
  inline: "PRESENTATION_INLINE",
1969
1984
  fullscreen: "PRESENTATION_FULLSCREEN",
1970
1985
  pip: "PRESENTATION_PIP"
1971
- }, $t = {
1986
+ }, en = {
1972
1987
  widevine: "KEY_SYSTEM_WIDEVINE",
1973
1988
  fairplay: "KEY_SYSTEM_FAIRPLAY",
1974
1989
  none: "KEY_SYSTEM_NONE"
1975
1990
  };
1976
- function en(e, t) {
1991
+ function tn(e, t) {
1977
1992
  return e.length === 0 ? 0 : e[Math.min(e.length, Math.max(1, Math.ceil(t / 100 * e.length))) - 1] ?? 0;
1978
1993
  }
1979
- var tn = /* @__PURE__ */ new Set([
1994
+ var nn = /* @__PURE__ */ new Set([
1980
1995
  "INVALID_URN",
1981
1996
  "NOT_FOUND",
1982
1997
  "GONE",
@@ -1996,7 +2011,7 @@ var tn = /* @__PURE__ */ new Set([
1996
2011
  ended: "PLAYER_STATE_ENDED",
1997
2012
  error: "PLAYER_STATE_ERROR"
1998
2013
  };
1999
- function nn(e) {
2014
+ function rn(e) {
2000
2015
  try {
2001
2016
  let t = new URL(e), [n, ...r] = t.hostname.split(".");
2002
2017
  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);
@@ -2004,8 +2019,8 @@ function nn(e) {
2004
2019
  return;
2005
2020
  }
2006
2021
  }
2007
- var rn = (e, t) => e.replace(/\/$/, "").toLowerCase() === t.replace(/\/$/, "").toLowerCase(), an = /* @__PURE__ */ new Set(), Y = /* @__PURE__ */ new Set(), X = /* @__PURE__ */ new Set(), on = (e) => `this page's Content-Security-Policy blocks QoE beacons. Add ${e} to connect-src; until then none are sent.`;
2008
- function sn(e) {
2022
+ var an = (e, t) => e.replace(/\/$/, "").toLowerCase() === t.replace(/\/$/, "").toLowerCase(), on = /* @__PURE__ */ new Set(), Y = /* @__PURE__ */ new Set(), X = /* @__PURE__ */ new Set(), sn = (e) => `this page's Content-Security-Policy blocks QoE beacons. Add ${e} to connect-src; until then none are sent.`;
2023
+ function cn(e) {
2009
2024
  if (e.disposition !== "enforce" || e.effectiveDirective !== "connect-src") return;
2010
2025
  let t;
2011
2026
  try {
@@ -2014,19 +2029,19 @@ function sn(e) {
2014
2029
  } catch {
2015
2030
  return;
2016
2031
  }
2017
- if (an.has(t) && !Y.has(t)) {
2018
- Y.add(t), g(on(new URL(t).origin));
2032
+ if (on.has(t) && !Y.has(t)) {
2033
+ Y.add(t), g(sn(new URL(t).origin));
2019
2034
  for (let e of X) e.noteCspBlocked();
2020
2035
  }
2021
2036
  }
2022
- function cn(e, t) {
2037
+ function ln(e, t) {
2023
2038
  return g(t, e), { disabledReason: t };
2024
2039
  }
2025
- function ln(e, t, n) {
2026
- if (n) return t && !rn(t, n) && g(`analytics.endpoint "${t}" ignored; the server named "${n}".`), { endpoint: n };
2040
+ function un(e, t, n) {
2041
+ if (n) return t && !an(t, n) && g(`analytics.endpoint "${t}" ignored; the server named "${n}".`), { endpoint: n };
2027
2042
  if (t) return { endpoint: t };
2028
- if (t !== void 0) return cn("empty-endpoint", "analytics is disabled: no QoE beacons will be sent. analytics.endpoint was supplied but empty. Pass a real beacons origin, analytics: { endpoint: 'https://beacons[-dev].<domain>' }, or omit the option to derive it from baseUrl (or analytics: false to opt out).");
2029
- let r = nn(e);
2043
+ if (t !== void 0) return ln("empty-endpoint", "analytics is disabled: no QoE beacons will be sent. analytics.endpoint was supplied but empty. Pass a real beacons origin, analytics: { endpoint: 'https://beacons[-dev].<domain>' }, or omit the option to derive it from baseUrl (or analytics: false to opt out).");
2044
+ let r = rn(e);
2030
2045
  if (r) return { endpoint: r };
2031
2046
  let i;
2032
2047
  try {
@@ -2034,9 +2049,9 @@ function ln(e, t, n) {
2034
2049
  } catch {
2035
2050
  i = e;
2036
2051
  }
2037
- return cn(i, `analytics is disabled: no QoE beacons will be sent. The beacons endpoint cannot be derived from the baseUrl host "${i}" — the convention is feed[-dev].<domain> → beacons[-dev].<domain>. Pass an explicit endpoint, analytics: { endpoint: 'https://beacons[-dev].<domain>' }, to createPlayer (or analytics: false to opt out deliberately).`);
2052
+ return ln(i, `analytics is disabled: no QoE beacons will be sent. The beacons endpoint cannot be derived from the baseUrl host "${i}" — the convention is feed[-dev].<domain> → beacons[-dev].<domain>. Pass an explicit endpoint, analytics: { endpoint: 'https://beacons[-dev].<domain>' }, to createPlayer (or analytics: false to opt out deliberately).`);
2038
2053
  }
2039
- var un = class {
2054
+ var dn = class {
2040
2055
  opts;
2041
2056
  url;
2042
2057
  cspKey;
@@ -2118,8 +2133,8 @@ var un = class {
2118
2133
  constructor(e) {
2119
2134
  this.opts = {
2120
2135
  ...e,
2121
- heartbeatMs: e.heartbeatMs ?? Gt,
2122
- idleMs: e.idleMs ?? Kt
2136
+ heartbeatMs: e.heartbeatMs ?? Kt,
2137
+ idleMs: e.idleMs ?? qt
2123
2138
  }, this.url = `${e.endpoint.replace(/\/$/, "")}/v1/beacons`;
2124
2139
  try {
2125
2140
  let e = new URL(this.url);
@@ -2136,11 +2151,11 @@ var un = class {
2136
2151
  sdkVersion: this.opts.sdkVersion,
2137
2152
  ...this.opts.environment
2138
2153
  };
2139
- this.pendingStart = e, this.enqueue({ sessionStart: e }), this.firstFlushTimer = setTimeout(() => this.flush(), Yt), this.lastTickMs = this.now(), this.timer = setInterval(() => this.heartbeat(), this.opts.heartbeatMs), typeof window < "u" && window.addEventListener("pagehide", this.onPagehide), typeof document < "u" && (document.addEventListener("visibilitychange", this.onVisibility), document.addEventListener("freeze", this.onFreeze), document.addEventListener("resume", this.onResume), this.cspKey && an.add(this.cspKey), document.addEventListener("securitypolicyviolation", sn), X.add(this), this.noteCspBlocked(), this.onVisibility());
2154
+ this.pendingStart = e, this.enqueue({ sessionStart: e }), this.firstFlushTimer = setTimeout(() => this.flush(), Xt), this.lastTickMs = this.now(), this.timer = setInterval(() => this.heartbeat(), this.opts.heartbeatMs), typeof window < "u" && window.addEventListener("pagehide", this.onPagehide), typeof document < "u" && (document.addEventListener("visibilitychange", this.onVisibility), document.addEventListener("freeze", this.onFreeze), document.addEventListener("resume", this.onResume), this.cspKey && on.add(this.cspKey), document.addEventListener("securitypolicyviolation", cn), X.add(this), this.noteCspBlocked(), this.onVisibility());
2140
2155
  }
2141
2156
  checkSuspension(e) {
2142
2157
  if (this.endedFlag || !this.opts.idleMs) return !1;
2143
- let t = Math.max(this.opts.idleMs, qt);
2158
+ let t = Math.max(this.opts.idleMs, Jt);
2144
2159
  return this.lastTickMs > 0 && e - this.lastTickMs >= t ? (this.endIdle(this.lastTickMs), !0) : (this.lastTickMs = e, !1);
2145
2160
  }
2146
2161
  syncIdle(e) {
@@ -2163,12 +2178,12 @@ var un = class {
2163
2178
  let a = {
2164
2179
  fromState: J[e] ?? "PLAYER_STATE_UNSPECIFIED",
2165
2180
  toState: J[t] ?? "PLAYER_STATE_UNSPECIFIED",
2166
- presentation: Qt[this.presentation]
2181
+ presentation: $t[this.presentation]
2167
2182
  }, o = this.causeOf(e, t, i);
2168
2183
  o && (a.cause = o), n > 0 && (a.positionMs = Math.round(n)), r && (a.renditionHeight = r), this.enqueue({ stateChange: a });
2169
2184
  }
2170
2185
  seekInduced(e) {
2171
- return this.lastSeekMs >= 0 && e - this.lastSeekMs < Zt;
2186
+ return this.lastSeekMs >= 0 && e - this.lastSeekMs < Qt;
2172
2187
  }
2173
2188
  causeOf(e, t, n) {
2174
2189
  if (t === "error") return "TRANSITION_CAUSE_ERROR";
@@ -2242,7 +2257,7 @@ var un = class {
2242
2257
  let r = n ?? "";
2243
2258
  if (r !== "") return /^fragLoad/i.test(r) ? this.sawFirstBytes ? "ERROR_STAGE_SEGMENT" : "ERROR_STAGE_FIRST_SEGMENT" : /^(manifest|level|audioTrack|subtitleTrack)/i.test(r) ? "ERROR_STAGE_MANIFEST" : /key|licen[cs]e/i.test(r) ? "ERROR_STAGE_DRM" : /parsing|append|buffer|remux|decrypt|media/i.test(r) ? "ERROR_STAGE_DECODE" : "ERROR_STAGE_OTHER";
2244
2259
  if (e === "DRM_CLIENT") return "ERROR_STAGE_DRM";
2245
- if (tn.has(e)) return "ERROR_STAGE_RESOLVE";
2260
+ if (nn.has(e)) return "ERROR_STAGE_RESOLVE";
2246
2261
  let i = t ?? "";
2247
2262
  return /frag|segment/i.test(i) ? this.sawFirstBytes ? "ERROR_STAGE_SEGMENT" : "ERROR_STAGE_FIRST_SEGMENT" : /manifest|level|playlist/i.test(i) ? "ERROR_STAGE_MANIFEST" : /key|licen[cs]e/i.test(i) ? "ERROR_STAGE_DRM" : /buffer|decode|media|append/i.test(i) ? "ERROR_STAGE_DECODE" : "ERROR_STAGE_OTHER";
2248
2263
  }
@@ -2289,14 +2304,14 @@ var un = class {
2289
2304
  if (this.lastRequestCount = g, _ > 0) {
2290
2305
  o.requestCountDelta = _;
2291
2306
  let e = t.requestTimesMs ?? [], n = e.slice(Math.max(0, e.length - _)).sort((e, t) => e - t);
2292
- n.length > 0 && (o.requestTimeP50Ms = en(n, 50), o.requestTimeP95Ms = en(n, 95));
2307
+ n.length > 0 && (o.requestTimeP50Ms = tn(n, 50), o.requestTimeP95Ms = tn(n, 95));
2293
2308
  }
2294
2309
  typeof t.latencySeconds == "number" && typeof t.targetLatencySeconds == "number" && (o.driftMs = Math.round((t.latencySeconds - t.targetLatencySeconds) * 1e3)), typeof t.playbackRate == "number" && t.playbackRate > 0 && (o.playbackRate = t.playbackRate), t.qualityPinned === !0 && (o.qualityPinned = !0), t.cdnHost && (o.cdnHost = t.cdnHost), this.maybeEmitMediaInfo(t), this.enqueue({ heartbeat: o }), this.flush(), s > 0 && this.noteFirstBytes();
2295
2310
  }
2296
2311
  maybeEmitMediaInfo(e) {
2297
2312
  if (!(e.videoCodec ?? e.keySystem ?? e.ladderHeights)) return;
2298
2313
  let t = {};
2299
- e.videoCodec && (t.videoCodec = e.videoCodec), e.audioCodec && (t.audioCodec = e.audioCodec), e.keySystem && (t.keySystem = $t[e.keySystem]), e.drmSecurityLevel && (t.drmSecurityLevel = e.drmSecurityLevel), e.ladderHeights?.length && (t.ladderHeights = e.ladderHeights), e.ladderTopBitrateKbps && (t.ladderTopBitrateKbps = e.ladderTopBitrateKbps), e.lowLatency && (t.lowLatency = !0);
2314
+ e.videoCodec && (t.videoCodec = e.videoCodec), e.audioCodec && (t.audioCodec = e.audioCodec), e.keySystem && (t.keySystem = en[e.keySystem]), e.drmSecurityLevel && (t.drmSecurityLevel = e.drmSecurityLevel), e.ladderHeights?.length && (t.ladderHeights = e.ladderHeights), e.ladderTopBitrateKbps && (t.ladderTopBitrateKbps = e.ladderTopBitrateKbps), e.lowLatency && (t.lowLatency = !0);
2300
2315
  let n = JSON.stringify(t);
2301
2316
  n !== this.mediaInfoSignature && (this.mediaInfoSignature = n, this.enqueue({ mediaInfo: t }));
2302
2317
  }
@@ -2316,7 +2331,7 @@ var un = class {
2316
2331
  let c = r?.positionMs ?? 0;
2317
2332
  c > 0 && (i.exitPositionMs = Math.round(c)), this.activeTotalMs > 0 && (i.activeWatchTimeMs = Math.round(this.activeTotalMs)), this.mutedTotalMs > 0 && (i.mutedTimeMs = Math.round(this.mutedTotalMs));
2318
2333
  let l = e !== "END_REASON_ENDED";
2319
- l && this.lastStallMs > 0 && n - this.lastStallMs <= Xt && (i.abandonAfterStallMs = Math.max(q, Math.round(n - this.lastStallMs))), l && this.lastErrorMs > 0 && n - this.lastErrorMs <= Xt && (i.abandonAfterErrorMs = Math.max(q, Math.round(n - this.lastErrorMs))), this.fullscreenTotalMs > 0 && (i.fullscreenTimeMs = Math.round(this.fullscreenTotalMs)), this.pipTotalMs > 0 && (i.pipTimeMs = Math.round(this.pipTotalMs)), this.volumeChanges > 0 && (i.volumeChanges = this.volumeChanges), this.muteChanges > 0 && (i.muteChanges = this.muteChanges);
2334
+ l && this.lastStallMs > 0 && n - this.lastStallMs <= Zt && (i.abandonAfterStallMs = Math.max(q, Math.round(n - this.lastStallMs))), l && this.lastErrorMs > 0 && n - this.lastErrorMs <= Zt && (i.abandonAfterErrorMs = Math.max(q, Math.round(n - this.lastErrorMs))), this.fullscreenTotalMs > 0 && (i.fullscreenTimeMs = Math.round(this.fullscreenTotalMs)), this.pipTotalMs > 0 && (i.pipTimeMs = Math.round(this.pipTotalMs)), this.volumeChanges > 0 && (i.volumeChanges = this.volumeChanges), this.muteChanges > 0 && (i.muteChanges = this.muteChanges);
2320
2335
  let u = r?.decodedFrames ?? this.lastDecoded;
2321
2336
  u > 0 && (i.decodedFramesTotal = u), e === "END_REASON_STOPPED" && this.startupTimeMs === 0 && this.lastErrorStage === null && (e = "END_REASON_ABORTED", i.reason = e), this.startupTimeMs === 0 && e !== "END_REASON_ABORTED" && (i.failedStage = this.lastErrorStage ?? (this.manifestLoadMs === 0 ? "ERROR_STAGE_MANIFEST" : this.firstFragLoadMs === 0 ? "ERROR_STAGE_FIRST_SEGMENT" : "ERROR_STAGE_OTHER")), this.pending.push(this.wrap({ sessionEnd: i })), this.flush(e === "END_REASON_PAGEHIDE");
2322
2337
  }
@@ -2336,13 +2351,13 @@ var un = class {
2336
2351
  };
2337
2352
  }
2338
2353
  noteCspBlocked() {
2339
- this.cspBlockedNoted || this.endedFlag || !this.cspBlocked() || (this.cspBlockedNoted = !0, this.opts.onBlocked?.(on(new URL(this.url).origin)));
2354
+ this.cspBlockedNoted || this.endedFlag || !this.cspBlocked() || (this.cspBlockedNoted = !0, this.opts.onBlocked?.(sn(new URL(this.url).origin)));
2340
2355
  }
2341
2356
  cspBlocked() {
2342
2357
  return !!this.cspKey && Y.has(this.cspKey);
2343
2358
  }
2344
2359
  enqueue(e) {
2345
- if (!this.cspBlocked()) for (this.pending.push(this.wrap(e)); this.pending.length > Jt;) {
2360
+ if (!this.cspBlocked()) for (this.pending.push(this.wrap(e)); this.pending.length > Yt;) {
2346
2361
  let e = this.pending.findIndex((e) => e.heartbeat || e.stateChange);
2347
2362
  this.pending.splice(e >= 0 ? e : 0, 1);
2348
2363
  }
@@ -2369,7 +2384,7 @@ var un = class {
2369
2384
  };
2370
2385
  //#endregion
2371
2386
  //#region src/core/environment.ts
2372
- function dn() {
2387
+ function fn() {
2373
2388
  if (typeof window > "u") return { integration: "INTEGRATION_KIND_SDK" };
2374
2389
  let e;
2375
2390
  try {
@@ -2394,14 +2409,14 @@ function dn() {
2394
2409
  integration: "INTEGRATION_KIND_IFRAME"
2395
2410
  };
2396
2411
  }
2397
- function fn() {
2412
+ function pn() {
2398
2413
  if (typeof navigator > "u") return "DEVICE_CLASS_OTHER";
2399
2414
  let e = navigator.userAgentData, t = e?.platform ?? "";
2400
2415
  if (/tv|tizen|webos/i.test(t)) return "DEVICE_CLASS_TV";
2401
2416
  let n = typeof screen < "u" ? Math.min(screen.width, screen.height) : 0;
2402
2417
  return e?.mobile ?? (typeof navigator.maxTouchPoints == "number" && navigator.maxTouchPoints > 1 && n > 0 && n < 900) ? n >= 600 ? "DEVICE_CLASS_TABLET" : "DEVICE_CLASS_PHONE" : e || n >= 900 || /mac|win|linux|chrome os/i.test(t) ? "DEVICE_CLASS_DESKTOP" : "DEVICE_CLASS_OTHER";
2403
2418
  }
2404
- function pn() {
2419
+ function mn() {
2405
2420
  let e = typeof navigator < "u" ? navigator.connection : void 0;
2406
2421
  if (!e) return { connectionType: "CONNECTION_TYPE_UNKNOWN" };
2407
2422
  let t = {
@@ -2420,17 +2435,17 @@ function pn() {
2420
2435
  }
2421
2436
  return typeof e.downlink == "number" && e.downlink > 0 && (i.downlinkKbps = Math.round(e.downlink * 1e3)), typeof e.rtt == "number" && e.rtt > 0 && (i.rttMs = Math.round(e.rtt)), e.saveData && (i.saveData = !0), i;
2422
2437
  }
2423
- function mn(e) {
2424
- let t = dn(), n = {
2425
- deviceClass: fn(),
2438
+ function hn(e) {
2439
+ let t = fn(), n = {
2440
+ deviceClass: pn(),
2426
2441
  integration: t.integration,
2427
- ...pn()
2442
+ ...mn()
2428
2443
  };
2429
2444
  t.host && (n.host = t.host), typeof screen < "u" && screen.width > 0 && (n.screenWidth = screen.width, n.screenHeight = screen.height), typeof window < "u" && window.devicePixelRatio > 0 && (n.devicePixelRatio = Math.round(window.devicePixelRatio * 100) / 100);
2430
2445
  let r = e.getBoundingClientRect?.();
2431
2446
  return r && r.width > 0 && (n.viewportWidth = Math.round(r.width), n.viewportHeight = Math.round(r.height)), n;
2432
2447
  }
2433
- async function hn() {
2448
+ async function gn() {
2434
2449
  let e = typeof navigator < "u" ? navigator.userAgentData : void 0;
2435
2450
  if (!e?.getHighEntropyValues) return {};
2436
2451
  try {
@@ -2442,7 +2457,7 @@ async function hn() {
2442
2457
  }
2443
2458
  //#endregion
2444
2459
  //#region src/core/version.ts
2445
- var gn = "2.16.4", _n = {
2460
+ var _n = "2.16.5", vn = {
2446
2461
  accent: "#E1B600",
2447
2462
  accentText: "#1B1C23",
2448
2463
  background: "#0e0f13",
@@ -2453,7 +2468,7 @@ var gn = "2.16.4", _n = {
2453
2468
  border: "rgba(255, 255, 255, 0.1)",
2454
2469
  radius: "8px",
2455
2470
  fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
2456
- }, vn = {
2471
+ }, yn = {
2457
2472
  accent: "--havik-accent",
2458
2473
  accentText: "--havik-accent-text",
2459
2474
  background: "--havik-bg",
@@ -2467,24 +2482,24 @@ var gn = "2.16.4", _n = {
2467
2482
  };
2468
2483
  function Z(e, t) {
2469
2484
  let n = {
2470
- ..._n,
2485
+ ...vn,
2471
2486
  ...t
2472
2487
  };
2473
- for (let t of Object.keys(vn)) e.style.setProperty(vn[t], n[t]);
2488
+ for (let t of Object.keys(yn)) e.style.setProperty(yn[t], n[t]);
2474
2489
  n.logoUrl && e.style.setProperty("--havik-logo", `url("${n.logoUrl}")`);
2475
2490
  }
2476
2491
  //#endregion
2477
2492
  //#region src/ui/styles.ts
2478
- var yn = "havik-player-styles", bn = "\n.havik-player{position:relative;display:block;width:100%;background:var(--havik-bg,#0e0f13);\n color:var(--havik-text,#fff);font-family:var(--havik-font,system-ui,sans-serif);\n overflow:hidden;line-height:1.4;-webkit-tap-highlight-color:transparent}\n.havik-player video{display:block;width:100%;height:auto;aspect-ratio:16/9;\n object-fit:contain;background:#000;outline:none}\n.havik-player:focus{outline:none}\n.havik-player *{box-sizing:border-box}\n\n.havik-player:fullscreen,.havik-player:-webkit-full-screen{display:flex;align-items:center;\n justify-content:center;width:100vw;height:100vh;aspect-ratio:auto}\n.havik-player:fullscreen video,.havik-player:-webkit-full-screen video{width:100%;height:100%;\n aspect-ratio:auto;object-fit:contain}\n\n.havik-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;cursor:pointer;\n touch-action:manipulation}\n.havik-gradient{position:absolute;left:0;right:0;bottom:0;height:120px;pointer-events:none;\n background:linear-gradient(to top,rgba(0,0,0,.78),transparent);\n opacity:1;transition:opacity .25s}\n.havik-player.havik-hide .havik-gradient,\n.havik-player.havik-hide .havik-bar{opacity:0;pointer-events:none}\n.havik-player.havik-hide{cursor:none}\n\n.havik-center{width:74px;height:74px;border-radius:50%;border:0;cursor:pointer;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);\n display:flex;align-items:center;justify-content:center;\n box-shadow:0 6px 24px rgba(0,0,0,.45);transition:transform .15s,opacity .15s}\n.havik-center:hover{transform:scale(1.06)}\n.havik-center svg{width:38px;height:38px;margin-left:2px}\n.havik-center.havik-gone{opacity:0;pointer-events:none;transform:scale(.8)}\n\n/* Absolutely centred, NOT a flex sibling of .havik-center: the hidden center\n button (.havik-gone) still occupies its 74px in the row, which pushed an\n in-flow spinner 37px right of centre. */\n.havik-spinner{position:absolute;inset:0;margin:auto;width:56px;height:56px;border-radius:50%;\n border:4px solid rgba(255,255,255,.2);border-top-color:var(--havik-accent,#E1B600);\n animation:havik-spin .8s linear infinite}\n@keyframes havik-spin{to{transform:rotate(360deg)}}\n\n/* Status card — branded full-bleed overlay for the stopped/pre-play states\n (ended / fatal error / pre-play / armed-waiting). */\n.havik-status{position:absolute;inset:0;display:flex;flex-direction:column;gap:16px;\n align-items:center;justify-content:center;background:rgba(14,15,19,.82);text-align:center;padding:24px}\n.havik-status__logo{width:128px;height:46px;background:var(--havik-logo) center/contain no-repeat;opacity:.95}\n.havik-status__msg{color:var(--havik-text,#fff);font-size:15px;font-weight:600;max-width:480px}\n.havik-status__btn{display:inline-flex;align-items:center;gap:8px;border:0;cursor:pointer;\n padding:10px 18px;border-radius:var(--havik-radius,8px);font-weight:700;font-size:14px;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);transition:transform .15s}\n.havik-status__btn:hover{transform:scale(1.04)}\n.havik-status__btn svg{width:18px;height:18px}\n/* Pre-play: lighter scrim so the poster shows through, and a round play CTA. */\n.havik-status--preplay{background:rgba(14,15,19,.4)}\n.havik-status--preplay .havik-status__btn--play{width:74px;height:74px;border-radius:50%;padding:0;\n justify-content:center;box-shadow:0 6px 24px rgba(0,0,0,.45)}\n.havik-status--preplay .havik-status__btn--play svg{width:38px;height:38px;margin-left:3px}\n\n.havik-watermark{position:absolute;top:12px;right:14px;height:22px;opacity:.85;pointer-events:none;\n content:'';background:var(--havik-logo) center/contain no-repeat;width:90px}\n/* A status card carries its own logo — the corner watermark would duplicate it. */\n.havik-player.havik-has-status .havik-watermark{display:none}\n\n.havik-bar{position:absolute;left:0;right:0;bottom:0;padding:0 12px 8px;\n opacity:1;transition:opacity .25s;z-index:2}\n.havik-seek{position:relative;height:16px;display:flex;align-items:center;cursor:pointer;margin-bottom:2px}\n.havik-seek__track{position:relative;width:100%;height:4px;border-radius:3px;background:rgba(255,255,255,.25)}\n.havik-seek__buffered{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:rgba(255,255,255,.4)}\n.havik-seek__played{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-seek__thumb{position:absolute;top:50%;width:13px;height:13px;border-radius:50%;\n background:var(--havik-accent,#E1B600);transform:translate(-50%,-50%) scale(0);transition:transform .1s}\n.havik-seek:hover .havik-seek__thumb{transform:translate(-50%,-50%) scale(1)}\n\n.havik-controls{display:flex;align-items:center;gap:6px;height:40px}\n.havik-spacer{flex:1}\n.havik-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;border-radius:var(--havik-radius,8px);\n padding:0;transition:background .12s,color .12s}\n.havik-btn:hover{background:rgba(255,255,255,.12)}\n.havik-btn:focus-visible{outline:2px solid var(--havik-accent,#E1B600);outline-offset:1px}\n.havik-btn svg{width:22px;height:22px}\n\n/* The volume level is filled in accent up to the thumb, like the seek bar's\n played span — a native range input paints no \"elapsed\" side, which left the\n level readable only from the dot's position. --havik-vol (0..1) is set by\n syncVolume(); the track width and thumb size live here as variables because\n the WebKit fill has to compute the thumb centre from both. */\n.havik-vol{display:flex;align-items:center}\n.havik-vol__slider{--havik-vol-w:70px;--havik-vol-thumb:12px;--havik-vol:1;\n --havik-vol-pos:calc(var(--havik-vol-thumb) / 2 + var(--havik-vol) * (var(--havik-vol-w) - var(--havik-vol-thumb)));\n width:0;opacity:0;transition:width .18s,opacity .18s;height:4px;cursor:pointer;\n appearance:none;-webkit-appearance:none;background:transparent;border-radius:3px;margin:0 4px}\n.havik-vol:hover .havik-vol__slider,.havik-vol:focus-within .havik-vol__slider{width:var(--havik-vol-w);opacity:1}\n/* WebKit/Blink have no progress pseudo-element: paint the fill as a hard-stop\n gradient that ends at the thumb centre. */\n.havik-vol__slider::-webkit-slider-runnable-track{height:4px;border-radius:3px;\n background:linear-gradient(to right,var(--havik-accent,#E1B600) var(--havik-vol-pos),\n rgba(255,255,255,.3) var(--havik-vol-pos))}\n/* Firefox fills natively, and tracks the thumb exactly. */\n.havik-vol__slider::-moz-range-track{height:4px;border-radius:3px;background:rgba(255,255,255,.3)}\n.havik-vol__slider::-moz-range-progress{height:4px;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-vol__slider::-webkit-slider-thumb{-webkit-appearance:none;width:var(--havik-vol-thumb);\n height:var(--havik-vol-thumb);border-radius:50%;background:var(--havik-accent,#E1B600);\n margin-top:calc((4px - var(--havik-vol-thumb)) / 2)}\n.havik-vol__slider::-moz-range-thumb{width:var(--havik-vol-thumb);height:var(--havik-vol-thumb);\n border:0;border-radius:50%;background:var(--havik-accent,#E1B600)}\n\n.havik-time{font-size:12px;color:var(--havik-text-muted,#9aa0ad);\n font-variant-numeric:tabular-nums;padding:0 6px;white-space:nowrap}\n.havik-live{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;\n letter-spacing:.4px;padding:0 8px;color:var(--havik-text,#fff)}\n.havik-live svg{width:9px;height:9px;color:var(--havik-text-muted,#9aa0ad)}\n.havik-live.is-live svg{color:var(--havik-accent,#E1B600)}\n.havik-live__go{margin-left:2px;border:0;cursor:pointer;background:transparent;color:var(--havik-text-muted,#9aa0ad);\n font:inherit;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 6px;border-radius:6px}\n.havik-live.is-live .havik-live__go{display:none}\n.havik-live__go:hover{color:var(--havik-accent,#E1B600)}\n\n.havik-menu{position:absolute;bottom:54px;right:12px;min-width:170px;max-height:230px;overflow:auto;\n background:var(--havik-surface,#1B1C23);border:1px solid var(--havik-border,rgba(255,255,255,.1));\n border-radius:var(--havik-radius,8px);box-shadow:0 10px 30px rgba(0,0,0,.5);padding:6px;z-index:3}\n.havik-menu[hidden]{display:none}\n.havik-menu__title{font-size:11px;text-transform:uppercase;letter-spacing:.6px;\n color:var(--havik-text-muted,#9aa0ad);padding:6px 8px 4px}\n.havik-menu__item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;font:inherit;font-size:13px;\n text-align:left;padding:7px 8px;border-radius:6px}\n.havik-menu__item:hover{background:var(--havik-surface-muted,#2a2c36)}\n.havik-menu__item[aria-checked=\"true\"]{color:var(--havik-accent,#E1B600);font-weight:600}\n.havik-menu__item[aria-checked=\"true\"]::after{content:'✓'}\n", xn = /* @__PURE__ */ new WeakMap();
2479
- function Sn(e = document, t) {
2480
- let n = e.querySelector(`style#${yn}`);
2481
- if (n && (!t || xn.get(n) === t)) return;
2493
+ var bn = "havik-player-styles", xn = "\n.havik-player{position:relative;display:block;width:100%;background:var(--havik-bg,#0e0f13);\n color:var(--havik-text,#fff);font-family:var(--havik-font,system-ui,sans-serif);\n overflow:hidden;line-height:1.4;-webkit-tap-highlight-color:transparent}\n.havik-player video{display:block;width:100%;height:auto;aspect-ratio:16/9;\n object-fit:contain;background:#000;outline:none}\n.havik-player:focus{outline:none}\n.havik-player *{box-sizing:border-box}\n\n.havik-player:fullscreen,.havik-player:-webkit-full-screen{display:flex;align-items:center;\n justify-content:center;width:100vw;height:100vh;aspect-ratio:auto}\n.havik-player:fullscreen video,.havik-player:-webkit-full-screen video{width:100%;height:100%;\n aspect-ratio:auto;object-fit:contain}\n\n.havik-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;cursor:pointer;\n touch-action:manipulation}\n.havik-gradient{position:absolute;left:0;right:0;bottom:0;height:120px;pointer-events:none;\n background:linear-gradient(to top,rgba(0,0,0,.78),transparent);\n opacity:1;transition:opacity .25s}\n.havik-player.havik-hide .havik-gradient,\n.havik-player.havik-hide .havik-bar{opacity:0;pointer-events:none}\n.havik-player.havik-hide{cursor:none}\n\n.havik-center{width:74px;height:74px;border-radius:50%;border:0;cursor:pointer;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);\n display:flex;align-items:center;justify-content:center;\n box-shadow:0 6px 24px rgba(0,0,0,.45);transition:transform .15s,opacity .15s}\n.havik-center:hover{transform:scale(1.06)}\n.havik-center svg{width:38px;height:38px;margin-left:2px}\n.havik-center.havik-gone{opacity:0;pointer-events:none;transform:scale(.8)}\n\n/* Absolutely centred, NOT a flex sibling of .havik-center: the hidden center\n button (.havik-gone) still occupies its 74px in the row, which pushed an\n in-flow spinner 37px right of centre. */\n.havik-spinner{position:absolute;inset:0;margin:auto;width:56px;height:56px;border-radius:50%;\n border:4px solid rgba(255,255,255,.2);border-top-color:var(--havik-accent,#E1B600);\n animation:havik-spin .8s linear infinite}\n@keyframes havik-spin{to{transform:rotate(360deg)}}\n\n/* Status card — branded full-bleed overlay for the stopped/pre-play states\n (ended / fatal error / pre-play / armed-waiting). */\n.havik-status{position:absolute;inset:0;display:flex;flex-direction:column;gap:16px;\n align-items:center;justify-content:center;background:rgba(14,15,19,.82);text-align:center;padding:24px}\n.havik-status__logo{width:128px;height:46px;background:var(--havik-logo) center/contain no-repeat;opacity:.95}\n.havik-status__msg{color:var(--havik-text,#fff);font-size:15px;font-weight:600;max-width:480px}\n.havik-status__btn{display:inline-flex;align-items:center;gap:8px;border:0;cursor:pointer;\n padding:10px 18px;border-radius:var(--havik-radius,8px);font-weight:700;font-size:14px;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);transition:transform .15s}\n.havik-status__btn:hover{transform:scale(1.04)}\n.havik-status__btn svg{width:18px;height:18px}\n/* Pre-play: lighter scrim so the poster shows through, and a round play CTA. */\n.havik-status--preplay{background:rgba(14,15,19,.4)}\n.havik-status--preplay .havik-status__btn--play{width:74px;height:74px;border-radius:50%;padding:0;\n justify-content:center;box-shadow:0 6px 24px rgba(0,0,0,.45)}\n.havik-status--preplay .havik-status__btn--play svg{width:38px;height:38px;margin-left:3px}\n\n.havik-watermark{position:absolute;top:12px;right:14px;height:22px;opacity:.85;pointer-events:none;\n content:'';background:var(--havik-logo) center/contain no-repeat;width:90px}\n/* A status card carries its own logo — the corner watermark would duplicate it. */\n.havik-player.havik-has-status .havik-watermark{display:none}\n\n.havik-bar{position:absolute;left:0;right:0;bottom:0;padding:0 12px 8px;\n opacity:1;transition:opacity .25s;z-index:2}\n.havik-seek{position:relative;height:16px;display:flex;align-items:center;cursor:pointer;margin-bottom:2px}\n.havik-seek__track{position:relative;width:100%;height:4px;border-radius:3px;background:rgba(255,255,255,.25)}\n.havik-seek__buffered{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:rgba(255,255,255,.4)}\n.havik-seek__played{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-seek__thumb{position:absolute;top:50%;width:13px;height:13px;border-radius:50%;\n background:var(--havik-accent,#E1B600);transform:translate(-50%,-50%) scale(0);transition:transform .1s}\n.havik-seek:hover .havik-seek__thumb{transform:translate(-50%,-50%) scale(1)}\n\n.havik-controls{display:flex;align-items:center;gap:6px;height:40px}\n.havik-spacer{flex:1}\n.havik-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;border-radius:var(--havik-radius,8px);\n padding:0;transition:background .12s,color .12s}\n.havik-btn:hover{background:rgba(255,255,255,.12)}\n.havik-btn:focus-visible{outline:2px solid var(--havik-accent,#E1B600);outline-offset:1px}\n.havik-btn svg{width:22px;height:22px}\n\n/* The volume level is filled in accent up to the thumb, like the seek bar's\n played span — a native range input paints no \"elapsed\" side, which left the\n level readable only from the dot's position. --havik-vol (0..1) is set by\n syncVolume(); the track width and thumb size live here as variables because\n the WebKit fill has to compute the thumb centre from both. */\n.havik-vol{display:flex;align-items:center}\n.havik-vol__slider{--havik-vol-w:70px;--havik-vol-thumb:12px;--havik-vol:1;\n --havik-vol-pos:calc(var(--havik-vol-thumb) / 2 + var(--havik-vol) * (var(--havik-vol-w) - var(--havik-vol-thumb)));\n width:0;opacity:0;transition:width .18s,opacity .18s;height:4px;cursor:pointer;\n appearance:none;-webkit-appearance:none;background:transparent;border-radius:3px;margin:0 4px}\n.havik-vol:hover .havik-vol__slider,.havik-vol:focus-within .havik-vol__slider{width:var(--havik-vol-w);opacity:1}\n/* WebKit/Blink have no progress pseudo-element: paint the fill as a hard-stop\n gradient that ends at the thumb centre. */\n.havik-vol__slider::-webkit-slider-runnable-track{height:4px;border-radius:3px;\n background:linear-gradient(to right,var(--havik-accent,#E1B600) var(--havik-vol-pos),\n rgba(255,255,255,.3) var(--havik-vol-pos))}\n/* Firefox fills natively, and tracks the thumb exactly. */\n.havik-vol__slider::-moz-range-track{height:4px;border-radius:3px;background:rgba(255,255,255,.3)}\n.havik-vol__slider::-moz-range-progress{height:4px;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-vol__slider::-webkit-slider-thumb{-webkit-appearance:none;width:var(--havik-vol-thumb);\n height:var(--havik-vol-thumb);border-radius:50%;background:var(--havik-accent,#E1B600);\n margin-top:calc((4px - var(--havik-vol-thumb)) / 2)}\n.havik-vol__slider::-moz-range-thumb{width:var(--havik-vol-thumb);height:var(--havik-vol-thumb);\n border:0;border-radius:50%;background:var(--havik-accent,#E1B600)}\n\n.havik-time{font-size:12px;color:var(--havik-text-muted,#9aa0ad);\n font-variant-numeric:tabular-nums;padding:0 6px;white-space:nowrap}\n.havik-live{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;\n letter-spacing:.4px;padding:0 8px;color:var(--havik-text,#fff)}\n.havik-live svg{width:9px;height:9px;color:var(--havik-text-muted,#9aa0ad)}\n.havik-live.is-live svg{color:var(--havik-accent,#E1B600)}\n.havik-live__go{margin-left:2px;border:0;cursor:pointer;background:transparent;color:var(--havik-text-muted,#9aa0ad);\n font:inherit;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 6px;border-radius:6px}\n.havik-live.is-live .havik-live__go{display:none}\n.havik-live__go:hover{color:var(--havik-accent,#E1B600)}\n\n.havik-menu{position:absolute;bottom:54px;right:12px;min-width:170px;max-height:230px;overflow:auto;\n background:var(--havik-surface,#1B1C23);border:1px solid var(--havik-border,rgba(255,255,255,.1));\n border-radius:var(--havik-radius,8px);box-shadow:0 10px 30px rgba(0,0,0,.5);padding:6px;z-index:3}\n.havik-menu[hidden]{display:none}\n.havik-menu__title{font-size:11px;text-transform:uppercase;letter-spacing:.6px;\n color:var(--havik-text-muted,#9aa0ad);padding:6px 8px 4px}\n.havik-menu__item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;font:inherit;font-size:13px;\n text-align:left;padding:7px 8px;border-radius:6px}\n.havik-menu__item:hover{background:var(--havik-surface-muted,#2a2c36)}\n.havik-menu__item[aria-checked=\"true\"]{color:var(--havik-accent,#E1B600);font-weight:600}\n.havik-menu__item[aria-checked=\"true\"]::after{content:'✓'}\n", Sn = /* @__PURE__ */ new WeakMap();
2494
+ function Cn(e = document, t) {
2495
+ let n = e.querySelector(`style#${bn}`);
2496
+ if (n && (!t || Sn.get(n) === t)) return;
2482
2497
  let r = e.createElement("style");
2483
- r.id = yn, t && (r.nonce = t, r.setAttribute("nonce", t)), r.textContent = bn, xn.set(r, t), n ? n.replaceWith(r) : e.head.appendChild(r);
2498
+ r.id = bn, t && (r.nonce = t, r.setAttribute("nonce", t)), r.textContent = xn, Sn.set(r, t), n ? n.replaceWith(r) : e.head.appendChild(r);
2484
2499
  }
2485
2500
  //#endregion
2486
2501
  //#region src/ui/icons.ts
2487
- var Cn = {
2502
+ var wn = {
2488
2503
  play: "<path d=\"M8 5v14l11-7z\"/>",
2489
2504
  pause: "<path d=\"M6 5h4v14H6zM14 5h4v14h-4z\"/>",
2490
2505
  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\"/>",
@@ -2498,20 +2513,20 @@ var Cn = {
2498
2513
  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\"/>"
2499
2514
  };
2500
2515
  function Q(e) {
2501
- return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${Cn[e]}</svg>`;
2516
+ return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${wn[e]}</svg>`;
2502
2517
  }
2503
2518
  //#endregion
2504
2519
  //#region src/ui/controls.ts
2505
- var wn = 3e3, Tn = 250, En = /* @__PURE__ */ new Set([
2520
+ var Tn = 3e3, En = 250, Dn = /* @__PURE__ */ new Set([
2506
2521
  "GONE",
2507
2522
  "NOT_FOUND",
2508
2523
  "INVALID_URN",
2509
2524
  "UNAUTHORIZED"
2510
2525
  ]);
2511
- function Dn(e) {
2512
- return new On(e);
2526
+ function On(e) {
2527
+ return new kn(e);
2513
2528
  }
2514
- var On = class {
2529
+ var kn = class {
2515
2530
  root;
2516
2531
  video;
2517
2532
  player;
@@ -2557,7 +2572,7 @@ var On = class {
2557
2572
  scrubbing = !1;
2558
2573
  offFns = [];
2559
2574
  constructor(e) {
2560
- this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? wn, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.interruptedMessage = e.interruptedMessage ?? "Reconnecting — please stand by", this.errorMessage = e.errorMessage, Sn(this.doc, e.cspNonce), this.root.classList.add("havik-player"), this.root.hasAttribute("tabindex") || (this.root.tabIndex = 0), this.root.setAttribute("role", "region"), this.root.setAttribute("aria-label", "Video player");
2575
+ this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? Tn, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.interruptedMessage = e.interruptedMessage ?? "Reconnecting — please stand by", this.errorMessage = e.errorMessage, Cn(this.doc, e.cspNonce), this.root.classList.add("havik-player"), this.root.hasAttribute("tabindex") || (this.root.tabIndex = 0), this.root.setAttribute("role", "region"), this.root.setAttribute("aria-label", "Video player");
2561
2576
  let t = (e, t, n) => {
2562
2577
  let r = this.doc.createElement(e);
2563
2578
  if (r.className = t, n) for (let [e, t] of Object.entries(n)) r.setAttribute(e, t);
@@ -2643,7 +2658,7 @@ var On = class {
2643
2658
  let t = this.surfaceWasHidden;
2644
2659
  this.tapTimer = setTimeout(() => {
2645
2660
  this.tapTimer = void 0, this.toggleBar(t);
2646
- }, Tn);
2661
+ }, En);
2647
2662
  }
2648
2663
  toggleBar(e) {
2649
2664
  e ? this.poke() : this.hideIfIdle();
@@ -2678,7 +2693,7 @@ var On = class {
2678
2693
  e.type = "button", e.className = "havik-status__btn havik-status__btn--play", e.setAttribute("aria-label", "Play"), e.innerHTML = Q("play"), e.addEventListener("click", () => void this.player.play()), n.append(e);
2679
2694
  } else {
2680
2695
  let r = this.doc.createElement("div");
2681
- if (r.className = "havik-status__msg", r.textContent = t ?? "", n.append(r), e === "error" && this.lastError && !En.has(this.lastError.code)) {
2696
+ if (r.className = "havik-status__msg", r.textContent = t ?? "", n.append(r), e === "error" && this.lastError && !Dn.has(this.lastError.code)) {
2682
2697
  let e = this.doc.createElement("button");
2683
2698
  e.type = "button", e.className = "havik-status__btn", e.innerHTML = `${Q("retry")}<span>Retry</span>`, e.addEventListener("click", () => void this.player.retry()), n.append(e);
2684
2699
  }
@@ -2719,7 +2734,7 @@ var On = class {
2719
2734
  let n = r.end(r.length - 1), i = Math.min(1, Math.max(0, (n - e.start) / t));
2720
2735
  this.seekBuffered.style.width = `${(i * 100).toFixed(2)}%`;
2721
2736
  }
2722
- !this.player.getStats().isLive && Number.isFinite(this.video.duration) && (this.time.textContent = `${kn(this.video.currentTime)} / ${kn(this.video.duration)}`);
2737
+ !this.player.getStats().isLive && Number.isFinite(this.video.duration) && (this.time.textContent = `${An(this.video.currentTime)} / ${An(this.video.duration)}`);
2723
2738
  }
2724
2739
  syncLive() {
2725
2740
  let e = this.player.getStats(), t = e.isLive !== !1;
@@ -2769,17 +2784,17 @@ var On = class {
2769
2784
  renderQualityMenu() {
2770
2785
  let e = this.player.getQualityLevels(), t = this.player.getCurrentQuality(), n = this.doc.createElement("div");
2771
2786
  n.className = "havik-menu__title", n.textContent = "Quality", this.menu.replaceChildren(n, this.menuItem("Auto", t === -1, () => this.player.setQuality("auto")));
2772
- for (let n of e) this.menu.append(this.menuItem(An(n), t === n.index, () => this.player.setQuality(n.index)));
2787
+ for (let n of e) this.menu.append(this.menuItem(jn(n), t === n.index, () => this.player.setQuality(n.index)));
2773
2788
  }
2774
2789
  renderAudioMenu() {
2775
2790
  let e = this.player.getAudioTracks(), t = this.doc.createElement("div");
2776
2791
  t.className = "havik-menu__title", t.textContent = "Audio", this.menu.replaceChildren(t);
2777
- for (let t of e) this.menu.append(this.menuItem(jn(t), t.default, () => this.player.setAudioTrack(t.id)));
2792
+ for (let t of e) this.menu.append(this.menuItem(Mn(t), t.default, () => this.player.setAudioTrack(t.id)));
2778
2793
  }
2779
2794
  renderCcMenu() {
2780
2795
  let e = this.player.getTextTracks(), t = this.doc.createElement("div");
2781
2796
  t.className = "havik-menu__title", t.textContent = "Subtitles", this.menu.replaceChildren(t, this.menuItem("Off", !1, () => this.player.setTextTrack(-1)));
2782
- for (let t of e) this.menu.append(this.menuItem(Mn(t), !1, () => this.player.setTextTrack(t.id)));
2797
+ for (let t of e) this.menu.append(this.menuItem(Nn(t), !1, () => this.player.setTextTrack(t.id)));
2783
2798
  }
2784
2799
  onTracksChanged() {
2785
2800
  this.openMenu && this.renderMenu(this.openMenu);
@@ -2880,41 +2895,41 @@ var On = class {
2880
2895
  this.root.classList.remove("havik-player", "havik-hide", "havik-has-status");
2881
2896
  }
2882
2897
  };
2883
- function kn(e) {
2898
+ function An(e) {
2884
2899
  (!Number.isFinite(e) || e < 0) && (e = 0);
2885
2900
  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");
2886
2901
  return r > 0 ? `${r}:${i}:${a}` : `${i}:${a}`;
2887
2902
  }
2888
- function An(e) {
2903
+ function jn(e) {
2889
2904
  return e.height ? `${e.height}p` : `${Math.round(e.bitrate / 1e3)} kbps`;
2890
2905
  }
2891
- function jn(e) {
2906
+ function Mn(e) {
2892
2907
  return e.name || e.lang || `Track ${e.id}`;
2893
2908
  }
2894
- function Mn(e) {
2909
+ function Nn(e) {
2895
2910
  return e.name || e.lang || `Track ${e.id}`;
2896
2911
  }
2897
2912
  //#endregion
2898
2913
  //#region src/managed/index.ts
2899
- var Nn = 6e3, Pn = 45e3, Fn = 2e3, In = 3, Ln = 2e3, Rn = 48e4;
2900
- function zn(e) {
2914
+ var Pn = 6e3, Fn = 45e3, In = 2e3, Ln = 3, Rn = 2e3, zn = 48e4, Bn = "keySystemLicenseRequestFailed";
2915
+ function Vn(e) {
2901
2916
  if (e.env !== void 0 && e.baseUrl !== void 0) throw new n("INTERNAL", 0, `createPlayer: pass env OR baseUrl, not both (got env=${JSON.stringify(e.env)} and baseUrl=${JSON.stringify(e.baseUrl)})`);
2902
2917
  if (e.env !== void 0) return Ke(e.env);
2903
2918
  if (!e.baseUrl) throw new n("INTERNAL", 0, "createPlayer: env or baseUrl is required (env: 'integration' | 'production')");
2904
2919
  return e.baseUrl;
2905
2920
  }
2906
- async function Bn(e) {
2921
+ async function Hn(e) {
2907
2922
  if (!e.video || typeof e.video.play != "function") throw new n("INTERNAL", 0, "createPlayer: opts.video must be an HTMLVideoElement");
2908
- let t = zn(e);
2923
+ let t = Vn(e);
2909
2924
  if (!e.matchUrn) throw new n("INVALID_URN", 0, "createPlayer: matchUrn is required");
2910
2925
  if (!e.credential) throw new n("INTERNAL", 0, "createPlayer: credential is required");
2911
- let r = new Vn({
2926
+ let r = new Un({
2912
2927
  ...e,
2913
2928
  baseUrl: t
2914
2929
  });
2915
2930
  return await r.start(), r;
2916
2931
  }
2917
- var Vn = class {
2932
+ var Un = class {
2918
2933
  opts;
2919
2934
  abort = new AbortController();
2920
2935
  listeners = {};
@@ -2925,6 +2940,7 @@ var Vn = class {
2925
2940
  detachPip;
2926
2941
  statsTimer;
2927
2942
  refreshTimer;
2943
+ staleLicenseReloaded = !1;
2928
2944
  _state = "idle";
2929
2945
  _descriptor = null;
2930
2946
  destroyed = !1;
@@ -3002,7 +3018,7 @@ var Vn = class {
3002
3018
  setTimeout(() => {
3003
3019
  a || this.destroyed || e !== this.loadGen || this.emit("waiting", {
3004
3020
  phase: i ? "unavailable" : "tooEarly",
3005
- retryInMs: Pn,
3021
+ retryInMs: Fn,
3006
3022
  attempt: 1,
3007
3023
  elapsedMs: 0,
3008
3024
  ...!i && f !== void 0 && f > Date.now() ? { ttkMs: f - Date.now() } : {}
@@ -3041,13 +3057,13 @@ var Vn = class {
3041
3057
  !a && !p() && h();
3042
3058
  }, e);
3043
3059
  };
3044
- _(Nn), this.armingSub = T({
3060
+ _(Pn), this.armingSub = T({
3045
3061
  eventsBaseUrl: t,
3046
3062
  matchUrn: this.opts.matchUrn,
3047
3063
  credential: this.opts.credential,
3048
- onAlive: () => _(Pn),
3064
+ onAlive: () => _(Fn),
3049
3065
  onState: (e) => {
3050
- a || p() || (e.state === "live" || e.state === "gone") && setTimeout(g, Math.random() * Fn);
3066
+ a || p() || (e.state === "live" || e.state === "gone") && setTimeout(g, Math.random() * In);
3051
3067
  },
3052
3068
  onError: () => h()
3053
3069
  });
@@ -3092,19 +3108,19 @@ var Vn = class {
3092
3108
  if (this.analyticsSession?.noteDestroyed(), this.analyticsSession = null, this.detachMediaListeners?.(), this.opts.analytics === !1) return;
3093
3109
  let i = e.analytics?.sid;
3094
3110
  if (!i) return;
3095
- let a = typeof this.opts.analytics == "object" ? this.opts.analytics.endpoint : void 0, { endpoint: o, disabledReason: s } = ln(this.opts.baseUrl, a, e.analytics?.endpoint);
3111
+ let a = typeof this.opts.analytics == "object" ? this.opts.analytics.endpoint : void 0, { endpoint: o, disabledReason: s } = un(this.opts.baseUrl, a, e.analytics?.endpoint);
3096
3112
  if (!o) {
3097
3113
  s && setTimeout(() => {
3098
3114
  this.destroyed || r !== this.loadGen || this.emit("warning", new n("INTERNAL", 0, s));
3099
3115
  }, 0);
3100
3116
  return;
3101
3117
  }
3102
- let c = this.opts.video, l = new un({
3118
+ let c = this.opts.video, l = new dn({
3103
3119
  endpoint: o,
3104
3120
  sid: i,
3105
3121
  player: t.name === "native" ? "PLAYER_SAFARI_NATIVE" : "PLAYER_HLSJS",
3106
- sdkVersion: gn,
3107
- environment: mn(c),
3122
+ sdkVersion: _n,
3123
+ environment: hn(c),
3108
3124
  idleMs: typeof this.opts.analytics == "object" ? this.opts.analytics.idleMs : void 0,
3109
3125
  onIdle: () => this.onAnalyticsIdle(t),
3110
3126
  onBlocked: (e) => setTimeout(() => {
@@ -3125,7 +3141,7 @@ var Vn = class {
3125
3141
  let u = () => l.noteSeek(), d = () => l.noteVolume(c.muted, c.volume);
3126
3142
  c.addEventListener("seeking", u), c.addEventListener("volumechange", d), this.detachMediaListeners = () => {
3127
3143
  c.removeEventListener("seeking", u), c.removeEventListener("volumechange", d);
3128
- }, l.start(), l.seedVolume(c.muted, c.volume), l.notePresentation(this.currentPresentation()), hn().then((e) => l.noteEnvironment(e)), this.opts.autoplay === !1 && l.noteAutoplay("AUTOPLAY_RESULT_NOT_ATTEMPTED");
3144
+ }, l.start(), l.seedVolume(c.muted, c.volume), l.notePresentation(this.currentPresentation()), gn().then((e) => l.noteEnvironment(e)), this.opts.autoplay === !1 && l.noteAutoplay("AUTOPLAY_RESULT_NOT_ATTEMPTED");
3129
3145
  }
3130
3146
  currentPresentation() {
3131
3147
  let e = this.opts.video;
@@ -3166,7 +3182,7 @@ var Vn = class {
3166
3182
  this.analyticsSession?.noteFirstBytes();
3167
3183
  break;
3168
3184
  case "drmReady":
3169
- this.analyticsSession?.noteDrmReady();
3185
+ this.analyticsSession?.noteDrmReady(), this.staleLicenseReloaded = !1;
3170
3186
  break;
3171
3187
  case "ready":
3172
3188
  this.analyticsSession?.noteReady(), this.emit("ready"), (this.opts.autoplay !== !1 || this.playRequested) && this.play();
@@ -3186,7 +3202,17 @@ var Vn = class {
3186
3202
  case "ended":
3187
3203
  this.onStreamEnded();
3188
3204
  break;
3189
- case "error": this.analyticsSession?.noteError(e.error.code, e.error.message, e.fatal, e.error.httpStatus ?? 0, Math.round(this.opts.video.currentTime * 1e3), e.detail), e.fatal ? this.fail(e.error) : this.emit("warning", e.error);
3205
+ case "error": {
3206
+ let t = e.fatal && this.isStaleLicenseDenial(e);
3207
+ if (this.analyticsSession?.noteError(e.error.code, e.error.message, e.fatal && !t, e.error.httpStatus ?? 0, Math.round(this.opts.video.currentTime * 1e3), e.detail), t) {
3208
+ this.staleLicenseReloaded = !0;
3209
+ let t = this.loadGen;
3210
+ this.emit("warning", e.error), Promise.resolve().then(() => {
3211
+ if (!(this.destroyed || t !== this.loadGen) && this._state !== "error" && this._state !== "ended") return this.reload().catch(() => void 0);
3212
+ });
3213
+ } else e.fatal ? this.fail(e.error) : this.emit("warning", e.error);
3214
+ break;
3215
+ }
3190
3216
  }
3191
3217
  }
3192
3218
  startLiveState(e) {
@@ -3206,7 +3232,7 @@ var Vn = class {
3206
3232
  r = 0, i = !1;
3207
3233
  },
3208
3234
  onError: () => {
3209
- this.destroyed || e !== this.loadGen || i || this.endedHandled || (r += 1, r >= In && (i = !0, this.emit("warning", new n("NETWORK", 0, `live-state events unreachable at ${t} after ${r} attempts — end-of-stream detection degraded to the HLS staleness watchdog. Check the events host, the api-key's allowed origins, and CSP connect-src.`))));
3235
+ this.destroyed || e !== this.loadGen || i || this.endedHandled || (r += 1, r >= Ln && (i = !0, this.emit("warning", new n("NETWORK", 0, `live-state events unreachable at ${t} after ${r} attempts — end-of-stream detection degraded to the HLS staleness watchdog. Check the events host, the api-key's allowed origins, and CSP connect-src.`))));
3210
3236
  }
3211
3237
  });
3212
3238
  }
@@ -3228,7 +3254,7 @@ var Vn = class {
3228
3254
  scheduleRejoin(e) {
3229
3255
  this.rejoinEnabled() && !this.rejoinTimer && (this.rejoinTimer = setTimeout(() => {
3230
3256
  this.rejoinTimer = null, !(this.destroyed || e !== this.loadGen || this._state !== "ended") && this.rejoin();
3231
- }, Math.random() * Fn));
3257
+ }, Math.random() * In));
3232
3258
  }
3233
3259
  cancelRejoin() {
3234
3260
  this.rejoinTimer &&= (clearTimeout(this.rejoinTimer), null);
@@ -3252,7 +3278,7 @@ var Vn = class {
3252
3278
  }
3253
3279
  t.controls = !1;
3254
3280
  let n = (t.ownerDocument ?? document).createElement("div"), r = t.parentNode;
3255
- r && r.insertBefore(n, t), n.appendChild(t), Z(n, this.opts.theme), this.container = n, this.controlBar = Dn({
3281
+ r && r.insertBefore(n, t), n.appendChild(t), Z(n, this.opts.theme), this.container = n, this.controlBar = On({
3256
3282
  root: n,
3257
3283
  video: t,
3258
3284
  player: this,
@@ -3283,7 +3309,7 @@ var Vn = class {
3283
3309
  }
3284
3310
  startStats() {
3285
3311
  this.stopStats();
3286
- let e = Math.max(500, this.opts.statsIntervalMs ?? Ln);
3312
+ let e = Math.max(500, this.opts.statsIntervalMs ?? Rn);
3287
3313
  this.statsTimer = setInterval(() => {
3288
3314
  if (!this.engine) return;
3289
3315
  let e = this.engine.getStats();
@@ -3293,9 +3319,12 @@ var Vn = class {
3293
3319
  stopStats() {
3294
3320
  this.statsTimer && clearInterval(this.statsTimer), this.statsTimer = void 0;
3295
3321
  }
3322
+ isStaleLicenseDenial(e) {
3323
+ return this.staleLicenseReloaded || this.opts.hlsConfig?.licenseXhrSetup || this._state === "ended" || this._state === "error" || e.detail !== Bn || e.error.httpStatus !== 403 ? !1 : Gt(this._descriptor?.drm?.widevine?.licenseUrl);
3324
+ }
3296
3325
  scheduleLicenseRefresh() {
3297
3326
  this.stopRefresh();
3298
- let e = this.opts.licenseRefreshMs ?? Rn;
3327
+ let e = this.opts.licenseRefreshMs ?? zn;
3299
3328
  e && this._descriptor?.drmEnabled && (this.refreshTimer = setInterval(() => void this.refreshLicense(), e));
3300
3329
  }
3301
3330
  stopRefresh() {
@@ -3450,7 +3479,7 @@ function $(e) {
3450
3479
  }
3451
3480
  //#endregion
3452
3481
  //#region src/embed/host.ts
3453
- function Hn(e) {
3482
+ function Wn(e) {
3454
3483
  if (!e.container || typeof e.container.appendChild != "function") throw Error("mountEmbed: opts.container must be a DOM element");
3455
3484
  if (!e.src) throw Error("mountEmbed: opts.src is required");
3456
3485
  if (!e.baseUrl || !e.matchUrn) throw Error("mountEmbed: opts.baseUrl and opts.matchUrn are required");
@@ -3509,15 +3538,15 @@ function Hn(e) {
3509
3538
  }
3510
3539
  //#endregion
3511
3540
  //#region src/embed/page.ts
3512
- var Un = /^[A-Za-z0-9:_.-]{1,128}$/;
3513
- function Wn(e) {
3541
+ var Gn = /^[A-Za-z0-9:_.-]{1,128}$/;
3542
+ function Kn(e) {
3514
3543
  if (typeof e == "boolean") return e;
3515
3544
  if (typeof e == "object" && e) {
3516
3545
  let t = e.endpoint;
3517
3546
  return typeof t == "string" ? { endpoint: t } : {};
3518
3547
  }
3519
3548
  }
3520
- function Gn() {
3549
+ function qn() {
3521
3550
  let e = window.HAVIK_EMBED_CONFIG;
3522
3551
  if (e) return {
3523
3552
  baseUrl: e.baseUrl ?? "",
@@ -3527,7 +3556,7 @@ function Gn() {
3527
3556
  autoplay: e.autoplay ?? !0,
3528
3557
  muted: e.muted ?? !0,
3529
3558
  waitForLive: e.waitForLive ?? !0,
3530
- analytics: Wn(e.analytics)
3559
+ analytics: Kn(e.analytics)
3531
3560
  };
3532
3561
  let t = new URLSearchParams(location.search);
3533
3562
  return {
@@ -3540,9 +3569,9 @@ function Gn() {
3540
3569
  waitForLive: !0
3541
3570
  };
3542
3571
  }
3543
- async function Kn() {
3544
- let e = Gn();
3545
- Jn();
3572
+ async function Jn() {
3573
+ let e = qn();
3574
+ Xn();
3546
3575
  let t = document.createElement("video");
3547
3576
  t.setAttribute("playsinline", ""), t.muted = e.muted ?? !0, t.style.cssText = "width:100%;height:100%;object-fit:contain;background:#000;aspect-ratio:auto", document.body.appendChild(t);
3548
3577
  let r = e.parentOrigin && e.parentOrigin !== "null" ? e.parentOrigin : null;
@@ -3597,12 +3626,12 @@ async function Kn() {
3597
3626
  code: e.code,
3598
3627
  message: e.message
3599
3628
  })), e.on("autoplayblocked", () => {
3600
- i({ type: "oddin:autoplayblocked" }), qn(t, e);
3629
+ i({ type: "oddin:autoplayblocked" }), Yn(t, e);
3601
3630
  });
3602
3631
  }, s = async (r) => {
3603
3632
  a?.destroy(), a = null;
3604
3633
  try {
3605
- let n = await Bn({
3634
+ let n = await Hn({
3606
3635
  video: t,
3607
3636
  baseUrl: e.baseUrl,
3608
3637
  matchUrn: r,
@@ -3664,7 +3693,7 @@ async function Kn() {
3664
3693
  a?.retry();
3665
3694
  break;
3666
3695
  case "oddin:load":
3667
- typeof e.matchUrn == "string" && Un.test(e.matchUrn) && s(e.matchUrn);
3696
+ typeof e.matchUrn == "string" && Gn.test(e.matchUrn) && s(e.matchUrn);
3668
3697
  break;
3669
3698
  case "oddin:destroy": c();
3670
3699
  }
@@ -3676,18 +3705,18 @@ async function Kn() {
3676
3705
  protocol: 1
3677
3706
  });
3678
3707
  }
3679
- function qn(e, t) {
3708
+ function Yn(e, t) {
3680
3709
  if (document.getElementById("havik-tap")) return;
3681
3710
  let n = document.createElement("button");
3682
3711
  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", () => {
3683
3712
  n.remove(), t.play();
3684
3713
  }), document.body.appendChild(n);
3685
3714
  }
3686
- function Jn() {
3715
+ function Xn() {
3687
3716
  let e = document.createElement("style");
3688
3717
  e.textContent = "html,body{margin:0;height:100%;background:#000;overflow:hidden}body{display:flex}", document.head.appendChild(e);
3689
3718
  }
3690
3719
  //#endregion
3691
- export { _n as ODDIN_THEME, n as PlaybackError, Z as applyTheme, Ke as baseUrlForEnv, Kn as bootEmbed, Te as clearDeviceId, Bn as createPlayer, E as deriveEventsBaseUrl, He as detectDrmSupport, C as enterVideoFullscreen, w as exitVideoFullscreen, _e as fetchCatalog, ye as fetchTournament, xe as flattenMatches, k as getDeviceId, We as isLowLatencyManifest, $ as isOddinMessage, De as licenseRequestHeaders, Hn as mountEmbed, de as pollToLive, y as resolveStream, T as subscribeLiveState, S as videoFullscreenActive, Ce as watchStatus };
3720
+ export { vn as ODDIN_THEME, n as PlaybackError, Z as applyTheme, Ke as baseUrlForEnv, Jn as bootEmbed, Te as clearDeviceId, Hn as createPlayer, E as deriveEventsBaseUrl, He as detectDrmSupport, C as enterVideoFullscreen, w as exitVideoFullscreen, _e as fetchCatalog, ye as fetchTournament, xe as flattenMatches, k as getDeviceId, We as isLowLatencyManifest, $ as isOddinMessage, De as licenseRequestHeaders, Wn as mountEmbed, de as pollToLive, y as resolveStream, T as subscribeLiveState, S as videoFullscreenActive, Ce as watchStatus };
3692
3721
 
3693
3722
  //# sourceMappingURL=index.js.map