@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/LICENSE +15 -0
- package/README.md +300 -0
- package/THIRD_PARTY_NOTICES.txt +59 -0
- package/dist/havik-player.global.js +147 -0
- package/dist/havik-player.global.js.map +1 -0
- package/dist/index.cjs +108 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +795 -0
- package/dist/index.d.ts +795 -0
- package/dist/index.js +1919 -0
- package/dist/index.js.map +1 -0
- package/docs/API.md +1115 -0
- package/docs/STABILITY.md +56 -0
- package/embed/index.html +26 -0
- package/examples/README.md +17 -0
- package/examples/Vue.vue +37 -0
- package/examples/react.tsx +47 -0
- package/examples/vanilla.html +41 -0
- package/package.json +101 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/core/errors.ts","../src/core/types.ts","../src/core/http.ts","../src/core/waitForLive.ts","../src/core/resolve.ts","../src/core/catalog.ts","../src/core/poll.ts","../src/core/events.ts","../src/core/drm/deviceId.ts","../src/core/drm/headers.ts","../src/core/manifest.ts","../src/core/stats.ts","../src/core/engine/hls.ts","../src/core/engine/native.ts","../src/ui/theme.ts","../src/ui/styles.ts","../src/ui/icons.ts","../src/ui/controls.ts","../src/managed/index.ts","../src/embed/protocol.ts","../src/embed/host.ts","../src/embed/page.ts"],"sourcesContent":["// Typed error taxonomy mapped from the havik-streams playback outcome table.\n\nexport type PlaybackErrorCode =\n | 'INVALID_URN' // 400 — malformed match URN (terminal)\n | 'NOT_FOUND' // 404 — unknown URN OR not entitled, indistinguishable (terminal)\n | 'GONE' // 410 — ended past the catchup window (terminal)\n | 'TOO_EARLY' // 425 — upcoming, session not upserted yet (retry honoring Retry-After)\n | 'UNAVAILABLE' // 503 — should-be-live but origin failing (retry with backoff)\n | 'UNAUTHORIZED' // 401 — bad/absent key (terminal)\n | 'FORBIDDEN' // 403 — origin not allowed / DRM signature rejected (terminal)\n | 'RATE_LIMITED' // 429 — per-IP limiter (back off Retry-After)\n | 'INTERNAL' // 5xx other / unexpected (terminal)\n | 'NETWORK' // fetch threw (transient — retryable in the live-poll loop)\n | 'TIMEOUT' // SDK gave up waiting for live (terminal)\n | 'ABORTED'; // caller aborted via AbortSignal (terminal)\n\nexport interface PlaybackErrorInit {\n retryAfterMs?: number;\n requestId?: string;\n /** The raw server-provided error code string, if any — forward-compatible with new server codes. */\n serverCode?: string;\n /**\n * Scheduled kickoff time (epoch ms), if the server included it on a TOO_EARLY\n * response body. The SDK uses this to widen poll cadence for far-future\n * matches without a separate catalog call.\n */\n liveStartsAtMs?: number;\n cause?: unknown;\n}\n\nexport class PlaybackError extends Error {\n readonly code: PlaybackErrorCode;\n readonly httpStatus: number;\n /** Present for TOO_EARLY / RATE_LIMITED / UNAVAILABLE when the server sent Retry-After. */\n readonly retryAfterMs?: number;\n /** Server request id, when surfaced — useful for correlating opaque 403/503 in server logs. */\n readonly requestId?: string;\n /** Raw server error code (e.g. \"TOO_EARLY\"), preserved even if `code` maps it to a known enum. */\n readonly serverCode?: string;\n /**\n * Scheduled kickoff time (epoch ms), if the server carried `liveStartsAt` on\n * a TOO_EARLY body. `pollToLive` reads this to widen poll cadence for\n * far-future matches.\n */\n readonly liveStartsAtMs?: number;\n\n constructor(\n code: PlaybackErrorCode,\n httpStatus: number,\n message: string,\n init?: PlaybackErrorInit,\n ) {\n super(message);\n this.name = 'PlaybackError';\n this.code = code;\n this.httpStatus = httpStatus;\n this.retryAfterMs = init?.retryAfterMs;\n this.requestId = init?.requestId;\n this.serverCode = init?.serverCode;\n this.liveStartsAtMs = init?.liveStartsAtMs;\n if (init?.cause !== undefined) {\n (this as { cause?: unknown }).cause = init.cause;\n }\n }\n}\n\n/** Map an HTTP status from /v1/playback to a typed code. */\nexport function classifyStatus(status: number): PlaybackErrorCode {\n switch (status) {\n case 400:\n return 'INVALID_URN';\n case 401:\n return 'UNAUTHORIZED';\n case 403:\n return 'FORBIDDEN';\n case 404:\n return 'NOT_FOUND';\n case 410:\n return 'GONE';\n case 425:\n return 'TOO_EARLY';\n case 429:\n return 'RATE_LIMITED';\n case 503:\n return 'UNAVAILABLE';\n default:\n return 'INTERNAL';\n }\n}\n\nconst WAITABLE: ReadonlySet<PlaybackErrorCode> = new Set<PlaybackErrorCode>([\n 'TOO_EARLY',\n 'UNAVAILABLE',\n 'RATE_LIMITED',\n 'NETWORK',\n]);\n\n/** True for codes the wait-for-live loop should keep polling on. */\nexport function isWaitable(code: PlaybackErrorCode): boolean {\n return WAITABLE.has(code);\n}\n","// Shared domain types for the Havik player SDK.\n\nexport type DrmSystem = 'widevine' | 'fairplay';\n\nexport interface WidevineInfo {\n licenseUrl: string;\n}\nexport interface FairPlayInfo {\n licenseUrl: string;\n certificateUrl: string;\n}\nexport interface DrmInfo {\n widevine?: WidevineInfo;\n fairplay?: FairPlayInfo;\n}\n\n/**\n * The per-match playback credential returned by `GET /v1/playback/{urn}`,\n * normalized for the SDK. `manifestUrl` is an unsigned HLS `.m3u8`; the DRM\n * license URLs are pre-signed by havik-streams (the signed query string IS the\n * token — never rewrite them).\n */\nexport interface StreamDescriptor {\n matchUrn: string;\n protocol: 'hls';\n /** Producer-set DRM decision. When false, init no EME/CDM at all. */\n drmEnabled: boolean;\n manifestUrl: string;\n /** Present only when drmEnabled. */\n drm?: DrmInfo;\n serverTime: string;\n /** Present only while the match is upcoming. */\n liveStartsAt?: string;\n /** Filled by the player once a media playlist with EXT-X-PART-INF is seen. */\n isLowLatency?: boolean;\n /** Any unrecognized top-level fields from the playback response (forward-compat). */\n extensions?: Record<string, unknown>;\n}\n\nexport interface Credential {\n /** A publishable api-key, e.g. `pk_live_…` / `pk_test_…`. */\n apiKey: string;\n}\n\n/**\n * A credential, or a function that produces one. The function form lets a host\n * plug in a future short-lived-token mint service without an API change; it is\n * invoked per request so refresh works transparently.\n */\nexport type CredentialSource = Credential | (() => Credential | Promise<Credential>);\n\n/** @internal */\nexport async function resolveCredential(source: CredentialSource): Promise<Credential> {\n const cred = typeof source === 'function' ? await source() : source;\n if (!cred || !cred.apiKey) {\n throw new Error('havik-player: credential is missing an apiKey');\n }\n return cred;\n}\n\nexport type MatchLiveStatus = 'upcoming' | 'live' | 'ended';\n\nexport interface MatchStatus {\n matchUrn: string;\n matchName?: string;\n status: MatchLiveStatus;\n datePlannedStart?: string;\n}\n","import { PlaybackError } from './errors';\nimport { resolveCredential, type CredentialSource } from './types';\n\n/**\n * Join a base URL and an absolute path into a validated absolute http(s) URL.\n * Rejects empty, protocol-relative (`//evil`), and non-http(s) bases up front so\n * an unvalidated baseUrl can never become the (credentialed) request origin.\n */\nexport function joinUrl(base: string, path: string): string {\n const trimmed = base.replace(/\\/+$/, '');\n let url: URL;\n try {\n url = new URL(trimmed + path);\n } catch {\n throw new PlaybackError('INTERNAL', 0, `invalid baseUrl: ${JSON.stringify(base)}`);\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new PlaybackError('INTERNAL', 0, `baseUrl must be http(s), got: ${JSON.stringify(base)}`);\n }\n return url.toString();\n}\n\n/**\n * Parse an HTTP `Retry-After` header (delta-seconds or HTTP-date) into ms.\n * Returns undefined when absent/unparseable.\n */\nexport function parseRetryAfterMs(value: string | null): number | undefined {\n if (!value) return undefined;\n const secs = Number(value);\n if (Number.isFinite(secs)) return Math.max(0, Math.round(secs * 1000));\n const when = Date.parse(value);\n if (!Number.isNaN(when)) return Math.max(0, when - Date.now());\n return undefined;\n}\n\nexport interface FetchOptions {\n credential: CredentialSource;\n signal?: AbortSignal;\n method?: string;\n headers?: Headers;\n body?: BodyInit;\n}\n\n/**\n * fetch() with the `x-api-key` header attached from the (possibly async)\n * credential source. Network failures and aborts are normalized to\n * PlaybackError so callers handle one error type. The credential is resolved\n * per call so a getCredential() hook can refresh transparently.\n */\nexport async function fetchWithCredential(url: string, opts: FetchOptions): Promise<Response> {\n const cred = await resolveCredential(opts.credential);\n const headers = opts.headers ?? new Headers();\n headers.set('x-api-key', cred.apiKey);\n try {\n return await fetch(url, {\n method: opts.method ?? 'GET',\n headers,\n body: opts.body,\n signal: opts.signal,\n cache: 'no-store',\n credentials: 'omit',\n mode: 'cors',\n });\n } catch (err) {\n if (opts.signal?.aborted) {\n throw new PlaybackError('ABORTED', 0, 'request aborted', { cause: err });\n }\n throw new PlaybackError('NETWORK', 0, `network request failed: ${describe(err)}`, {\n cause: err,\n });\n }\n}\n\nfunction describe(err: unknown): string {\n if (err instanceof Error) return err.message;\n return String(err);\n}\n","import { isWaitable, PlaybackError } from './errors';\n\nexport interface WaitForLiveOptions {\n /** Overall budget before giving up with a TIMEOUT error. Default: unbounded. */\n timeoutMs?: number;\n /** Minimum delay between polls. Floored at 1000ms (the server's min / limiter). Default: 1000. */\n floorMs?: number;\n /**\n * Maximum delay between polls in the IMMINENT window (≤ {@link imminentMs}\n * before kickoff). Far-from-kickoff polls are widened automatically using\n * `kickoffAt` (see below) up to {@link sanityFloorMs}, regardless of this\n * value — so this knob really controls only the near-kickoff cadence. Default: 30000.\n */\n ceilMs?: number;\n /**\n * Caller-supplied scheduled kickoff time (epoch ms, or an ISO string), used to\n * widen the long-tail poll cadence so the SDK does not hammer the per-viewer\n * `/v1/playback` endpoint when a viewer is armed hours early.\n *\n * Far-from-kickoff (time-to-kickoff > {@link imminentMs}) the poll interval is\n * stretched to `min(ttk / 10, sanityFloorMs)` (never tighter than the server's\n * `Retry-After`, never longer than {@link sanityFloorMs}, never longer than\n * the time remaining to kickoff). Inside the imminent window the cadence\n * collapses to the tight `[floorMs, ceilMs]` range so a 200 is caught fast.\n *\n * When omitted, the SDK also reads `liveStartsAt` off the 425 body if the\n * server returns it (`PlaybackError.liveStartsAtMs`), so newer servers tune\n * automatically without any client change.\n */\n kickoffAt?: number | string;\n /**\n * Threshold (ms) for \"imminent kickoff\" — inside this window the cadence\n * collapses to the tight `[floorMs, ceilMs]` range. Default: 10 minutes.\n */\n imminentMs?: number;\n /**\n * Absolute hard ceiling on any single wait, even when far from kickoff or\n * when the server returns a very large `Retry-After`. Sanity floor so an\n * early go-live or a schedule change is caught within bounded latency.\n * Default: 5 minutes (300_000ms).\n */\n sanityFloorMs?: number;\n /** Add up to +15% jitter to each delay (never reduces below the server's ask). Default: true. */\n jitter?: boolean;\n /** Called before each wait with the upcoming retry. */\n onState?: (state: WaitState) => void;\n}\n\nexport type WaitForLive = boolean | WaitForLiveOptions;\n\nexport interface WaitState {\n phase: 'tooEarly' | 'unavailable' | 'rateLimited' | 'network';\n /** How long until the next poll, in ms. */\n retryInMs: number;\n /** 1-based retry count. */\n attempt: number;\n /** Wall-clock ms since the wait started. */\n elapsedMs: number;\n /**\n * Time-to-kickoff (ms) used to compute this delay, if known. `undefined` when\n * no kickoff hint is available (caller didn't pass `kickoffAt` and the server\n * didn't carry `liveStartsAt` on the error).\n */\n ttkMs?: number;\n}\n\ninterface NormalizedWait {\n timeoutMs?: number;\n floorMs: number;\n ceilMs: number;\n imminentMs: number;\n sanityFloorMs: number;\n /** Caller-supplied kickoff (epoch ms), or undefined. */\n kickoffMs?: number;\n jitter: boolean;\n onState?: (state: WaitState) => void;\n}\n\nconst DEFAULT_FLOOR_MS = 1000;\nconst DEFAULT_CEIL_MS = 30_000;\nconst DEFAULT_IMMINENT_MS = 10 * 60 * 1000; // 10 min — collapse to tight cadence\nconst DEFAULT_SANITY_FLOOR_MS = 5 * 60 * 1000; // 5 min — hard cap on any single sleep\nconst SERVER_MIN_MS = 1000; // never poll /v1/playback faster than this (per-IP limiter)\n\nconst PHASE: Record<string, WaitState['phase']> = {\n TOO_EARLY: 'tooEarly',\n UNAVAILABLE: 'unavailable',\n RATE_LIMITED: 'rateLimited',\n NETWORK: 'network',\n};\n\nfunction normalize(cfg: WaitForLive): NormalizedWait {\n const o: WaitForLiveOptions = cfg === true || cfg === false ? {} : cfg;\n const floorMs = Math.max(SERVER_MIN_MS, o.floorMs ?? DEFAULT_FLOOR_MS);\n const ceilMs = Math.max(floorMs, o.ceilMs ?? DEFAULT_CEIL_MS);\n const imminentMs = Math.max(0, o.imminentMs ?? DEFAULT_IMMINENT_MS);\n const sanityFloorMs = Math.max(ceilMs, o.sanityFloorMs ?? DEFAULT_SANITY_FLOOR_MS);\n return {\n timeoutMs: o.timeoutMs,\n floorMs,\n ceilMs,\n imminentMs,\n sanityFloorMs,\n kickoffMs: parseKickoff(o.kickoffAt),\n jitter: o.jitter ?? true,\n onState: o.onState,\n };\n}\n\nfunction parseKickoff(v: number | string | undefined): number | undefined {\n if (v == null) return undefined;\n if (typeof v === 'number') return Number.isFinite(v) ? v : undefined;\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : undefined;\n}\n\nfunction clamp(value: number, lo: number, hi: number): number {\n return Math.min(hi, Math.max(lo, value));\n}\n\nfunction addJitter(delay: number): number {\n // Only ADDS time, so we never poll faster than the server asked.\n return Math.round(delay * (1 + 0.15 * Math.random()));\n}\n\n/**\n * Compute the next poll delay, given the server's hint, an optional kickoff\n * time, and the caller's bounds.\n *\n * The rule:\n * 1. **Server `Retry-After` is the floor.** If the server says \"wait 2s,\" we\n * never poll sooner, regardless of how far away kickoff is. This preserves\n * the server's authority to tighten cadence near kickoff.\n * 2. **Inside the imminent window** (`ttk ≤ imminentMs`, or no ttk known) the\n * delay is the classic `clamp(retryAfter, floor, ceil)`.\n * 3. **Far from kickoff** (`ttk > imminentMs`) the delay is widened to\n * `min(ttk / 10, sanityFloorMs)`, but **never below** the server's\n * `Retry-After` and **never longer than** the time remaining to kickoff\n * (so we don't sleep past it).\n * 4. **Sanity floor caps everything** so an early go-live or a schedule slip\n * is caught within `sanityFloorMs` (default 5 min).\n *\n * Returns `{ delay, ttkMs }` so the caller can surface `ttk` to UI.\n */\nexport function nextDelayMs(\n retryAfterMs: number | undefined,\n now: number,\n o: Pick<NormalizedWait, 'floorMs' | 'ceilMs' | 'imminentMs' | 'sanityFloorMs' | 'kickoffMs'>,\n kickoffOverrideMs?: number,\n): { delay: number; ttkMs: number | undefined } {\n const kickoffMs = kickoffOverrideMs ?? o.kickoffMs;\n const ttkMs = kickoffMs != null ? Math.max(0, kickoffMs - now) : undefined;\n const serverFloor = Math.max(o.floorMs, retryAfterMs ?? 0);\n\n // Imminent (or no ttk known): use the classic tight band.\n if (ttkMs == null || ttkMs <= o.imminentMs) {\n return { delay: clamp(retryAfterMs ?? o.floorMs, serverFloor, o.ceilMs), ttkMs };\n }\n\n // Far from kickoff: widen to ~10% of remaining time, capped by sanity floor\n // and by ttk itself (don't sleep past kickoff).\n const widened = Math.min(Math.floor(ttkMs / 10), o.sanityFloorMs, ttkMs);\n // Server can always tighten with a smaller Retry-After (treated as floor).\n return { delay: Math.max(serverFloor, widened), ttkMs };\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new PlaybackError('ABORTED', 0, 'wait-for-live aborted'));\n return;\n }\n const onAbort = () => {\n clearTimeout(timer);\n reject(new PlaybackError('ABORTED', 0, 'wait-for-live aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/**\n * Poll `attempt` until it resolves, retrying on waitable PlaybackErrors\n * (TOO_EARLY / UNAVAILABLE / RATE_LIMITED / NETWORK). Each retry's delay is\n * computed by {@link nextDelayMs} from (a) the server's `Retry-After` (always a\n * floor) and (b) the time-to-kickoff from `kickoffAt` and/or\n * `PlaybackError.liveStartsAtMs`. Far from kickoff the cadence widens (to\n * `~ttk/10`, capped at `sanityFloorMs`) so the SDK does not hammer the\n * per-viewer `/v1/playback` endpoint when a viewer is armed hours early; inside\n * the imminent window it collapses to the tight `[floorMs, ceilMs]` band so a\n * 200 — i.e. the instant the match goes live — is caught fast. Terminal errors\n * (404 / 410 / 400 / 401 / 403 / INTERNAL) reject straight away.\n */\nexport async function pollToLive<T>(\n attempt: () => Promise<T>,\n cfg: WaitForLive,\n signal?: AbortSignal,\n): Promise<T> {\n const o = normalize(cfg);\n const start = Date.now();\n const deadline = o.timeoutMs != null ? start + o.timeoutMs : Infinity;\n let attemptNo = 0;\n // Cache the latest kickoff hint we've learned from a 425 body. Re-read every\n // loop (don't snapshot once) so a schedule change visible to the server is\n // honored — see the four correctness guards in the design note.\n let lastKickoffHintMs: number | undefined;\n\n for (;;) {\n if (signal?.aborted) throw new PlaybackError('ABORTED', 0, 'wait-for-live aborted');\n try {\n return await attempt();\n } catch (err) {\n if (!(err instanceof PlaybackError) || !isWaitable(err.code)) throw err;\n attemptNo += 1;\n\n // If the server now carries a kickoff hint (newer havik-streams), refresh\n // it. Caller-supplied `kickoffAt` still wins (set in normalize()).\n if (err.liveStartsAtMs != null && Number.isFinite(err.liveStartsAtMs)) {\n lastKickoffHintMs = err.liveStartsAtMs;\n }\n\n const now = Date.now();\n const computed = nextDelayMs(err.retryAfterMs, now, o, lastKickoffHintMs);\n const ttkMs = computed.ttkMs;\n let delay = computed.delay;\n if (o.jitter) delay = addJitter(delay);\n\n if (now + delay > deadline) {\n throw new PlaybackError(\n 'TIMEOUT',\n err.httpStatus,\n `timed out after ${o.timeoutMs}ms waiting for the stream to go live`,\n { retryAfterMs: err.retryAfterMs, cause: err },\n );\n }\n\n o.onState?.({\n phase: PHASE[err.code] ?? 'unavailable',\n retryInMs: delay,\n attempt: attemptNo,\n elapsedMs: now - start,\n ttkMs,\n });\n\n await sleep(delay, signal);\n }\n }\n}\n","import { classifyStatus, PlaybackError } from './errors';\nimport { fetchWithCredential, joinUrl, parseRetryAfterMs } from './http';\nimport { type CredentialSource, type StreamDescriptor } from './types';\nimport { pollToLive, type WaitForLive } from './waitForLive';\n\nexport interface ResolveOptions {\n /** Base URL of the havik-streams API, e.g. https://streams.oddin.gg. */\n baseUrl: string;\n matchUrn: string;\n credential: CredentialSource;\n /**\n * When set, resolve retries internally on 425 TOO_EARLY honoring Retry-After\n * and resolves the instant the stream is live. See WaitForLive.\n */\n waitForLive?: WaitForLive;\n signal?: AbortSignal;\n}\n\ninterface RawPlayback {\n matchUrn?: string;\n protocol?: string;\n drmEnabled?: boolean;\n manifestUrl?: string;\n drm?: {\n widevine?: { licenseUrl?: string };\n fairplay?: { licenseUrl?: string; certificateUrl?: string };\n };\n serverTime?: string;\n liveStartsAt?: string;\n}\n\n/**\n * Single GET /v1/playback/{urn}. Throws a typed PlaybackError on non-2xx.\n * @internal Use resolveStream (which adds the live-pickup loop); this is the SDK's internal one-shot.\n */\nexport async function resolvePlaybackOnce(opts: ResolveOptions): Promise<StreamDescriptor> {\n if (!opts.matchUrn || typeof opts.matchUrn !== 'string') {\n throw new PlaybackError('INVALID_URN', 0, 'resolveStream: a non-empty matchUrn is required');\n }\n const url = joinUrl(opts.baseUrl, `/v1/playback/${encodeURIComponent(opts.matchUrn)}`);\n const res = await fetchWithCredential(url, {\n credential: opts.credential,\n signal: opts.signal,\n });\n if (!res.ok) throw await toPlaybackError(res);\n const raw = (await res.json()) as RawPlayback;\n return toDescriptor(raw, opts.matchUrn);\n}\n\n/**\n * Resolve a match into a playback descriptor (manifest URL + DRM config). With\n * `waitForLive`, blocks (politely, server-paced) until the stream is live.\n */\nexport function resolveStream(opts: ResolveOptions): Promise<StreamDescriptor> {\n const attempt = () => resolvePlaybackOnce(opts);\n return opts.waitForLive ? pollToLive(attempt, opts.waitForLive, opts.signal) : attempt();\n}\n\nasync function toPlaybackError(res: Response): Promise<PlaybackError> {\n const retryAfterMs = parseRetryAfterMs(res.headers.get('retry-after'));\n const requestId =\n res.headers.get('x-request-id') ?? res.headers.get('x-amzn-requestid') ?? undefined;\n let message = res.statusText || `HTTP ${res.status}`;\n let serverCode: string | undefined;\n let liveStartsAtMs: number | undefined;\n try {\n const body = (await res.json()) as {\n error?: { code?: string; message?: string };\n liveStartsAt?: string;\n };\n if (body?.error?.message) message = body.error.message;\n if (body?.error?.code) serverCode = body.error.code;\n // On a 425 TOO_EARLY body, newer havik-streams carries `liveStartsAt` so the\n // SDK can widen far-future poll cadence without a separate catalog call.\n if (typeof body?.liveStartsAt === 'string') {\n const t = Date.parse(body.liveStartsAt);\n if (Number.isFinite(t)) liveStartsAtMs = t;\n }\n } catch {\n // non-JSON / empty body — keep the status-text message\n }\n return new PlaybackError(classifyStatus(res.status), res.status, message, {\n retryAfterMs,\n requestId,\n serverCode,\n liveStartsAtMs,\n });\n}\n\nconst KNOWN_PLAYBACK_KEYS = new Set([\n 'matchUrn',\n 'protocol',\n 'drmEnabled',\n 'manifestUrl',\n 'drm',\n 'serverTime',\n 'liveStartsAt',\n 'expiresAt',\n]);\n\n// Reject obviously-wrong URL schemes before any of these strings reach a live\n// sink (video.src, hls.loadSource, the EME license XHR). The streams API is the\n// trust anchor, so this is defense-in-depth against a malformed/compromised\n// response — not a substitute for server-side trust. httpStatus 0 marks an\n// SDK-side (non-transport) error, matching NETWORK/ABORTED.\nfunction assertHttpUrl(label: string, value: string): string {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n throw new PlaybackError('INTERNAL', 0, `playback ${label} is not a valid URL`);\n }\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new PlaybackError(\n 'INTERNAL',\n 0,\n `playback ${label} has a disallowed scheme: ${parsed.protocol}`,\n );\n }\n return value;\n}\n\nfunction toDescriptor(raw: RawPlayback, fallbackUrn: string): StreamDescriptor {\n if (!raw.manifestUrl) {\n throw new PlaybackError('INTERNAL', 0, 'playback response is missing manifestUrl');\n }\n const drmEnabled = raw.drmEnabled ?? Boolean(raw.drm);\n const descriptor: StreamDescriptor = {\n matchUrn: raw.matchUrn ?? fallbackUrn,\n protocol: 'hls',\n drmEnabled,\n manifestUrl: assertHttpUrl('manifestUrl', raw.manifestUrl),\n serverTime: raw.serverTime ?? '',\n };\n if (raw.liveStartsAt) descriptor.liveStartsAt = raw.liveStartsAt;\n\n if (drmEnabled && raw.drm) {\n const drm: NonNullable<StreamDescriptor['drm']> = {};\n if (raw.drm.widevine?.licenseUrl) {\n drm.widevine = {\n licenseUrl: assertHttpUrl('widevine licenseUrl', raw.drm.widevine.licenseUrl),\n };\n }\n if (raw.drm.fairplay?.licenseUrl && raw.drm.fairplay.certificateUrl) {\n drm.fairplay = {\n licenseUrl: assertHttpUrl('fairplay licenseUrl', raw.drm.fairplay.licenseUrl),\n certificateUrl: assertHttpUrl('fairplay certificateUrl', raw.drm.fairplay.certificateUrl),\n };\n }\n if (drm.widevine || drm.fairplay) descriptor.drm = drm;\n }\n\n // Forward-compat: carry any unrecognized top-level fields the server adds (e.g.\n // a future token TTL, CDN hint, or variant) so Mode-B consumers aren't blind to them.\n const extensions: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {\n if (!KNOWN_PLAYBACK_KEYS.has(k)) extensions[k] = v;\n }\n if (Object.keys(extensions).length > 0) descriptor.extensions = extensions;\n\n return descriptor;\n}\n","import { classifyStatus, PlaybackError } from './errors';\nimport { fetchWithCredential, joinUrl } from './http';\nimport { type CredentialSource, type MatchLiveStatus } from './types';\n\nexport interface CatalogMatch {\n matchUrn: string;\n matchName: string;\n status: MatchLiveStatus;\n datePlannedStart?: string;\n tournamentUrn: string;\n tournamentName: string;\n sport: string;\n}\n\nexport interface CatalogTournament {\n urn: string;\n name: string;\n sport: string;\n isOffline: boolean;\n matches: CatalogMatch[];\n}\n\nexport interface Catalog {\n tournaments: CatalogTournament[];\n /** ETag for a cheap conditional refresh next time. */\n etag?: string;\n /** True when the server returned 304 (the prior etag is still fresh); tournaments is empty. */\n notModified: boolean;\n}\n\nexport interface FetchCatalogOptions {\n baseUrl: string;\n credential: CredentialSource;\n signal?: AbortSignal;\n /** Prior ETag for a conditional GET (304 when unchanged). */\n etag?: string;\n status?: MatchLiveStatus | MatchLiveStatus[];\n sport?: string;\n}\n\ninterface RawCatalog {\n tournaments?: Array<{\n urn?: string;\n name?: string;\n sport?: string;\n isOffline?: boolean;\n matches?: Array<{\n matchUrn?: string;\n matchName?: string;\n status?: string;\n datePlannedStart?: string;\n }>;\n }>;\n}\n\n/** GET /v1/catalog — metadata only (no manifest/DRM). Supports conditional refresh. */\nexport async function fetchCatalog(opts: FetchCatalogOptions): Promise<Catalog> {\n const params = new URLSearchParams();\n if (opts.status) {\n params.set('status', Array.isArray(opts.status) ? opts.status.join(',') : opts.status);\n }\n if (opts.sport) params.set('sport', opts.sport);\n const qs = params.toString();\n const url = joinUrl(opts.baseUrl, '/v1/catalog' + (qs ? `?${qs}` : ''));\n\n const headers = new Headers();\n if (opts.etag) headers.set('If-None-Match', opts.etag);\n\n const res = await fetchWithCredential(url, {\n credential: opts.credential,\n signal: opts.signal,\n headers,\n });\n\n if (res.status === 304) {\n return { tournaments: [], etag: opts.etag, notModified: true };\n }\n if (!res.ok) {\n throw new PlaybackError(\n classifyStatus(res.status),\n res.status,\n `catalog request failed (HTTP ${res.status})`,\n );\n }\n\n const raw = (await res.json()) as RawCatalog;\n return {\n tournaments: parseCatalog(raw),\n etag: res.headers.get('etag') ?? undefined,\n notModified: false,\n };\n}\n\nfunction parseCatalog(raw: RawCatalog): CatalogTournament[] {\n return (raw.tournaments ?? []).map((t) => {\n const urn = t.urn ?? '';\n const name = t.name ?? '';\n const sport = t.sport ?? '';\n return {\n urn,\n name,\n sport,\n isOffline: Boolean(t.isOffline),\n matches: (t.matches ?? []).map((m) => ({\n matchUrn: m.matchUrn ?? '',\n matchName: m.matchName ?? '',\n status: (m.status as MatchLiveStatus) ?? 'upcoming',\n datePlannedStart: m.datePlannedStart,\n tournamentUrn: urn,\n tournamentName: name,\n sport,\n })),\n };\n });\n}\n\nexport interface FetchTournamentOptions {\n baseUrl: string;\n credential: CredentialSource;\n /** Tournament URN, e.g. `od:tournament:42`. */\n urn: string;\n signal?: AbortSignal;\n /** Prior ETag for a conditional GET (304 when unchanged) — only effective once havik-streams ETags `/v1/tournaments/{urn}` (parity with `/v1/matches/{urn}`). */\n etag?: string;\n}\n\nexport interface TournamentResult {\n /** Present unless the tournament is unknown (404) or the server returned 304. */\n tournament?: CatalogTournament;\n /** ETag for a cheap conditional refresh next time, when the server emits one. */\n etag?: string;\n /** True when the server returned 304 (the prior etag is still fresh); `tournament` is undefined. */\n notModified: boolean;\n /** True when the URN is not in the catalog (or unknown to the caller's brand). */\n notFound: boolean;\n}\n\ninterface RawTournament {\n urn?: string;\n name?: string;\n sport?: string;\n isOffline?: boolean;\n matches?: Array<{\n matchUrn?: string;\n matchName?: string;\n status?: string;\n datePlannedStart?: string;\n }>;\n}\n\n/**\n * `GET /v1/tournaments/{urn}` — single-tournament metadata (matches included,\n * never any signed/per-viewer URLs; those live on `/v1/playback/{urn}`).\n *\n * Returns `notFound: true` on 404 (unknown URN OR caller-not-entitled — the\n * server intentionally collapses both so existence isn't leaked). Other waitable\n * statuses (5xx) reject as a typed `PlaybackError`. Supports ETag/304.\n */\nexport async function fetchTournament(opts: FetchTournamentOptions): Promise<TournamentResult> {\n const url = joinUrl(opts.baseUrl, `/v1/tournaments/${encodeURIComponent(opts.urn)}`);\n const headers = new Headers();\n if (opts.etag) headers.set('If-None-Match', opts.etag);\n\n const res = await fetchWithCredential(url, {\n credential: opts.credential,\n signal: opts.signal,\n headers,\n });\n\n if (res.status === 304) {\n return { etag: opts.etag, notModified: true, notFound: false };\n }\n if (res.status === 404) {\n return { notModified: false, notFound: true };\n }\n if (!res.ok) {\n throw new PlaybackError(\n classifyStatus(res.status),\n res.status,\n `tournament request failed (HTTP ${res.status})`,\n );\n }\n\n const raw = (await res.json()) as RawTournament;\n return {\n tournament: parseTournament(raw),\n etag: res.headers.get('etag') ?? undefined,\n notModified: false,\n notFound: false,\n };\n}\n\nfunction parseTournament(raw: RawTournament): CatalogTournament {\n const urn = raw.urn ?? '';\n const name = raw.name ?? '';\n const sport = raw.sport ?? '';\n return {\n urn,\n name,\n sport,\n isOffline: Boolean(raw.isOffline),\n matches: (raw.matches ?? []).map((m) => ({\n matchUrn: m.matchUrn ?? '',\n matchName: m.matchName ?? '',\n status: (m.status as MatchLiveStatus) ?? 'upcoming',\n datePlannedStart: m.datePlannedStart,\n tournamentUrn: urn,\n tournamentName: name,\n sport,\n })),\n };\n}\n\n/** Flatten a Catalog to a single match list (handy for grids). */\nexport function flattenMatches(catalog: Catalog): CatalogMatch[] {\n return catalog.tournaments.flatMap((t) => t.matches);\n}\n","import { classifyStatus, PlaybackError } from './errors';\nimport { fetchWithCredential, joinUrl } from './http';\nimport { type CredentialSource, type MatchLiveStatus, type MatchStatus } from './types';\n\nexport interface WatchStatusOptions {\n baseUrl: string;\n matchUrn: string;\n credential: CredentialSource;\n /** Fired whenever the status changes (and once on first read). */\n onChange: (status: MatchStatus) => void;\n /** Fired on a poll error; polling continues. */\n onError?: (err: PlaybackError) => void;\n /** Poll interval in ms. Floored at 2000. Default: 10000 (catalog max-age). */\n intervalMs?: number;\n}\n\nexport interface StatusWatcher {\n stop(): void;\n}\n\n/**\n * Poll `GET /v1/matches/{urn}` for the live status (there is no push channel),\n * firing onChange on transitions. Uses ETag/If-None-Match so unchanged polls\n * are cheap 304s. This drives discovery; actual readiness is the /v1/playback\n * 200 (see resolveStream + waitForLive).\n */\nexport function watchStatus(opts: WatchStatusOptions): StatusWatcher {\n const interval = Math.max(2000, opts.intervalMs ?? 10_000);\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let etag: string | undefined;\n let lastStatus: MatchLiveStatus | undefined;\n let stopped = false;\n\n const schedule = () => {\n if (!stopped) timer = setTimeout(tick, interval);\n };\n\n const tick = async () => {\n try {\n const url = joinUrl(opts.baseUrl, `/v1/matches/${encodeURIComponent(opts.matchUrn)}`);\n const headers = new Headers();\n if (etag) headers.set('If-None-Match', etag);\n const res = await fetchWithCredential(url, {\n credential: opts.credential,\n signal: controller.signal,\n headers,\n });\n if (res.status !== 304) {\n if (!res.ok) {\n throw new PlaybackError(\n classifyStatus(res.status),\n res.status,\n `match status request failed (HTTP ${res.status})`,\n );\n }\n etag = res.headers.get('etag') ?? etag;\n const m = (await res.json()) as {\n matchUrn?: string;\n matchName?: string;\n status?: string;\n datePlannedStart?: string;\n };\n const status = (m.status as MatchLiveStatus) ?? 'upcoming';\n if (status !== lastStatus) {\n lastStatus = status;\n opts.onChange({\n matchUrn: m.matchUrn ?? opts.matchUrn,\n matchName: m.matchName,\n status,\n datePlannedStart: m.datePlannedStart,\n });\n }\n }\n } catch (err) {\n if (!controller.signal.aborted) {\n opts.onError?.(\n err instanceof PlaybackError ? err : new PlaybackError('NETWORK', 0, String(err)),\n );\n }\n } finally {\n schedule();\n }\n };\n\n void tick();\n\n return {\n stop() {\n stopped = true;\n controller.abort();\n if (timer) clearTimeout(timer);\n },\n };\n}\n","import { joinUrl } from './http';\nimport { resolveCredential, type CredentialSource } from './types';\n\n/**\n * Coarse, player-facing live state pushed by havik-streams over SSE. Mirrors\n * the `/v1/playback` outcome ladder so a pushed event and a follow-up\n * `/v1/playback` call always agree:\n * live ↔ 200 OK (a serveable session exists — play now)\n * upcoming ↔ 425 Too Early (scheduled, not serving yet)\n * ended ↔ 503 (was live; media stopped, still in catchup)\n * gone ↔ 410 Gone (ended past the catchup window)\n */\nexport type LiveState = 'live' | 'upcoming' | 'ended' | 'gone';\n\nexport interface LiveStateEvent {\n matchUrn: string;\n state: LiveState;\n /** RFC3339 server timestamp the transition was stamped at. */\n serverTime: string;\n}\n\nexport interface SubscribeLiveStateOptions {\n /** Base URL of the SSE endpoint, e.g. `https://events.feed.oddin-video.gg`. */\n eventsBaseUrl: string;\n matchUrn: string;\n credential: CredentialSource;\n /** Fired for the initial snapshot on connect and every subsequent transition. */\n onState: (ev: LiveStateEvent) => void;\n /**\n * Fired on a non-fatal subscription problem (network drop, HTTP error,\n * server lifetime-cap close). The client keeps retrying with backoff; this\n * is observational only — it never tears playback down.\n */\n onError?: (err: unknown) => void;\n /**\n * Fired on EVERY received frame, including heartbeat comments — a liveness\n * tick. Lets a caller treat the subscription as healthy (e.g. suspend\n * polling) while frames keep arriving, and fall back when they stop.\n */\n onAlive?: () => void;\n /** Reconnect backoff floor / ceiling (ms). Defaults: 1000 / 15000. */\n minBackoffMs?: number;\n maxBackoffMs?: number;\n}\n\nexport interface LiveStateSubscription {\n /** Stop the subscription and abort the in-flight request. Idempotent. */\n close(): void;\n}\n\n/**\n * Subscribe to a match's live-state stream (`GET /v1/events/{urn}`).\n *\n * Uses `fetch` rather than the native `EventSource` because the endpoint\n * authenticates with the `x-api-key` HEADER, which `EventSource` cannot set.\n * The body is a `text/event-stream` we parse incrementally. The connection is\n * long-lived (the server caps it at ~30min and sends heartbeats); on any\n * close/error the client reconnects with exponential backoff and re-syncs from\n * the initial snapshot the server replays on connect. A subscription failure\n * is always non-fatal to playback — callers keep the HLS staleness watchdog as\n * the layer-fallback.\n */\nexport function subscribeLiveState(opts: SubscribeLiveStateOptions): LiveStateSubscription {\n const minBackoff = Math.max(250, opts.minBackoffMs ?? 1000);\n const maxBackoff = Math.max(minBackoff, opts.maxBackoffMs ?? 15_000);\n const ctrl = new AbortController();\n let closed = false;\n\n const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n if (ctrl.signal.aborted) return resolve();\n const t = setTimeout(resolve, ms);\n ctrl.signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(t);\n resolve();\n },\n { once: true },\n );\n });\n\n const dispatch = (frame: string): void => {\n opts.onAlive?.(); // any frame (incl. a heartbeat comment) is a liveness tick\n let event = 'message';\n const data: string[] = [];\n for (const raw of frame.split('\\n')) {\n const line = raw.endsWith('\\r') ? raw.slice(0, -1) : raw;\n if (line === '' || line.startsWith(':')) continue; // blank / comment (heartbeat)\n const i = line.indexOf(':');\n const field = i < 0 ? line : line.slice(0, i);\n let value = i < 0 ? '' : line.slice(i + 1);\n if (value.startsWith(' ')) value = value.slice(1);\n if (field === 'event') event = value;\n else if (field === 'data') data.push(value);\n }\n if (event !== 'state' || data.length === 0) return;\n try {\n const parsed = JSON.parse(data.join('\\n')) as LiveStateEvent;\n if (parsed && typeof parsed.state === 'string') opts.onState(parsed);\n } catch {\n /* ignore a malformed frame; the next one re-syncs state */\n }\n };\n\n // SSE frame separator = a blank line. Accept LF, CRLF, and lone-CR servers\n // (per the spec, lines may end in \\n, \\r\\n, or \\r). Non-global so .exec always\n // scans from 0; it yields both the boundary index AND its length. A trailing\n // partial boundary (e.g. a chunk ending mid-\"\\r\\n\\r\\n\") simply stays buffered\n // until the next chunk completes it.\n const FRAME_BOUNDARY = /\\r\\n\\r\\n|\\n\\n|\\r\\r/;\n\n const readStream = async (\n body: ReadableStream<Uint8Array>,\n onData: () => void,\n ): Promise<void> => {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buf = '';\n let gotData = false;\n try {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) return;\n if (!gotData && value && value.length > 0) {\n gotData = true;\n onData(); // first bytes flowed — the connection is genuinely healthy\n }\n buf += decoder.decode(value, { stream: true });\n let m: RegExpExecArray | null;\n while ((m = FRAME_BOUNDARY.exec(buf)) !== null) {\n dispatch(buf.slice(0, m.index));\n buf = buf.slice(m.index + m[0].length);\n }\n if (closed) return;\n }\n } finally {\n await reader.cancel().catch(() => {});\n }\n };\n\n const run = async (): Promise<void> => {\n let backoff = minBackoff;\n while (!closed) {\n try {\n const cred = await resolveCredential(opts.credential);\n if (closed) return;\n const url = joinUrl(opts.eventsBaseUrl, `/v1/events/${encodeURIComponent(opts.matchUrn)}`);\n const res = await fetch(url, {\n method: 'GET',\n headers: { 'x-api-key': cred.apiKey, accept: 'text/event-stream' },\n signal: ctrl.signal,\n cache: 'no-store',\n credentials: 'omit',\n mode: 'cors',\n });\n if (closed) return;\n if (!res.ok || !res.body) {\n opts.onError?.(new Error(`live-state subscribe failed: HTTP ${res.status}`));\n } else {\n // Reset the backoff only once data actually flows (readStream's onData),\n // NOT on bare accept — an accept-then-immediate-close server would\n // otherwise reconnect in a tight loop with no backoff growth.\n await readStream(res.body, () => {\n backoff = minBackoff;\n });\n }\n } catch (err) {\n if (closed || ctrl.signal.aborted) return;\n opts.onError?.(err);\n }\n if (closed) return;\n await sleep(backoff);\n backoff = Math.min(maxBackoff, backoff * 2);\n }\n };\n\n void run();\n\n return {\n close() {\n if (closed) return;\n closed = true;\n ctrl.abort();\n },\n };\n}\n\n/**\n * Derive the SSE endpoint base from the API base by prefixing the host with\n * `events.` — matching the deployed convention (`feed.<domain>` →\n * `events.<domain>`, served ALB-direct because CloudFront can't stream SSE).\n * Returns `undefined` if `baseUrl` can't be parsed.\n */\nexport function deriveEventsBaseUrl(baseUrl: string): string | undefined {\n try {\n const u = new URL(baseUrl);\n u.hostname = `events.${u.hostname}`;\n u.pathname = '';\n u.search = '';\n return u.origin;\n } catch {\n return undefined;\n }\n}\n","// Persistent per-device id required by the DRM license POST (X-Device-Id).\n// havik-drm rejects a missing id (400 missing_device_id) and validates the\n// format against ^[a-zA-Z0-9\\-_.@:]+$ — a UUID passes. We persist it in\n// localStorage so it is stable across reloads, falling back to an in-memory id\n// when storage is unavailable (private mode), which is still valid per request.\n\nconst STORAGE_KEY = 'havik.player.deviceId';\nlet cached: string | undefined;\n\nexport function getDeviceId(): string {\n if (cached) return cached;\n try {\n const stored = globalThis.localStorage?.getItem(STORAGE_KEY);\n if (stored) {\n cached = stored;\n return stored;\n }\n } catch {\n // storage blocked — fall through to generate\n }\n const id = generateUuid();\n cached = id;\n try {\n globalThis.localStorage?.setItem(STORAGE_KEY, id);\n } catch {\n // best-effort persistence\n }\n return id;\n}\n\n/**\n * Clear the persisted device id (localStorage + in-memory cache). The next\n * getDeviceId() generates a fresh one. Provide this to viewers as a privacy /\n * \"reset my device identifier\" control — the device id is sent as X-Device-Id\n * on every DRM license request and persists across sessions.\n */\nexport function clearDeviceId(): void {\n cached = undefined;\n try {\n globalThis.localStorage?.removeItem(STORAGE_KEY);\n } catch {\n // storage unavailable — the in-memory cache is already cleared\n }\n}\n\nfunction generateUuid(): string {\n const c = globalThis.crypto as Crypto | undefined;\n if (c && typeof c.randomUUID === 'function') return c.randomUUID();\n\n const bytes = new Uint8Array(16);\n if (c && typeof c.getRandomValues === 'function') {\n c.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4\n bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return (\n hex.slice(0, 4).join('') +\n '-' +\n hex.slice(4, 6).join('') +\n '-' +\n hex.slice(6, 8).join('') +\n '-' +\n hex.slice(8, 10).join('') +\n '-' +\n hex.slice(10, 16).join('')\n );\n}\n","import { type Credential, type StreamDescriptor } from '../types';\nimport { getDeviceId } from './deviceId';\n\nexport interface LicenseHeaderOptions {\n /** Override the persistent device id (defaults to getDeviceId()). */\n deviceId?: string;\n /** Optional viewer id, forwarded as X-User-Id for billing/tracking. */\n userId?: string;\n}\n\n/**\n * Headers a host player must attach to the DRM license POST. The license URL\n * itself must be sent VERBATIM (its signed query string is the token) — these\n * headers are the rest of the contract:\n * - x-api-key : the SAME key used to resolve playback (cid binding)\n * - X-Device-Id : required, persistent\n * - X-Match-Urn : advisory (the signed `m` param is authoritative)\n * - X-User-Id : optional\n * Body must be the raw EME challenge bytes (application/octet-stream).\n */\nexport function licenseRequestHeaders(\n descriptor: StreamDescriptor,\n credential: Credential,\n opts: LicenseHeaderOptions = {},\n): Record<string, string> {\n const headers: Record<string, string> = {\n 'x-api-key': credential.apiKey,\n 'X-Match-Urn': descriptor.matchUrn,\n 'X-Device-Id': opts.deviceId ?? getDeviceId(),\n 'Content-Type': 'application/octet-stream',\n };\n if (opts.userId) headers['X-User-Id'] = opts.userId;\n return headers;\n}\n","// LL-HLS detection from a playlist body. LL-HLS is per-distributor and OFF by\n// default, and the tags live on the *media* playlist — so we detect rather than\n// assume. The managed player uses this on level updates to report isLowLatency.\n\nconst LOW_LATENCY_TAG = /^#EXT-X-(PART-INF|PART|PRELOAD-HINT)[:\\s]/m;\n\nexport function isLowLatencyManifest(playlistText: string): boolean {\n return LOW_LATENCY_TAG.test(playlistText);\n}\n","export type PlayerState =\n | 'idle'\n | 'waiting' // armed, polling for go-live (waitForLive)\n | 'loading'\n | 'playing'\n | 'buffering'\n | 'paused'\n | 'ended'\n | 'error';\n\nexport interface PlaybackStats {\n droppedFrames: number;\n /** Live-edge latency in seconds (hls.js engine only). */\n latencySeconds?: number;\n /** The live-sync target latency hls.js is converging to, in seconds. Compare\n * against latencySeconds to see drift. */\n targetLatencySeconds?: number;\n /** Estimated bandwidth in kbps (hls.js engine only). */\n bandwidthKbps?: number;\n /** Height of the current rendition, when known. */\n levelHeight?: number;\n /** Whether the stream is being played in low-latency mode. */\n lowLatency?: boolean;\n /** Whether the current media playlist is live. */\n isLive?: boolean;\n /** Whether playback is at/near the live edge (within the live sync window). */\n atLiveEdge?: boolean;\n}\n\nexport const EMPTY_STATS: PlaybackStats = { droppedFrames: 0 };\n\n/** A selectable video rendition. */\nexport interface QualityLevel {\n /** Index into getQualityLevels() — pass to setQuality(). */\n index: number;\n width?: number;\n height?: number;\n bitrate: number;\n codecs?: string;\n}\n\n/** A selectable audio rendition. */\nexport interface AudioTrackInfo {\n id: number;\n name: string;\n lang?: string;\n default: boolean;\n}\n\n/** A selectable text (caption/subtitle) track. */\nexport interface TextTrackInfo {\n id: number;\n name: string;\n lang?: string;\n}\n\n/** Dropped-frame count via the standard media quality API (engine-agnostic). */\nexport function readDroppedFrames(video: HTMLVideoElement): number {\n const q = (\n video as HTMLVideoElement & { getVideoPlaybackQuality?: () => VideoPlaybackQuality }\n ).getVideoPlaybackQuality?.();\n return q?.droppedVideoFrames ?? 0;\n}\n\n/**\n * Map raw <video> media events to player states. Engine-agnostic: both the\n * hls.js and native engines drive the same state machine through this.\n */\nexport function attachMediaStateListeners(\n video: HTMLVideoElement,\n onState: (state: PlayerState) => void,\n): () => void {\n const handlers: Array<[keyof HTMLMediaElementEventMap, () => void]> = [\n ['playing', () => onState('playing')],\n ['waiting', () => onState('buffering')],\n ['stalled', () => onState('buffering')],\n [\n 'pause',\n () => {\n if (!video.ended) onState('paused');\n },\n ],\n ['ended', () => onState('ended')],\n ];\n for (const [ev, fn] of handlers) video.addEventListener(ev, fn);\n return () => {\n for (const [ev, fn] of handlers) video.removeEventListener(ev, fn);\n };\n}\n","import Hls, { type ErrorData, type HlsConfig, type LevelLoadedData } from 'hls.js';\nimport { PlaybackError } from '../errors';\nimport {\n readDroppedFrames,\n type AudioTrackInfo,\n type PlaybackStats,\n type QualityLevel,\n type TextTrackInfo,\n} from '../stats';\nimport { type EngineDeps, type PlaybackEngine } from './types';\n\nexport function hlsSupported(): boolean {\n return Hls.isSupported();\n}\n\nconst MAX_MEDIA_RECOVERIES = 3;\nconst MAX_NETWORK_RECOVERIES = 5;\n// hls.js has no license-request retry policy (there's certLoadPolicy/keyLoadPolicy\n// but no licenseLoadPolicy), and KEY_SYSTEM_LICENSE_REQUEST_FAILED is fatal by\n// default. A transient transport failure of the license POST is recoverable by\n// rebuilding the instance (fresh key session → fresh license request).\nconst MAX_LICENSE_RECOVERIES = 3;\nconst RECOVERY_RESET_MS = 30_000;\n// End-of-stream watchdog (live only). The live playlist not advancing for this\n// long — with no #EXT-X-ENDLIST — means the stream ended: Tencent's origin\n// freezes the manifest at end (HTTP 200, media sequence static) instead of\n// publishing ENDLIST, so hls.js would otherwise buffer forever. 12s is safely\n// above the worst observed edge refresh hiccup (~8s).\nconst END_STALE_MS = 12_000;\nconst END_WATCH_INTERVAL_MS = 3_000;\n\n/** Default live-edge target (seconds behind the edge) when the caller sets none. */\nconst LIVE_SYNC_DURATION_S = 3;\n/** Recovery band above the target; the drift ceiling is target + this (min 12s). */\nconst LIVE_LATENCY_MARGIN_S = 9;\n\n/**\n * Resolve the live-edge target (`liveSyncDuration`) + drift ceiling\n * (`liveMaxLatencyDuration`) on the final, caller-merged config, keeping it legal\n * for hls.js — mutates `config` in place.\n *\n * Why a target + ceiling at all: `lowLatencyMode` otherwise pins playback to\n * `PART-HOLD-BACK` (the bleeding edge), where Tencent's newest parts aren't\n * reliably published yet → constant rebuffering; and after a stall, latency\n * drifts unbounded toward the DVR window (~90s seen in the field). The target\n * parks ~3s back where parts are settled; the ceiling force-seeks back once\n * latency exceeds it, capping the drift. (`maxLiveSyncPlaybackRate` recovers\n * smoothly inside the band first.) Live/LL only — ignored on VOD.\n *\n * Why normalize rather than hard-code: hls.js THROWS at construction if the\n * Count and Duration variants are mixed, or if `liveMaxLatencyDuration <=\n * liveSyncDuration`. The `hlsConfig` escape hatch lets a caller pass either, so:\n * - caller opted into the Count variants → defer entirely (never inject seconds);\n * - otherwise default the target to 3 and keep the ceiling strictly above it,\n * repairing a one-sided or illegal Duration override instead of throwing.\n */\nfunction normalizeLiveSync(config: Partial<HlsConfig>): void {\n if (config.liveSyncDurationCount != null || config.liveMaxLatencyDurationCount != null) {\n // Caller chose count-based live sync — leave it; never mix in seconds keys.\n delete config.liveSyncDuration;\n delete config.liveMaxLatencyDuration;\n return;\n }\n if (config.liveSyncDuration == null) config.liveSyncDuration = LIVE_SYNC_DURATION_S;\n const ceiling = Math.max(12, config.liveSyncDuration + LIVE_LATENCY_MARGIN_S);\n if (\n config.liveMaxLatencyDuration == null ||\n config.liveMaxLatencyDuration <= config.liveSyncDuration\n ) {\n config.liveMaxLatencyDuration = ceiling;\n }\n}\n\n/** hls.js adapter — LL-HLS + Widevine EME, ported from the proven Demo Player config. */\nexport class HlsEngine implements PlaybackEngine {\n readonly name = 'hls' as const;\n private hls: Hls | null = null;\n private readonly deps: EngineDeps;\n /** Current signed license URL; mutable so it can be refreshed mid-session. */\n private licenseUrl: string | undefined;\n /** Whether the live media playlist actually carries LL-HLS tags (detected, not requested). */\n private detectedLowLatency = false;\n // Recovery budgets are windowed: they reset after a stretch of healthy playback\n // (FRAG_BUFFERED), so a long live session isn't permanently disabled by a few\n // unrelated transient glitches spread across hours.\n private mediaRecoveries = 0;\n private networkRecoveries = 0;\n private licenseRecoveries = 0;\n private recoveryResetTimer: ReturnType<typeof setTimeout> | null = null;\n private networkRetryTimer: ReturnType<typeof setTimeout> | null = null;\n private licenseRetryTimer: ReturnType<typeof setTimeout> | null = null;\n /** visibilitychange handler — snaps back to the live edge after a backgrounded tab. */\n private onVisibility: (() => void) | null = null;\n // End-of-stream watchdog state (armed once the stream is seen LIVE).\n private sawLive = false;\n private lastEndSN = -1;\n private lastAdvanceMs = 0;\n private endWatchdog: ReturnType<typeof setInterval> | null = null;\n private endedEmitted = false;\n\n constructor(deps: EngineDeps) {\n this.deps = deps;\n this.licenseUrl = deps.descriptor.drm?.widevine?.licenseUrl;\n }\n\n async load(): Promise<void> {\n const { descriptor, snapToLiveOnRefocus, video, emit } = this.deps;\n\n // Fail fast on a DRM-required stream this engine can't satisfy (e.g. a\n // FairPlay-only descriptor reaching hls.js), instead of loading with EME off\n // and surfacing an opaque MEDIA_ERROR after the manifest parses.\n if (descriptor.drmEnabled && !descriptor.drm?.widevine?.licenseUrl) {\n emit({\n type: 'error',\n error: new PlaybackError(\n 'INTERNAL',\n 0,\n 'DRM is required but no Widevine license is available for this stream on this browser',\n ),\n fatal: true,\n });\n return;\n }\n\n this.startHls();\n\n // A backgrounded tab throttles timers, so the live-sync controller can't\n // chase the edge and latency balloons while hidden. On refocus, if we're\n // well past the controller's max-latency ceiling, snap straight to live\n // rather than catch-up-playing through minutes of backlog. Opt out via\n // snapToLiveOnRefocus: false. Registered once here (not in startHls) so a\n // license-retry rebuild doesn't stack duplicate visibilitychange listeners.\n if (snapToLiveOnRefocus !== false && typeof document !== 'undefined') {\n this.onVisibility = () => {\n const h = this.hls;\n if (document.visibilityState !== 'visible' || !h) return;\n const pos = h.liveSyncPosition;\n if (typeof pos !== 'number') return;\n // maxLatency is Infinity unless a count-based max is configured (we\n // deliberately don't — it's the illegal-config trap), so key the snap\n // off the sync target instead of h.maxLatency.\n const target =\n typeof h.targetLatency === 'number' && h.targetLatency > 0 ? h.targetLatency : 3;\n if (pos - video.currentTime > Math.max(target * 3, 8)) video.currentTime = pos;\n };\n document.addEventListener('visibilitychange', this.onVisibility);\n }\n }\n\n /**\n * Build (or rebuild) the hls.js instance, wire its events, and start loading.\n * Split out of load() so a transient license-request failure can re-drive EME\n * with a fresh instance (new key session → new license request) without\n * re-adding the one-time visibility listener.\n */\n private startHls(): void {\n const { video, descriptor, credential, deviceId, userId, lowLatency, hlsConfig, emit } =\n this.deps;\n\n const drmEnabled = descriptor.drmEnabled && Boolean(descriptor.drm?.widevine?.licenseUrl);\n\n const config: Partial<HlsConfig> = {\n // Workers are disabled in low-latency mode: on hls.js 1.6.x the\n // LL-HLS partial-segment append path hits a bufferAppendError race\n // (\"SourceBuffer is still processing an appendBuffer/remove\") that\n // reproduces ~100% with workers ON (video-dev/hls.js#7321). Disabling\n // the worker is the documented mitigation; standard (non-LL) playback\n // keeps workers for off-main-thread parsing. Re-enable once a hls.js\n // release fixes the worker append race.\n enableWorker: !lowLatency,\n lowLatencyMode: lowLatency,\n backBufferLength: 90,\n // Audio-hole bridge. Tencent's LL-HLS audio packager cuts a ~0.45s\n // \"remainder\" part at each segment boundary; the hls.js default\n // maxBufferHole (0.1s) is smaller than the hole, so the player stalls\n // and gap-skips ~0.5s every few seconds. 0.5 bridges it as a\n // sub-perceptible hitch instead of a rebuffer.\n maxBufferHole: 0.5,\n // Live-edge target (liveSyncDuration) + drift ceiling (liveMaxLatencyDuration)\n // are applied by normalizeLiveSync() AFTER the caller's hlsConfig is merged\n // below — so a raw override (incl. the Count variants) can never produce an\n // illegal pair that throws at construction. See that helper for the why.\n // Catch up at up to 1.5x (ramped by how far behind) to recover drift inside\n // the target..ceiling band before the hard seek. Internal — not a public\n // knob (1.0 silently re-introduces drift); overridable via raw hlsConfig.\n maxLiveSyncPlaybackRate: 1.5,\n // ABR start quality — start conservative, then ramp. Seeding high (~4 Mbps\n // → a ~720p first rendition) and relying on \"drop fast\" to recover was still\n // rebuffering in the field on constrained links: the high first grab stalls\n // to fill the buffer before ABR can drop. Per the root-cause investigation\n // (docs/llhls-buffering-investigation.md §3), seed ~1 Mbps so the first\n // rendition is one a modest link sustains (~360p) and ABR ramps up within a\n // few seconds when real bandwidth allows — trading a slightly softer first\n // ~2s for not stalling on startup (the first-20s buffering we were chasing).\n // capLevelToPlayerSize still caps to the player's pixel box (re-evaluates on\n // resize). Caller startLevel / maxBitrate still override.\n abrEwmaDefaultEstimate: 1_000_000,\n capLevelToPlayerSize: true,\n ...(drmEnabled\n ? {\n emeEnabled: true,\n drmSystems: {\n 'com.widevine.alpha': { licenseUrl: this.licenseUrl as string },\n },\n // Pin Widevine L3 desktop minimums + the cbcs scheme the CMAF\n // content uses. Without this hls.js sends robustness:'' (Chrome\n // warns) and encryptionScheme:null.\n drmSystemOptions: {\n videoRobustness: 'SW_SECURE_DECODE',\n audioRobustness: 'SW_SECURE_CRYPTO',\n videoEncryptionScheme: 'cbcs',\n audioEncryptionScheme: 'cbcs',\n },\n // EME license requests route through licenseXhrSetup — NOT the\n // segment-side xhrSetup (a hard-won Demo Player lesson; putting the\n // key in xhrSetup makes every license 401). Open the XHR ourselves\n // before setting any headers (setRequestHeader throws if the XHR\n // isn't OPENED) so we never depend on hls.js's internal open-ordering;\n // opening against the current licenseUrl also picks up a refreshed URL.\n licenseXhrSetup: (xhr: XMLHttpRequest, url: string) => {\n xhr.open('POST', this.licenseUrl ?? url, true);\n xhr.setRequestHeader('x-api-key', credential.apiKey);\n xhr.setRequestHeader('X-Match-Urn', descriptor.matchUrn);\n xhr.setRequestHeader('X-Device-Id', deviceId);\n if (userId) xhr.setRequestHeader('X-User-Id', userId);\n },\n }\n : {}),\n ...hlsConfig,\n };\n // Resolve the live-edge target/ceiling on the FINAL merged config so a raw\n // hlsConfig override can't produce an illegal hls.js pair (see helper).\n normalizeLiveSync(config);\n\n const hls = new Hls(config);\n this.hls = hls;\n\n hls.on(Hls.Events.MANIFEST_PARSED, () => {\n this.mediaRecoveries = 0;\n this.networkRecoveries = 0;\n emit({ type: 'ready' });\n });\n // After a stretch of uninterrupted buffering, refund the recovery budget so a\n // healthy long session isn't permanently capped. A fresh error cancels the\n // pending refund (see clearRecoveryReset in onError), preserving tight-loop protection.\n hls.on(Hls.Events.FRAG_BUFFERED, () => {\n if (\n (this.mediaRecoveries > 0 || this.networkRecoveries > 0 || this.licenseRecoveries > 0) &&\n !this.recoveryResetTimer\n ) {\n this.recoveryResetTimer = setTimeout(() => {\n this.mediaRecoveries = 0;\n this.networkRecoveries = 0;\n this.licenseRecoveries = 0;\n this.recoveryResetTimer = null;\n }, RECOVERY_RESET_MS);\n }\n });\n hls.on(Hls.Events.LEVEL_LOADED, (_e, data: LevelLoadedData) => {\n if (this.detectedLowLatency) return;\n const details = data.details;\n const isLL = Boolean(details?.partList?.length) || (details?.partTarget ?? 0) > 0;\n if (isLL) {\n this.detectedLowLatency = true;\n emit({ type: 'lowLatencyDetected', value: true });\n }\n });\n // End-of-stream detection (separate handler so the LL-detection early-return\n // above never short-circuits it). ENDLIST flips details.live→false; a frozen\n // manifest is caught by the staleness watchdog interval below.\n hls.on(Hls.Events.LEVEL_LOADED, (_e, data: LevelLoadedData) => this.trackEnd(data));\n this.endWatchdog = setInterval(() => this.checkEndStale(), END_WATCH_INTERVAL_MS);\n hls.on(Hls.Events.LEVEL_SWITCHED, (_e, data) =>\n emit({ type: 'qualitychange', index: data.level }),\n );\n hls.on(Hls.Events.AUDIO_TRACK_SWITCHED, (_e, data) =>\n emit({ type: 'audiotrackchange', id: data.id }),\n );\n hls.on(Hls.Events.SUBTITLE_TRACK_SWITCH, (_e, data) =>\n emit({ type: 'texttrackchange', id: data.id }),\n );\n hls.on(Hls.Events.ERROR, (_e, data: ErrorData) => this.onError(data));\n\n hls.loadSource(descriptor.manifestUrl);\n hls.attachMedia(video);\n }\n\n getQualityLevels(): QualityLevel[] {\n if (!this.hls) return [];\n return this.hls.levels.map((l, index) => ({\n index,\n width: l.width || undefined,\n height: l.height || undefined,\n bitrate: l.bitrate,\n codecs: l.videoCodec || l.codecSet || undefined,\n }));\n }\n\n getCurrentQuality(): number {\n if (!this.hls) return -1;\n return this.hls.autoLevelEnabled ? -1 : this.hls.currentLevel;\n }\n\n setQuality(index: number): void {\n if (this.hls) this.hls.currentLevel = index; // -1 re-enables ABR\n }\n\n setMaxBitrate(bitrate: number | null): void {\n const hls = this.hls;\n if (!hls) return;\n if (bitrate == null) {\n hls.autoLevelCapping = -1;\n return;\n }\n // Cap ABR at the highest level whose bitrate fits the budget.\n let cap = -1;\n hls.levels.forEach((l, i) => {\n if (l.bitrate <= bitrate) cap = i;\n });\n hls.autoLevelCapping = cap;\n }\n\n getAudioTracks(): AudioTrackInfo[] {\n if (!this.hls) return [];\n return this.hls.audioTracks.map((t) => ({\n id: t.id,\n name: t.name,\n lang: t.lang || undefined,\n default: Boolean(t.default),\n }));\n }\n\n setAudioTrack(id: number): void {\n if (this.hls) this.hls.audioTrack = id;\n }\n\n getTextTracks(): TextTrackInfo[] {\n if (!this.hls) return [];\n return this.hls.subtitleTracks.map((t) => ({\n id: t.id,\n name: t.name,\n lang: t.lang || undefined,\n }));\n }\n\n setTextTrack(id: number): void {\n const hls = this.hls;\n if (!hls) return;\n if (id < 0) {\n hls.subtitleTrack = -1;\n hls.subtitleDisplay = false;\n } else {\n hls.subtitleTrack = id;\n hls.subtitleDisplay = true;\n }\n }\n\n seekToLive(): void {\n const hls = this.hls;\n if (hls && typeof hls.liveSyncPosition === 'number') {\n this.deps.video.currentTime = hls.liveSyncPosition;\n }\n }\n\n setLicenseUrl(url: string): void {\n this.licenseUrl = url;\n }\n\n getStats(): PlaybackStats {\n const hls = this.hls;\n const stats: PlaybackStats = {\n droppedFrames: readDroppedFrames(this.deps.video),\n lowLatency: this.detectedLowLatency,\n };\n if (hls) {\n if (typeof hls.latency === 'number') stats.latencySeconds = hls.latency;\n if (typeof hls.targetLatency === 'number' && hls.targetLatency > 0) {\n stats.targetLatencySeconds = hls.targetLatency;\n }\n if (typeof hls.bandwidthEstimate === 'number') {\n stats.bandwidthKbps = Math.round(hls.bandwidthEstimate / 1000);\n }\n const level = hls.currentLevel >= 0 ? hls.levels[hls.currentLevel] : undefined;\n if (level) stats.levelHeight = level.height;\n if (typeof level?.details?.live === 'boolean') {\n stats.isLive = level.details.live;\n if (typeof hls.liveSyncPosition === 'number') {\n stats.atLiveEdge = this.deps.video.currentTime >= hls.liveSyncPosition - 1.5;\n }\n }\n }\n return stats;\n }\n\n /**\n * Track live-playlist progress for end detection. Once the stream has been\n * seen LIVE, a transition to `details.live === false` means the origin\n * published #EXT-X-ENDLIST (clean end); otherwise the staleness watchdog\n * (checkEndStale) catches a frozen-but-200 manifest.\n */\n private trackEnd(data: LevelLoadedData): void {\n const d = data.details;\n if (!d) return;\n if (d.live === true) {\n this.sawLive = true;\n const sn = d.endSN ?? -1;\n if (sn !== this.lastEndSN) {\n this.lastEndSN = sn;\n this.lastAdvanceMs = Date.now();\n }\n return;\n }\n // VOD that was never live is not an \"end\" — only treat live→false as ENDLIST.\n if (this.sawLive) this.emitEnded('endlist');\n }\n\n private checkEndStale(): void {\n if (this.endedEmitted || !this.sawLive || this.lastAdvanceMs === 0) return;\n // Chrome's intensive throttling (muted tab hidden >5 min) batches ALL timers\n // to ~1/min — including hls.js's playlist-reload timer — so advance stamps\n // gap to ~60s on a healthy stream (this check wakes in the same batch as the\n // reload, before its XHR completes). Re-stamp instead of evaluating while\n // hidden: staleness detection resumes with a fresh window on refocus, and\n // the SSE live-state push still catches a real end immediately. Mirrors the\n // Demo Player guard (havik-ops#68).\n if (typeof document !== 'undefined' && document.hidden) {\n this.lastAdvanceMs = Date.now();\n return;\n }\n if (Date.now() - this.lastAdvanceMs > END_STALE_MS) this.emitEnded('stalled');\n }\n\n private emitEnded(reason: 'endlist' | 'stalled'): void {\n if (this.endedEmitted) return;\n this.endedEmitted = true;\n if (this.endWatchdog) {\n clearInterval(this.endWatchdog);\n this.endWatchdog = null;\n }\n this.deps.emit({ type: 'ended', reason });\n }\n\n stop(): void {\n if (this.endWatchdog) {\n clearInterval(this.endWatchdog);\n this.endWatchdog = null;\n }\n try {\n this.hls?.stopLoad();\n } catch {\n /* ignore */\n }\n try {\n this.deps.video.pause();\n } catch {\n /* ignore */\n }\n }\n\n destroy(): void {\n this.clearRecoveryReset();\n if (this.endWatchdog) {\n clearInterval(this.endWatchdog);\n this.endWatchdog = null;\n }\n if (this.onVisibility && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.onVisibility);\n this.onVisibility = null;\n }\n if (this.networkRetryTimer) {\n clearTimeout(this.networkRetryTimer);\n this.networkRetryTimer = null;\n }\n if (this.licenseRetryTimer) {\n clearTimeout(this.licenseRetryTimer);\n this.licenseRetryTimer = null;\n }\n if (this.hls) {\n try {\n this.hls.destroy();\n } catch (e) {\n // Surface a failed teardown rather than silently leaking the worker/EME session.\n console.warn('havik-player: hls.js destroy() threw during teardown', e);\n }\n this.hls = null;\n }\n }\n\n private clearRecoveryReset(): void {\n if (this.recoveryResetTimer) {\n clearTimeout(this.recoveryResetTimer);\n this.recoveryResetTimer = null;\n }\n }\n\n /**\n * A transient transport failure of the EME license (or service-certificate)\n * request — no response, timeout, 429, or 5xx — is recoverable: hls.js has no\n * license-retry policy and reports it as a fatal KEY_SYSTEM error, so we\n * rebuild the instance (a fresh key session re-issues the request), bounded\n * and backed off. A real auth/entitlement denial (401/403 or any other 4xx)\n * is NOT retried. Returns true iff a retry was scheduled.\n */\n private scheduleLicenseRetry(data: ErrorData): boolean {\n const requestFailure =\n data.details === Hls.ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED ||\n data.details === Hls.ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED;\n if (!requestFailure) return false;\n // 0 = network/CORS/timeout (the POST never got a response); 408/429/5xx are\n // transient server-side. Any other 4xx (esp. 401/403) is a real denial.\n const status = data.response?.code ?? 0;\n const transient = status === 0 || status === 408 || status === 429 || status >= 500;\n if (!transient || this.licenseRecoveries >= MAX_LICENSE_RECOVERIES) return false;\n this.licenseRecoveries += 1;\n const delay = Math.min(8000, 500 * 2 ** (this.licenseRecoveries - 1));\n this.licenseRetryTimer = setTimeout(() => {\n this.licenseRetryTimer = null;\n if (!this.hls) return;\n try {\n this.hls.destroy();\n } catch (e) {\n console.warn('havik-player: hls.js destroy() threw during license retry', e);\n }\n this.hls = null;\n this.startHls(); // fresh key session re-issues the license request\n }, delay);\n return true;\n }\n\n private onError(data: ErrorData): void {\n if (!data.fatal) return; // hls.js recovers non-fatal gaps internally\n const hls = this.hls;\n if (!hls) return;\n\n // A fatal error cancels any pending recovery-budget refund so a tight\n // recover-loop still trips the caps.\n this.clearRecoveryReset();\n\n // A transient transport failure of the license/cert request is recoverable\n // by rebuilding hls.js; a real auth/entitlement denial and other key-system\n // errors are not.\n if (data.type === Hls.ErrorTypes.KEY_SYSTEM_ERROR) {\n if (this.scheduleLicenseRetry(data)) return;\n this.deps.emit({ type: 'error', error: this.toError(data, 'DRM'), fatal: true });\n return;\n }\n if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {\n // Transient origin/segment/license network failure: restart the loader with\n // exponential backoff, but cap retries so a persistently-failing origin\n // eventually surfaces a fatal error instead of restarting forever.\n if (this.networkRecoveries < MAX_NETWORK_RECOVERIES) {\n this.networkRecoveries += 1;\n const delay = Math.min(8000, 1000 * 2 ** (this.networkRecoveries - 1));\n this.networkRetryTimer = setTimeout(() => {\n this.networkRetryTimer = null;\n if (this.hls) this.hls.startLoad();\n }, delay);\n return;\n }\n this.deps.emit({ type: 'error', error: this.toError(data, 'NETWORK'), fatal: true });\n return;\n }\n if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {\n if (this.mediaRecoveries < MAX_MEDIA_RECOVERIES) {\n this.mediaRecoveries += 1;\n hls.recoverMediaError();\n return;\n }\n this.deps.emit({ type: 'error', error: this.toError(data), fatal: true });\n return;\n }\n this.deps.emit({ type: 'error', error: this.toError(data), fatal: true });\n }\n\n private toError(data: ErrorData, kind = 'PLAYBACK'): PlaybackError {\n const status = data.response?.code ?? 0;\n const detail = data.details ?? data.type;\n const reason = data.reason ?? data.error?.message ?? '';\n const code =\n status === 401\n ? 'UNAUTHORIZED'\n : status === 403\n ? 'FORBIDDEN'\n : kind === 'DRM'\n ? 'FORBIDDEN'\n : 'INTERNAL';\n return new PlaybackError(\n code,\n status,\n `${kind} error: ${detail}${reason ? ` (${reason})` : ''}`,\n );\n }\n}\n","import { PlaybackError } from '../errors';\nimport {\n readDroppedFrames,\n type AudioTrackInfo,\n type PlaybackStats,\n type QualityLevel,\n type TextTrackInfo,\n} from '../stats';\nimport { type EngineDeps, type PlaybackEngine } from './types';\n\n// Non-standard `video.audioTracks` (Safari) — minimal structural shape.\ninterface NativeAudioTrack {\n enabled: boolean;\n label: string;\n language: string;\n}\ninterface NativeAudioTrackList {\n readonly length: number;\n [index: number]: NativeAudioTrack;\n}\n\n/** True when the browser plays HLS natively (Safari / iOS). */\nexport function nativeHlsSupported(video?: HTMLVideoElement): boolean {\n const el =\n video ?? (typeof document !== 'undefined' ? document.createElement('video') : undefined);\n return Boolean(el?.canPlayType('application/vnd.apple.mpegurl'));\n}\n\n/**\n * Native-HLS engine (Safari / iOS). Plays via `video.src`. NOTE: FairPlay EME\n * interception is NOT implemented in v1 — DRM playback only works where the OS\n * already trusts the license/cert origins. A full FairPlay (SPC/CKC) flow is a\n * tracked fast-follow (see drm/fairplay.ts).\n */\nexport class NativeHlsEngine implements PlaybackEngine {\n readonly name = 'native' as const;\n private readonly deps: EngineDeps;\n private onLoaded?: () => void;\n private onError?: () => void;\n\n constructor(deps: EngineDeps) {\n this.deps = deps;\n }\n\n async load(): Promise<void> {\n const { video, descriptor, emit } = this.deps;\n\n if (descriptor.drmEnabled) {\n if (!descriptor.drm?.fairplay) {\n // DRM required but no FairPlay material (e.g. a Widevine-only stream): the\n // native engine cannot decrypt it. Fail fast with a clear, typed error\n // instead of an opaque 'native HLS playback error' downstream.\n emit({\n type: 'error',\n error: new PlaybackError(\n 'INTERNAL',\n 0,\n 'native HLS engine cannot satisfy this DRM (Widevine requires the hls.js engine); use engine \"auto\"/\"hls\" on a Widevine-capable browser',\n ),\n fatal: true,\n });\n return;\n }\n // FairPlay present: best-effort native passthrough; surfaced as a warning.\n emit({\n type: 'error',\n error: new PlaybackError(\n 'INTERNAL',\n 0,\n 'FairPlay EME is not implemented in this SDK version; relying on native passthrough',\n ),\n fatal: false,\n });\n }\n\n this.onLoaded = () => emit({ type: 'ready' });\n this.onError = () =>\n emit({\n type: 'error',\n error: new PlaybackError('INTERNAL', 0, 'native HLS playback error'),\n fatal: true,\n });\n video.addEventListener('loadedmetadata', this.onLoaded, { once: true });\n video.addEventListener('error', this.onError);\n video.src = descriptor.manifestUrl;\n video.load();\n }\n\n setLicenseUrl(_url: string): void {\n // No EME management on the native path.\n }\n\n // Native HLS handles ABR internally and does not expose a rendition ladder.\n getQualityLevels(): QualityLevel[] {\n return [];\n }\n getCurrentQuality(): number {\n return -1;\n }\n setQuality(_index: number): void {}\n setMaxBitrate(_bitrate: number | null): void {}\n\n getAudioTracks(): AudioTrackInfo[] {\n const list = (this.deps.video as HTMLVideoElement & { audioTracks?: NativeAudioTrackList })\n .audioTracks;\n if (!list) return [];\n return Array.from({ length: list.length }, (_v, id) => {\n const t = list[id];\n return {\n id,\n name: t.label || t.language || `Audio ${id + 1}`,\n lang: t.language || undefined,\n default: t.enabled,\n };\n });\n }\n setAudioTrack(id: number): void {\n const list = (this.deps.video as HTMLVideoElement & { audioTracks?: NativeAudioTrackList })\n .audioTracks;\n if (!list) return;\n for (let i = 0; i < list.length; i += 1) list[i].enabled = i === id;\n }\n\n getTextTracks(): TextTrackInfo[] {\n const list = this.deps.video.textTracks;\n return Array.from({ length: list.length }, (_v, id) => {\n const t = list[id];\n return { id, name: t.label || t.language || `Text ${id + 1}`, lang: t.language || undefined };\n });\n }\n setTextTrack(id: number): void {\n const list = this.deps.video.textTracks;\n for (let i = 0; i < list.length; i += 1) list[i].mode = i === id ? 'showing' : 'disabled';\n }\n\n seekToLive(): void {\n const v = this.deps.video;\n if (v.seekable.length > 0) v.currentTime = v.seekable.end(v.seekable.length - 1);\n }\n\n getStats(): PlaybackStats {\n const v = this.deps.video;\n const stats: PlaybackStats = { droppedFrames: readDroppedFrames(v) };\n if (v.duration === Infinity) {\n stats.isLive = true;\n if (v.seekable.length > 0) {\n stats.atLiveEdge = v.currentTime >= v.seekable.end(v.seekable.length - 1) - 2;\n }\n }\n return stats;\n }\n\n stop(): void {\n // Native HLS: pause to halt fetching; keep src so the last frame stays up.\n try {\n this.deps.video.pause();\n } catch {\n // ignore\n }\n }\n\n destroy(): void {\n const { video } = this.deps;\n if (this.onLoaded) video.removeEventListener('loadedmetadata', this.onLoaded);\n if (this.onError) video.removeEventListener('error', this.onError);\n try {\n video.pause();\n } catch {\n // ignore\n }\n video.removeAttribute('src');\n try {\n video.load();\n } catch {\n // ignore\n }\n }\n}\n","// Player theming. The control bar is styled entirely from CSS custom properties\n// (--havik-*), so a host can re-skin it by passing a `theme` to createPlayer OR\n// by overriding the variables in their own stylesheet targeting `.havik-player`.\n\nexport interface HavikTheme {\n /** Primary accent (play button, progress fill, live dot, focus). */\n accent: string;\n /** Text/icon color that sits ON the accent (e.g. on the play button). */\n accentText: string;\n /** Letterbox / behind-video background. */\n background: string;\n /** Control bar + menu surface color. */\n surface: string;\n /** Hover/active surface color. */\n surfaceMuted: string;\n /** Primary text/icon color. */\n text: string;\n /** Secondary text color (timestamps, inactive). */\n textMuted: string;\n /** Hairline borders. */\n border: string;\n /** Corner radius for buttons/menus. */\n radius: string;\n /** Font stack for control-bar text. */\n fontFamily: string;\n /** Optional logo shown as a small corner watermark (URL). */\n logoUrl?: string;\n}\n\n/**\n * Default skin — Oddin.gg brand identity: gold accent on dark navy, white text.\n */\nexport const ODDIN_THEME: HavikTheme = {\n accent: '#E1B600',\n accentText: '#1B1C23',\n background: '#0e0f13',\n surface: '#1B1C23',\n surfaceMuted: '#2a2c36',\n text: '#ffffff',\n textMuted: '#9aa0ad',\n border: 'rgba(255, 255, 255, 0.1)',\n radius: '8px',\n fontFamily:\n \"ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif\",\n};\n\nconst VAR: Record<keyof Omit<HavikTheme, 'logoUrl'>, string> = {\n accent: '--havik-accent',\n accentText: '--havik-accent-text',\n background: '--havik-bg',\n surface: '--havik-surface',\n surfaceMuted: '--havik-surface-muted',\n text: '--havik-text',\n textMuted: '--havik-text-muted',\n border: '--havik-border',\n radius: '--havik-radius',\n fontFamily: '--havik-font',\n};\n\n/** Apply a (possibly partial) theme to a player root element as CSS variables. */\nexport function applyTheme(root: HTMLElement, theme?: Partial<HavikTheme>): void {\n const merged: HavikTheme = { ...ODDIN_THEME, ...theme };\n for (const key of Object.keys(VAR) as Array<keyof typeof VAR>) {\n root.style.setProperty(VAR[key], merged[key]);\n }\n if (merged.logoUrl) root.style.setProperty('--havik-logo', `url(\"${merged.logoUrl}\")`);\n}\n","// Control-bar styles, injected once. Every color/radius/font reads from a\n// --havik-* CSS variable (set by applyTheme), so the whole skin is themeable.\n\nconst STYLE_ID = 'havik-player-styles';\n\nconst CSS = `\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`;\n\nexport function injectStyles(doc: Document = document): void {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n doc.head.appendChild(style);\n}\n","// Compact inline SVG icons for the control bar (currentColor-filled, 24x24).\nexport const ICONS = {\n play: '<path d=\"M8 5v14l11-7z\"/>',\n pause: '<path d=\"M6 5h4v14H6zM14 5h4v14h-4z\"/>',\n volumeHigh:\n '<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\"/>',\n volumeMute:\n '<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\"/>',\n 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\"/>',\n settings:\n '<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\"/>',\n 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\"/>',\n fullscreen:\n '<path d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\"/>',\n fullscreenExit:\n '<path d=\"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z\"/>',\n liveDot: '<circle cx=\"12\" cy=\"12\" r=\"6\"/>',\n retry:\n '<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\"/>',\n} as const;\n\nexport type IconName = keyof typeof ICONS;\n\nexport function svgIcon(name: IconName): string {\n return `<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\" fill=\"currentColor\">${ICONS[name]}</svg>`;\n}\n","// A full, skinnable control bar rendered in vanilla DOM over a managed Player.\n// Everything visual is driven by --havik-* CSS variables (see theme.ts), so a\n// customer re-skins it purely with a `theme` object or their own stylesheet.\n//\n// It speaks only the public Player surface (play/pause/setMuted/…/on), so it has\n// no privileged access to engine internals and could equally wrap a BYO player.\n\nimport { injectStyles } from './styles';\nimport { svgIcon, type IconName } from './icons';\nimport type { Player } from '../managed';\nimport type { AudioTrackInfo, PlayerState, QualityLevel, TextTrackInfo } from '../core/stats';\nimport type { PlaybackError } from '../core/errors';\n\nexport interface ControlBarOptions {\n /** The `.havik-player` container that wraps the <video>. */\n root: HTMLElement;\n video: HTMLVideoElement;\n player: Player;\n /** Optional corner watermark + status-card logo (logo URL). */\n logoUrl?: string;\n /** Milliseconds of pointer inactivity before the bar auto-hides. Default 3000. */\n autoHideMs?: number;\n /**\n * Branded full-bleed cards for the stopped/pre-play states (ended, fatal\n * error, pre-play, armed-waiting) instead of a frozen frame + dead play\n * button. Default true. Set false if you render your own end/error UI from\n * the player events.\n */\n statusOverlays?: boolean;\n /** Message on the end-of-stream card. Default \"This stream has ended\". */\n endedMessage?: string;\n /** Message on the fatal-error card. Default: the error's own message. */\n errorMessage?: string;\n}\n\nexport interface ControlBarHandle {\n destroy(): void;\n}\n\ntype MenuKind = 'quality' | 'audio' | 'cc';\n\nconst HIDE_AFTER_MS = 3000;\n\n// Terminal error codes get NO Retry button — re-resolving returns the same\n// outcome (stream gone / unknown URN / bad key / malformed URN). Everything else\n// (network / media / DRM / internal / transient) keeps Retry, since retry()\n// re-resolves AND re-attaches and can recover — including refreshing an expired\n// signed DRM license URL.\nconst NON_RETRYABLE_ERROR_CODES = new Set(['GONE', 'NOT_FOUND', 'INVALID_URN', 'UNAUTHORIZED']);\n\n/** Mount the Oddin control bar onto a managed player. Returns a teardown handle. */\nexport function mountControlBar(opts: ControlBarOptions): ControlBarHandle {\n return new ControlBar(opts);\n}\n\nclass ControlBar implements ControlBarHandle {\n private readonly root: HTMLElement;\n private readonly video: HTMLVideoElement;\n private readonly player: Player;\n private readonly doc: Document;\n private readonly autoHideMs: number;\n\n // Elements\n private readonly overlay: HTMLElement;\n private readonly center: HTMLButtonElement;\n private readonly spinner: HTMLElement;\n private readonly bar: HTMLElement;\n private readonly gradient: HTMLElement;\n private readonly seek: HTMLElement;\n private readonly seekBuffered: HTMLElement;\n private readonly seekPlayed: HTMLElement;\n private readonly seekThumb: HTMLElement;\n private readonly playBtn: HTMLButtonElement;\n private readonly muteBtn: HTMLButtonElement;\n private readonly volSlider: HTMLInputElement;\n private readonly live: HTMLElement;\n private readonly liveGo: HTMLButtonElement;\n private readonly time: HTMLElement;\n private readonly ccBtn: HTMLButtonElement;\n private readonly audioBtn: HTMLButtonElement;\n private readonly qualityBtn: HTMLButtonElement;\n private readonly pipBtn: HTMLButtonElement;\n private readonly fsBtn: HTMLButtonElement;\n private readonly menu: HTMLElement;\n\n // Status card (ended / error / pre-play / waiting) — see syncStatus().\n private statusBox: HTMLElement | null = null;\n private statusKind: 'ended' | 'error' | 'preplay' | 'waiting' | null = null;\n private hasPlayed = false;\n private lastError: PlaybackError | null = null;\n private readonly logoUrl?: string;\n private readonly statusOverlays: boolean;\n private readonly endedMessage: string;\n private readonly errorMessage?: string;\n\n private hideTimer?: ReturnType<typeof setTimeout>;\n private openMenu: MenuKind | null = null;\n private scrubbing = false;\n private readonly offFns: Array<() => void> = [];\n\n constructor(o: ControlBarOptions) {\n this.root = o.root;\n this.video = o.video;\n this.player = o.player;\n this.doc = o.root.ownerDocument ?? document;\n this.autoHideMs = o.autoHideMs ?? HIDE_AFTER_MS;\n this.logoUrl = o.logoUrl;\n this.statusOverlays = o.statusOverlays !== false;\n this.endedMessage = o.endedMessage ?? 'This stream has ended';\n this.errorMessage = o.errorMessage;\n injectStyles(this.doc);\n\n this.root.classList.add('havik-player');\n if (!this.root.hasAttribute('tabindex')) this.root.tabIndex = 0;\n this.root.setAttribute('role', 'region');\n this.root.setAttribute('aria-label', 'Video player');\n\n const el = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n cls: string,\n attrs?: Record<string, string>,\n ): HTMLElementTagNameMap[K] => {\n const node = this.doc.createElement(tag);\n node.className = cls;\n if (attrs) for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v);\n return node;\n };\n const btn = (cls: string, icon: IconName, label: string): HTMLButtonElement => {\n const b = el('button', cls, { type: 'button', 'aria-label': label, title: label });\n b.innerHTML = svgIcon(icon);\n return b;\n };\n\n // Overlay (click = play/pause) + big center button + spinner.\n this.overlay = el('div', 'havik-overlay');\n this.center = el('button', 'havik-center', { type: 'button', 'aria-label': 'Play' });\n this.center.innerHTML = svgIcon('play');\n this.spinner = el('div', 'havik-spinner');\n this.spinner.style.display = 'none';\n this.overlay.append(this.center, this.spinner);\n\n this.gradient = el('div', 'havik-gradient');\n\n // Seek bar.\n this.bar = el('div', 'havik-bar');\n this.seek = el('div', 'havik-seek', {\n role: 'slider',\n 'aria-label': 'Seek',\n 'aria-valuemin': '0',\n 'aria-valuemax': '100',\n tabindex: '0',\n });\n const track = el('div', 'havik-seek__track');\n this.seekBuffered = el('div', 'havik-seek__buffered');\n this.seekPlayed = el('div', 'havik-seek__played');\n this.seekThumb = el('div', 'havik-seek__thumb');\n track.append(this.seekBuffered, this.seekPlayed, this.seekThumb);\n this.seek.append(track);\n\n // Controls row.\n const controls = el('div', 'havik-controls');\n this.playBtn = btn('havik-btn', 'play', 'Play');\n this.muteBtn = btn('havik-btn', 'volumeHigh', 'Mute');\n this.volSlider = el('input', 'havik-vol__slider', {\n type: 'range',\n min: '0',\n max: '1',\n step: '0.05',\n 'aria-label': 'Volume',\n });\n const vol = el('div', 'havik-vol');\n vol.append(this.muteBtn, this.volSlider);\n\n this.live = el('div', 'havik-live');\n this.live.innerHTML = svgIcon('liveDot');\n const liveLabel = this.doc.createElement('span');\n liveLabel.textContent = 'LIVE';\n this.liveGo = el('button', 'havik-live__go', { type: 'button' });\n this.liveGo.textContent = 'Go live';\n this.live.append(liveLabel, this.liveGo);\n this.time = el('div', 'havik-time');\n this.time.textContent = '0:00';\n this.time.style.display = 'none';\n\n const spacer = el('div', 'havik-spacer');\n this.ccBtn = btn('havik-btn', 'cc', 'Subtitles');\n this.audioBtn = btn('havik-btn', 'settings', 'Audio track');\n this.qualityBtn = btn('havik-btn', 'settings', 'Quality');\n this.pipBtn = btn('havik-btn', 'pip', 'Picture in picture');\n this.fsBtn = btn('havik-btn', 'fullscreen', 'Fullscreen');\n this.ccBtn.style.display = 'none';\n this.audioBtn.style.display = 'none';\n this.qualityBtn.style.display = 'none';\n if (!this.pipSupported()) this.pipBtn.style.display = 'none';\n\n controls.append(\n this.playBtn,\n vol,\n this.live,\n this.time,\n spacer,\n this.ccBtn,\n this.audioBtn,\n this.qualityBtn,\n this.pipBtn,\n this.fsBtn,\n );\n this.bar.append(this.seek, controls);\n\n this.menu = el('div', 'havik-menu', { role: 'menu' });\n this.menu.hidden = true;\n\n this.root.append(this.gradient, this.overlay, this.bar, this.menu);\n if (o.logoUrl) {\n const wm = el('div', 'havik-watermark');\n wm.style.backgroundImage = `url(\"${o.logoUrl}\")`;\n this.root.append(wm);\n }\n\n this.wire();\n this.syncPlayPause();\n this.syncVolume();\n this.scheduleHide();\n }\n\n // ---- wiring -------------------------------------------------------------\n\n private on<T extends EventTarget>(\n target: T,\n type: string,\n handler: EventListenerOrEventListenerObject,\n options?: AddEventListenerOptions,\n ): void {\n target.addEventListener(type, handler, options);\n this.offFns.push(() => target.removeEventListener(type, handler, options));\n }\n\n private wire(): void {\n const v = this.video;\n\n // Buttons → player.\n this.on(this.playBtn, 'click', () => this.togglePlay());\n this.on(this.center, 'click', () => this.togglePlay());\n this.on(this.overlay, 'click', (e) => {\n if (e.target === this.overlay) this.togglePlay();\n });\n this.on(this.muteBtn, 'click', () => this.player.setMuted(!v.muted));\n this.on(this.volSlider, 'input', () => {\n const value = Number(this.volSlider.value);\n this.player.setVolume(value);\n if (value > 0 && v.muted) this.player.setMuted(false);\n });\n this.on(this.liveGo, 'click', () => this.player.seekToLive());\n this.on(this.pipBtn, 'click', () => void this.togglePip());\n this.on(this.fsBtn, 'click', () => void this.toggleFullscreen());\n this.on(this.ccBtn, 'click', () => this.toggleMenu('cc'));\n this.on(this.audioBtn, 'click', () => this.toggleMenu('audio'));\n this.on(this.qualityBtn, 'click', () => this.toggleMenu('quality'));\n\n // Seek.\n this.on(this.seek, 'pointerdown', (e) => this.onSeekDown(e as PointerEvent));\n this.on(this.seek, 'keydown', (e) => this.onSeekKey(e as KeyboardEvent));\n\n // Video element → UI.\n this.on(v, 'play', () => this.syncPlayPause());\n this.on(v, 'pause', () => this.syncPlayPause());\n this.on(v, 'volumechange', () => this.syncVolume());\n this.on(v, 'timeupdate', () => this.syncProgress());\n this.on(v, 'progress', () => this.syncProgress());\n this.on(v, 'durationchange', () => this.syncProgress());\n\n // Player events → UI.\n this.offFns.push(this.player.on('statechange', () => this.syncState()));\n this.offFns.push(this.player.on('ready', () => this.refreshTracks()));\n this.offFns.push(this.player.on('qualitychange', () => this.onTracksChanged()));\n this.offFns.push(this.player.on('audiotrackchange', () => this.onTracksChanged()));\n this.offFns.push(this.player.on('texttrackchange', () => this.onTracksChanged()));\n this.offFns.push(this.player.on('pipchange', (active) => this.syncPip(active)));\n this.offFns.push(this.player.on('stats', () => this.syncLive()));\n this.offFns.push(\n this.player.on('error', (err) => {\n this.lastError = err;\n this.statusKind = null; // force the error card to re-render with this error\n this.syncState();\n }),\n );\n\n // Auto-hide + keyboard.\n this.on(this.root, 'pointermove', () => this.poke());\n this.on(this.root, 'pointerleave', () => this.scheduleHide(0));\n this.on(this.root, 'focusin', () => this.poke());\n this.on(this.root, 'keydown', (e) => this.onKey(e as KeyboardEvent));\n this.on(this.doc, 'pointerdown', (e) => this.onDocPointer(e), { capture: true });\n this.on(this.doc, 'fullscreenchange', () => this.syncFullscreen());\n\n this.syncState();\n }\n\n // ---- play / pause -------------------------------------------------------\n\n private togglePlay(): void {\n if (this.video.paused || this.video.ended) void this.player.play();\n else this.player.pause();\n }\n\n private syncPlayPause(): void {\n const playing = !this.video.paused && !this.video.ended;\n this.setIcon(this.playBtn, playing ? 'pause' : 'play');\n this.playBtn.setAttribute('aria-label', playing ? 'Pause' : 'Play');\n this.setIcon(this.center, playing ? 'pause' : 'play');\n // A status card (pre-play / ended / error) owns the CTA, so keep the\n // mid-playback center toggle hidden while one is shown.\n this.center.classList.toggle('havik-gone', playing || !!this.statusBox);\n if (playing) this.scheduleHide();\n else this.show();\n }\n\n private syncState(): void {\n const state = this.player.state;\n if (state === 'playing' || state === 'buffering') this.hasPlayed = true;\n this.syncStatus(state);\n // Spinner only while genuinely buffering with no card up (the card owns the\n // visual otherwise); the center play/pause button is hidden whenever a card\n // is shown (the card carries its own CTA) or while busy.\n const busy = state === 'loading' || state === 'buffering';\n this.spinner.style.display = busy && !this.statusBox ? '' : 'none';\n if (busy || this.statusBox) this.center.classList.add('havik-gone');\n this.syncLive();\n }\n\n /** Map the player state to a branded status card (ended / fatal error /\n * pre-play / armed-waiting), or none. Disabled via `statusOverlays: false`. */\n private syncStatus(state: PlayerState): void {\n let kind: ControlBar['statusKind'] = null;\n if (this.statusOverlays) {\n if (state === 'ended') kind = 'ended';\n else if (state === 'error') kind = 'error';\n else if (state === 'waiting') kind = 'waiting';\n else if (!this.hasPlayed && (state === 'idle' || state === 'paused')) kind = 'preplay';\n }\n this.renderStatus(kind);\n }\n\n /** Build (or tear down) the status card for `kind`. No-op when unchanged so\n * repeated statechange events don't rebuild/flicker the DOM. */\n private renderStatus(kind: ControlBar['statusKind']): void {\n if (kind === this.statusKind) return;\n this.statusKind = kind;\n this.statusBox?.remove();\n this.statusBox = null;\n if (!kind) return;\n\n const box = this.doc.createElement('div');\n box.className = `havik-status havik-status--${kind}`;\n if (this.logoUrl) {\n const logo = this.doc.createElement('div');\n logo.className = 'havik-status__logo';\n box.append(logo);\n }\n\n if (kind === 'preplay') {\n // Poster shows through a light scrim; a single branded play CTA.\n const play = this.doc.createElement('button');\n play.type = 'button';\n play.className = 'havik-status__btn havik-status__btn--play';\n play.setAttribute('aria-label', 'Play');\n play.innerHTML = svgIcon('play');\n play.addEventListener('click', () => void this.player.play());\n box.append(play);\n } else {\n const msg = this.doc.createElement('div');\n msg.className = 'havik-status__msg';\n msg.textContent =\n kind === 'ended'\n ? this.endedMessage\n : kind === 'waiting'\n ? 'Starting soon…'\n : (this.errorMessage ?? this.lastError?.message ?? 'Playback unavailable');\n box.append(msg);\n // Retry only when re-resolving can plausibly succeed (terminal errors\n // would just fail again).\n if (\n kind === 'error' &&\n this.lastError &&\n !NON_RETRYABLE_ERROR_CODES.has(this.lastError.code)\n ) {\n const retry = this.doc.createElement('button');\n retry.type = 'button';\n retry.className = 'havik-status__btn';\n retry.innerHTML = `${svgIcon('retry')}<span>Retry</span>`;\n retry.addEventListener('click', () => void this.player.retry());\n box.append(retry);\n }\n }\n\n this.root.append(box);\n this.statusBox = box;\n this.show();\n }\n\n // ---- volume -------------------------------------------------------------\n\n private syncVolume(): void {\n const v = this.video;\n const muted = v.muted || v.volume === 0;\n this.setIcon(this.muteBtn, muted ? 'volumeMute' : 'volumeHigh');\n this.muteBtn.setAttribute('aria-label', muted ? 'Unmute' : 'Mute');\n this.volSlider.value = String(muted ? 0 : v.volume);\n }\n\n // ---- progress / live ----------------------------------------------------\n\n private seekableRange(): { start: number; end: number } | null {\n const v = this.video;\n if (v.seekable && v.seekable.length > 0) {\n const start = v.seekable.start(0);\n const end = v.seekable.end(v.seekable.length - 1);\n if (end > start) return { start, end };\n }\n if (Number.isFinite(v.duration) && v.duration > 0) return { start: 0, end: v.duration };\n return null;\n }\n\n private syncProgress(): void {\n if (this.scrubbing) return;\n const range = this.seekableRange();\n if (!range) {\n this.seekPlayed.style.width = '100%';\n return;\n }\n const span = range.end - range.start;\n const played = Math.min(1, Math.max(0, (this.video.currentTime - range.start) / span));\n this.seekPlayed.style.width = `${(played * 100).toFixed(2)}%`;\n this.seekThumb.style.left = `${(played * 100).toFixed(2)}%`;\n this.seek.setAttribute('aria-valuenow', String(Math.round(played * 100)));\n\n const b = this.video.buffered;\n if (b && b.length > 0) {\n const end = b.end(b.length - 1);\n const buf = Math.min(1, Math.max(0, (end - range.start) / span));\n this.seekBuffered.style.width = `${(buf * 100).toFixed(2)}%`;\n }\n\n // Time readout only matters for non-live (finite, non-DVR) content.\n const stats = this.player.getStats();\n if (!stats.isLive && Number.isFinite(this.video.duration)) {\n this.time.textContent = `${clock(this.video.currentTime)} / ${clock(this.video.duration)}`;\n }\n }\n\n private syncLive(): void {\n const stats = this.player.getStats();\n const isLive = stats.isLive !== false; // default to live (these are live streams)\n this.live.style.display = isLive ? '' : 'none';\n this.time.style.display = isLive ? 'none' : '';\n this.live.classList.toggle('is-live', stats.atLiveEdge !== false && isLive);\n }\n\n // ---- seeking ------------------------------------------------------------\n\n private onSeekDown(e: PointerEvent): void {\n const range = this.seekableRange();\n if (!range) return;\n e.preventDefault();\n this.scrubbing = true;\n const move = (ev: PointerEvent) => this.seekToPointer(ev, range);\n const up = () => {\n this.scrubbing = false;\n this.doc.removeEventListener('pointermove', move);\n this.doc.removeEventListener('pointerup', up);\n };\n this.doc.addEventListener('pointermove', move);\n this.doc.addEventListener('pointerup', up);\n this.seekToPointer(e, range);\n }\n\n private seekToPointer(e: PointerEvent, range: { start: number; end: number }): void {\n const rect = this.seek.getBoundingClientRect();\n const pct = rect.width > 0 ? Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width)) : 0;\n this.seekPlayed.style.width = `${(pct * 100).toFixed(2)}%`;\n this.seekThumb.style.left = `${(pct * 100).toFixed(2)}%`;\n this.video.currentTime = range.start + pct * (range.end - range.start);\n }\n\n private onSeekKey(e: KeyboardEvent): void {\n if (e.key === 'ArrowLeft') {\n this.nudge(-5);\n e.preventDefault();\n } else if (e.key === 'ArrowRight') {\n this.nudge(5);\n e.preventDefault();\n }\n }\n\n private nudge(seconds: number): void {\n const range = this.seekableRange();\n if (!range) return;\n this.video.currentTime = Math.min(\n range.end,\n Math.max(range.start, this.video.currentTime + seconds),\n );\n }\n\n // ---- menus --------------------------------------------------------------\n\n private toggleMenu(kind: MenuKind): void {\n if (this.openMenu === kind) {\n this.closeMenu();\n return;\n }\n this.openMenu = kind;\n this.renderMenu(kind);\n this.menu.hidden = false;\n this.show();\n }\n\n private closeMenu(): void {\n this.openMenu = null;\n this.menu.hidden = true;\n this.menu.replaceChildren();\n }\n\n private renderMenu(kind: MenuKind): void {\n if (kind === 'quality') this.renderQualityMenu();\n else if (kind === 'audio') this.renderAudioMenu();\n else this.renderCcMenu();\n }\n\n private menuItem(label: string, checked: boolean, onSelect: () => void): HTMLButtonElement {\n const item = this.doc.createElement('button');\n item.type = 'button';\n item.className = 'havik-menu__item';\n item.setAttribute('role', 'menuitemradio');\n item.setAttribute('aria-checked', checked ? 'true' : 'false');\n const text = this.doc.createElement('span');\n text.textContent = label;\n item.append(text);\n item.addEventListener('click', () => {\n onSelect();\n this.closeMenu();\n });\n return item;\n }\n\n private renderQualityMenu(): void {\n const levels = this.player.getQualityLevels();\n const current = this.player.getCurrentQuality();\n const title = this.doc.createElement('div');\n title.className = 'havik-menu__title';\n title.textContent = 'Quality';\n this.menu.replaceChildren(\n title,\n this.menuItem('Auto', current === -1, () => this.player.setQuality('auto')),\n );\n for (const lvl of levels) {\n this.menu.append(\n this.menuItem(qualityLabel(lvl), current === lvl.index, () =>\n this.player.setQuality(lvl.index),\n ),\n );\n }\n }\n\n private renderAudioMenu(): void {\n const tracks = this.player.getAudioTracks();\n const title = this.doc.createElement('div');\n title.className = 'havik-menu__title';\n title.textContent = 'Audio';\n this.menu.replaceChildren(title);\n for (const t of tracks) {\n this.menu.append(\n this.menuItem(audioLabel(t), t.default, () => this.player.setAudioTrack(t.id)),\n );\n }\n }\n\n private renderCcMenu(): void {\n const tracks = this.player.getTextTracks();\n const title = this.doc.createElement('div');\n title.className = 'havik-menu__title';\n title.textContent = 'Subtitles';\n this.menu.replaceChildren(\n title,\n this.menuItem('Off', false, () => this.player.setTextTrack(-1)),\n );\n for (const t of tracks) {\n this.menu.append(this.menuItem(textLabel(t), false, () => this.player.setTextTrack(t.id)));\n }\n }\n\n private onTracksChanged(): void {\n if (this.openMenu) this.renderMenu(this.openMenu);\n }\n\n private refreshTracks(): void {\n const quality = this.player.getQualityLevels();\n const audio = this.player.getAudioTracks();\n const text = this.player.getTextTracks();\n this.qualityBtn.style.display = quality.length > 1 ? '' : 'none';\n this.audioBtn.style.display = audio.length > 1 ? '' : 'none';\n this.ccBtn.style.display = text.length > 0 ? '' : 'none';\n this.onTracksChanged();\n }\n\n // ---- pip / fullscreen ---------------------------------------------------\n\n private pipSupported(): boolean {\n return (\n 'pictureInPictureEnabled' in this.doc &&\n typeof (this.video as unknown as { requestPictureInPicture?: unknown })\n .requestPictureInPicture === 'function'\n );\n }\n\n private async togglePip(): Promise<void> {\n const doc = this.doc as Document & { pictureInPictureElement?: Element | null };\n if (doc.pictureInPictureElement === this.video) await this.player.exitPictureInPicture();\n else await this.player.enterPictureInPicture();\n }\n\n private syncPip(active: boolean): void {\n this.pipBtn.setAttribute('aria-pressed', active ? 'true' : 'false');\n }\n\n private async toggleFullscreen(): Promise<void> {\n const root = this.root as HTMLElement & { requestFullscreen?: () => Promise<void> };\n const doc = this.doc as Document & {\n fullscreenElement?: Element | null;\n exitFullscreen?: () => Promise<void>;\n };\n try {\n if (doc.fullscreenElement) await doc.exitFullscreen?.();\n else await root.requestFullscreen?.();\n } catch {\n // fullscreen can be blocked by policy; ignore\n }\n }\n\n private syncFullscreen(): void {\n const doc = this.doc as Document & { fullscreenElement?: Element | null };\n const full = doc.fullscreenElement === this.root;\n this.setIcon(this.fsBtn, full ? 'fullscreenExit' : 'fullscreen');\n this.fsBtn.setAttribute('aria-label', full ? 'Exit fullscreen' : 'Fullscreen');\n }\n\n // ---- auto-hide ----------------------------------------------------------\n\n private poke(): void {\n this.show();\n this.scheduleHide();\n }\n\n private show(): void {\n this.root.classList.remove('havik-hide');\n }\n\n private scheduleHide(delay = this.autoHideMs): void {\n if (this.hideTimer) clearTimeout(this.hideTimer);\n this.hideTimer = setTimeout(() => {\n const playing = !this.video.paused && !this.video.ended;\n if (playing && !this.openMenu && !this.statusBox) this.root.classList.add('havik-hide');\n }, delay);\n }\n\n private onDocPointer(e: Event): void {\n if (this.openMenu && !this.menu.contains(e.target as Node)) {\n // a click on the toggle button is handled by its own handler\n const onToggle = [this.qualityBtn, this.audioBtn, this.ccBtn].some((b) =>\n b.contains(e.target as Node),\n );\n if (!onToggle) this.closeMenu();\n }\n }\n\n // ---- keyboard -----------------------------------------------------------\n\n private onKey(e: KeyboardEvent): void {\n const target = e.target as HTMLElement | null;\n if (target && target !== this.root && target !== this.seek && target.tagName === 'INPUT')\n return;\n switch (e.key) {\n case ' ':\n case 'k':\n this.togglePlay();\n break;\n case 'm':\n this.player.setMuted(!this.video.muted);\n break;\n case 'f':\n void this.toggleFullscreen();\n break;\n case 'c':\n if (this.ccBtn.style.display !== 'none') this.toggleMenu('cc');\n break;\n case 'l':\n this.player.seekToLive();\n break;\n case 'ArrowUp':\n this.player.setVolume(Math.min(1, this.video.volume + 0.1));\n break;\n case 'ArrowDown':\n this.player.setVolume(Math.max(0, this.video.volume - 0.1));\n break;\n case 'ArrowLeft':\n this.nudge(-5);\n break;\n case 'ArrowRight':\n this.nudge(5);\n break;\n default:\n return;\n }\n this.poke();\n e.preventDefault();\n }\n\n // ---- helpers ------------------------------------------------------------\n\n private setIcon(btnEl: HTMLElement, icon: IconName): void {\n btnEl.innerHTML = svgIcon(icon);\n }\n\n destroy(): void {\n if (this.hideTimer) clearTimeout(this.hideTimer);\n for (const off of this.offFns) {\n try {\n off();\n } catch {\n // best effort\n }\n }\n this.offFns.length = 0;\n this.closeMenu();\n this.statusBox?.remove();\n for (const node of [this.overlay, this.gradient, this.bar, this.menu]) node.remove();\n this.root.classList.remove('havik-player', 'havik-hide');\n }\n}\n\nfunction clock(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;\n const s = Math.floor(seconds % 60);\n const m = Math.floor((seconds / 60) % 60);\n const h = Math.floor(seconds / 3600);\n const mm = h > 0 ? String(m).padStart(2, '0') : String(m);\n const ss = String(s).padStart(2, '0');\n return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;\n}\n\nfunction qualityLabel(lvl: QualityLevel): string {\n if (lvl.height) return `${lvl.height}p`;\n return `${Math.round(lvl.bitrate / 1000)} kbps`;\n}\n\nfunction audioLabel(t: AudioTrackInfo): string {\n return t.name || t.lang || `Track ${t.id}`;\n}\n\nfunction textLabel(t: TextTrackInfo): string {\n return t.name || t.lang || `Track ${t.id}`;\n}\n","import { type HlsConfig } from 'hls.js';\nimport { isWaitable, PlaybackError } from '../core/errors';\nimport { resolvePlaybackOnce, resolveStream, type ResolveOptions } from '../core/resolve';\nimport { HlsEngine, hlsSupported } from '../core/engine/hls';\nimport { NativeHlsEngine, nativeHlsSupported } from '../core/engine/native';\nimport { type EngineEvent, type PlaybackEngine } from '../core/engine/types';\nimport {\n attachMediaStateListeners,\n EMPTY_STATS,\n type AudioTrackInfo,\n type PlaybackStats,\n type PlayerState,\n type QualityLevel,\n type TextTrackInfo,\n} from '../core/stats';\nimport { getDeviceId } from '../core/drm/deviceId';\nimport {\n resolveCredential,\n type Credential,\n type CredentialSource,\n type StreamDescriptor,\n} from '../core/types';\nimport { type WaitForLive, type WaitForLiveOptions, type WaitState } from '../core/waitForLive';\nimport {\n subscribeLiveState,\n deriveEventsBaseUrl,\n type LiveStateSubscription,\n} from '../core/events';\nimport { applyTheme, type HavikTheme } from '../ui/theme';\nimport { mountControlBar, type ControlBarHandle } from '../ui/controls';\n\n// Armed-phase SSE tuning. While SSE is healthy the player does NOT poll\n// /v1/playback — it waits for the pushed `live`. These bound the fallback:\nconst SSE_ARM_CONNECT_GRACE_MS = 6_000; // no SSE frame by now → start polling\nconst SSE_ARM_LIVENESS_MS = 45_000; // heartbeat is 20s; 2 missed → SSE stale → poll\nconst SSE_ARM_RESOLVE_JITTER_MS = 2_000; // stagger the go-live resolve so 1000s of armed viewers don't resolve in lockstep\nconst LIVE_STATE_WARN_AFTER = 3; // consecutive no-data failures before the one-time degraded-end-detection warning\n\nexport interface CreatePlayerOptions {\n /** The <video> element the SDK will own. */\n video: HTMLVideoElement;\n baseUrl: string;\n matchUrn: string;\n credential: CredentialSource;\n /** Start playback once ready. Default: true. */\n autoplay?: boolean;\n /** Mute the element (required for reliable autoplay with sound policies). Default: false. */\n muted?: boolean;\n /** Arm on an upcoming match and auto-attach at go-live. See WaitForLive. */\n waitForLive?: WaitForLive;\n /** 'auto' lets hls.js decide from the manifest (safe default). Default: 'auto'. */\n lowLatency?: 'auto' | boolean;\n /** Poster image shown before playback starts. */\n poster?: string;\n /** Initial rendition index (-1 = auto, the default). */\n startLevel?: number;\n /** Cap ABR to renditions at/below this bitrate (bps). */\n maxBitrate?: number;\n /**\n * Target live-edge latency in seconds (maps to hls.js liveSyncDuration).\n * Default: 3 (a balance the engine ships — close to live but back from the\n * bleeding PART-HOLD-BACK edge, where parts aren't reliably published yet).\n * Lower = closer to live but more rebuffer-prone; higher = steadier. The\n * engine derives a matching latency ceiling internally, so this is the only\n * live-latency knob you need. Live/LL only.\n */\n liveLatencyTarget?: number;\n /**\n * Snap straight to the live edge when a backgrounded tab is refocused and\n * playback has fallen well behind. Default: true. Live/LL only.\n */\n snapToLiveOnRefocus?: boolean;\n /** Force an engine; 'auto' picks native HLS on Safari, hls.js elsewhere. Default: 'auto'. */\n engine?: 'auto' | 'hls' | 'native';\n /** Optional viewer id (X-User-Id on the license POST). */\n userId?: string;\n /**\n * Which controls to render:\n * - 'custom' (default): the Oddin-branded, skinnable control bar.\n * - 'native': the browser's built-in `<video controls>` UI.\n * - 'none': no controls (drive the player via its API).\n */\n controls?: 'custom' | 'native' | 'none';\n /**\n * Re-skin the custom control bar. Merged over the Oddin default; any omitted\n * key keeps the brand value. Equivalent to overriding the `--havik-*` CSS\n * variables on the player element. Ignored unless controls is 'custom'.\n */\n theme?: Partial<HavikTheme>;\n /**\n * Show branded full-bleed cards for the stopped/pre-play states — ended\n * (\"This stream has ended\"), fatal error (message + Retry when retryable),\n * pre-play (poster + play CTA), and armed-waiting (\"Starting soon…\") — instead\n * of a frozen frame + a dead play button. Themed via the control-bar tokens +\n * `theme.logoUrl`. Default: true. Set false if you render your own end/error\n * UI from the player events (`ended`, `error`). Ignored unless controls is\n * 'custom'.\n */\n statusOverlays?: boolean;\n /** Message on the end-of-stream card. Default: \"This stream has ended\". */\n endedMessage?: string;\n /** Message on the fatal-error card. Default: the error's own message. */\n errorMessage?: string;\n /** Escape hatch for low-level hls.js tuning. */\n hlsConfig?: Partial<HlsConfig>;\n /** Stats sampling cadence. Default: 2000ms. */\n statsIntervalMs?: number;\n /**\n * How often to silently refresh the signed DRM license URL for long live\n * sessions (the HMAC expires ~10min). Default: 8min. Set 0 to disable.\n */\n licenseRefreshMs?: number;\n /**\n * Subscribe to push-based live-state over SSE (`events.<domain>/v1/events/{urn}`)\n * to detect end-of-stream the instant the bridge flips, instead of waiting on\n * the frozen Tencent manifest (which never emits #EXT-X-ENDLIST). On `ended`/\n * `gone` the player stops buffering and emits `ended`. Default: true — and\n * fully graceful: if the endpoint is unreachable the subscription retries\n * quietly in the background while the HLS staleness watchdog remains the\n * layer-fallback, so playback is never affected. Set false to disable.\n */\n liveStateEvents?: boolean;\n /**\n * Override the SSE endpoint base URL. Default: derived from {@link baseUrl} by\n * prefixing the host with `events.` (e.g. `feed.<domain>` →\n * `events.<domain>`, served ALB-direct). Set this if your events subdomain\n * differs from that convention.\n */\n eventsBaseUrl?: string;\n}\n\nexport type PlayerEvent =\n | 'ready'\n | 'waiting'\n | 'playing'\n | 'buffering'\n | 'paused'\n | 'ended'\n | 'error'\n | 'warning'\n | 'autoplayblocked'\n | 'qualitychange'\n | 'audiotrackchange'\n | 'texttrackchange'\n | 'pipchange'\n | 'stats'\n | 'statechange';\n\nexport interface PlayerEventMap {\n ready: void;\n waiting: WaitState;\n playing: void;\n buffering: void;\n paused: void;\n ended: void;\n error: PlaybackError;\n /** Non-fatal condition (e.g. FairPlay-not-implemented passthrough, a failed license refresh). Playback continues. */\n warning: PlaybackError;\n /** The browser blocked autoplay (NotAllowedError). Prompt a tap-to-play / tap-to-unmute affordance. */\n autoplayblocked: void;\n /** Active rendition changed; payload is the level index (-1 = auto). */\n qualitychange: number;\n /** Active audio track changed; payload is the track id. */\n audiotrackchange: number;\n /** Active text track changed; payload is the track id (-1 = off). */\n texttrackchange: number;\n /** Picture-in-Picture entered (true) or exited (false). */\n pipchange: boolean;\n stats: PlaybackStats;\n statechange: PlayerState;\n}\n\nexport interface Player {\n readonly state: PlayerState;\n readonly descriptor: StreamDescriptor | null;\n play(): Promise<void>;\n pause(): void;\n setMuted(muted: boolean): void;\n setVolume(volume: number): void;\n getStats(): PlaybackStats;\n /** Available renditions (empty on the native engine, which does ABR internally). */\n getQualityLevels(): QualityLevel[];\n /** Current rendition index, or -1 when ABR is choosing automatically. */\n getCurrentQuality(): number;\n /** Pick a rendition by index, or 'auto'/-1 to re-enable ABR. */\n setQuality(index: number | 'auto'): void;\n /** Cap ABR to renditions at/below this bitrate (bps), or null to clear. */\n setMaxBitrate(bitrate: number | null): void;\n getAudioTracks(): AudioTrackInfo[];\n setAudioTrack(id: number): void;\n getTextTracks(): TextTrackInfo[];\n /** Select a text track by id, or -1 to disable captions. */\n setTextTrack(id: number): void;\n /** Jump to the live edge (live streams). */\n seekToLive(): void;\n enterPictureInPicture(): Promise<void>;\n exitPictureInPicture(): Promise<void>;\n /** Retry playback after an error (re-resolves and re-attaches). */\n retry(): Promise<void>;\n /** Re-resolve playback (refreshes the signed license URL) and re-attach. */\n reload(): Promise<void>;\n on<E extends PlayerEvent>(event: E, cb: (payload: PlayerEventMap[E]) => void): () => void;\n destroy(): void;\n}\n\nconst DEFAULT_STATS_INTERVAL_MS = 2000;\nconst DEFAULT_LICENSE_REFRESH_MS = 8 * 60 * 1000;\n\n/**\n * Mode A — managed player. Owns the <video> and a bundled hls.js, wiring\n * LL-HLS + DRM automatically. With `waitForLive`, arms on an upcoming match and\n * auto-plays the instant it goes live. Resolves to a Player as soon as it is\n * armed/attached — terminal errors (404/410/401/...) reject; the live wait does\n * not block and is surfaced via 'waiting' events.\n */\nexport async function createPlayer(opts: CreatePlayerOptions): Promise<Player> {\n // Fail fast with typed errors on misuse, before any async/DOM work.\n if (!opts.video || typeof (opts.video as HTMLVideoElement).play !== 'function') {\n throw new PlaybackError('INTERNAL', 0, 'createPlayer: opts.video must be an HTMLVideoElement');\n }\n if (!opts.baseUrl) throw new PlaybackError('INTERNAL', 0, 'createPlayer: baseUrl is required');\n if (!opts.matchUrn)\n throw new PlaybackError('INVALID_URN', 0, 'createPlayer: matchUrn is required');\n if (!opts.credential) {\n throw new PlaybackError('INTERNAL', 0, 'createPlayer: credential is required');\n }\n const player = new ManagedPlayer(opts);\n await player.start();\n return player;\n}\n\nclass ManagedPlayer implements Player {\n private readonly opts: CreatePlayerOptions;\n private readonly abort = new AbortController();\n private readonly listeners: { [E in PlayerEvent]?: Set<(p: never) => void> } = {};\n private engine: PlaybackEngine | null = null;\n private controlBar: ControlBarHandle | null = null;\n private container: HTMLElement | null = null;\n private detachMedia?: () => void;\n private detachPip?: () => void;\n private statsTimer?: ReturnType<typeof setInterval>;\n private refreshTimer?: ReturnType<typeof setInterval>;\n private _state: PlayerState = 'idle';\n private _descriptor: StreamDescriptor | null = null;\n private destroyed = false;\n /** Monotonic load generation; a load whose generation is stale must not commit. */\n private loadGen = 0;\n /** Push-based live-state subscription (SSE), active during committed playback. */\n private liveStateSub: LiveStateSubscription | null = null;\n /** Guards the end-of-stream handler so SSE and the HLS watchdog can't double-fire. */\n private endedHandled = false;\n /** Live-state subscription used during the pre-live armed/waiting phase. */\n private armingSub: LiveStateSubscription | null = null;\n /** Connect-grace / heartbeat-liveness timer for the armed SSE subscription. */\n private armSseTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(opts: CreatePlayerOptions) {\n this.opts = opts;\n }\n\n get state(): PlayerState {\n return this._state;\n }\n get descriptor(): StreamDescriptor | null {\n return this._descriptor;\n }\n\n async start(): Promise<void> {\n if (this.opts.muted) this.opts.video.muted = true;\n if (this.opts.poster) this.opts.video.poster = this.opts.poster;\n this.mountControls();\n this.attachPipListeners();\n this.setState('loading');\n const gen = ++this.loadGen;\n const resolveOpts = this.resolveOptions();\n try {\n const descriptor = await resolvePlaybackOnce(resolveOpts);\n await this.onResolved(descriptor, gen);\n } catch (err) {\n if (this.opts.waitForLive && err instanceof PlaybackError && isWaitable(err.code)) {\n // Don't block: return the (armed) player; keep polling in the background.\n this.setState('waiting');\n this.beginArming(gen);\n return;\n }\n throw err;\n }\n }\n\n private resolveOptions(): ResolveOptions {\n return {\n baseUrl: this.opts.baseUrl,\n matchUrn: this.opts.matchUrn,\n credential: this.opts.credential,\n signal: this.abort.signal,\n };\n }\n\n private beginArming(gen: number): void {\n const base =\n this.opts.liveStateEvents === false\n ? undefined\n : (this.opts.eventsBaseUrl ?? deriveEventsBaseUrl(this.opts.baseUrl));\n if (base) this.armWithSse(gen, base);\n else this.beginArmingByPolling(gen);\n }\n\n /** Fallback / SSE-disabled path: the original kickoff-aware /v1/playback poll. */\n private beginArmingByPolling(gen: number, signal?: AbortSignal): void {\n resolveStream({\n ...this.resolveOptions(),\n signal: signal ?? this.abort.signal,\n waitForLive: this.buildWaitConfig(),\n })\n .then((descriptor) => this.onResolved(descriptor, gen))\n .catch((err) => {\n if (this.destroyed || this.abort.signal.aborted || signal?.aborted || gen !== this.loadGen)\n return;\n this.fail(err);\n });\n }\n\n /**\n * Armed wait, SSE-first. While the live-state stream is healthy the player\n * does NOT poll /v1/playback — it waits for the pushed `live`, then resolves\n * exactly once (jittered, to stagger the go-live herd). Polling only starts\n * if SSE fails to connect within a grace, goes stale (missed heartbeats), or\n * errors — so an unreachable events endpoint is no worse than today.\n */\n private armWithSse(gen: number, eventsBaseUrl: string): void {\n let committed = false;\n let fallbackStarted = false;\n const fallbackCtrl = new AbortController();\n this.abort.signal.addEventListener('abort', () => fallbackCtrl.abort(), { once: true });\n this.setState('waiting');\n\n const stale = (): boolean =>\n this.destroyed || this.abort.signal.aborted || gen !== this.loadGen;\n\n const commit = async (descriptor: StreamDescriptor): Promise<void> => {\n if (committed || stale()) return;\n committed = true;\n this.closeArming();\n fallbackCtrl.abort(); // supersede any running poll fallback\n await this.onResolved(descriptor, gen);\n };\n\n const startFallback = (): void => {\n if (committed || fallbackStarted || stale()) return;\n fallbackStarted = true;\n this.beginArmingByPolling(gen, fallbackCtrl.signal);\n };\n\n const resolveOnce = (): void => {\n if (committed || stale()) return;\n resolvePlaybackOnce(this.resolveOptions())\n .then((d) => commit(d))\n .catch((err) => {\n if (committed || stale()) return;\n // Not live yet (425/transient): keep waiting — SSE or the fallback retries.\n if (err instanceof PlaybackError && isWaitable(err.code)) return;\n // Terminal (e.g. 410 GONE — cancelled before go-live): surface it.\n committed = true;\n this.closeArming();\n fallbackCtrl.abort();\n this.fail(\n err instanceof PlaybackError ? err : new PlaybackError('INTERNAL', 0, String(err)),\n );\n });\n };\n\n const rearmStale = (ms: number): void => {\n if (this.armSseTimer) clearTimeout(this.armSseTimer);\n this.armSseTimer = setTimeout(() => {\n if (!committed && !stale()) startFallback(); // SSE didn't connect / went silent\n }, ms);\n };\n rearmStale(SSE_ARM_CONNECT_GRACE_MS);\n\n this.armingSub = subscribeLiveState({\n eventsBaseUrl,\n matchUrn: this.opts.matchUrn,\n credential: this.opts.credential,\n onAlive: () => rearmStale(SSE_ARM_LIVENESS_MS),\n onState: (ev) => {\n if (committed || stale()) return;\n // live → go play; gone → resolve (will 410) so the terminal surfaces.\n if (ev.state === 'live' || ev.state === 'gone') {\n setTimeout(resolveOnce, Math.random() * SSE_ARM_RESOLVE_JITTER_MS);\n }\n },\n onError: () => startFallback(),\n });\n }\n\n private closeArming(): void {\n if (this.armSseTimer) {\n clearTimeout(this.armSseTimer);\n this.armSseTimer = null;\n }\n this.armingSub?.close();\n this.armingSub = null;\n }\n\n private buildWaitConfig(): WaitForLive {\n const base: WaitForLiveOptions =\n this.opts.waitForLive && this.opts.waitForLive !== true ? this.opts.waitForLive : {};\n return {\n ...base,\n onState: (s) => {\n this.setState('waiting');\n this.emit('waiting', s);\n base.onState?.(s);\n },\n };\n }\n\n /**\n * Build and attach the engine for a resolved descriptor. `gen` is the load\n * generation this attempt belongs to: it is re-checked after every await so a\n * destroy()/reload() that lands mid-flight cannot leave an orphaned engine or\n * timers behind (the continuation cleans up its locals and returns).\n */\n private async onResolved(descriptor: StreamDescriptor, gen: number): Promise<void> {\n if (this.destroyed || gen !== this.loadGen) return;\n this._descriptor = descriptor;\n this.endedHandled = false; // fresh playback can end again\n\n const credential = await resolveCredential(this.opts.credential);\n if (this.destroyed || gen !== this.loadGen) return; // superseded during credential mint\n this.setState('loading');\n\n const engine = this.createEngine(descriptor, credential);\n const detachMedia = attachMediaStateListeners(this.opts.video, (s) => this.setState(s));\n await engine.load();\n if (this.destroyed || gen !== this.loadGen) {\n // Torn down or superseded while loading — release what we just built.\n detachMedia();\n engine.destroy();\n return;\n }\n\n this.engine = engine;\n this.detachMedia = detachMedia;\n if (this.opts.maxBitrate != null) engine.setMaxBitrate(this.opts.maxBitrate);\n this.startStats();\n this.scheduleLicenseRefresh();\n this.startLiveState(gen);\n }\n\n private createEngine(descriptor: StreamDescriptor, credential: Credential): PlaybackEngine {\n const hlsConfig: Partial<HlsConfig> = { ...this.opts.hlsConfig };\n if (this.opts.startLevel != null) hlsConfig.startLevel = this.opts.startLevel;\n // liveLatencyTarget -> hls.js liveSyncDuration. The engine's normalizeLiveSync\n // derives the matching drift ceiling (liveMaxLatencyDuration, kept strictly\n // above the target) on the final merged config, so we set only the target\n // here and never risk an illegal pair. When unset, the engine default target\n // (3s) applies. liveCatchUpRate is no longer a public knob — the engine's\n // 1.5x catch-up + that ceiling handle drift; advanced callers can still set\n // maxLiveSyncPlaybackRate (or the raw live-sync keys) via hlsConfig.\n if (this.opts.liveLatencyTarget != null)\n hlsConfig.liveSyncDuration = this.opts.liveLatencyTarget;\n\n const deps = {\n video: this.opts.video,\n descriptor,\n credential,\n deviceId: getDeviceId(),\n userId: this.opts.userId,\n lowLatency:\n this.opts.lowLatency === undefined || this.opts.lowLatency === 'auto'\n ? true\n : this.opts.lowLatency,\n snapToLiveOnRefocus: this.opts.snapToLiveOnRefocus,\n hlsConfig,\n emit: (ev: EngineEvent) => this.onEngineEvent(ev),\n };\n const choice = this.opts.engine ?? 'auto';\n if (choice === 'native') return new NativeHlsEngine(deps);\n if (choice === 'hls') return new HlsEngine(deps);\n // auto: prefer hls.js (gives us DRM + LL control); fall back to native HLS.\n if (hlsSupported()) return new HlsEngine(deps);\n if (nativeHlsSupported(this.opts.video)) return new NativeHlsEngine(deps);\n throw new PlaybackError('INTERNAL', 0, 'no supported HLS playback engine in this browser');\n }\n\n private onEngineEvent(ev: EngineEvent): void {\n if (this.destroyed) return;\n switch (ev.type) {\n case 'ready':\n this.emit('ready');\n if (this.opts.autoplay !== false) void this.play();\n break;\n case 'lowLatencyDetected':\n if (this._descriptor) this._descriptor.isLowLatency = ev.value;\n break;\n case 'qualitychange':\n this.emit('qualitychange', ev.index);\n break;\n case 'audiotrackchange':\n this.emit('audiotrackchange', ev.id);\n break;\n case 'texttrackchange':\n this.emit('texttrackchange', ev.id);\n break;\n case 'ended':\n // HLS-layer end signal (ENDLIST, or the staleness watchdog catching a\n // frozen Tencent manifest). The SSE live-state path can reach the same\n // handler first; endedHandled dedupes.\n this.onStreamEnded();\n break;\n case 'error':\n if (ev.fatal) this.fail(ev.error);\n else this.emit('warning', ev.error);\n break;\n }\n }\n\n /**\n * Start the push-based live-state subscription for the committed playback.\n * On `ended`/`gone` it routes to onStreamEnded — the fast, clean end signal\n * (fires when the bridge flips, before the manifest would even 503). Failures\n * are non-fatal: the subscription retries in the background and the HLS\n * staleness watchdog (engine 'ended') is the layer-fallback, so a missing or\n * unreachable events endpoint never affects playback. But a PERSISTENTLY\n * failing subscription (blocked origin, wrong events host, CSP) used to be\n * fully invisible — end detection silently degraded to the watchdog with no\n * way to tell from the outside. After LIVE_STATE_WARN_AFTER consecutive\n * failures with no data ever flowing, a one-time non-fatal 'warning' is\n * emitted so integrators can see it; a later successful connect resets the\n * counter (and re-arms the warning) since the outage evidently healed.\n */\n private startLiveState(gen: number): void {\n if (this.opts.liveStateEvents === false) return;\n const base = this.opts.eventsBaseUrl ?? deriveEventsBaseUrl(this.opts.baseUrl);\n if (!base) return;\n this.closeLiveState();\n let failures = 0;\n let warned = false;\n this.liveStateSub = subscribeLiveState({\n eventsBaseUrl: base,\n matchUrn: this.opts.matchUrn,\n credential: this.opts.credential,\n onState: (ev) => {\n if (this.destroyed || gen !== this.loadGen) return;\n if (ev.state === 'ended' || ev.state === 'gone') this.onStreamEnded();\n },\n onAlive: () => {\n // Data flowed — the subscription is healthy. Reset so only a NEW\n // sustained outage (not routine reconnects) can trigger the warning.\n failures = 0;\n warned = false;\n },\n onError: () => {\n if (this.destroyed || gen !== this.loadGen || warned) return;\n failures += 1;\n if (failures >= LIVE_STATE_WARN_AFTER) {\n warned = true;\n this.emit(\n 'warning',\n new PlaybackError(\n 'NETWORK',\n 0,\n `live-state events unreachable at ${base} after ${failures} attempts — ` +\n `end-of-stream detection degraded to the HLS staleness watchdog. ` +\n `Check the events host, the api-key's allowed origins, and CSP connect-src.`,\n ),\n );\n }\n },\n });\n }\n\n private closeLiveState(): void {\n this.liveStateSub?.close();\n this.liveStateSub = null;\n }\n\n /**\n * Terminal end-of-stream: stop the engine's buffer/poll loop (keeping the\n * last frame), close the live-state subscription, and surface `ended` (via\n * setState). Idempotent across the SSE and HLS-watchdog signals.\n */\n private onStreamEnded(): void {\n if (this.destroyed || this.endedHandled) return;\n this.endedHandled = true;\n this.closeLiveState();\n this.stopRefresh();\n this.engine?.stop();\n this.setState('ended');\n }\n\n /**\n * Set up the chosen controls. 'custom' (default) wraps the <video> in a\n * `.havik-player` container, applies the (themeable) Oddin skin, and mounts\n * the control bar. 'native' shows the browser UI; 'none' shows nothing.\n */\n private mountControls(): void {\n const mode = this.opts.controls ?? 'custom';\n const video = this.opts.video;\n if (mode === 'native') {\n video.controls = true;\n return;\n }\n if (mode === 'none') {\n video.controls = false;\n return;\n }\n video.controls = false;\n const doc = video.ownerDocument ?? document;\n const container = doc.createElement('div');\n const parent = video.parentNode;\n if (parent) parent.insertBefore(container, video);\n container.appendChild(video);\n applyTheme(container, this.opts.theme);\n this.container = container;\n this.controlBar = mountControlBar({\n root: container,\n video,\n player: this,\n logoUrl: this.opts.theme?.logoUrl,\n statusOverlays: this.opts.statusOverlays,\n endedMessage: this.opts.endedMessage,\n errorMessage: this.opts.errorMessage,\n });\n }\n\n private unmountControls(): void {\n this.controlBar?.destroy();\n this.controlBar = null;\n const container = this.container;\n if (container?.parentNode) {\n container.parentNode.insertBefore(this.opts.video, container);\n container.remove();\n }\n this.container = null;\n }\n\n private attachPipListeners(): void {\n const v = this.opts.video;\n const onEnter = () => this.emit('pipchange', true);\n const onLeave = () => this.emit('pipchange', false);\n v.addEventListener('enterpictureinpicture', onEnter);\n v.addEventListener('leavepictureinpicture', onLeave);\n this.detachPip = () => {\n v.removeEventListener('enterpictureinpicture', onEnter);\n v.removeEventListener('leavepictureinpicture', onLeave);\n };\n }\n\n private startStats(): void {\n this.stopStats();\n const interval = Math.max(500, this.opts.statsIntervalMs ?? DEFAULT_STATS_INTERVAL_MS);\n this.statsTimer = setInterval(() => {\n if (!this.engine) return;\n this.emit('stats', this.engine.getStats());\n }, interval);\n }\n\n private stopStats(): void {\n if (this.statsTimer) clearInterval(this.statsTimer);\n this.statsTimer = undefined;\n }\n\n private scheduleLicenseRefresh(): void {\n this.stopRefresh();\n const interval = this.opts.licenseRefreshMs ?? DEFAULT_LICENSE_REFRESH_MS;\n if (!interval || !this._descriptor?.drmEnabled) return;\n this.refreshTimer = setInterval(() => void this.refreshLicense(), interval);\n }\n\n private stopRefresh(): void {\n if (this.refreshTimer) clearInterval(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n\n private async refreshLicense(): Promise<void> {\n const engine = this.engine;\n if (this.destroyed || !engine) return;\n try {\n const fresh = await resolvePlaybackOnce(this.resolveOptions());\n // A reload()/destroy() may have swapped or dropped the engine while the\n // refresh GET was in flight — don't apply a stale URL to a newer engine.\n if (this.destroyed || this.engine !== engine) return;\n const url = fresh.drm?.widevine?.licenseUrl;\n if (url) {\n engine.setLicenseUrl(url);\n this._descriptor = fresh;\n }\n } catch (err) {\n // A terminal error (e.g. 410 ended) means there's nothing left to refresh;\n // surface it as a non-fatal warning so a stalling refresh is observable.\n if (err instanceof PlaybackError && !isWaitable(err.code)) {\n this.stopRefresh();\n if (!this.destroyed) this.emit('warning', err);\n }\n }\n }\n\n async play(): Promise<void> {\n if (this.destroyed) return;\n try {\n await this.opts.video.play();\n } catch (err) {\n // Autoplay blocked by browser policy → emit 'autoplayblocked' so the host\n // can show a tap-to-play / tap-to-unmute affordance. Other rejections\n // (e.g. a competing-load AbortError) are benign; genuine playback failures\n // arrive via the 'error' event from the engine.\n if (err instanceof DOMException && err.name === 'NotAllowedError') {\n this.emit('autoplayblocked');\n }\n }\n }\n\n pause(): void {\n if (this.destroyed) return;\n this.opts.video.pause();\n }\n setMuted(muted: boolean): void {\n if (this.destroyed) return;\n this.opts.video.muted = muted;\n }\n setVolume(volume: number): void {\n if (this.destroyed) return;\n this.opts.video.volume = Math.min(1, Math.max(0, volume));\n }\n\n getStats(): PlaybackStats {\n return this.engine?.getStats() ?? EMPTY_STATS;\n }\n\n getQualityLevels(): QualityLevel[] {\n return this.engine?.getQualityLevels() ?? [];\n }\n getCurrentQuality(): number {\n return this.engine?.getCurrentQuality() ?? -1;\n }\n setQuality(index: number | 'auto'): void {\n if (this.destroyed) return;\n this.engine?.setQuality(index === 'auto' ? -1 : index);\n }\n setMaxBitrate(bitrate: number | null): void {\n if (this.destroyed) return;\n this.engine?.setMaxBitrate(bitrate);\n }\n getAudioTracks(): AudioTrackInfo[] {\n return this.engine?.getAudioTracks() ?? [];\n }\n setAudioTrack(id: number): void {\n if (this.destroyed) return;\n this.engine?.setAudioTrack(id);\n }\n getTextTracks(): TextTrackInfo[] {\n return this.engine?.getTextTracks() ?? [];\n }\n setTextTrack(id: number): void {\n if (this.destroyed) return;\n this.engine?.setTextTrack(id);\n }\n seekToLive(): void {\n if (this.destroyed) return;\n this.engine?.seekToLive();\n }\n async enterPictureInPicture(): Promise<void> {\n if (this.destroyed) return;\n const v = this.opts.video as HTMLVideoElement & {\n requestPictureInPicture?: () => Promise<unknown>;\n };\n if (typeof v.requestPictureInPicture === 'function') await v.requestPictureInPicture();\n }\n async exitPictureInPicture(): Promise<void> {\n const doc = document as Document & {\n exitPictureInPicture?: () => Promise<void>;\n pictureInPictureElement?: Element | null;\n };\n if (\n doc.pictureInPictureElement === this.opts.video &&\n typeof doc.exitPictureInPicture === 'function'\n ) {\n await doc.exitPictureInPicture();\n }\n }\n retry(): Promise<void> {\n return this.reload();\n }\n\n async reload(): Promise<void> {\n if (this.destroyed) return;\n const gen = ++this.loadGen; // supersedes any in-flight load/arming\n this.stopStats();\n this.stopRefresh();\n this.closeArming();\n this.closeLiveState();\n this.detachMedia?.();\n this.engine?.destroy();\n this.engine = null;\n try {\n const descriptor = await resolveStream({\n ...this.resolveOptions(),\n waitForLive: this.opts.waitForLive ? this.buildWaitConfig() : undefined,\n });\n await this.onResolved(descriptor, gen);\n } catch (err) {\n if (this.destroyed || this.abort.signal.aborted || gen !== this.loadGen) return;\n // Route a failed reload through the error channel (consistent with start/arming)\n // AND reject so an awaiting caller observes it.\n this.fail(err instanceof PlaybackError ? err : new PlaybackError('INTERNAL', 0, String(err)));\n throw err;\n }\n }\n\n on<E extends PlayerEvent>(event: E, cb: (payload: PlayerEventMap[E]) => void): () => void {\n const set = (this.listeners[event] ??= new Set()) as Set<(p: PlayerEventMap[E]) => void>;\n set.add(cb);\n return () => set.delete(cb);\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.loadGen += 1; // invalidate any in-flight load/arming continuation\n this.abort.abort();\n this.stopStats();\n this.stopRefresh();\n // Tear the control bar down (and unsubscribe its listeners) before the final\n // 'idle' emit so it never touches a half-dismantled DOM.\n this.unmountControls();\n this.closeArming();\n this.closeLiveState();\n this.detachMedia?.();\n this.detachPip?.();\n this.engine?.destroy();\n this.engine = null;\n this.setState('idle'); // final terminal signal to any still-registered listeners\n this.clearListeners();\n }\n\n private clearListeners(): void {\n for (const key of Object.keys(this.listeners)) {\n (this.listeners as Record<string, Set<unknown> | undefined>)[key]?.clear();\n }\n }\n\n private fail(err: PlaybackError): void {\n this.setState('error');\n this.emit('error', err);\n }\n\n private setState(state: PlayerState): void {\n if (state === this._state) return;\n this._state = state;\n this.emit('statechange', state);\n switch (state) {\n case 'playing':\n this.emit('playing');\n break;\n case 'buffering':\n this.emit('buffering');\n break;\n case 'paused':\n this.emit('paused');\n break;\n case 'ended':\n this.emit('ended');\n break;\n default:\n break;\n }\n }\n\n private emit<E extends PlayerEvent>(event: E, payload?: PlayerEventMap[E]): void {\n const set = this.listeners[event] as\n Set<(p: PlayerEventMap[E] | undefined) => void> | undefined;\n set?.forEach((cb) => {\n try {\n cb(payload);\n } catch {\n // never let a listener break the player\n }\n });\n }\n}\n","import { type PlaybackErrorCode } from '../core/errors';\nimport {\n type AudioTrackInfo,\n type PlayerState,\n type QualityLevel,\n type TextTrackInfo,\n} from '../core/stats';\n\n/** postMessage protocol version. Bump on a breaking change; sent in `oddin:ready`. */\nexport const PROTOCOL_VERSION = 1;\n\n/** Commands the host page sends down to the iframe. */\nexport type HostToFrame =\n | { type: 'oddin:play' }\n | { type: 'oddin:pause' }\n | { type: 'oddin:setMuted'; muted: boolean }\n | { type: 'oddin:setVolume'; volume: number }\n | { type: 'oddin:setQuality'; index: number } // -1 = auto\n | { type: 'oddin:setMaxBitrate'; bitrate: number | null }\n | { type: 'oddin:setAudioTrack'; id: number }\n | { type: 'oddin:setTextTrack'; id: number } // -1 = off\n | { type: 'oddin:seekToLive' }\n | { type: 'oddin:enterPip' }\n | { type: 'oddin:exitPip' }\n | { type: 'oddin:retry' }\n | { type: 'oddin:load'; matchUrn: string }\n | { type: 'oddin:destroy' };\n\n/** Events the iframe sends up to the host page. */\nexport type FrameToHost =\n | { type: 'oddin:ready'; protocol: number }\n | { type: 'oddin:autoplayblocked' }\n | { type: 'oddin:state'; state: PlayerState }\n | { type: 'oddin:waiting'; retryInMs: number; attempt: number; phase: string }\n | {\n type: 'oddin:tracks';\n quality: QualityLevel[];\n audio: AudioTrackInfo[];\n text: TextTrackInfo[];\n }\n | { type: 'oddin:qualitychange'; index: number }\n | { type: 'oddin:audiotrackchange'; id: number }\n | { type: 'oddin:texttrackchange'; id: number }\n | {\n type: 'oddin:stats';\n droppedFrames: number;\n latencySeconds?: number;\n bandwidthKbps?: number;\n levelHeight?: number;\n lowLatency?: boolean;\n isLive?: boolean;\n atLiveEdge?: boolean;\n }\n | { type: 'oddin:error'; code: PlaybackErrorCode; message: string };\n\nexport function isOddinMessage(data: unknown): data is { type: string } {\n return (\n !!data &&\n typeof data === 'object' &&\n typeof (data as { type?: unknown }).type === 'string' &&\n (data as { type: string }).type.startsWith('oddin:')\n );\n}\n","import { type FrameToHost, type HostToFrame, isOddinMessage, PROTOCOL_VERSION } from './protocol';\n\nexport interface MountEmbedOptions {\n /** Element to mount the iframe into. */\n container: HTMLElement;\n /** URL of the hosted embed page, e.g. https://player.oddin.gg/embed/. */\n src: string;\n baseUrl: string;\n matchUrn: string;\n /**\n * DEV/DEMO ONLY: appended as a query param so the embed page can read it.\n * In production the embed page is hosted by you and injects the key\n * server-side — do not pass it here.\n */\n apiKey?: string;\n /** Origin of the embed page used to verify its messages. Defaults to new URL(src).origin. */\n allowedFrameOrigin?: string;\n muted?: boolean;\n /**\n * iframe sandbox attribute. Default keeps playback + EME + fullscreen working\n * while blocking top-frame navigation and popups. Override only if you know\n * what you're doing.\n */\n sandbox?: string;\n /** Convenience callback for every event from the iframe. */\n onEvent?: (msg: FrameToHost) => void;\n}\n\nexport interface EmbedHandle {\n readonly iframe: HTMLIFrameElement;\n play(): void;\n pause(): void;\n setMuted(muted: boolean): void;\n setVolume(volume: number): void;\n setQuality(index: number): void; // -1 = auto\n setMaxBitrate(bitrate: number | null): void;\n setAudioTrack(id: number): void;\n setTextTrack(id: number): void; // -1 = off\n seekToLive(): void;\n enterPip(): void;\n exitPip(): void;\n retry(): void;\n load(matchUrn: string): void;\n on(cb: (msg: FrameToHost) => void): () => void;\n destroy(): void;\n}\n\n/**\n * Mode C — mount the hosted iframe player and bridge it over postMessage. Both\n * ends verify event.origin on every message; commands are posted only to the\n * embed page's origin.\n */\nexport function mountEmbed(opts: MountEmbedOptions): EmbedHandle {\n if (!opts.container || typeof opts.container.appendChild !== 'function') {\n throw new Error('mountEmbed: opts.container must be a DOM element');\n }\n if (!opts.src) throw new Error('mountEmbed: opts.src is required');\n if (!opts.baseUrl || !opts.matchUrn) {\n throw new Error('mountEmbed: opts.baseUrl and opts.matchUrn are required');\n }\n const url = new URL(opts.src, location.href);\n const frameOrigin = opts.allowedFrameOrigin ?? url.origin;\n\n url.searchParams.set('baseUrl', opts.baseUrl);\n url.searchParams.set('matchUrn', opts.matchUrn);\n url.searchParams.set('parentOrigin', location.origin);\n if (opts.muted != null) url.searchParams.set('muted', String(opts.muted));\n if (opts.apiKey) {\n url.searchParams.set('apiKey', opts.apiKey);\n console.warn(\n 'havik-player: passing apiKey to mountEmbed places it in the iframe URL — DEV/DEMO only. ' +\n 'In production inject it server-side into window.HAVIK_EMBED_CONFIG on the embed page.',\n );\n }\n\n const iframe = document.createElement('iframe');\n iframe.src = url.toString();\n iframe.allow = 'autoplay; encrypted-media; fullscreen';\n iframe.setAttribute('allowfullscreen', '');\n // Keep any api-key-bearing URL out of Referer, and sandbox the embed so a\n // compromised embed page can't navigate the top frame or open popups.\n // allow-same-origin is required for EME/MediaKeys and the embed's own storage.\n iframe.referrerPolicy = 'no-referrer';\n iframe.setAttribute(\n 'sandbox',\n opts.sandbox ?? 'allow-scripts allow-same-origin allow-presentation',\n );\n iframe.style.cssText = 'border:0;width:100%;height:100%;display:block';\n opts.container.appendChild(iframe);\n\n const listeners = new Set<(msg: FrameToHost) => void>();\n\n const onMessage = (ev: MessageEvent) => {\n if (ev.source !== iframe.contentWindow) return;\n if (ev.origin !== frameOrigin) return;\n if (!isOddinMessage(ev.data)) return;\n const msg = ev.data as FrameToHost;\n if (msg.type === 'oddin:ready' && msg.protocol !== PROTOCOL_VERSION) {\n console.warn(\n `havik-player: embed protocol v${msg.protocol} != host v${PROTOCOL_VERSION}. ` +\n 'Pin the embed page version to the SDK version to avoid skew.',\n );\n }\n opts.onEvent?.(msg);\n listeners.forEach((cb) => cb(msg));\n };\n window.addEventListener('message', onMessage);\n\n const send = (msg: HostToFrame) => iframe.contentWindow?.postMessage(msg, frameOrigin);\n\n return {\n iframe,\n play: () => send({ type: 'oddin:play' }),\n pause: () => send({ type: 'oddin:pause' }),\n setMuted: (muted) => send({ type: 'oddin:setMuted', muted }),\n setVolume: (volume) => send({ type: 'oddin:setVolume', volume }),\n setQuality: (index) => send({ type: 'oddin:setQuality', index }),\n setMaxBitrate: (bitrate) => send({ type: 'oddin:setMaxBitrate', bitrate }),\n setAudioTrack: (id) => send({ type: 'oddin:setAudioTrack', id }),\n setTextTrack: (id) => send({ type: 'oddin:setTextTrack', id }),\n seekToLive: () => send({ type: 'oddin:seekToLive' }),\n enterPip: () => send({ type: 'oddin:enterPip' }),\n exitPip: () => send({ type: 'oddin:exitPip' }),\n retry: () => send({ type: 'oddin:retry' }),\n load: (matchUrn) => send({ type: 'oddin:load', matchUrn }),\n on: (cb) => {\n listeners.add(cb);\n return () => listeners.delete(cb);\n },\n destroy: () => {\n send({ type: 'oddin:destroy' });\n window.removeEventListener('message', onMessage);\n iframe.remove();\n },\n };\n}\n","import { createPlayer, type Player } from '../managed';\nimport { PlaybackError } from '../core/errors';\nimport { type WaitForLive } from '../core/waitForLive';\nimport { type FrameToHost, type HostToFrame, isOddinMessage, PROTOCOL_VERSION } from './protocol';\n\nexport interface EmbedConfig {\n baseUrl: string;\n matchUrn: string;\n /** DEV/DEMO ONLY when read from the query string — in production inject server-side. */\n apiKey: string;\n /** Origin to post events to and accept commands from. Required for the bridge. */\n parentOrigin?: string;\n autoplay?: boolean;\n muted?: boolean;\n waitForLive?: WaitForLive;\n}\n\ndeclare global {\n interface Window {\n HAVIK_EMBED_CONFIG?: Partial<EmbedConfig>;\n }\n}\n\nconst URN_RE = /^[A-Za-z0-9:_.-]{1,128}$/;\n\n/**\n * Config is read in ONE of two mutually-exclusive modes:\n * - Production: when window.HAVIK_EMBED_CONFIG is injected server-side, ONLY it is\n * trusted. The query string is ignored entirely, so a tampered `?baseUrl=` can\n * never redirect the injected api-key to an attacker origin.\n * - Dev/demo: when no injected config is present, the query string is used.\n */\nfunction readConfig(): EmbedConfig {\n const injected = window.HAVIK_EMBED_CONFIG;\n if (injected) {\n return {\n baseUrl: injected.baseUrl ?? '',\n matchUrn: injected.matchUrn ?? '',\n apiKey: injected.apiKey ?? '',\n parentOrigin: injected.parentOrigin,\n autoplay: injected.autoplay ?? true,\n muted: injected.muted ?? true,\n waitForLive: injected.waitForLive ?? true,\n };\n }\n const q = new URLSearchParams(location.search);\n return {\n baseUrl: q.get('baseUrl') ?? '',\n matchUrn: q.get('matchUrn') ?? '',\n apiKey: q.get('apiKey') ?? '',\n parentOrigin: q.get('parentOrigin') ?? undefined,\n autoplay: true,\n muted: q.get('muted') !== 'false',\n waitForLive: true,\n };\n}\n\n/**\n * Boots the iframe-side player and speaks the postMessage protocol with the host.\n * The api-key is NEVER accepted over postMessage. The bridge fails CLOSED: with no\n * trusted parentOrigin it neither accepts commands nor broadcasts events.\n */\nexport async function bootEmbed(): Promise<void> {\n const cfg = readConfig();\n applyFullBleedStyles();\n\n const video = document.createElement('video');\n video.setAttribute('playsinline', '');\n // createPlayer mounts the branded custom control bar (controls: 'custom' default).\n video.muted = cfg.muted ?? true;\n video.style.cssText =\n 'width:100%;height:100%;object-fit:contain;background:#000;aspect-ratio:auto';\n document.body.appendChild(video);\n\n const trustedOrigin = cfg.parentOrigin && cfg.parentOrigin !== 'null' ? cfg.parentOrigin : null;\n if (!trustedOrigin) {\n console.warn(\n 'havik-player: embed has no parentOrigin — the postMessage bridge is disabled ' +\n '(no events posted, no commands accepted). mountEmbed sets parentOrigin automatically.',\n );\n }\n\n const post = (msg: FrameToHost) => {\n if (!trustedOrigin) return; // never broadcast to '*'\n try {\n window.parent?.postMessage(msg, trustedOrigin);\n } catch {\n // parent may be gone\n }\n };\n\n if (!cfg.baseUrl || !cfg.matchUrn || !cfg.apiKey) {\n post({\n type: 'oddin:error',\n code: 'INTERNAL',\n message: 'embed missing baseUrl/matchUrn/apiKey',\n });\n return;\n }\n\n let player: Player | null = null;\n\n const wire = (p: Player) => {\n p.on('statechange', (state) => post({ type: 'oddin:state', state }));\n p.on('ready', () =>\n post({\n type: 'oddin:tracks',\n quality: p.getQualityLevels(),\n audio: p.getAudioTracks(),\n text: p.getTextTracks(),\n }),\n );\n p.on('waiting', (w) =>\n post({ type: 'oddin:waiting', retryInMs: w.retryInMs, attempt: w.attempt, phase: w.phase }),\n );\n p.on('qualitychange', (index) => post({ type: 'oddin:qualitychange', index }));\n p.on('audiotrackchange', (id) => post({ type: 'oddin:audiotrackchange', id }));\n p.on('texttrackchange', (id) => post({ type: 'oddin:texttrackchange', id }));\n p.on('stats', (s) =>\n post({\n type: 'oddin:stats',\n droppedFrames: s.droppedFrames,\n latencySeconds: s.latencySeconds,\n bandwidthKbps: s.bandwidthKbps,\n levelHeight: s.levelHeight,\n lowLatency: s.lowLatency,\n isLive: s.isLive,\n atLiveEdge: s.atLiveEdge,\n }),\n );\n p.on('error', (e) => post({ type: 'oddin:error', code: e.code, message: e.message }));\n p.on('autoplayblocked', () => {\n post({ type: 'oddin:autoplayblocked' });\n showTapToPlay(video, p);\n });\n };\n\n // Switch matches in place (destroy + re-create) rather than re-navigating — this\n // preserves injected config and keeps the api-key out of the URL/history.\n const startPlayer = async (matchUrn: string): Promise<void> => {\n player?.destroy();\n player = null;\n try {\n const p = await createPlayer({\n video,\n baseUrl: cfg.baseUrl,\n matchUrn,\n credential: { apiKey: cfg.apiKey },\n autoplay: cfg.autoplay,\n muted: cfg.muted,\n waitForLive: cfg.waitForLive,\n });\n player = p;\n // The custom bar wraps <video> in a `.havik-player` container; make it\n // fill the iframe (the default skin is content-sized 16:9 otherwise).\n const container = video.parentElement;\n if (container?.classList.contains('havik-player')) {\n container.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;aspect-ratio:auto';\n }\n wire(p);\n } catch (e) {\n const code = e instanceof PlaybackError ? e.code : 'INTERNAL';\n const message = e instanceof Error ? e.message : String(e);\n post({ type: 'oddin:error', code, message });\n }\n };\n\n const dispose = () => {\n window.removeEventListener('message', onMessage);\n window.removeEventListener('pagehide', dispose);\n player?.destroy();\n player = null;\n };\n\n const handleCommand = (msg: HostToFrame): void => {\n switch (msg.type) {\n case 'oddin:play':\n void player?.play();\n break;\n case 'oddin:pause':\n player?.pause();\n break;\n case 'oddin:setMuted':\n if (typeof msg.muted === 'boolean') player?.setMuted(msg.muted);\n break;\n case 'oddin:setVolume':\n if (typeof msg.volume === 'number' && Number.isFinite(msg.volume)) {\n player?.setVolume(msg.volume);\n }\n break;\n case 'oddin:setQuality':\n if (typeof msg.index === 'number') player?.setQuality(msg.index);\n break;\n case 'oddin:setMaxBitrate':\n if (\n msg.bitrate === null ||\n (typeof msg.bitrate === 'number' && Number.isFinite(msg.bitrate))\n ) {\n player?.setMaxBitrate(msg.bitrate);\n }\n break;\n case 'oddin:setAudioTrack':\n if (typeof msg.id === 'number') player?.setAudioTrack(msg.id);\n break;\n case 'oddin:setTextTrack':\n if (typeof msg.id === 'number') player?.setTextTrack(msg.id);\n break;\n case 'oddin:seekToLive':\n player?.seekToLive();\n break;\n case 'oddin:enterPip':\n void player?.enterPictureInPicture();\n break;\n case 'oddin:exitPip':\n void player?.exitPictureInPicture();\n break;\n case 'oddin:retry':\n void player?.retry();\n break;\n case 'oddin:load':\n if (typeof msg.matchUrn === 'string' && URN_RE.test(msg.matchUrn)) {\n void startPlayer(msg.matchUrn);\n }\n break;\n case 'oddin:destroy':\n dispose();\n break;\n }\n };\n\n const onMessage = (ev: MessageEvent) => {\n if (!trustedOrigin || ev.origin !== trustedOrigin) return;\n if (!isOddinMessage(ev.data)) return;\n handleCommand(ev.data as HostToFrame);\n };\n\n if (trustedOrigin) window.addEventListener('message', onMessage);\n window.addEventListener('pagehide', dispose, { once: true });\n\n await startPlayer(cfg.matchUrn);\n post({ type: 'oddin:ready', protocol: PROTOCOL_VERSION });\n}\n\nfunction showTapToPlay(video: HTMLVideoElement, player: Player): void {\n if (document.getElementById('havik-tap')) return;\n const overlay = document.createElement('button');\n overlay.id = 'havik-tap';\n overlay.textContent = '▶ Tap to play';\n overlay.style.cssText =\n 'position:fixed;inset:0;width:100%;height:100%;border:0;background:rgba(0,0,0,.55);' +\n 'color:#fff;font:600 18px system-ui,sans-serif;cursor:pointer;z-index:9';\n overlay.addEventListener('click', () => {\n overlay.remove();\n void player.play(); // a user gesture — play() now succeeds\n });\n document.body.appendChild(overlay);\n void video; // overlay covers the element; kept for signature clarity\n}\n\nfunction applyFullBleedStyles(): void {\n const style = document.createElement('style');\n style.textContent =\n 'html,body{margin:0;height:100%;background:#000;overflow:hidden}body{display:flex}';\n document.head.appendChild(style);\n}\n"],"mappings":"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"}
|