@oddin-gg/havik-player 1.0.2

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 ADDED
@@ -0,0 +1,1919 @@
1
+ import e from "hls.js";
2
+ //#region src/core/errors.ts
3
+ var t = class extends Error {
4
+ code;
5
+ httpStatus;
6
+ retryAfterMs;
7
+ requestId;
8
+ serverCode;
9
+ liveStartsAtMs;
10
+ constructor(e, t, n, r) {
11
+ super(n), this.name = "PlaybackError", this.code = e, this.httpStatus = t, this.retryAfterMs = r?.retryAfterMs, this.requestId = r?.requestId, this.serverCode = r?.serverCode, this.liveStartsAtMs = r?.liveStartsAtMs, r?.cause !== void 0 && (this.cause = r.cause);
12
+ }
13
+ };
14
+ function n(e) {
15
+ switch (e) {
16
+ case 400: return "INVALID_URN";
17
+ case 401: return "UNAUTHORIZED";
18
+ case 403: return "FORBIDDEN";
19
+ case 404: return "NOT_FOUND";
20
+ case 410: return "GONE";
21
+ case 425: return "TOO_EARLY";
22
+ case 429: return "RATE_LIMITED";
23
+ case 503: return "UNAVAILABLE";
24
+ default: return "INTERNAL";
25
+ }
26
+ }
27
+ var r = /* @__PURE__ */ new Set([
28
+ "TOO_EARLY",
29
+ "UNAVAILABLE",
30
+ "RATE_LIMITED",
31
+ "NETWORK"
32
+ ]);
33
+ function i(e) {
34
+ return r.has(e);
35
+ }
36
+ //#endregion
37
+ //#region src/core/types.ts
38
+ async function a(e) {
39
+ let t = typeof e == "function" ? await e() : e;
40
+ if (!t || !t.apiKey) throw Error("havik-player: credential is missing an apiKey");
41
+ return t;
42
+ }
43
+ //#endregion
44
+ //#region src/core/http.ts
45
+ function o(e, n) {
46
+ let r = e.replace(/\/+$/, ""), i;
47
+ try {
48
+ i = new URL(r + n);
49
+ } catch {
50
+ throw new t("INTERNAL", 0, `invalid baseUrl: ${JSON.stringify(e)}`);
51
+ }
52
+ if (i.protocol !== "https:" && i.protocol !== "http:") throw new t("INTERNAL", 0, `baseUrl must be http(s), got: ${JSON.stringify(e)}`);
53
+ return i.toString();
54
+ }
55
+ function s(e) {
56
+ if (!e) return;
57
+ let t = Number(e);
58
+ if (Number.isFinite(t)) return Math.max(0, Math.round(t * 1e3));
59
+ let n = Date.parse(e);
60
+ if (!Number.isNaN(n)) return Math.max(0, n - Date.now());
61
+ }
62
+ async function c(e, n) {
63
+ let r = await a(n.credential), i = n.headers ?? new Headers();
64
+ i.set("x-api-key", r.apiKey);
65
+ try {
66
+ return await fetch(e, {
67
+ method: n.method ?? "GET",
68
+ headers: i,
69
+ body: n.body,
70
+ signal: n.signal,
71
+ cache: "no-store",
72
+ credentials: "omit",
73
+ mode: "cors"
74
+ });
75
+ } catch (e) {
76
+ throw n.signal?.aborted ? new t("ABORTED", 0, "request aborted", { cause: e }) : new t("NETWORK", 0, `network request failed: ${l(e)}`, { cause: e });
77
+ }
78
+ }
79
+ function l(e) {
80
+ return e instanceof Error ? e.message : String(e);
81
+ }
82
+ //#endregion
83
+ //#region src/core/waitForLive.ts
84
+ var u = 1e3, d = 3e4, f = 600 * 1e3, p = 300 * 1e3, ee = 1e3, te = {
85
+ TOO_EARLY: "tooEarly",
86
+ UNAVAILABLE: "unavailable",
87
+ RATE_LIMITED: "rateLimited",
88
+ NETWORK: "network"
89
+ };
90
+ function ne(e) {
91
+ let t = e === !0 || e === !1 ? {} : e, n = Math.max(ee, t.floorMs ?? u), r = Math.max(n, t.ceilMs ?? d), i = Math.max(0, t.imminentMs ?? f), a = Math.max(r, t.sanityFloorMs ?? p);
92
+ return {
93
+ timeoutMs: t.timeoutMs,
94
+ floorMs: n,
95
+ ceilMs: r,
96
+ imminentMs: i,
97
+ sanityFloorMs: a,
98
+ kickoffMs: m(t.kickoffAt),
99
+ jitter: t.jitter ?? !0,
100
+ onState: t.onState
101
+ };
102
+ }
103
+ function m(e) {
104
+ if (e == null) return;
105
+ if (typeof e == "number") return Number.isFinite(e) ? e : void 0;
106
+ let t = Date.parse(e);
107
+ return Number.isFinite(t) ? t : void 0;
108
+ }
109
+ function h(e, t, n) {
110
+ return Math.min(n, Math.max(t, e));
111
+ }
112
+ function g(e) {
113
+ return Math.round(e * (1 + .15 * Math.random()));
114
+ }
115
+ function _(e, t, n, r) {
116
+ let i = r ?? n.kickoffMs, a = i == null ? void 0 : Math.max(0, i - t), o = Math.max(n.floorMs, e ?? 0);
117
+ if (a == null || a <= n.imminentMs) return {
118
+ delay: h(e ?? n.floorMs, o, n.ceilMs),
119
+ ttkMs: a
120
+ };
121
+ let s = Math.min(Math.floor(a / 10), n.sanityFloorMs, a);
122
+ return {
123
+ delay: Math.max(o, s),
124
+ ttkMs: a
125
+ };
126
+ }
127
+ function v(e, n) {
128
+ return new Promise((r, i) => {
129
+ if (n?.aborted) {
130
+ i(new t("ABORTED", 0, "wait-for-live aborted"));
131
+ return;
132
+ }
133
+ let a = () => {
134
+ clearTimeout(o), i(new t("ABORTED", 0, "wait-for-live aborted"));
135
+ }, o = setTimeout(() => {
136
+ n?.removeEventListener("abort", a), r();
137
+ }, e);
138
+ n?.addEventListener("abort", a, { once: !0 });
139
+ });
140
+ }
141
+ async function y(e, n, r) {
142
+ let a = ne(n), o = Date.now(), s = a.timeoutMs == null ? Infinity : o + a.timeoutMs, c = 0, l;
143
+ for (;;) {
144
+ if (r?.aborted) throw new t("ABORTED", 0, "wait-for-live aborted");
145
+ try {
146
+ return await e();
147
+ } catch (e) {
148
+ if (!(e instanceof t) || !i(e.code)) throw e;
149
+ c += 1, e.liveStartsAtMs != null && Number.isFinite(e.liveStartsAtMs) && (l = e.liveStartsAtMs);
150
+ let n = Date.now(), u = _(e.retryAfterMs, n, a, l), d = u.ttkMs, f = u.delay;
151
+ if (a.jitter && (f = g(f)), n + f > s) throw new t("TIMEOUT", e.httpStatus, `timed out after ${a.timeoutMs}ms waiting for the stream to go live`, {
152
+ retryAfterMs: e.retryAfterMs,
153
+ cause: e
154
+ });
155
+ a.onState?.({
156
+ phase: te[e.code] ?? "unavailable",
157
+ retryInMs: f,
158
+ attempt: c,
159
+ elapsedMs: n - o,
160
+ ttkMs: d
161
+ }), await v(f, r);
162
+ }
163
+ }
164
+ }
165
+ //#endregion
166
+ //#region src/core/resolve.ts
167
+ async function b(e) {
168
+ if (!e.matchUrn || typeof e.matchUrn != "string") throw new t("INVALID_URN", 0, "resolveStream: a non-empty matchUrn is required");
169
+ let n = await c(o(e.baseUrl, `/v1/playback/${encodeURIComponent(e.matchUrn)}`), {
170
+ credential: e.credential,
171
+ signal: e.signal
172
+ });
173
+ if (!n.ok) throw await re(n);
174
+ return ae(await n.json(), e.matchUrn);
175
+ }
176
+ function x(e) {
177
+ let t = () => b(e);
178
+ return e.waitForLive ? y(t, e.waitForLive, e.signal) : t();
179
+ }
180
+ async function re(e) {
181
+ let r = s(e.headers.get("retry-after")), i = e.headers.get("x-request-id") ?? e.headers.get("x-amzn-requestid") ?? void 0, a = e.statusText || `HTTP ${e.status}`, o, c;
182
+ try {
183
+ let t = await e.json();
184
+ if (t?.error?.message && (a = t.error.message), t?.error?.code && (o = t.error.code), typeof t?.liveStartsAt == "string") {
185
+ let e = Date.parse(t.liveStartsAt);
186
+ Number.isFinite(e) && (c = e);
187
+ }
188
+ } catch {}
189
+ return new t(n(e.status), e.status, a, {
190
+ retryAfterMs: r,
191
+ requestId: i,
192
+ serverCode: o,
193
+ liveStartsAtMs: c
194
+ });
195
+ }
196
+ var ie = /* @__PURE__ */ new Set([
197
+ "matchUrn",
198
+ "protocol",
199
+ "drmEnabled",
200
+ "manifestUrl",
201
+ "drm",
202
+ "serverTime",
203
+ "liveStartsAt",
204
+ "expiresAt"
205
+ ]);
206
+ function S(e, n) {
207
+ let r;
208
+ try {
209
+ r = new URL(n);
210
+ } catch {
211
+ throw new t("INTERNAL", 0, `playback ${e} is not a valid URL`);
212
+ }
213
+ if (r.protocol !== "https:" && r.protocol !== "http:") throw new t("INTERNAL", 0, `playback ${e} has a disallowed scheme: ${r.protocol}`);
214
+ return n;
215
+ }
216
+ function ae(e, n) {
217
+ if (!e.manifestUrl) throw new t("INTERNAL", 0, "playback response is missing manifestUrl");
218
+ let r = e.drmEnabled ?? !!e.drm, i = {
219
+ matchUrn: e.matchUrn ?? n,
220
+ protocol: "hls",
221
+ drmEnabled: r,
222
+ manifestUrl: S("manifestUrl", e.manifestUrl),
223
+ serverTime: e.serverTime ?? ""
224
+ };
225
+ if (e.liveStartsAt && (i.liveStartsAt = e.liveStartsAt), r && e.drm) {
226
+ let t = {};
227
+ e.drm.widevine?.licenseUrl && (t.widevine = { licenseUrl: S("widevine licenseUrl", e.drm.widevine.licenseUrl) }), e.drm.fairplay?.licenseUrl && e.drm.fairplay.certificateUrl && (t.fairplay = {
228
+ licenseUrl: S("fairplay licenseUrl", e.drm.fairplay.licenseUrl),
229
+ certificateUrl: S("fairplay certificateUrl", e.drm.fairplay.certificateUrl)
230
+ }), (t.widevine || t.fairplay) && (i.drm = t);
231
+ }
232
+ let a = {};
233
+ for (let [t, n] of Object.entries(e)) ie.has(t) || (a[t] = n);
234
+ return Object.keys(a).length > 0 && (i.extensions = a), i;
235
+ }
236
+ //#endregion
237
+ //#region src/core/catalog.ts
238
+ async function oe(e) {
239
+ let r = new URLSearchParams();
240
+ e.status && r.set("status", Array.isArray(e.status) ? e.status.join(",") : e.status), e.sport && r.set("sport", e.sport);
241
+ let i = r.toString(), a = o(e.baseUrl, "/v1/catalog" + (i ? `?${i}` : "")), s = new Headers();
242
+ e.etag && s.set("If-None-Match", e.etag);
243
+ let l = await c(a, {
244
+ credential: e.credential,
245
+ signal: e.signal,
246
+ headers: s
247
+ });
248
+ if (l.status === 304) return {
249
+ tournaments: [],
250
+ etag: e.etag,
251
+ notModified: !0
252
+ };
253
+ if (!l.ok) throw new t(n(l.status), l.status, `catalog request failed (HTTP ${l.status})`);
254
+ return {
255
+ tournaments: se(await l.json()),
256
+ etag: l.headers.get("etag") ?? void 0,
257
+ notModified: !1
258
+ };
259
+ }
260
+ function se(e) {
261
+ return (e.tournaments ?? []).map((e) => {
262
+ let t = e.urn ?? "", n = e.name ?? "", r = e.sport ?? "";
263
+ return {
264
+ urn: t,
265
+ name: n,
266
+ sport: r,
267
+ isOffline: !!e.isOffline,
268
+ matches: (e.matches ?? []).map((e) => ({
269
+ matchUrn: e.matchUrn ?? "",
270
+ matchName: e.matchName ?? "",
271
+ status: e.status ?? "upcoming",
272
+ datePlannedStart: e.datePlannedStart,
273
+ tournamentUrn: t,
274
+ tournamentName: n,
275
+ sport: r
276
+ }))
277
+ };
278
+ });
279
+ }
280
+ async function ce(e) {
281
+ let r = o(e.baseUrl, `/v1/tournaments/${encodeURIComponent(e.urn)}`), i = new Headers();
282
+ e.etag && i.set("If-None-Match", e.etag);
283
+ let a = await c(r, {
284
+ credential: e.credential,
285
+ signal: e.signal,
286
+ headers: i
287
+ });
288
+ if (a.status === 304) return {
289
+ etag: e.etag,
290
+ notModified: !0,
291
+ notFound: !1
292
+ };
293
+ if (a.status === 404) return {
294
+ notModified: !1,
295
+ notFound: !0
296
+ };
297
+ if (!a.ok) throw new t(n(a.status), a.status, `tournament request failed (HTTP ${a.status})`);
298
+ return {
299
+ tournament: le(await a.json()),
300
+ etag: a.headers.get("etag") ?? void 0,
301
+ notModified: !1,
302
+ notFound: !1
303
+ };
304
+ }
305
+ function le(e) {
306
+ let t = e.urn ?? "", n = e.name ?? "", r = e.sport ?? "";
307
+ return {
308
+ urn: t,
309
+ name: n,
310
+ sport: r,
311
+ isOffline: !!e.isOffline,
312
+ matches: (e.matches ?? []).map((e) => ({
313
+ matchUrn: e.matchUrn ?? "",
314
+ matchName: e.matchName ?? "",
315
+ status: e.status ?? "upcoming",
316
+ datePlannedStart: e.datePlannedStart,
317
+ tournamentUrn: t,
318
+ tournamentName: n,
319
+ sport: r
320
+ }))
321
+ };
322
+ }
323
+ function ue(e) {
324
+ return e.tournaments.flatMap((e) => e.matches);
325
+ }
326
+ //#endregion
327
+ //#region src/core/poll.ts
328
+ function C(e) {
329
+ let r = Math.max(2e3, e.intervalMs ?? 1e4), i = new AbortController(), a, s, l, u = !1, d = () => {
330
+ u || (a = setTimeout(f, r));
331
+ }, f = async () => {
332
+ try {
333
+ let r = o(e.baseUrl, `/v1/matches/${encodeURIComponent(e.matchUrn)}`), a = new Headers();
334
+ s && a.set("If-None-Match", s);
335
+ let u = await c(r, {
336
+ credential: e.credential,
337
+ signal: i.signal,
338
+ headers: a
339
+ });
340
+ if (u.status !== 304) {
341
+ if (!u.ok) throw new t(n(u.status), u.status, `match status request failed (HTTP ${u.status})`);
342
+ s = u.headers.get("etag") ?? s;
343
+ let r = await u.json(), i = r.status ?? "upcoming";
344
+ i !== l && (l = i, e.onChange({
345
+ matchUrn: r.matchUrn ?? e.matchUrn,
346
+ matchName: r.matchName,
347
+ status: i,
348
+ datePlannedStart: r.datePlannedStart
349
+ }));
350
+ }
351
+ } catch (n) {
352
+ i.signal.aborted || e.onError?.(n instanceof t ? n : new t("NETWORK", 0, String(n)));
353
+ } finally {
354
+ d();
355
+ }
356
+ };
357
+ return f(), { stop() {
358
+ u = !0, i.abort(), a && clearTimeout(a);
359
+ } };
360
+ }
361
+ //#endregion
362
+ //#region src/core/events.ts
363
+ function w(e) {
364
+ let t = Math.max(250, e.minBackoffMs ?? 1e3), n = Math.max(t, e.maxBackoffMs ?? 15e3), r = new AbortController(), i = !1, s = (e) => new Promise((t) => {
365
+ if (r.signal.aborted) return t();
366
+ let n = setTimeout(t, e);
367
+ r.signal.addEventListener("abort", () => {
368
+ clearTimeout(n), t();
369
+ }, { once: !0 });
370
+ }), c = (t) => {
371
+ e.onAlive?.();
372
+ let n = "message", r = [];
373
+ for (let e of t.split("\n")) {
374
+ let t = e.endsWith("\r") ? e.slice(0, -1) : e;
375
+ if (t === "" || t.startsWith(":")) continue;
376
+ let i = t.indexOf(":"), a = i < 0 ? t : t.slice(0, i), o = i < 0 ? "" : t.slice(i + 1);
377
+ o.startsWith(" ") && (o = o.slice(1)), a === "event" ? n = o : a === "data" && r.push(o);
378
+ }
379
+ if (!(n !== "state" || r.length === 0)) try {
380
+ let t = JSON.parse(r.join("\n"));
381
+ t && typeof t.state == "string" && e.onState(t);
382
+ } catch {}
383
+ }, l = /\r\n\r\n|\n\n|\r\r/, u = async (e, t) => {
384
+ let n = e.getReader(), r = new TextDecoder(), a = "", o = !1;
385
+ try {
386
+ for (;;) {
387
+ let { value: e, done: s } = await n.read();
388
+ if (s) return;
389
+ !o && e && e.length > 0 && (o = !0, t()), a += r.decode(e, { stream: !0 });
390
+ let u;
391
+ for (; (u = l.exec(a)) !== null;) c(a.slice(0, u.index)), a = a.slice(u.index + u[0].length);
392
+ if (i) return;
393
+ }
394
+ } finally {
395
+ await n.cancel().catch(() => {});
396
+ }
397
+ };
398
+ return (async () => {
399
+ let c = t;
400
+ for (; !i;) {
401
+ try {
402
+ let n = await a(e.credential);
403
+ if (i) return;
404
+ let s = o(e.eventsBaseUrl, `/v1/events/${encodeURIComponent(e.matchUrn)}`), l = await fetch(s, {
405
+ method: "GET",
406
+ headers: {
407
+ "x-api-key": n.apiKey,
408
+ accept: "text/event-stream"
409
+ },
410
+ signal: r.signal,
411
+ cache: "no-store",
412
+ credentials: "omit",
413
+ mode: "cors"
414
+ });
415
+ if (i) return;
416
+ !l.ok || !l.body ? e.onError?.(/* @__PURE__ */ Error(`live-state subscribe failed: HTTP ${l.status}`)) : await u(l.body, () => {
417
+ c = t;
418
+ });
419
+ } catch (t) {
420
+ if (i || r.signal.aborted) return;
421
+ e.onError?.(t);
422
+ }
423
+ if (i) return;
424
+ await s(c), c = Math.min(n, c * 2);
425
+ }
426
+ })(), { close() {
427
+ i || (i = !0, r.abort());
428
+ } };
429
+ }
430
+ function T(e) {
431
+ try {
432
+ let t = new URL(e);
433
+ return t.hostname = `events.${t.hostname}`, t.pathname = "", t.search = "", t.origin;
434
+ } catch {
435
+ return;
436
+ }
437
+ }
438
+ //#endregion
439
+ //#region src/core/drm/deviceId.ts
440
+ var E = "havik.player.deviceId", D;
441
+ function O() {
442
+ if (D) return D;
443
+ try {
444
+ let e = globalThis.localStorage?.getItem(E);
445
+ if (e) return D = e, e;
446
+ } catch {}
447
+ let e = A();
448
+ D = e;
449
+ try {
450
+ globalThis.localStorage?.setItem(E, e);
451
+ } catch {}
452
+ return e;
453
+ }
454
+ function k() {
455
+ D = void 0;
456
+ try {
457
+ globalThis.localStorage?.removeItem(E);
458
+ } catch {}
459
+ }
460
+ function A() {
461
+ let e = globalThis.crypto;
462
+ if (e && typeof e.randomUUID == "function") return e.randomUUID();
463
+ let t = /* @__PURE__ */ new Uint8Array(16);
464
+ if (e && typeof e.getRandomValues == "function") e.getRandomValues(t);
465
+ else for (let e = 0; e < 16; e += 1) t[e] = Math.floor(Math.random() * 256);
466
+ t[6] = t[6] & 15 | 64, t[8] = t[8] & 63 | 128;
467
+ let n = Array.from(t, (e) => e.toString(16).padStart(2, "0"));
468
+ return n.slice(0, 4).join("") + "-" + n.slice(4, 6).join("") + "-" + n.slice(6, 8).join("") + "-" + n.slice(8, 10).join("") + "-" + n.slice(10, 16).join("");
469
+ }
470
+ //#endregion
471
+ //#region src/core/drm/headers.ts
472
+ function j(e, t, n = {}) {
473
+ let r = {
474
+ "x-api-key": t.apiKey,
475
+ "X-Match-Urn": e.matchUrn,
476
+ "X-Device-Id": n.deviceId ?? O(),
477
+ "Content-Type": "application/octet-stream"
478
+ };
479
+ return n.userId && (r["X-User-Id"] = n.userId), r;
480
+ }
481
+ //#endregion
482
+ //#region src/core/manifest.ts
483
+ var M = /^#EXT-X-(PART-INF|PART|PRELOAD-HINT)[:\s]/m;
484
+ function N(e) {
485
+ return M.test(e);
486
+ }
487
+ //#endregion
488
+ //#region src/core/stats.ts
489
+ var P = { droppedFrames: 0 };
490
+ function F(e) {
491
+ return e.getVideoPlaybackQuality?.()?.droppedVideoFrames ?? 0;
492
+ }
493
+ function I(e, t) {
494
+ let n = [
495
+ ["playing", () => t("playing")],
496
+ ["waiting", () => t("buffering")],
497
+ ["stalled", () => t("buffering")],
498
+ ["pause", () => {
499
+ e.ended || t("paused");
500
+ }],
501
+ ["ended", () => t("ended")]
502
+ ];
503
+ for (let [t, r] of n) e.addEventListener(t, r);
504
+ return () => {
505
+ for (let [t, r] of n) e.removeEventListener(t, r);
506
+ };
507
+ }
508
+ //#endregion
509
+ //#region src/core/engine/hls.ts
510
+ function L() {
511
+ return e.isSupported();
512
+ }
513
+ var R = 3, z = 5, B = 3, V = 3e4, H = 12e3, U = 3e3, de = 3, fe = 9;
514
+ function pe(e) {
515
+ if (e.liveSyncDurationCount != null || e.liveMaxLatencyDurationCount != null) {
516
+ delete e.liveSyncDuration, delete e.liveMaxLatencyDuration;
517
+ return;
518
+ }
519
+ e.liveSyncDuration ??= de;
520
+ let t = Math.max(12, e.liveSyncDuration + fe);
521
+ (e.liveMaxLatencyDuration == null || e.liveMaxLatencyDuration <= e.liveSyncDuration) && (e.liveMaxLatencyDuration = t);
522
+ }
523
+ var me = class {
524
+ name = "hls";
525
+ hls = null;
526
+ deps;
527
+ licenseUrl;
528
+ detectedLowLatency = !1;
529
+ mediaRecoveries = 0;
530
+ networkRecoveries = 0;
531
+ licenseRecoveries = 0;
532
+ recoveryResetTimer = null;
533
+ networkRetryTimer = null;
534
+ licenseRetryTimer = null;
535
+ onVisibility = null;
536
+ sawLive = !1;
537
+ lastEndSN = -1;
538
+ lastAdvanceMs = 0;
539
+ endWatchdog = null;
540
+ endedEmitted = !1;
541
+ constructor(e) {
542
+ this.deps = e, this.licenseUrl = e.descriptor.drm?.widevine?.licenseUrl;
543
+ }
544
+ async load() {
545
+ let { descriptor: e, snapToLiveOnRefocus: n, video: r, emit: i } = this.deps;
546
+ if (e.drmEnabled && !e.drm?.widevine?.licenseUrl) {
547
+ i({
548
+ type: "error",
549
+ error: new t("INTERNAL", 0, "DRM is required but no Widevine license is available for this stream on this browser"),
550
+ fatal: !0
551
+ });
552
+ return;
553
+ }
554
+ this.startHls(), n !== !1 && typeof document < "u" && (this.onVisibility = () => {
555
+ let e = this.hls;
556
+ if (document.visibilityState !== "visible" || !e) return;
557
+ let t = e.liveSyncPosition;
558
+ if (typeof t != "number") return;
559
+ let n = typeof e.targetLatency == "number" && e.targetLatency > 0 ? e.targetLatency : 3;
560
+ t - r.currentTime > Math.max(n * 3, 8) && (r.currentTime = t);
561
+ }, document.addEventListener("visibilitychange", this.onVisibility));
562
+ }
563
+ startHls() {
564
+ let { video: t, descriptor: n, credential: r, deviceId: i, userId: a, lowLatency: o, hlsConfig: s, emit: c } = this.deps, l = n.drmEnabled && !!n.drm?.widevine?.licenseUrl, u = {
565
+ enableWorker: !o,
566
+ lowLatencyMode: o,
567
+ backBufferLength: 90,
568
+ maxBufferHole: .5,
569
+ maxLiveSyncPlaybackRate: 1.5,
570
+ abrEwmaDefaultEstimate: 1e6,
571
+ capLevelToPlayerSize: !0,
572
+ ...l ? {
573
+ emeEnabled: !0,
574
+ drmSystems: { "com.widevine.alpha": { licenseUrl: this.licenseUrl } },
575
+ drmSystemOptions: {
576
+ videoRobustness: "SW_SECURE_DECODE",
577
+ audioRobustness: "SW_SECURE_CRYPTO",
578
+ videoEncryptionScheme: "cbcs",
579
+ audioEncryptionScheme: "cbcs"
580
+ },
581
+ licenseXhrSetup: (e, t) => {
582
+ 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);
583
+ }
584
+ } : {},
585
+ ...s
586
+ };
587
+ pe(u);
588
+ let d = new e(u);
589
+ this.hls = d, d.on(e.Events.MANIFEST_PARSED, () => {
590
+ this.mediaRecoveries = 0, this.networkRecoveries = 0, c({ type: "ready" });
591
+ }), d.on(e.Events.FRAG_BUFFERED, () => {
592
+ (this.mediaRecoveries > 0 || this.networkRecoveries > 0 || this.licenseRecoveries > 0) && !this.recoveryResetTimer && (this.recoveryResetTimer = setTimeout(() => {
593
+ this.mediaRecoveries = 0, this.networkRecoveries = 0, this.licenseRecoveries = 0, this.recoveryResetTimer = null;
594
+ }, V));
595
+ }), d.on(e.Events.LEVEL_LOADED, (e, t) => {
596
+ if (this.detectedLowLatency) return;
597
+ let n = t.details;
598
+ (n?.partList?.length || (n?.partTarget ?? 0) > 0) && (this.detectedLowLatency = !0, c({
599
+ type: "lowLatencyDetected",
600
+ value: !0
601
+ }));
602
+ }), d.on(e.Events.LEVEL_LOADED, (e, t) => this.trackEnd(t)), this.endWatchdog = setInterval(() => this.checkEndStale(), U), d.on(e.Events.LEVEL_SWITCHED, (e, t) => c({
603
+ type: "qualitychange",
604
+ index: t.level
605
+ })), d.on(e.Events.AUDIO_TRACK_SWITCHED, (e, t) => c({
606
+ type: "audiotrackchange",
607
+ id: t.id
608
+ })), d.on(e.Events.SUBTITLE_TRACK_SWITCH, (e, t) => c({
609
+ type: "texttrackchange",
610
+ id: t.id
611
+ })), d.on(e.Events.ERROR, (e, t) => this.onError(t)), d.loadSource(n.manifestUrl), d.attachMedia(t);
612
+ }
613
+ getQualityLevels() {
614
+ return this.hls ? this.hls.levels.map((e, t) => ({
615
+ index: t,
616
+ width: e.width || void 0,
617
+ height: e.height || void 0,
618
+ bitrate: e.bitrate,
619
+ codecs: e.videoCodec || e.codecSet || void 0
620
+ })) : [];
621
+ }
622
+ getCurrentQuality() {
623
+ return this.hls ? this.hls.autoLevelEnabled ? -1 : this.hls.currentLevel : -1;
624
+ }
625
+ setQuality(e) {
626
+ this.hls && (this.hls.currentLevel = e);
627
+ }
628
+ setMaxBitrate(e) {
629
+ let t = this.hls;
630
+ if (!t) return;
631
+ if (e == null) {
632
+ t.autoLevelCapping = -1;
633
+ return;
634
+ }
635
+ let n = -1;
636
+ t.levels.forEach((t, r) => {
637
+ t.bitrate <= e && (n = r);
638
+ }), t.autoLevelCapping = n;
639
+ }
640
+ getAudioTracks() {
641
+ return this.hls ? this.hls.audioTracks.map((e) => ({
642
+ id: e.id,
643
+ name: e.name,
644
+ lang: e.lang || void 0,
645
+ default: !!e.default
646
+ })) : [];
647
+ }
648
+ setAudioTrack(e) {
649
+ this.hls && (this.hls.audioTrack = e);
650
+ }
651
+ getTextTracks() {
652
+ return this.hls ? this.hls.subtitleTracks.map((e) => ({
653
+ id: e.id,
654
+ name: e.name,
655
+ lang: e.lang || void 0
656
+ })) : [];
657
+ }
658
+ setTextTrack(e) {
659
+ let t = this.hls;
660
+ t && (e < 0 ? (t.subtitleTrack = -1, t.subtitleDisplay = !1) : (t.subtitleTrack = e, t.subtitleDisplay = !0));
661
+ }
662
+ seekToLive() {
663
+ let e = this.hls;
664
+ e && typeof e.liveSyncPosition == "number" && (this.deps.video.currentTime = e.liveSyncPosition);
665
+ }
666
+ setLicenseUrl(e) {
667
+ this.licenseUrl = e;
668
+ }
669
+ getStats() {
670
+ let e = this.hls, t = {
671
+ droppedFrames: F(this.deps.video),
672
+ lowLatency: this.detectedLowLatency
673
+ };
674
+ if (e) {
675
+ 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));
676
+ let n = e.currentLevel >= 0 ? e.levels[e.currentLevel] : void 0;
677
+ 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));
678
+ }
679
+ return t;
680
+ }
681
+ trackEnd(e) {
682
+ let t = e.details;
683
+ if (t) {
684
+ if (t.live === !0) {
685
+ this.sawLive = !0;
686
+ let e = t.endSN ?? -1;
687
+ e !== this.lastEndSN && (this.lastEndSN = e, this.lastAdvanceMs = Date.now());
688
+ return;
689
+ }
690
+ this.sawLive && this.emitEnded("endlist");
691
+ }
692
+ }
693
+ checkEndStale() {
694
+ if (!(this.endedEmitted || !this.sawLive || this.lastAdvanceMs === 0)) {
695
+ if (typeof document < "u" && document.hidden) {
696
+ this.lastAdvanceMs = Date.now();
697
+ return;
698
+ }
699
+ Date.now() - this.lastAdvanceMs > H && this.emitEnded("stalled");
700
+ }
701
+ }
702
+ emitEnded(e) {
703
+ this.endedEmitted || (this.endedEmitted = !0, this.endWatchdog &&= (clearInterval(this.endWatchdog), null), this.deps.emit({
704
+ type: "ended",
705
+ reason: e
706
+ }));
707
+ }
708
+ stop() {
709
+ this.endWatchdog &&= (clearInterval(this.endWatchdog), null);
710
+ try {
711
+ this.hls?.stopLoad();
712
+ } catch {}
713
+ try {
714
+ this.deps.video.pause();
715
+ } catch {}
716
+ }
717
+ destroy() {
718
+ if (this.clearRecoveryReset(), this.endWatchdog &&= (clearInterval(this.endWatchdog), null), this.onVisibility && typeof document < "u" && (document.removeEventListener("visibilitychange", this.onVisibility), this.onVisibility = null), this.networkRetryTimer &&= (clearTimeout(this.networkRetryTimer), null), this.licenseRetryTimer &&= (clearTimeout(this.licenseRetryTimer), null), this.hls) {
719
+ try {
720
+ this.hls.destroy();
721
+ } catch (e) {
722
+ console.warn("havik-player: hls.js destroy() threw during teardown", e);
723
+ }
724
+ this.hls = null;
725
+ }
726
+ }
727
+ clearRecoveryReset() {
728
+ this.recoveryResetTimer &&= (clearTimeout(this.recoveryResetTimer), null);
729
+ }
730
+ scheduleLicenseRetry(t) {
731
+ if (!(t.details === e.ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED || t.details === e.ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED)) return !1;
732
+ let n = t.response?.code ?? 0;
733
+ if (!(n === 0 || n === 408 || n === 429 || n >= 500) || this.licenseRecoveries >= B) return !1;
734
+ this.licenseRecoveries += 1;
735
+ let r = Math.min(8e3, 500 * 2 ** (this.licenseRecoveries - 1));
736
+ return this.licenseRetryTimer = setTimeout(() => {
737
+ if (this.licenseRetryTimer = null, this.hls) {
738
+ try {
739
+ this.hls.destroy();
740
+ } catch (e) {
741
+ console.warn("havik-player: hls.js destroy() threw during license retry", e);
742
+ }
743
+ this.hls = null, this.startHls();
744
+ }
745
+ }, r), !0;
746
+ }
747
+ onError(t) {
748
+ if (!t.fatal) return;
749
+ let n = this.hls;
750
+ if (n) {
751
+ if (this.clearRecoveryReset(), t.type === e.ErrorTypes.KEY_SYSTEM_ERROR) {
752
+ if (this.scheduleLicenseRetry(t)) return;
753
+ this.deps.emit({
754
+ type: "error",
755
+ error: this.toError(t, "DRM"),
756
+ fatal: !0
757
+ });
758
+ return;
759
+ }
760
+ if (t.type === e.ErrorTypes.NETWORK_ERROR) {
761
+ if (this.networkRecoveries < z) {
762
+ this.networkRecoveries += 1;
763
+ let e = Math.min(8e3, 1e3 * 2 ** (this.networkRecoveries - 1));
764
+ this.networkRetryTimer = setTimeout(() => {
765
+ this.networkRetryTimer = null, this.hls && this.hls.startLoad();
766
+ }, e);
767
+ return;
768
+ }
769
+ this.deps.emit({
770
+ type: "error",
771
+ error: this.toError(t, "NETWORK"),
772
+ fatal: !0
773
+ });
774
+ return;
775
+ }
776
+ if (t.type === e.ErrorTypes.MEDIA_ERROR) {
777
+ if (this.mediaRecoveries < R) {
778
+ this.mediaRecoveries += 1, n.recoverMediaError();
779
+ return;
780
+ }
781
+ this.deps.emit({
782
+ type: "error",
783
+ error: this.toError(t),
784
+ fatal: !0
785
+ });
786
+ return;
787
+ }
788
+ this.deps.emit({
789
+ type: "error",
790
+ error: this.toError(t),
791
+ fatal: !0
792
+ });
793
+ }
794
+ }
795
+ toError(e, n = "PLAYBACK") {
796
+ let r = e.response?.code ?? 0, i = e.details ?? e.type, a = e.reason ?? e.error?.message ?? "";
797
+ return new t(r === 401 ? "UNAUTHORIZED" : r === 403 || n === "DRM" ? "FORBIDDEN" : "INTERNAL", r, `${n} error: ${i}${a ? ` (${a})` : ""}`);
798
+ }
799
+ };
800
+ //#endregion
801
+ //#region src/core/engine/native.ts
802
+ function he(e) {
803
+ return !!(e ?? (typeof document < "u" ? document.createElement("video") : void 0))?.canPlayType("application/vnd.apple.mpegurl");
804
+ }
805
+ var W = class {
806
+ name = "native";
807
+ deps;
808
+ onLoaded;
809
+ onError;
810
+ constructor(e) {
811
+ this.deps = e;
812
+ }
813
+ async load() {
814
+ let { video: e, descriptor: n, emit: r } = this.deps;
815
+ if (n.drmEnabled) {
816
+ if (!n.drm?.fairplay) {
817
+ r({
818
+ type: "error",
819
+ error: new t("INTERNAL", 0, "native HLS engine cannot satisfy this DRM (Widevine requires the hls.js engine); use engine \"auto\"/\"hls\" on a Widevine-capable browser"),
820
+ fatal: !0
821
+ });
822
+ return;
823
+ }
824
+ r({
825
+ type: "error",
826
+ error: new t("INTERNAL", 0, "FairPlay EME is not implemented in this SDK version; relying on native passthrough"),
827
+ fatal: !1
828
+ });
829
+ }
830
+ this.onLoaded = () => r({ type: "ready" }), this.onError = () => r({
831
+ type: "error",
832
+ error: new t("INTERNAL", 0, "native HLS playback error"),
833
+ fatal: !0
834
+ }), e.addEventListener("loadedmetadata", this.onLoaded, { once: !0 }), e.addEventListener("error", this.onError), e.src = n.manifestUrl, e.load();
835
+ }
836
+ setLicenseUrl(e) {}
837
+ getQualityLevels() {
838
+ return [];
839
+ }
840
+ getCurrentQuality() {
841
+ return -1;
842
+ }
843
+ setQuality(e) {}
844
+ setMaxBitrate(e) {}
845
+ getAudioTracks() {
846
+ let e = this.deps.video.audioTracks;
847
+ return e ? Array.from({ length: e.length }, (t, n) => {
848
+ let r = e[n];
849
+ return {
850
+ id: n,
851
+ name: r.label || r.language || `Audio ${n + 1}`,
852
+ lang: r.language || void 0,
853
+ default: r.enabled
854
+ };
855
+ }) : [];
856
+ }
857
+ setAudioTrack(e) {
858
+ let t = this.deps.video.audioTracks;
859
+ if (t) for (let n = 0; n < t.length; n += 1) t[n].enabled = n === e;
860
+ }
861
+ getTextTracks() {
862
+ let e = this.deps.video.textTracks;
863
+ return Array.from({ length: e.length }, (t, n) => {
864
+ let r = e[n];
865
+ return {
866
+ id: n,
867
+ name: r.label || r.language || `Text ${n + 1}`,
868
+ lang: r.language || void 0
869
+ };
870
+ });
871
+ }
872
+ setTextTrack(e) {
873
+ let t = this.deps.video.textTracks;
874
+ for (let n = 0; n < t.length; n += 1) t[n].mode = n === e ? "showing" : "disabled";
875
+ }
876
+ seekToLive() {
877
+ let e = this.deps.video;
878
+ e.seekable.length > 0 && (e.currentTime = e.seekable.end(e.seekable.length - 1));
879
+ }
880
+ getStats() {
881
+ let e = this.deps.video, t = { droppedFrames: F(e) };
882
+ return e.duration === Infinity && (t.isLive = !0, e.seekable.length > 0 && (t.atLiveEdge = e.currentTime >= e.seekable.end(e.seekable.length - 1) - 2)), t;
883
+ }
884
+ stop() {
885
+ try {
886
+ this.deps.video.pause();
887
+ } catch {}
888
+ }
889
+ destroy() {
890
+ let { video: e } = this.deps;
891
+ this.onLoaded && e.removeEventListener("loadedmetadata", this.onLoaded), this.onError && e.removeEventListener("error", this.onError);
892
+ try {
893
+ e.pause();
894
+ } catch {}
895
+ e.removeAttribute("src");
896
+ try {
897
+ e.load();
898
+ } catch {}
899
+ }
900
+ }, G = {
901
+ accent: "#E1B600",
902
+ accentText: "#1B1C23",
903
+ background: "#0e0f13",
904
+ surface: "#1B1C23",
905
+ surfaceMuted: "#2a2c36",
906
+ text: "#ffffff",
907
+ textMuted: "#9aa0ad",
908
+ border: "rgba(255, 255, 255, 0.1)",
909
+ radius: "8px",
910
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
911
+ }, K = {
912
+ accent: "--havik-accent",
913
+ accentText: "--havik-accent-text",
914
+ background: "--havik-bg",
915
+ surface: "--havik-surface",
916
+ surfaceMuted: "--havik-surface-muted",
917
+ text: "--havik-text",
918
+ textMuted: "--havik-text-muted",
919
+ border: "--havik-border",
920
+ radius: "--havik-radius",
921
+ fontFamily: "--havik-font"
922
+ };
923
+ function q(e, t) {
924
+ let n = {
925
+ ...G,
926
+ ...t
927
+ };
928
+ for (let t of Object.keys(K)) e.style.setProperty(K[t], n[t]);
929
+ n.logoUrl && e.style.setProperty("--havik-logo", `url("${n.logoUrl}")`);
930
+ }
931
+ //#endregion
932
+ //#region src/ui/styles.ts
933
+ var J = "havik-player-styles", ge = "\n.havik-player{position:relative;display:block;width:100%;background:var(--havik-bg,#0e0f13);\n color:var(--havik-text,#fff);font-family:var(--havik-font,system-ui,sans-serif);\n overflow:hidden;line-height:1.4;-webkit-tap-highlight-color:transparent}\n.havik-player video{display:block;width:100%;height:auto;aspect-ratio:16/9;\n object-fit:contain;background:#000;outline:none}\n.havik-player:focus{outline:none}\n.havik-player *{box-sizing:border-box}\n\n.havik-player:fullscreen,.havik-player:-webkit-full-screen{display:flex;align-items:center;\n justify-content:center;width:100vw;height:100vh;aspect-ratio:auto}\n.havik-player:fullscreen video,.havik-player:-webkit-full-screen video{width:100%;height:100%;\n aspect-ratio:auto;object-fit:contain}\n\n.havik-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;cursor:pointer}\n.havik-gradient{position:absolute;left:0;right:0;bottom:0;height:120px;pointer-events:none;\n background:linear-gradient(to top,rgba(0,0,0,.78),transparent);\n opacity:1;transition:opacity .25s}\n.havik-player.havik-hide .havik-gradient,\n.havik-player.havik-hide .havik-bar{opacity:0;pointer-events:none}\n.havik-player.havik-hide{cursor:none}\n\n.havik-center{width:74px;height:74px;border-radius:50%;border:0;cursor:pointer;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);\n display:flex;align-items:center;justify-content:center;\n box-shadow:0 6px 24px rgba(0,0,0,.45);transition:transform .15s,opacity .15s}\n.havik-center:hover{transform:scale(1.06)}\n.havik-center svg{width:38px;height:38px;margin-left:2px}\n.havik-center.havik-gone{opacity:0;pointer-events:none;transform:scale(.8)}\n\n.havik-spinner{width:56px;height:56px;border-radius:50%;\n border:4px solid rgba(255,255,255,.2);border-top-color:var(--havik-accent,#E1B600);\n animation:havik-spin .8s linear infinite}\n@keyframes havik-spin{to{transform:rotate(360deg)}}\n\n/* Status card — branded full-bleed overlay for the stopped/pre-play states\n (ended / fatal error / pre-play / armed-waiting). */\n.havik-status{position:absolute;inset:0;display:flex;flex-direction:column;gap:16px;\n align-items:center;justify-content:center;background:rgba(14,15,19,.82);text-align:center;padding:24px}\n.havik-status__logo{width:128px;height:46px;background:var(--havik-logo) center/contain no-repeat;opacity:.95}\n.havik-status__msg{color:var(--havik-text,#fff);font-size:15px;font-weight:600;max-width:480px}\n.havik-status__btn{display:inline-flex;align-items:center;gap:8px;border:0;cursor:pointer;\n padding:10px 18px;border-radius:var(--havik-radius,8px);font-weight:700;font-size:14px;\n background:var(--havik-accent,#E1B600);color:var(--havik-accent-text,#1B1C23);transition:transform .15s}\n.havik-status__btn:hover{transform:scale(1.04)}\n.havik-status__btn svg{width:18px;height:18px}\n/* Pre-play: lighter scrim so the poster shows through, and a round play CTA. */\n.havik-status--preplay{background:rgba(14,15,19,.4)}\n.havik-status--preplay .havik-status__btn--play{width:74px;height:74px;border-radius:50%;padding:0;\n justify-content:center;box-shadow:0 6px 24px rgba(0,0,0,.45)}\n.havik-status--preplay .havik-status__btn--play svg{width:38px;height:38px;margin-left:3px}\n\n.havik-watermark{position:absolute;top:12px;right:14px;height:22px;opacity:.85;pointer-events:none;\n content:'';background:var(--havik-logo) center/contain no-repeat;width:90px}\n\n.havik-bar{position:absolute;left:0;right:0;bottom:0;padding:0 12px 8px;\n opacity:1;transition:opacity .25s;z-index:2}\n.havik-seek{position:relative;height:16px;display:flex;align-items:center;cursor:pointer;margin-bottom:2px}\n.havik-seek__track{position:relative;width:100%;height:4px;border-radius:3px;background:rgba(255,255,255,.25)}\n.havik-seek__buffered{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:rgba(255,255,255,.4)}\n.havik-seek__played{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-seek__thumb{position:absolute;top:50%;width:13px;height:13px;border-radius:50%;\n background:var(--havik-accent,#E1B600);transform:translate(-50%,-50%) scale(0);transition:transform .1s}\n.havik-seek:hover .havik-seek__thumb{transform:translate(-50%,-50%) scale(1)}\n\n.havik-controls{display:flex;align-items:center;gap:6px;height:40px}\n.havik-spacer{flex:1}\n.havik-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;border-radius:var(--havik-radius,8px);\n padding:0;transition:background .12s,color .12s}\n.havik-btn:hover{background:rgba(255,255,255,.12)}\n.havik-btn:focus-visible{outline:2px solid var(--havik-accent,#E1B600);outline-offset:1px}\n.havik-btn svg{width:22px;height:22px}\n\n.havik-vol{display:flex;align-items:center}\n.havik-vol__slider{width:0;opacity:0;transition:width .18s,opacity .18s;height:4px;cursor:pointer;\n appearance:none;-webkit-appearance:none;background:rgba(255,255,255,.3);border-radius:3px;margin:0 4px}\n.havik-vol:hover .havik-vol__slider,.havik-vol:focus-within .havik-vol__slider{width:70px;opacity:1}\n.havik-vol__slider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--havik-accent,#E1B600)}\n.havik-vol__slider::-moz-range-thumb{width:12px;height:12px;border:0;border-radius:50%;background:var(--havik-accent,#E1B600)}\n\n.havik-time{font-size:12px;color:var(--havik-text-muted,#9aa0ad);\n font-variant-numeric:tabular-nums;padding:0 6px;white-space:nowrap}\n.havik-live{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;\n letter-spacing:.4px;padding:0 8px;color:var(--havik-text,#fff)}\n.havik-live svg{width:9px;height:9px;color:var(--havik-text-muted,#9aa0ad)}\n.havik-live.is-live svg{color:var(--havik-accent,#E1B600)}\n.havik-live__go{margin-left:2px;border:0;cursor:pointer;background:transparent;color:var(--havik-text-muted,#9aa0ad);\n font:inherit;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 6px;border-radius:6px}\n.havik-live.is-live .havik-live__go{display:none}\n.havik-live__go:hover{color:var(--havik-accent,#E1B600)}\n\n.havik-menu{position:absolute;bottom:54px;right:12px;min-width:170px;max-height:230px;overflow:auto;\n background:var(--havik-surface,#1B1C23);border:1px solid var(--havik-border,rgba(255,255,255,.1));\n border-radius:var(--havik-radius,8px);box-shadow:0 10px 30px rgba(0,0,0,.5);padding:6px;z-index:3}\n.havik-menu[hidden]{display:none}\n.havik-menu__title{font-size:11px;text-transform:uppercase;letter-spacing:.6px;\n color:var(--havik-text-muted,#9aa0ad);padding:6px 8px 4px}\n.havik-menu__item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;\n border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;font:inherit;font-size:13px;\n text-align:left;padding:7px 8px;border-radius:6px}\n.havik-menu__item:hover{background:var(--havik-surface-muted,#2a2c36)}\n.havik-menu__item[aria-checked=\"true\"]{color:var(--havik-accent,#E1B600);font-weight:600}\n.havik-menu__item[aria-checked=\"true\"]::after{content:'✓'}\n";
934
+ function _e(e = document) {
935
+ if (e.getElementById(J)) return;
936
+ let t = e.createElement("style");
937
+ t.id = J, t.textContent = ge, e.head.appendChild(t);
938
+ }
939
+ //#endregion
940
+ //#region src/ui/icons.ts
941
+ var ve = {
942
+ play: "<path d=\"M8 5v14l11-7z\"/>",
943
+ pause: "<path d=\"M6 5h4v14H6zM14 5h4v14h-4z\"/>",
944
+ 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\"/>",
945
+ volumeMute: "<path d=\"M3 10v4h4l5 5V5L7 10H3zm16.5 2 2.5-2.5-1.4-1.4L18 10.6 15.4 8 14 9.4l2.6 2.6L14 14.6l1.4 1.4 2.6-2.6 2.6 2.6 1.4-1.4z\"/>",
946
+ cc: "<path d=\"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zM10 11H8.5v-.5h-2v3h2V13H10v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H17v1a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1z\"/>",
947
+ settings: "<path d=\"m19.4 13 .1-1-.1-1 2-1.6-2-3.4-2.4 1a7 7 0 0 0-1.7-1l-.4-2.5H9.1L8.7 6a7 7 0 0 0-1.7 1l-2.4-1-2 3.4L2.6 11l-.1 1 .1 1-2 1.6 2 3.4 2.4-1c.5.4 1 .7 1.7 1l.4 2.5h5.8l.4-2.5c.6-.3 1.2-.6 1.7-1l2.4 1 2-3.4-2-1.6zM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7z\"/>",
948
+ pip: "<path d=\"M19 7h-8v6h8V7zm4 12V5a2 2 0 0 0-2-2H3a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2zm-2 .02H3V4.98h18v14.04z\"/>",
949
+ fullscreen: "<path d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\"/>",
950
+ fullscreenExit: "<path d=\"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z\"/>",
951
+ liveDot: "<circle cx=\"12\" cy=\"12\" r=\"6\"/>",
952
+ 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\"/>"
953
+ };
954
+ function Y(e) {
955
+ return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">${ve[e]}</svg>`;
956
+ }
957
+ //#endregion
958
+ //#region src/ui/controls.ts
959
+ var ye = 3e3, be = /* @__PURE__ */ new Set([
960
+ "GONE",
961
+ "NOT_FOUND",
962
+ "INVALID_URN",
963
+ "UNAUTHORIZED"
964
+ ]);
965
+ function xe(e) {
966
+ return new Se(e);
967
+ }
968
+ var Se = class {
969
+ root;
970
+ video;
971
+ player;
972
+ doc;
973
+ autoHideMs;
974
+ overlay;
975
+ center;
976
+ spinner;
977
+ bar;
978
+ gradient;
979
+ seek;
980
+ seekBuffered;
981
+ seekPlayed;
982
+ seekThumb;
983
+ playBtn;
984
+ muteBtn;
985
+ volSlider;
986
+ live;
987
+ liveGo;
988
+ time;
989
+ ccBtn;
990
+ audioBtn;
991
+ qualityBtn;
992
+ pipBtn;
993
+ fsBtn;
994
+ menu;
995
+ statusBox = null;
996
+ statusKind = null;
997
+ hasPlayed = !1;
998
+ lastError = null;
999
+ logoUrl;
1000
+ statusOverlays;
1001
+ endedMessage;
1002
+ errorMessage;
1003
+ hideTimer;
1004
+ openMenu = null;
1005
+ scrubbing = !1;
1006
+ offFns = [];
1007
+ constructor(e) {
1008
+ this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? ye, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.errorMessage = e.errorMessage, _e(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");
1009
+ let t = (e, t, n) => {
1010
+ let r = this.doc.createElement(e);
1011
+ if (r.className = t, n) for (let [e, t] of Object.entries(n)) r.setAttribute(e, t);
1012
+ return r;
1013
+ }, n = (e, n, r) => {
1014
+ let i = t("button", e, {
1015
+ type: "button",
1016
+ "aria-label": r,
1017
+ title: r
1018
+ });
1019
+ return i.innerHTML = Y(n), i;
1020
+ };
1021
+ this.overlay = t("div", "havik-overlay"), this.center = t("button", "havik-center", {
1022
+ type: "button",
1023
+ "aria-label": "Play"
1024
+ }), this.center.innerHTML = Y("play"), this.spinner = t("div", "havik-spinner"), this.spinner.style.display = "none", this.overlay.append(this.center, this.spinner), this.gradient = t("div", "havik-gradient"), this.bar = t("div", "havik-bar"), this.seek = t("div", "havik-seek", {
1025
+ role: "slider",
1026
+ "aria-label": "Seek",
1027
+ "aria-valuemin": "0",
1028
+ "aria-valuemax": "100",
1029
+ tabindex: "0"
1030
+ });
1031
+ let r = t("div", "havik-seek__track");
1032
+ this.seekBuffered = t("div", "havik-seek__buffered"), this.seekPlayed = t("div", "havik-seek__played"), this.seekThumb = t("div", "havik-seek__thumb"), r.append(this.seekBuffered, this.seekPlayed, this.seekThumb), this.seek.append(r);
1033
+ let i = t("div", "havik-controls");
1034
+ this.playBtn = n("havik-btn", "play", "Play"), this.muteBtn = n("havik-btn", "volumeHigh", "Mute"), this.volSlider = t("input", "havik-vol__slider", {
1035
+ type: "range",
1036
+ min: "0",
1037
+ max: "1",
1038
+ step: "0.05",
1039
+ "aria-label": "Volume"
1040
+ });
1041
+ let a = t("div", "havik-vol");
1042
+ a.append(this.muteBtn, this.volSlider), this.live = t("div", "havik-live"), this.live.innerHTML = Y("liveDot");
1043
+ let o = this.doc.createElement("span");
1044
+ o.textContent = "LIVE", this.liveGo = t("button", "havik-live__go", { type: "button" }), this.liveGo.textContent = "Go live", this.live.append(o, this.liveGo), this.time = t("div", "havik-time"), this.time.textContent = "0:00", this.time.style.display = "none";
1045
+ let s = t("div", "havik-spacer");
1046
+ if (this.ccBtn = n("havik-btn", "cc", "Subtitles"), this.audioBtn = n("havik-btn", "settings", "Audio track"), this.qualityBtn = n("havik-btn", "settings", "Quality"), this.pipBtn = n("havik-btn", "pip", "Picture in picture"), this.fsBtn = n("havik-btn", "fullscreen", "Fullscreen"), this.ccBtn.style.display = "none", this.audioBtn.style.display = "none", this.qualityBtn.style.display = "none", this.pipSupported() || (this.pipBtn.style.display = "none"), i.append(this.playBtn, a, this.live, this.time, s, this.ccBtn, this.audioBtn, this.qualityBtn, this.pipBtn, this.fsBtn), this.bar.append(this.seek, i), this.menu = t("div", "havik-menu", { role: "menu" }), this.menu.hidden = !0, this.root.append(this.gradient, this.overlay, this.bar, this.menu), e.logoUrl) {
1047
+ let n = t("div", "havik-watermark");
1048
+ n.style.backgroundImage = `url("${e.logoUrl}")`, this.root.append(n);
1049
+ }
1050
+ this.wire(), this.syncPlayPause(), this.syncVolume(), this.scheduleHide();
1051
+ }
1052
+ on(e, t, n, r) {
1053
+ e.addEventListener(t, n, r), this.offFns.push(() => e.removeEventListener(t, n, r));
1054
+ }
1055
+ wire() {
1056
+ let e = this.video;
1057
+ this.on(this.playBtn, "click", () => this.togglePlay()), this.on(this.center, "click", () => this.togglePlay()), this.on(this.overlay, "click", (e) => {
1058
+ e.target === this.overlay && this.togglePlay();
1059
+ }), this.on(this.muteBtn, "click", () => this.player.setMuted(!e.muted)), this.on(this.volSlider, "input", () => {
1060
+ let t = Number(this.volSlider.value);
1061
+ this.player.setVolume(t), t > 0 && e.muted && this.player.setMuted(!1);
1062
+ }), this.on(this.liveGo, "click", () => this.player.seekToLive()), this.on(this.pipBtn, "click", () => void this.togglePip()), this.on(this.fsBtn, "click", () => void this.toggleFullscreen()), this.on(this.ccBtn, "click", () => this.toggleMenu("cc")), this.on(this.audioBtn, "click", () => this.toggleMenu("audio")), this.on(this.qualityBtn, "click", () => this.toggleMenu("quality")), this.on(this.seek, "pointerdown", (e) => this.onSeekDown(e)), this.on(this.seek, "keydown", (e) => this.onSeekKey(e)), this.on(e, "play", () => this.syncPlayPause()), this.on(e, "pause", () => this.syncPlayPause()), this.on(e, "volumechange", () => this.syncVolume()), this.on(e, "timeupdate", () => this.syncProgress()), this.on(e, "progress", () => this.syncProgress()), this.on(e, "durationchange", () => this.syncProgress()), this.offFns.push(this.player.on("statechange", () => this.syncState())), this.offFns.push(this.player.on("ready", () => this.refreshTracks())), this.offFns.push(this.player.on("qualitychange", () => this.onTracksChanged())), this.offFns.push(this.player.on("audiotrackchange", () => this.onTracksChanged())), this.offFns.push(this.player.on("texttrackchange", () => this.onTracksChanged())), this.offFns.push(this.player.on("pipchange", (e) => this.syncPip(e))), this.offFns.push(this.player.on("stats", () => this.syncLive())), this.offFns.push(this.player.on("error", (e) => {
1063
+ this.lastError = e, this.statusKind = null, this.syncState();
1064
+ })), this.on(this.root, "pointermove", () => this.poke()), this.on(this.root, "pointerleave", () => this.scheduleHide(0)), this.on(this.root, "focusin", () => this.poke()), this.on(this.root, "keydown", (e) => this.onKey(e)), this.on(this.doc, "pointerdown", (e) => this.onDocPointer(e), { capture: !0 }), this.on(this.doc, "fullscreenchange", () => this.syncFullscreen()), this.syncState();
1065
+ }
1066
+ togglePlay() {
1067
+ this.video.paused || this.video.ended ? this.player.play() : this.player.pause();
1068
+ }
1069
+ syncPlayPause() {
1070
+ let e = !this.video.paused && !this.video.ended;
1071
+ this.setIcon(this.playBtn, e ? "pause" : "play"), this.playBtn.setAttribute("aria-label", e ? "Pause" : "Play"), this.setIcon(this.center, e ? "pause" : "play"), this.center.classList.toggle("havik-gone", e || !!this.statusBox), e ? this.scheduleHide() : this.show();
1072
+ }
1073
+ syncState() {
1074
+ let e = this.player.state;
1075
+ (e === "playing" || e === "buffering") && (this.hasPlayed = !0), this.syncStatus(e);
1076
+ let t = e === "loading" || e === "buffering";
1077
+ this.spinner.style.display = t && !this.statusBox ? "" : "none", (t || this.statusBox) && this.center.classList.add("havik-gone"), this.syncLive();
1078
+ }
1079
+ syncStatus(e) {
1080
+ let t = null;
1081
+ this.statusOverlays && (e === "ended" ? t = "ended" : e === "error" ? t = "error" : e === "waiting" ? t = "waiting" : !this.hasPlayed && (e === "idle" || e === "paused") && (t = "preplay")), this.renderStatus(t);
1082
+ }
1083
+ renderStatus(e) {
1084
+ if (e === this.statusKind || (this.statusKind = e, this.statusBox?.remove(), this.statusBox = null, !e)) return;
1085
+ let t = this.doc.createElement("div");
1086
+ if (t.className = `havik-status havik-status--${e}`, this.logoUrl) {
1087
+ let e = this.doc.createElement("div");
1088
+ e.className = "havik-status__logo", t.append(e);
1089
+ }
1090
+ if (e === "preplay") {
1091
+ let e = this.doc.createElement("button");
1092
+ e.type = "button", e.className = "havik-status__btn havik-status__btn--play", e.setAttribute("aria-label", "Play"), e.innerHTML = Y("play"), e.addEventListener("click", () => void this.player.play()), t.append(e);
1093
+ } else {
1094
+ let n = this.doc.createElement("div");
1095
+ 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 && !be.has(this.lastError.code)) {
1096
+ let e = this.doc.createElement("button");
1097
+ e.type = "button", e.className = "havik-status__btn", e.innerHTML = `${Y("retry")}<span>Retry</span>`, e.addEventListener("click", () => void this.player.retry()), t.append(e);
1098
+ }
1099
+ }
1100
+ this.root.append(t), this.statusBox = t, this.show();
1101
+ }
1102
+ syncVolume() {
1103
+ let e = this.video, t = e.muted || e.volume === 0;
1104
+ this.setIcon(this.muteBtn, t ? "volumeMute" : "volumeHigh"), this.muteBtn.setAttribute("aria-label", t ? "Unmute" : "Mute"), this.volSlider.value = String(t ? 0 : e.volume);
1105
+ }
1106
+ seekableRange() {
1107
+ let e = this.video;
1108
+ if (e.seekable && e.seekable.length > 0) {
1109
+ let t = e.seekable.start(0), n = e.seekable.end(e.seekable.length - 1);
1110
+ if (n > t) return {
1111
+ start: t,
1112
+ end: n
1113
+ };
1114
+ }
1115
+ return Number.isFinite(e.duration) && e.duration > 0 ? {
1116
+ start: 0,
1117
+ end: e.duration
1118
+ } : null;
1119
+ }
1120
+ syncProgress() {
1121
+ if (this.scrubbing) return;
1122
+ let e = this.seekableRange();
1123
+ if (!e) {
1124
+ this.seekPlayed.style.width = "100%";
1125
+ return;
1126
+ }
1127
+ let t = e.end - e.start, n = Math.min(1, Math.max(0, (this.video.currentTime - e.start) / t));
1128
+ this.seekPlayed.style.width = `${(n * 100).toFixed(2)}%`, this.seekThumb.style.left = `${(n * 100).toFixed(2)}%`, this.seek.setAttribute("aria-valuenow", String(Math.round(n * 100)));
1129
+ let r = this.video.buffered;
1130
+ if (r && r.length > 0) {
1131
+ let n = r.end(r.length - 1), i = Math.min(1, Math.max(0, (n - e.start) / t));
1132
+ this.seekBuffered.style.width = `${(i * 100).toFixed(2)}%`;
1133
+ }
1134
+ !this.player.getStats().isLive && Number.isFinite(this.video.duration) && (this.time.textContent = `${X(this.video.currentTime)} / ${X(this.video.duration)}`);
1135
+ }
1136
+ syncLive() {
1137
+ let e = this.player.getStats(), t = e.isLive !== !1;
1138
+ this.live.style.display = t ? "" : "none", this.time.style.display = t ? "none" : "", this.live.classList.toggle("is-live", e.atLiveEdge !== !1 && t);
1139
+ }
1140
+ onSeekDown(e) {
1141
+ let t = this.seekableRange();
1142
+ if (!t) return;
1143
+ e.preventDefault(), this.scrubbing = !0;
1144
+ let n = (e) => this.seekToPointer(e, t), r = () => {
1145
+ this.scrubbing = !1, this.doc.removeEventListener("pointermove", n), this.doc.removeEventListener("pointerup", r);
1146
+ };
1147
+ this.doc.addEventListener("pointermove", n), this.doc.addEventListener("pointerup", r), this.seekToPointer(e, t);
1148
+ }
1149
+ seekToPointer(e, t) {
1150
+ let n = this.seek.getBoundingClientRect(), r = n.width > 0 ? Math.min(1, Math.max(0, (e.clientX - n.left) / n.width)) : 0;
1151
+ this.seekPlayed.style.width = `${(r * 100).toFixed(2)}%`, this.seekThumb.style.left = `${(r * 100).toFixed(2)}%`, this.video.currentTime = t.start + r * (t.end - t.start);
1152
+ }
1153
+ onSeekKey(e) {
1154
+ e.key === "ArrowLeft" ? (this.nudge(-5), e.preventDefault()) : e.key === "ArrowRight" && (this.nudge(5), e.preventDefault());
1155
+ }
1156
+ nudge(e) {
1157
+ let t = this.seekableRange();
1158
+ t && (this.video.currentTime = Math.min(t.end, Math.max(t.start, this.video.currentTime + e)));
1159
+ }
1160
+ toggleMenu(e) {
1161
+ if (this.openMenu === e) {
1162
+ this.closeMenu();
1163
+ return;
1164
+ }
1165
+ this.openMenu = e, this.renderMenu(e), this.menu.hidden = !1, this.show();
1166
+ }
1167
+ closeMenu() {
1168
+ this.openMenu = null, this.menu.hidden = !0, this.menu.replaceChildren();
1169
+ }
1170
+ renderMenu(e) {
1171
+ e === "quality" ? this.renderQualityMenu() : e === "audio" ? this.renderAudioMenu() : this.renderCcMenu();
1172
+ }
1173
+ menuItem(e, t, n) {
1174
+ let r = this.doc.createElement("button");
1175
+ r.type = "button", r.className = "havik-menu__item", r.setAttribute("role", "menuitemradio"), r.setAttribute("aria-checked", t ? "true" : "false");
1176
+ let i = this.doc.createElement("span");
1177
+ return i.textContent = e, r.append(i), r.addEventListener("click", () => {
1178
+ n(), this.closeMenu();
1179
+ }), r;
1180
+ }
1181
+ renderQualityMenu() {
1182
+ let e = this.player.getQualityLevels(), t = this.player.getCurrentQuality(), n = this.doc.createElement("div");
1183
+ n.className = "havik-menu__title", n.textContent = "Quality", this.menu.replaceChildren(n, this.menuItem("Auto", t === -1, () => this.player.setQuality("auto")));
1184
+ for (let n of e) this.menu.append(this.menuItem(Ce(n), t === n.index, () => this.player.setQuality(n.index)));
1185
+ }
1186
+ renderAudioMenu() {
1187
+ let e = this.player.getAudioTracks(), t = this.doc.createElement("div");
1188
+ t.className = "havik-menu__title", t.textContent = "Audio", this.menu.replaceChildren(t);
1189
+ for (let t of e) this.menu.append(this.menuItem(we(t), t.default, () => this.player.setAudioTrack(t.id)));
1190
+ }
1191
+ renderCcMenu() {
1192
+ let e = this.player.getTextTracks(), t = this.doc.createElement("div");
1193
+ t.className = "havik-menu__title", t.textContent = "Subtitles", this.menu.replaceChildren(t, this.menuItem("Off", !1, () => this.player.setTextTrack(-1)));
1194
+ for (let t of e) this.menu.append(this.menuItem(Te(t), !1, () => this.player.setTextTrack(t.id)));
1195
+ }
1196
+ onTracksChanged() {
1197
+ this.openMenu && this.renderMenu(this.openMenu);
1198
+ }
1199
+ refreshTracks() {
1200
+ let e = this.player.getQualityLevels(), t = this.player.getAudioTracks(), n = this.player.getTextTracks();
1201
+ this.qualityBtn.style.display = e.length > 1 ? "" : "none", this.audioBtn.style.display = t.length > 1 ? "" : "none", this.ccBtn.style.display = n.length > 0 ? "" : "none", this.onTracksChanged();
1202
+ }
1203
+ pipSupported() {
1204
+ return "pictureInPictureEnabled" in this.doc && typeof this.video.requestPictureInPicture == "function";
1205
+ }
1206
+ async togglePip() {
1207
+ this.doc.pictureInPictureElement === this.video ? await this.player.exitPictureInPicture() : await this.player.enterPictureInPicture();
1208
+ }
1209
+ syncPip(e) {
1210
+ this.pipBtn.setAttribute("aria-pressed", e ? "true" : "false");
1211
+ }
1212
+ async toggleFullscreen() {
1213
+ let e = this.root, t = this.doc;
1214
+ try {
1215
+ t.fullscreenElement ? await t.exitFullscreen?.() : await e.requestFullscreen?.();
1216
+ } catch {}
1217
+ }
1218
+ syncFullscreen() {
1219
+ let e = this.doc.fullscreenElement === this.root;
1220
+ this.setIcon(this.fsBtn, e ? "fullscreenExit" : "fullscreen"), this.fsBtn.setAttribute("aria-label", e ? "Exit fullscreen" : "Fullscreen");
1221
+ }
1222
+ poke() {
1223
+ this.show(), this.scheduleHide();
1224
+ }
1225
+ show() {
1226
+ this.root.classList.remove("havik-hide");
1227
+ }
1228
+ scheduleHide(e = this.autoHideMs) {
1229
+ this.hideTimer && clearTimeout(this.hideTimer), this.hideTimer = setTimeout(() => {
1230
+ !this.video.paused && !this.video.ended && !this.openMenu && !this.statusBox && this.root.classList.add("havik-hide");
1231
+ }, e);
1232
+ }
1233
+ onDocPointer(e) {
1234
+ this.openMenu && !this.menu.contains(e.target) && ([
1235
+ this.qualityBtn,
1236
+ this.audioBtn,
1237
+ this.ccBtn
1238
+ ].some((t) => t.contains(e.target)) || this.closeMenu());
1239
+ }
1240
+ onKey(e) {
1241
+ let t = e.target;
1242
+ if (!(t && t !== this.root && t !== this.seek && t.tagName === "INPUT")) {
1243
+ switch (e.key) {
1244
+ case " ":
1245
+ case "k":
1246
+ this.togglePlay();
1247
+ break;
1248
+ case "m":
1249
+ this.player.setMuted(!this.video.muted);
1250
+ break;
1251
+ case "f":
1252
+ this.toggleFullscreen();
1253
+ break;
1254
+ case "c":
1255
+ this.ccBtn.style.display !== "none" && this.toggleMenu("cc");
1256
+ break;
1257
+ case "l":
1258
+ this.player.seekToLive();
1259
+ break;
1260
+ case "ArrowUp":
1261
+ this.player.setVolume(Math.min(1, this.video.volume + .1));
1262
+ break;
1263
+ case "ArrowDown":
1264
+ this.player.setVolume(Math.max(0, this.video.volume - .1));
1265
+ break;
1266
+ case "ArrowLeft":
1267
+ this.nudge(-5);
1268
+ break;
1269
+ case "ArrowRight":
1270
+ this.nudge(5);
1271
+ break;
1272
+ default: return;
1273
+ }
1274
+ this.poke(), e.preventDefault();
1275
+ }
1276
+ }
1277
+ setIcon(e, t) {
1278
+ e.innerHTML = Y(t);
1279
+ }
1280
+ destroy() {
1281
+ this.hideTimer && clearTimeout(this.hideTimer);
1282
+ for (let e of this.offFns) try {
1283
+ e();
1284
+ } catch {}
1285
+ this.offFns.length = 0, this.closeMenu(), this.statusBox?.remove();
1286
+ for (let e of [
1287
+ this.overlay,
1288
+ this.gradient,
1289
+ this.bar,
1290
+ this.menu
1291
+ ]) e.remove();
1292
+ this.root.classList.remove("havik-player", "havik-hide");
1293
+ }
1294
+ };
1295
+ function X(e) {
1296
+ (!Number.isFinite(e) || e < 0) && (e = 0);
1297
+ 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");
1298
+ return r > 0 ? `${r}:${i}:${a}` : `${i}:${a}`;
1299
+ }
1300
+ function Ce(e) {
1301
+ return e.height ? `${e.height}p` : `${Math.round(e.bitrate / 1e3)} kbps`;
1302
+ }
1303
+ function we(e) {
1304
+ return e.name || e.lang || `Track ${e.id}`;
1305
+ }
1306
+ function Te(e) {
1307
+ return e.name || e.lang || `Track ${e.id}`;
1308
+ }
1309
+ //#endregion
1310
+ //#region src/managed/index.ts
1311
+ var Ee = 6e3, De = 45e3, Oe = 2e3, ke = 3, Z = 2e3, Ae = 480 * 1e3;
1312
+ async function Q(e) {
1313
+ if (!e.video || typeof e.video.play != "function") throw new t("INTERNAL", 0, "createPlayer: opts.video must be an HTMLVideoElement");
1314
+ if (!e.baseUrl) throw new t("INTERNAL", 0, "createPlayer: baseUrl is required");
1315
+ if (!e.matchUrn) throw new t("INVALID_URN", 0, "createPlayer: matchUrn is required");
1316
+ if (!e.credential) throw new t("INTERNAL", 0, "createPlayer: credential is required");
1317
+ let n = new je(e);
1318
+ return await n.start(), n;
1319
+ }
1320
+ var je = class {
1321
+ opts;
1322
+ abort = new AbortController();
1323
+ listeners = {};
1324
+ engine = null;
1325
+ controlBar = null;
1326
+ container = null;
1327
+ detachMedia;
1328
+ detachPip;
1329
+ statsTimer;
1330
+ refreshTimer;
1331
+ _state = "idle";
1332
+ _descriptor = null;
1333
+ destroyed = !1;
1334
+ loadGen = 0;
1335
+ liveStateSub = null;
1336
+ endedHandled = !1;
1337
+ armingSub = null;
1338
+ armSseTimer = null;
1339
+ constructor(e) {
1340
+ this.opts = e;
1341
+ }
1342
+ get state() {
1343
+ return this._state;
1344
+ }
1345
+ get descriptor() {
1346
+ return this._descriptor;
1347
+ }
1348
+ async start() {
1349
+ this.opts.muted && (this.opts.video.muted = !0), this.opts.poster && (this.opts.video.poster = this.opts.poster), this.mountControls(), this.attachPipListeners(), this.setState("loading");
1350
+ let e = ++this.loadGen, n = this.resolveOptions();
1351
+ try {
1352
+ let t = await b(n);
1353
+ await this.onResolved(t, e);
1354
+ } catch (n) {
1355
+ if (this.opts.waitForLive && n instanceof t && i(n.code)) {
1356
+ this.setState("waiting"), this.beginArming(e);
1357
+ return;
1358
+ }
1359
+ throw n;
1360
+ }
1361
+ }
1362
+ resolveOptions() {
1363
+ return {
1364
+ baseUrl: this.opts.baseUrl,
1365
+ matchUrn: this.opts.matchUrn,
1366
+ credential: this.opts.credential,
1367
+ signal: this.abort.signal
1368
+ };
1369
+ }
1370
+ beginArming(e) {
1371
+ let t = this.opts.liveStateEvents === !1 ? void 0 : this.opts.eventsBaseUrl ?? T(this.opts.baseUrl);
1372
+ t ? this.armWithSse(e, t) : this.beginArmingByPolling(e);
1373
+ }
1374
+ beginArmingByPolling(e, t) {
1375
+ x({
1376
+ ...this.resolveOptions(),
1377
+ signal: t ?? this.abort.signal,
1378
+ waitForLive: this.buildWaitConfig()
1379
+ }).then((t) => this.onResolved(t, e)).catch((n) => {
1380
+ this.destroyed || this.abort.signal.aborted || t?.aborted || e !== this.loadGen || this.fail(n);
1381
+ });
1382
+ }
1383
+ armWithSse(e, n) {
1384
+ let r = !1, a = !1, o = new AbortController();
1385
+ this.abort.signal.addEventListener("abort", () => o.abort(), { once: !0 }), this.setState("waiting");
1386
+ let s = () => this.destroyed || this.abort.signal.aborted || e !== this.loadGen, c = async (t) => {
1387
+ r || s() || (r = !0, this.closeArming(), o.abort(), await this.onResolved(t, e));
1388
+ }, l = () => {
1389
+ r || a || s() || (a = !0, this.beginArmingByPolling(e, o.signal));
1390
+ }, u = () => {
1391
+ r || s() || b(this.resolveOptions()).then((e) => c(e)).catch((e) => {
1392
+ r || s() || e instanceof t && i(e.code) || (r = !0, this.closeArming(), o.abort(), this.fail(e instanceof t ? e : new t("INTERNAL", 0, String(e))));
1393
+ });
1394
+ }, d = (e) => {
1395
+ this.armSseTimer && clearTimeout(this.armSseTimer), this.armSseTimer = setTimeout(() => {
1396
+ !r && !s() && l();
1397
+ }, e);
1398
+ };
1399
+ d(Ee), this.armingSub = w({
1400
+ eventsBaseUrl: n,
1401
+ matchUrn: this.opts.matchUrn,
1402
+ credential: this.opts.credential,
1403
+ onAlive: () => d(De),
1404
+ onState: (e) => {
1405
+ r || s() || (e.state === "live" || e.state === "gone") && setTimeout(u, Math.random() * Oe);
1406
+ },
1407
+ onError: () => l()
1408
+ });
1409
+ }
1410
+ closeArming() {
1411
+ this.armSseTimer &&= (clearTimeout(this.armSseTimer), null), this.armingSub?.close(), this.armingSub = null;
1412
+ }
1413
+ buildWaitConfig() {
1414
+ let e = this.opts.waitForLive && this.opts.waitForLive !== !0 ? this.opts.waitForLive : {};
1415
+ return {
1416
+ ...e,
1417
+ onState: (t) => {
1418
+ this.setState("waiting"), this.emit("waiting", t), e.onState?.(t);
1419
+ }
1420
+ };
1421
+ }
1422
+ async onResolved(e, t) {
1423
+ if (this.destroyed || t !== this.loadGen) return;
1424
+ this._descriptor = e, this.endedHandled = !1;
1425
+ let n = await a(this.opts.credential);
1426
+ if (this.destroyed || t !== this.loadGen) return;
1427
+ this.setState("loading");
1428
+ let r = this.createEngine(e, n), i = I(this.opts.video, (e) => this.setState(e));
1429
+ if (await r.load(), this.destroyed || t !== this.loadGen) {
1430
+ i(), r.destroy();
1431
+ return;
1432
+ }
1433
+ this.engine = r, this.detachMedia = i, this.opts.maxBitrate != null && r.setMaxBitrate(this.opts.maxBitrate), this.startStats(), this.scheduleLicenseRefresh(), this.startLiveState(t);
1434
+ }
1435
+ createEngine(e, n) {
1436
+ let r = { ...this.opts.hlsConfig };
1437
+ this.opts.startLevel != null && (r.startLevel = this.opts.startLevel), this.opts.liveLatencyTarget != null && (r.liveSyncDuration = this.opts.liveLatencyTarget);
1438
+ let i = {
1439
+ video: this.opts.video,
1440
+ descriptor: e,
1441
+ credential: n,
1442
+ deviceId: O(),
1443
+ userId: this.opts.userId,
1444
+ lowLatency: this.opts.lowLatency === void 0 || this.opts.lowLatency === "auto" || this.opts.lowLatency,
1445
+ snapToLiveOnRefocus: this.opts.snapToLiveOnRefocus,
1446
+ hlsConfig: r,
1447
+ emit: (e) => this.onEngineEvent(e)
1448
+ }, a = this.opts.engine ?? "auto";
1449
+ if (a === "native") return new W(i);
1450
+ if (a === "hls" || L()) return new me(i);
1451
+ if (he(this.opts.video)) return new W(i);
1452
+ throw new t("INTERNAL", 0, "no supported HLS playback engine in this browser");
1453
+ }
1454
+ onEngineEvent(e) {
1455
+ if (!this.destroyed) switch (e.type) {
1456
+ case "ready":
1457
+ this.emit("ready"), this.opts.autoplay !== !1 && this.play();
1458
+ break;
1459
+ case "lowLatencyDetected":
1460
+ this._descriptor && (this._descriptor.isLowLatency = e.value);
1461
+ break;
1462
+ case "qualitychange":
1463
+ this.emit("qualitychange", e.index);
1464
+ break;
1465
+ case "audiotrackchange":
1466
+ this.emit("audiotrackchange", e.id);
1467
+ break;
1468
+ case "texttrackchange":
1469
+ this.emit("texttrackchange", e.id);
1470
+ break;
1471
+ case "ended":
1472
+ this.onStreamEnded();
1473
+ break;
1474
+ case "error":
1475
+ e.fatal ? this.fail(e.error) : this.emit("warning", e.error);
1476
+ break;
1477
+ }
1478
+ }
1479
+ startLiveState(e) {
1480
+ if (this.opts.liveStateEvents === !1) return;
1481
+ let n = this.opts.eventsBaseUrl ?? T(this.opts.baseUrl);
1482
+ if (!n) return;
1483
+ this.closeLiveState();
1484
+ let r = 0, i = !1;
1485
+ this.liveStateSub = w({
1486
+ eventsBaseUrl: n,
1487
+ matchUrn: this.opts.matchUrn,
1488
+ credential: this.opts.credential,
1489
+ onState: (t) => {
1490
+ this.destroyed || e !== this.loadGen || (t.state === "ended" || t.state === "gone") && this.onStreamEnded();
1491
+ },
1492
+ onAlive: () => {
1493
+ r = 0, i = !1;
1494
+ },
1495
+ onError: () => {
1496
+ this.destroyed || e !== this.loadGen || i || (r += 1, r >= ke && (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.`))));
1497
+ }
1498
+ });
1499
+ }
1500
+ closeLiveState() {
1501
+ this.liveStateSub?.close(), this.liveStateSub = null;
1502
+ }
1503
+ onStreamEnded() {
1504
+ this.destroyed || this.endedHandled || (this.endedHandled = !0, this.closeLiveState(), this.stopRefresh(), this.engine?.stop(), this.setState("ended"));
1505
+ }
1506
+ mountControls() {
1507
+ let e = this.opts.controls ?? "custom", t = this.opts.video;
1508
+ if (e === "native") {
1509
+ t.controls = !0;
1510
+ return;
1511
+ }
1512
+ if (e === "none") {
1513
+ t.controls = !1;
1514
+ return;
1515
+ }
1516
+ t.controls = !1;
1517
+ let n = (t.ownerDocument ?? document).createElement("div"), r = t.parentNode;
1518
+ r && r.insertBefore(n, t), n.appendChild(t), q(n, this.opts.theme), this.container = n, this.controlBar = xe({
1519
+ root: n,
1520
+ video: t,
1521
+ player: this,
1522
+ logoUrl: this.opts.theme?.logoUrl,
1523
+ statusOverlays: this.opts.statusOverlays,
1524
+ endedMessage: this.opts.endedMessage,
1525
+ errorMessage: this.opts.errorMessage
1526
+ });
1527
+ }
1528
+ unmountControls() {
1529
+ this.controlBar?.destroy(), this.controlBar = null;
1530
+ let e = this.container;
1531
+ e?.parentNode && (e.parentNode.insertBefore(this.opts.video, e), e.remove()), this.container = null;
1532
+ }
1533
+ attachPipListeners() {
1534
+ let e = this.opts.video, t = () => this.emit("pipchange", !0), n = () => this.emit("pipchange", !1);
1535
+ e.addEventListener("enterpictureinpicture", t), e.addEventListener("leavepictureinpicture", n), this.detachPip = () => {
1536
+ e.removeEventListener("enterpictureinpicture", t), e.removeEventListener("leavepictureinpicture", n);
1537
+ };
1538
+ }
1539
+ startStats() {
1540
+ this.stopStats();
1541
+ let e = Math.max(500, this.opts.statsIntervalMs ?? Z);
1542
+ this.statsTimer = setInterval(() => {
1543
+ this.engine && this.emit("stats", this.engine.getStats());
1544
+ }, e);
1545
+ }
1546
+ stopStats() {
1547
+ this.statsTimer && clearInterval(this.statsTimer), this.statsTimer = void 0;
1548
+ }
1549
+ scheduleLicenseRefresh() {
1550
+ this.stopRefresh();
1551
+ let e = this.opts.licenseRefreshMs ?? Ae;
1552
+ !e || !this._descriptor?.drmEnabled || (this.refreshTimer = setInterval(() => void this.refreshLicense(), e));
1553
+ }
1554
+ stopRefresh() {
1555
+ this.refreshTimer && clearInterval(this.refreshTimer), this.refreshTimer = void 0;
1556
+ }
1557
+ async refreshLicense() {
1558
+ let e = this.engine;
1559
+ if (!(this.destroyed || !e)) try {
1560
+ let t = await b(this.resolveOptions());
1561
+ if (this.destroyed || this.engine !== e) return;
1562
+ let n = t.drm?.widevine?.licenseUrl;
1563
+ n && (e.setLicenseUrl(n), this._descriptor = t);
1564
+ } catch (e) {
1565
+ e instanceof t && !i(e.code) && (this.stopRefresh(), this.destroyed || this.emit("warning", e));
1566
+ }
1567
+ }
1568
+ async play() {
1569
+ if (!this.destroyed) try {
1570
+ await this.opts.video.play();
1571
+ } catch (e) {
1572
+ e instanceof DOMException && e.name === "NotAllowedError" && this.emit("autoplayblocked");
1573
+ }
1574
+ }
1575
+ pause() {
1576
+ this.destroyed || this.opts.video.pause();
1577
+ }
1578
+ setMuted(e) {
1579
+ this.destroyed || (this.opts.video.muted = e);
1580
+ }
1581
+ setVolume(e) {
1582
+ this.destroyed || (this.opts.video.volume = Math.min(1, Math.max(0, e)));
1583
+ }
1584
+ getStats() {
1585
+ return this.engine?.getStats() ?? P;
1586
+ }
1587
+ getQualityLevels() {
1588
+ return this.engine?.getQualityLevels() ?? [];
1589
+ }
1590
+ getCurrentQuality() {
1591
+ return this.engine?.getCurrentQuality() ?? -1;
1592
+ }
1593
+ setQuality(e) {
1594
+ this.destroyed || this.engine?.setQuality(e === "auto" ? -1 : e);
1595
+ }
1596
+ setMaxBitrate(e) {
1597
+ this.destroyed || this.engine?.setMaxBitrate(e);
1598
+ }
1599
+ getAudioTracks() {
1600
+ return this.engine?.getAudioTracks() ?? [];
1601
+ }
1602
+ setAudioTrack(e) {
1603
+ this.destroyed || this.engine?.setAudioTrack(e);
1604
+ }
1605
+ getTextTracks() {
1606
+ return this.engine?.getTextTracks() ?? [];
1607
+ }
1608
+ setTextTrack(e) {
1609
+ this.destroyed || this.engine?.setTextTrack(e);
1610
+ }
1611
+ seekToLive() {
1612
+ this.destroyed || this.engine?.seekToLive();
1613
+ }
1614
+ async enterPictureInPicture() {
1615
+ if (this.destroyed) return;
1616
+ let e = this.opts.video;
1617
+ typeof e.requestPictureInPicture == "function" && await e.requestPictureInPicture();
1618
+ }
1619
+ async exitPictureInPicture() {
1620
+ let e = document;
1621
+ e.pictureInPictureElement === this.opts.video && typeof e.exitPictureInPicture == "function" && await e.exitPictureInPicture();
1622
+ }
1623
+ retry() {
1624
+ return this.reload();
1625
+ }
1626
+ async reload() {
1627
+ if (this.destroyed) return;
1628
+ let e = ++this.loadGen;
1629
+ this.stopStats(), this.stopRefresh(), this.closeArming(), this.closeLiveState(), this.detachMedia?.(), this.engine?.destroy(), this.engine = null;
1630
+ try {
1631
+ let t = await x({
1632
+ ...this.resolveOptions(),
1633
+ waitForLive: this.opts.waitForLive ? this.buildWaitConfig() : void 0
1634
+ });
1635
+ await this.onResolved(t, e);
1636
+ } catch (n) {
1637
+ if (this.destroyed || this.abort.signal.aborted || e !== this.loadGen) return;
1638
+ throw this.fail(n instanceof t ? n : new t("INTERNAL", 0, String(n))), n;
1639
+ }
1640
+ }
1641
+ on(e, t) {
1642
+ let n = this.listeners[e] ??= /* @__PURE__ */ new Set();
1643
+ return n.add(t), () => n.delete(t);
1644
+ }
1645
+ destroy() {
1646
+ 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());
1647
+ }
1648
+ clearListeners() {
1649
+ for (let e of Object.keys(this.listeners)) this.listeners[e]?.clear();
1650
+ }
1651
+ fail(e) {
1652
+ this.setState("error"), this.emit("error", e);
1653
+ }
1654
+ setState(e) {
1655
+ if (e !== this._state) switch (this._state = e, this.emit("statechange", e), e) {
1656
+ case "playing":
1657
+ this.emit("playing");
1658
+ break;
1659
+ case "buffering":
1660
+ this.emit("buffering");
1661
+ break;
1662
+ case "paused":
1663
+ this.emit("paused");
1664
+ break;
1665
+ case "ended":
1666
+ this.emit("ended");
1667
+ break;
1668
+ default: break;
1669
+ }
1670
+ }
1671
+ emit(e, t) {
1672
+ this.listeners[e]?.forEach((e) => {
1673
+ try {
1674
+ e(t);
1675
+ } catch {}
1676
+ });
1677
+ }
1678
+ };
1679
+ //#endregion
1680
+ //#region src/embed/protocol.ts
1681
+ function $(e) {
1682
+ return !!e && typeof e == "object" && typeof e.type == "string" && e.type.startsWith("oddin:");
1683
+ }
1684
+ //#endregion
1685
+ //#region src/embed/host.ts
1686
+ function Me(e) {
1687
+ if (!e.container || typeof e.container.appendChild != "function") throw Error("mountEmbed: opts.container must be a DOM element");
1688
+ if (!e.src) throw Error("mountEmbed: opts.src is required");
1689
+ if (!e.baseUrl || !e.matchUrn) throw Error("mountEmbed: opts.baseUrl and opts.matchUrn are required");
1690
+ let t = new URL(e.src, location.href), n = e.allowedFrameOrigin ?? t.origin;
1691
+ t.searchParams.set("baseUrl", e.baseUrl), t.searchParams.set("matchUrn", e.matchUrn), t.searchParams.set("parentOrigin", location.origin), e.muted != null && t.searchParams.set("muted", String(e.muted)), e.apiKey && (t.searchParams.set("apiKey", e.apiKey), console.warn("havik-player: passing apiKey to mountEmbed places it in the iframe URL — DEV/DEMO only. In production inject it server-side into window.HAVIK_EMBED_CONFIG on the embed page."));
1692
+ let r = document.createElement("iframe");
1693
+ r.src = t.toString(), r.allow = "autoplay; encrypted-media; fullscreen", r.setAttribute("allowfullscreen", ""), r.referrerPolicy = "no-referrer", r.setAttribute("sandbox", e.sandbox ?? "allow-scripts allow-same-origin allow-presentation"), r.style.cssText = "border:0;width:100%;height:100%;display:block", e.container.appendChild(r);
1694
+ let i = /* @__PURE__ */ new Set(), a = (t) => {
1695
+ if (t.source !== r.contentWindow || t.origin !== n || !$(t.data)) return;
1696
+ let a = t.data;
1697
+ a.type === "oddin:ready" && a.protocol !== 1 && console.warn(`havik-player: embed protocol v${a.protocol} != host v1. Pin the embed page version to the SDK version to avoid skew.`), e.onEvent?.(a), i.forEach((e) => e(a));
1698
+ };
1699
+ window.addEventListener("message", a);
1700
+ let o = (e) => r.contentWindow?.postMessage(e, n);
1701
+ return {
1702
+ iframe: r,
1703
+ play: () => o({ type: "oddin:play" }),
1704
+ pause: () => o({ type: "oddin:pause" }),
1705
+ setMuted: (e) => o({
1706
+ type: "oddin:setMuted",
1707
+ muted: e
1708
+ }),
1709
+ setVolume: (e) => o({
1710
+ type: "oddin:setVolume",
1711
+ volume: e
1712
+ }),
1713
+ setQuality: (e) => o({
1714
+ type: "oddin:setQuality",
1715
+ index: e
1716
+ }),
1717
+ setMaxBitrate: (e) => o({
1718
+ type: "oddin:setMaxBitrate",
1719
+ bitrate: e
1720
+ }),
1721
+ setAudioTrack: (e) => o({
1722
+ type: "oddin:setAudioTrack",
1723
+ id: e
1724
+ }),
1725
+ setTextTrack: (e) => o({
1726
+ type: "oddin:setTextTrack",
1727
+ id: e
1728
+ }),
1729
+ seekToLive: () => o({ type: "oddin:seekToLive" }),
1730
+ enterPip: () => o({ type: "oddin:enterPip" }),
1731
+ exitPip: () => o({ type: "oddin:exitPip" }),
1732
+ retry: () => o({ type: "oddin:retry" }),
1733
+ load: (e) => o({
1734
+ type: "oddin:load",
1735
+ matchUrn: e
1736
+ }),
1737
+ on: (e) => (i.add(e), () => i.delete(e)),
1738
+ destroy: () => {
1739
+ o({ type: "oddin:destroy" }), window.removeEventListener("message", a), r.remove();
1740
+ }
1741
+ };
1742
+ }
1743
+ //#endregion
1744
+ //#region src/embed/page.ts
1745
+ var Ne = /^[A-Za-z0-9:_.-]{1,128}$/;
1746
+ function Pe() {
1747
+ let e = window.HAVIK_EMBED_CONFIG;
1748
+ if (e) return {
1749
+ baseUrl: e.baseUrl ?? "",
1750
+ matchUrn: e.matchUrn ?? "",
1751
+ apiKey: e.apiKey ?? "",
1752
+ parentOrigin: e.parentOrigin,
1753
+ autoplay: e.autoplay ?? !0,
1754
+ muted: e.muted ?? !0,
1755
+ waitForLive: e.waitForLive ?? !0
1756
+ };
1757
+ let t = new URLSearchParams(location.search);
1758
+ return {
1759
+ baseUrl: t.get("baseUrl") ?? "",
1760
+ matchUrn: t.get("matchUrn") ?? "",
1761
+ apiKey: t.get("apiKey") ?? "",
1762
+ parentOrigin: t.get("parentOrigin") ?? void 0,
1763
+ autoplay: !0,
1764
+ muted: t.get("muted") !== "false",
1765
+ waitForLive: !0
1766
+ };
1767
+ }
1768
+ async function Fe() {
1769
+ let e = Pe();
1770
+ Le();
1771
+ let n = document.createElement("video");
1772
+ 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);
1773
+ let r = e.parentOrigin && e.parentOrigin !== "null" ? e.parentOrigin : null;
1774
+ r || console.warn("havik-player: embed has no parentOrigin — the postMessage bridge is disabled (no events posted, no commands accepted). mountEmbed sets parentOrigin automatically.");
1775
+ let i = (e) => {
1776
+ if (r) try {
1777
+ window.parent?.postMessage(e, r);
1778
+ } catch {}
1779
+ };
1780
+ if (!e.baseUrl || !e.matchUrn || !e.apiKey) {
1781
+ i({
1782
+ type: "oddin:error",
1783
+ code: "INTERNAL",
1784
+ message: "embed missing baseUrl/matchUrn/apiKey"
1785
+ });
1786
+ return;
1787
+ }
1788
+ let a = null, o = (e) => {
1789
+ e.on("statechange", (e) => i({
1790
+ type: "oddin:state",
1791
+ state: e
1792
+ })), e.on("ready", () => i({
1793
+ type: "oddin:tracks",
1794
+ quality: e.getQualityLevels(),
1795
+ audio: e.getAudioTracks(),
1796
+ text: e.getTextTracks()
1797
+ })), e.on("waiting", (e) => i({
1798
+ type: "oddin:waiting",
1799
+ retryInMs: e.retryInMs,
1800
+ attempt: e.attempt,
1801
+ phase: e.phase
1802
+ })), e.on("qualitychange", (e) => i({
1803
+ type: "oddin:qualitychange",
1804
+ index: e
1805
+ })), e.on("audiotrackchange", (e) => i({
1806
+ type: "oddin:audiotrackchange",
1807
+ id: e
1808
+ })), e.on("texttrackchange", (e) => i({
1809
+ type: "oddin:texttrackchange",
1810
+ id: e
1811
+ })), e.on("stats", (e) => i({
1812
+ type: "oddin:stats",
1813
+ droppedFrames: e.droppedFrames,
1814
+ latencySeconds: e.latencySeconds,
1815
+ bandwidthKbps: e.bandwidthKbps,
1816
+ levelHeight: e.levelHeight,
1817
+ lowLatency: e.lowLatency,
1818
+ isLive: e.isLive,
1819
+ atLiveEdge: e.atLiveEdge
1820
+ })), e.on("error", (e) => i({
1821
+ type: "oddin:error",
1822
+ code: e.code,
1823
+ message: e.message
1824
+ })), e.on("autoplayblocked", () => {
1825
+ i({ type: "oddin:autoplayblocked" }), Ie(n, e);
1826
+ });
1827
+ }, s = async (r) => {
1828
+ a?.destroy(), a = null;
1829
+ try {
1830
+ let t = await Q({
1831
+ video: n,
1832
+ baseUrl: e.baseUrl,
1833
+ matchUrn: r,
1834
+ credential: { apiKey: e.apiKey },
1835
+ autoplay: e.autoplay,
1836
+ muted: e.muted,
1837
+ waitForLive: e.waitForLive
1838
+ });
1839
+ a = t;
1840
+ let i = n.parentElement;
1841
+ i?.classList.contains("havik-player") && (i.style.cssText = "position:absolute;inset:0;width:100%;height:100%;aspect-ratio:auto"), o(t);
1842
+ } catch (e) {
1843
+ let n = e instanceof t ? e.code : "INTERNAL", r = e instanceof Error ? e.message : String(e);
1844
+ i({
1845
+ type: "oddin:error",
1846
+ code: n,
1847
+ message: r
1848
+ });
1849
+ }
1850
+ }, c = () => {
1851
+ window.removeEventListener("message", u), window.removeEventListener("pagehide", c), a?.destroy(), a = null;
1852
+ }, l = (e) => {
1853
+ switch (e.type) {
1854
+ case "oddin:play":
1855
+ a?.play();
1856
+ break;
1857
+ case "oddin:pause":
1858
+ a?.pause();
1859
+ break;
1860
+ case "oddin:setMuted":
1861
+ typeof e.muted == "boolean" && a?.setMuted(e.muted);
1862
+ break;
1863
+ case "oddin:setVolume":
1864
+ typeof e.volume == "number" && Number.isFinite(e.volume) && a?.setVolume(e.volume);
1865
+ break;
1866
+ case "oddin:setQuality":
1867
+ typeof e.index == "number" && a?.setQuality(e.index);
1868
+ break;
1869
+ case "oddin:setMaxBitrate":
1870
+ (e.bitrate === null || typeof e.bitrate == "number" && Number.isFinite(e.bitrate)) && a?.setMaxBitrate(e.bitrate);
1871
+ break;
1872
+ case "oddin:setAudioTrack":
1873
+ typeof e.id == "number" && a?.setAudioTrack(e.id);
1874
+ break;
1875
+ case "oddin:setTextTrack":
1876
+ typeof e.id == "number" && a?.setTextTrack(e.id);
1877
+ break;
1878
+ case "oddin:seekToLive":
1879
+ a?.seekToLive();
1880
+ break;
1881
+ case "oddin:enterPip":
1882
+ a?.enterPictureInPicture();
1883
+ break;
1884
+ case "oddin:exitPip":
1885
+ a?.exitPictureInPicture();
1886
+ break;
1887
+ case "oddin:retry":
1888
+ a?.retry();
1889
+ break;
1890
+ case "oddin:load":
1891
+ typeof e.matchUrn == "string" && Ne.test(e.matchUrn) && s(e.matchUrn);
1892
+ break;
1893
+ case "oddin:destroy":
1894
+ c();
1895
+ break;
1896
+ }
1897
+ }, u = (e) => {
1898
+ !r || e.origin !== r || $(e.data) && l(e.data);
1899
+ };
1900
+ r && window.addEventListener("message", u), window.addEventListener("pagehide", c, { once: !0 }), await s(e.matchUrn), i({
1901
+ type: "oddin:ready",
1902
+ protocol: 1
1903
+ });
1904
+ }
1905
+ function Ie(e, t) {
1906
+ if (document.getElementById("havik-tap")) return;
1907
+ let n = document.createElement("button");
1908
+ 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", () => {
1909
+ n.remove(), t.play();
1910
+ }), document.body.appendChild(n);
1911
+ }
1912
+ function Le() {
1913
+ let e = document.createElement("style");
1914
+ e.textContent = "html,body{margin:0;height:100%;background:#000;overflow:hidden}body{display:flex}", document.head.appendChild(e);
1915
+ }
1916
+ //#endregion
1917
+ export { G as ODDIN_THEME, t as PlaybackError, q as applyTheme, Fe as bootEmbed, k as clearDeviceId, Q as createPlayer, T as deriveEventsBaseUrl, oe as fetchCatalog, ce as fetchTournament, ue as flattenMatches, O as getDeviceId, N as isLowLatencyManifest, $ as isOddinMessage, j as licenseRequestHeaders, Me as mountEmbed, y as pollToLive, x as resolveStream, w as subscribeLiveState, C as watchStatus };
1918
+
1919
+ //# sourceMappingURL=index.js.map