{"version":3,"file":"index.cjs","names":[],"sources":["../src/core/errors.ts","../src/core/types.ts","../src/core/http.ts","../src/core/waitForLive.ts","../src/core/resolve.ts","../src/core/fullscreen.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/drm/cdm.ts","../src/core/drm/support.ts","../src/core/manifest.ts","../src/core/env.ts","../src/core/stats.ts","../src/core/engine/native.ts","../src/core/engine/hls.ts","../src/core/analytics.ts","../src/core/environment.ts","../src/core/version.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  | 'DRM_CLIENT' // client-side DRM failure: key system unusable on this platform, CDM/EME error, or license transport exhausted — NOT a server entitlement denial (httpStatus carries the license status when there was one, else 0)\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\n/**\n * Map the HTTP status of a DRM license/certificate exchange to a typed code.\n * Only a genuine server auth/entitlement denial keeps the server taxonomy;\n * everything else (no response, transport exhausted, CDM/EME failure) is a\n * client-side DRM failure. Shared by both engines so they cannot drift.\n */\nexport function classifyDrmStatus(status: number): PlaybackErrorCode {\n  return status === 401 ? 'UNAUTHORIZED' : status === 403 ? 'FORBIDDEN' : 'DRM_CLIENT';\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  /**\n   * Analytics session binding minted by havik-streams (present when the\n   * server has analytics enabled). The sid is an opaque per-playback-session\n   * id — non-secret by design (DRM remains the access gate) — echoed in CMCD\n   * query params on CDN requests and carried by QoE beacons. It lives only in\n   * memory for the duration of the session and is never persisted.\n   */\n  analytics?: { sid?: string };\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   * Extra headers for this request — and strictly extra: an entry whose\n   * name the SDK already set (e.g. `If-None-Match` on conditional catalog\n   * and status polls) is ignored rather than allowed to clobber it, and\n   * `x-api-key` is applied after this map so the credential can never be\n   * replaced either.\n   */\n  apiHeaders?: Record<string, string>;\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  if (opts.apiHeaders) {\n    for (const [name, value] of Object.entries(opts.apiHeaders)) {\n      if (!headers.has(name)) headers.set(name, value);\n    }\n  }\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  /**\n   * Base URL of the havik-streams API — `https://feed.oddin-video.gg`\n   * (production) or `https://feed-dev.oddin-video.gg` (integration). Prefer\n   * resolving it from the endpoint directory at runtime over hard-coding it;\n   * see Service URL discovery in the API reference.\n   */\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   * Extra headers attached to every request this call makes to {@link baseUrl}\n   * — and ONLY there: never to CDN manifest/segment hosts or DRM license\n   * endpoints. For integrations whose API front door wants its own auth\n   * signal on top of the publishable key (gateways, operator consoles).\n   * `x-api-key` cannot be overridden.\n   */\n  apiHeaders?: Record<string, string>;\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  analytics?: { sid?: 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    apiHeaders: opts.apiHeaders,\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  'analytics',\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  if (typeof raw.analytics?.sid === 'string' && raw.analytics.sid) {\n    descriptor.analytics = { sid: raw.analytics.sid };\n  }\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","// Cross-platform fullscreen for a playing <video>. Exported for Mode B\n// (bring-your-own-player) integrators and shared by the managed player API\n// and the custom control bar, so the platform quirks live in exactly one\n// place.\n//\n// Capability ladder, because the element Fullscreen API is not universal:\n//  1. Standard element fullscreen on `container` (or the video itself) — a\n//     container keeps custom DOM controls/overlays on screen.\n//  2. Prefixed element fullscreen — older iPad/macOS Safari (< 16.4) ships\n//     only webkitRequestFullscreen / webkitExitFullscreen.\n//  3. Video-native fullscreen — iPhone Safari has NEVER shipped the element\n//     API in any form (`requestFullscreen` is undefined on every element),\n//     so the only fullscreen there is the AVPlayer-style\n//     `video.webkitEnterFullscreen()` with NATIVE controls (the YouTube\n//     behavior). Custom DOM is not visible inside it, it requires loaded\n//     media (a pre-play call is a no-op), and it must run from a user\n//     gesture.\n\ntype FullscreenTarget = HTMLElement & {\n  requestFullscreen?: () => Promise<void>;\n  webkitRequestFullscreen?: () => void;\n};\n\ntype FullscreenDocument = Document & {\n  fullscreenElement?: Element | null;\n  exitFullscreen?: () => Promise<void>;\n  webkitFullscreenElement?: Element | null;\n  webkitExitFullscreen?: () => void;\n};\n\ntype WebkitVideo = HTMLVideoElement & {\n  webkitSupportsFullscreen?: boolean;\n  webkitDisplayingFullscreen?: boolean;\n  webkitEnterFullscreen?: () => void;\n  webkitExitFullscreen?: () => void;\n};\n\nfunction ownerDoc(video: HTMLVideoElement): FullscreenDocument {\n  return (video.ownerDocument ?? document) as FullscreenDocument;\n}\n\n/** The element currently in element fullscreen (unprefixed or prefixed), if any. */\nfunction fullscreenElement(doc: FullscreenDocument): Element | null {\n  return doc.fullscreenElement ?? doc.webkitFullscreenElement ?? null;\n}\n\n/**\n * True while this video's fullscreen presentation is active: element\n * fullscreen on `container` or the video itself, or iPhone's video-native\n * fullscreen. Pass the same `container` you pass to enterVideoFullscreen.\n */\nexport function videoFullscreenActive(video: HTMLVideoElement, container?: HTMLElement): boolean {\n  const el = fullscreenElement(ownerDoc(video));\n  if (el != null && (el === video || (container != null && el === container))) return true;\n  return (video as WebkitVideo).webkitDisplayingFullscreen === true;\n}\n\n/**\n * Enter fullscreen for `video`, walking the capability ladder above.\n * `container` is the element to present in element fullscreen (defaults to\n * the video) — pass your player wrapper to keep your own controls visible on\n * platforms that support it. Returns false when NO mechanism exists (some\n * webviews) so the caller can hide or disable its button; rejections from a\n * denied request (permissions policy) propagate.\n *\n * Call from a user gesture: every branch of the ladder requires one.\n */\nexport async function enterVideoFullscreen(\n  video: HTMLVideoElement,\n  container?: HTMLElement,\n): Promise<boolean> {\n  const target = (container ?? video) as FullscreenTarget;\n  if (typeof target.requestFullscreen === 'function') {\n    await target.requestFullscreen();\n    return true;\n  }\n  if (typeof target.webkitRequestFullscreen === 'function') {\n    target.webkitRequestFullscreen();\n    return true;\n  }\n  const v = video as WebkitVideo;\n  if (v.webkitSupportsFullscreen && typeof v.webkitEnterFullscreen === 'function') {\n    v.webkitEnterFullscreen();\n    return true;\n  }\n  return false;\n}\n\n/**\n * Leave whichever fullscreen presentation `video` is in: iPhone's video-native\n * fullscreen first (it reports only through the video, never the document),\n * then element fullscreen via the document (unprefixed or prefixed). Safe to\n * call when not fullscreen — and scoped to THIS video's presentation: an\n * unrelated element's fullscreen (a lightbox, another player) is left alone,\n * mirroring the ownership check exitPictureInPicture makes on\n * pictureInPictureElement. Pass the same `container` you entered with.\n */\nexport async function exitVideoFullscreen(\n  video: HTMLVideoElement,\n  container?: HTMLElement,\n): Promise<void> {\n  const v = video as WebkitVideo;\n  if (v.webkitDisplayingFullscreen) {\n    v.webkitExitFullscreen?.();\n    return;\n  }\n  const doc = ownerDoc(video);\n  const el = fullscreenElement(doc);\n  if (el != null && (el === video || (container != null && el === container))) {\n    if (typeof doc.exitFullscreen === 'function') await doc.exitFullscreen();\n    else doc.webkitExitFullscreen?.();\n  }\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   * Extra headers attached to every request to {@link baseUrl} — and only\n   * there: never to CDN or license hosts. `x-api-key` cannot be overridden.\n   */\n  apiHeaders?: Record<string, 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    apiHeaders: opts.apiHeaders,\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   * Extra headers attached to every request to {@link baseUrl} — and only\n   * there: never to CDN or license hosts. `x-api-key` cannot be overridden.\n   */\n  apiHeaders?: Record<string, 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    apiHeaders: opts.apiHeaders,\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   * Extra headers attached to every request to {@link baseUrl} — and only\n   * there: never to CDN or license hosts. `x-api-key` cannot be overridden.\n   */\n  apiHeaders?: Record<string, string>;\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        apiHeaders: opts.apiHeaders,\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\n/**\n * Why a stream ended (havik-streams #191). `ended` alone cannot tell a viewer\n * whether the MATCH finished or only its stream dropped — two situations that\n * deserve opposite messaging:\n *\n * - `interrupted` — the match is still running; no session is serving (an\n *   ingest outage that outlived the bridge's reconnect grace). Play is\n *   expected to resume, so surface \"reconnecting\", never \"this has ended\".\n * - `match_ended` — the match itself finished. A real end.\n *\n * Absent on every other state, and absent entirely from servers predating the\n * field — so treat `undefined` as \"unknown, assume a real end\" (what clients\n * did before it existed).\n */\nexport type EndedReason = 'interrupted' | 'match_ended';\n\nexport interface LiveStateEvent {\n  matchUrn: string;\n  state: LiveState;\n  /** Set only on `ended`; see {@link EndedReason}. */\n  reason?: EndedReason;\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   * How long a request may stay silent before it is treated as dead and\n   * reconnected. Applies to both phases: waiting for response headers, and\n   * waiting for the next byte on an open connection. Default 45000 — two\n   * missed 20s server heartbeats. Set `0` to disable the watchdog (a silent\n   * connection then stays open forever, which is what this subscription did\n   * before the watchdog existed).\n   */\n  idleTimeoutMs?: number;\n}\n\n/** Two missed 20s server heartbeats. See {@link SubscribeLiveStateOptions.idleTimeoutMs}. */\nconst DEFAULT_IDLE_TIMEOUT_MS = 45_000;\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 connection that stops\n * delivering without closing is caught by the idle watchdog\n * (`idleTimeoutMs`) — see the note on it in `readStream`. A subscription\n * failure is always non-fatal to playback — callers keep the HLS staleness\n * watchdog as 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  // Not floored: a caller that asks for a very short window (a test) gets it —\n  // the reconnect rate is bounded by minBackoff either way. 0/negative/NaN\n  // disable the watchdog.\n  const idleRaw = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;\n  const idleMs = Number.isFinite(idleRaw) && idleRaw > 0 ? idleRaw : 0;\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    onIdle: () => void,\n  ): Promise<void> => {\n    const reader = body.getReader();\n    const decoder = new TextDecoder();\n    let buf = '';\n    let gotData = false;\n    let idleTimer: ReturnType<typeof setTimeout> | undefined;\n    // Idle watchdog. `reader.read()` below parks until the body yields, ends,\n    // or errors — and a black-holed socket (laptop sleep, NAT rebind, proxy\n    // half-close) does none of those: no FIN ever reaches us, so the read\n    // never wakes and the subscription is permanently dead AND silent (no\n    // onError, no reconnect). The server's ~30min lifetime cap only helps when\n    // its close actually arrives. So: arm on open (this also covers\n    // headers-then-no-body-ever), rearm on every chunk, and on expiry tear the\n    // attempt down so run()'s loop reconnects.\n    const rearmIdle = (): void => {\n      if (!idleMs) return;\n      clearTimeout(idleTimer);\n      idleTimer = setTimeout(() => {\n        onIdle(); // flags the attempt and aborts the request (socket teardown)\n        // The abort alone is not enough: the body stream is already handed to\n        // this reader, and cancel() is what resolves the parked read() as\n        // `done` and unblocks the loop.\n        void reader.cancel().catch(() => {});\n      }, idleMs);\n    };\n    try {\n      rearmIdle();\n      for (;;) {\n        const { value, done } = await reader.read();\n        if (done) return;\n        if (value && value.length > 0) {\n          rearmIdle();\n          if (!gotData) {\n            gotData = true;\n            onData(); // first bytes flowed — the connection is genuinely healthy\n          }\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      clearTimeout(idleTimer);\n      await reader.cancel().catch(() => {});\n    }\n  };\n\n  const run = async (): Promise<void> => {\n    let backoff = minBackoff;\n    while (!closed) {\n      // Per-attempt abort handle. The subscription-wide `ctrl` must stay\n      // unaborted across reconnects (the catch below returns on it, and a\n      // later fetch would be handed an already-aborted signal), so the idle\n      // watchdog needs its own handle to tear down just THIS request. Chained\n      // so close() still kills the in-flight one; the relay is removed in the\n      // finally, or one closure would leak per reconnect on a long session.\n      const attempt = new AbortController();\n      const relayAbort = (): void => attempt.abort();\n      ctrl.signal.addEventListener('abort', relayAbort, { once: true });\n      // Set by whichever watchdog tore this attempt down, and reported after\n      // the try/finally. Also suppresses the AbortError the teardown raises —\n      // that error IS the teardown, and these messages name the cause.\n      let idleReason: string | null = null;\n      let connectTimer: ReturnType<typeof setTimeout> | undefined;\n      const onIdle = (): void => {\n        idleReason = `no data for ${idleMs}ms`;\n        attempt.abort();\n      };\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        // Connect watchdog. `fetch` has no timeout of its own, so a socket that\n        // black-holes during the HANDSHAKE (dropped SYN, captive portal, a NAT\n        // that lost the mapping) parks this await forever — the same silent\n        // death readStream's watchdog closes, one step earlier and not reachable\n        // from there, since that one is only armed once a body exists. The two\n        // belong together: a reconnect triggered BY the stream watchdog goes out\n        // over a network that just changed under us, so it is exactly the\n        // request most likely to hit a black hole. (A `credential` provider that\n        // never settles is the caller's own promise and out of scope — aborting\n        // cannot unpark the await above it.)\n        if (idleMs) {\n          connectTimer = setTimeout(() => {\n            idleReason = `no response headers within ${idleMs}ms`;\n            attempt.abort();\n          }, idleMs);\n        }\n        const res = await fetch(url, {\n          method: 'GET',\n          headers: { 'x-api-key': cred.apiKey, accept: 'text/event-stream' },\n          signal: attempt.signal,\n          cache: 'no-store',\n          credentials: 'omit',\n          mode: 'cors',\n        });\n        clearTimeout(connectTimer); // headers are in; readStream arms its own\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(\n            res.body,\n            () => {\n              backoff = minBackoff;\n            },\n            onIdle,\n          );\n        }\n      } catch (err) {\n        if (closed || ctrl.signal.aborted) return;\n        // A watchdog teardown reports itself below. The AbortError it raises\n        // here is the teardown, not a second failure — and this IS the usual\n        // path in a real browser, where aborting errors the body stream and\n        // rejects the parked read() before cancel() can resolve it as `done`.\n        if (!idleReason) opts.onError?.(err);\n      } finally {\n        clearTimeout(connectTimer);\n        ctrl.signal.removeEventListener('abort', relayAbort);\n        attempt.abort(); // leave no request dangling behind a finished attempt\n      }\n      if (closed) return;\n      if (idleReason) {\n        opts.onError?.(new Error(`live-state subscribe idle: ${idleReason} — reconnecting`));\n      }\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","/**\n * Per-element CDM reuse, so rebuilding the player never has to give a\n * `<video>` a second ContentDecryptionModule — which Chrome forbids outright.\n *\n * ## The browser rule\n *\n * Once a media element has both a MediaKeys and a media player, Chrome\n * refuses to change or remove that CDM. BOTH of these reject:\n *\n *     video.setMediaKeys(null)          // remove\n *     video.setMediaKeys(otherKeys)     // replace\n *\n * with `InvalidStateError: Failed to execute 'setMediaKeys' on\n * 'HTMLMediaElement': The existing ContentDecryptionModule object cannot be\n * removed at this time.`\n *\n * Measured on Chromium 152 (`org.w3.clearkey`, secure context): removal is\n * allowed while the element has no `src`, and starts failing the moment a\n * MediaSource is attached — with `readyState` still 0, nothing buffered,\n * nothing played, and no encrypted media involved. So the bar is not \"a\n * decoder holds the key\"; it is \"the element has a player\". Every DRM rebuild\n * crosses it.\n *\n * ## Why hls.js's own mitigation cannot help\n *\n * hls.js serialises CDM teardown through a static `EMEController`\n * `CDMCleanupPromise`: the destroyed instance's `_clear()` calls\n * `setMediaKeys(null)` and the next instance waits on it before setting its\n * own keys. On Chrome that clear is exactly as forbidden as the set it is\n * meant to make room for. `_clear()` swallows the rejection as a NON-FATAL\n * `KEY_SYSTEM_DESTROY_MEDIA_KEYS_ERROR`, the cleanup promise resolves anyway,\n * and the new instance then fails its own `setMediaKeys` — that one fatally,\n * as `KEY_SYSTEM_NO_KEYS`. Ordering was never the problem, so no amount of\n * queueing fixes it. (Verified against the bundled 1.7.2: `_clear` at\n * `dist/hls.js:27580`, the swallowed catch at `:27604`.)\n *\n * ## What we do instead\n *\n * Hand every hls.js instance on a given element the SAME MediaKeys, via the\n * `requestMediaKeySystemAccessFunc` config hook. Nothing then tries to\n * replace the CDM:\n *\n *  - `EMEController.onMediaAttached` seeds `this.mediaKeys` from\n *    `media.mediaKeys` (`dist/hls.js:27568`), and `attemptSetMediaKeys`\n *    early-returns when the keys it is asked to set are already the element's\n *    (`:27026`) — so the forbidden call is never made; and\n *  - even if that early-return is missed, `setMediaKeys(sameObject)` is\n *    permitted by the browser (measured), so the fallback is benign.\n *\n * Two independent layers, which is what makes this safe to rest on across\n * hls.js upgrades: the first is an hls.js implementation detail, the second is\n * the platform's own behaviour.\n *\n * This is not a compromise on freshness. Once an element has a CDM, that CDM\n * is the only one it can ever have — the alternative is not \"a fresh CDM\", it\n * is \"a fresh `<video>`\", and the element belongs to the host application\n * (`createPlayer({ video })`), which may hold its own reference, listeners and\n * styling on it. Reuse is what lets a rebuild stay invisible to the host.\n *\n * What a rebuild actually needs is fresh *hls.js* state — the wedged\n * fragment pipeline, the orphaned key-load promise — not a fresh CDM, and it\n * still gets that. Fresh key SESSIONS also still work: closing a session and\n * opening another on a reused MediaKeys is permitted (measured), which is what\n * the license retry depends on.\n *\n * Scope: reuse covers keys this module created for the element. An element\n * that already carries a MediaKeys set by the host is not adopted — it is not\n * a case the SDK creates, and adopting a CDM whose key system we cannot\n * confirm would trade a clear error for a confusing one.\n */\n\n/** Shape of the hls.js `requestMediaKeySystemAccessFunc` hook. */\ntype MediaKeyRequest = (\n  keySystem: string,\n  supportedConfigurations: MediaKeySystemConfiguration[],\n) => Promise<MediaKeySystemAccess>;\n\n/**\n * element → key system → its one MediaKeys, as a promise so concurrent\n * requests (hls.js can attempt several key systems) share a single creation.\n * Weak on the element so a discarded `<video>` takes its CDM entry with it.\n */\nconst keysByElement = new WeakMap<HTMLMediaElement, Map<string, Promise<MediaKeys>>>();\n\nfunction mediaKeysFor(media: HTMLMediaElement, access: MediaKeySystemAccess): Promise<MediaKeys> {\n  let bySystem = keysByElement.get(media);\n  if (!bySystem) {\n    bySystem = new Map();\n    keysByElement.set(media, bySystem);\n  }\n  const cached = bySystem.get(access.keySystem);\n  if (cached) return cached;\n  // Evict on failure: a transient createMediaKeys() rejection must not pin a\n  // permanently-rejected promise to the element for the rest of the page.\n  const created = access.createMediaKeys().catch((err: unknown) => {\n    if (bySystem.get(access.keySystem) === created) bySystem.delete(access.keySystem);\n    throw err;\n  });\n  bySystem.set(access.keySystem, created);\n  return created;\n}\n\n/**\n * Whether this page can request key-system access at all. False in a\n * non-secure context (EME is secure-context-only, so plain `http://` embeds\n * see no API), in a non-browser runtime, and in an embedder that strips it.\n *\n * The engine installs the hook below only when this holds, so that when it does\n * NOT hold hls.js falls back to its own null `requestMediaKeySystemAccessFunc`\n * and reports the cause — including the specific \"not available over insecure\n * protocol\" message (`dist/hls.js:26637`), which is the likeliest way an\n * integrator meets this. Installing our hook unconditionally would shadow that\n * diagnostic with an opaque TypeError.\n */\nexport function emeAvailable(): boolean {\n  return (\n    typeof navigator !== 'undefined' && typeof navigator.requestMediaKeySystemAccess === 'function'\n  );\n}\n\n/**\n * Video robustness rungs appended below whatever the caller asked for, weakest\n * last. Widevine's ladder continues to '' below this, and that rung is\n * deliberately absent: '' means \"no requirement\", which would turn a precise\n * `keySystemNoAccess` at access time into an opaque decrypt failure much later,\n * and no measured CDM has needed it. Audio is not laddered — the engine already\n * asks for the weakest named audio level (SW_SECURE_CRYPTO).\n */\nconst VIDEO_ROBUSTNESS_FALLBACKS = ['SW_SECURE_DECODE', 'SW_SECURE_CRYPTO'] as const;\n\ntype VideoRobustness = (typeof VIDEO_ROBUSTNESS_FALLBACKS)[number];\n\n/** True for the Widevine key system, the only one whose robustness we ladder. */\nfunction isWidevine(keySystem: string): boolean {\n  return keySystem.startsWith('com.widevine');\n}\n\n/**\n * Expand each configuration into an ordered preference list: the caller's own\n * request first, then the same request at successively weaker VIDEO robustness.\n *\n * `requestMediaKeySystemAccess` takes candidates in descending preference and\n * returns the FIRST supported one, so this can only widen — a CDM that accepts\n * today's request still matches on entry 0 and is granted exactly what it is\n * granted now. It is the CDMs that accept NONE of our pins that change: they\n * get a weaker but working configuration instead of a hard rejection.\n *\n * Measured (Android 15 emulator, Chrome 124, Widevine L3, 2026-09-16): that\n * CDM supports video robustness SW_SECURE_CRYPTO and '' only. The engine's\n * SW_SECURE_DECODE — and every HW_* level — rejected with `NotSupportedError:\n * Unsupported keySystem or supportedConfigurations.`, which hls.js reports as a\n * fatal keySystemNoAccess and the SDK surfaces as an unplayable stream. With\n * the ladder the same call resolves at SW_SECURE_CRYPTO, keeping cbcs, and\n * createMediaKeys() succeeds.\n *\n * Exported for tests; the hook below is the only production caller.\n */\nexport function withVideoRobustnessFallbacks(\n  keySystem: string,\n  configurations: MediaKeySystemConfiguration[],\n): MediaKeySystemConfiguration[] {\n  if (!isWidevine(keySystem)) return configurations;\n\n  /** True when `rung` is strictly weaker than every level this config asks for. */\n  const weakens = (config: MediaKeySystemConfiguration, rung: VideoRobustness): boolean => {\n    const video = config.videoCapabilities;\n    // Nothing to weaken: a config that pins no video robustness already\n    // imposes no video requirement, so a rung below it would be identical.\n    if (!video?.length || !video.some((cap) => cap.robustness)) return false;\n    return video.every((cap) => {\n      const asked = VIDEO_ROBUSTNESS_FALLBACKS.indexOf(cap.robustness as VideoRobustness);\n      // An unrecognised level (HW_*, or a future name) sorts above the whole\n      // ladder: every rung here is weaker, so all of them are offered.\n      return asked === -1 || VIDEO_ROBUSTNESS_FALLBACKS.indexOf(rung) > asked;\n    });\n  };\n\n  // EVERY caller configuration comes before ANY weakening we synthesized.\n  // Interleaving per-config (config, its rungs, next config, its rungs) would\n  // let a weakened first configuration outrank a later one the caller asked\n  // for outright: given [strictA, strictB] on a CDM that rejects strictA but\n  // accepts both weakA and strictB, the browser returns the FIRST match — it\n  // would pick weakA and silently drop to a lower protection level while the\n  // caller's own strictB was available. Fallbacks are ours, not the caller's,\n  // so they rank last; within them, rung-major order so the strongest\n  // remaining protection is offered first across all configurations.\n  const fallbacks = VIDEO_ROBUSTNESS_FALLBACKS.flatMap((robustness) =>\n    configurations\n      .filter((config) => weakens(config, robustness))\n      .map((config) => ({\n        ...config,\n        videoCapabilities: config.videoCapabilities?.map((cap) => ({ ...cap, robustness })),\n      })),\n  );\n  return [...configurations, ...fallbacks];\n}\n\n/**\n * Build the `requestMediaKeySystemAccessFunc` for an element: a real access\n * request (so the caller's `supportedConfigurations` are honoured and a\n * genuinely unsupported system still rejects), wrapped so `createMediaKeys()`\n * resolves to the element's one MediaKeys.\n */\nexport function reuseMediaKeysFor(media: HTMLMediaElement): MediaKeyRequest {\n  return (keySystem, supportedConfigurations) => {\n    // hls.js calls this hook and returns its result straight into a promise\n    // chain, so a synchronous throw would escape its error handling. Keep the\n    // contract the default hook has: always a promise, rejected on failure.\n    if (!emeAvailable()) {\n      return Promise.reject(\n        new Error(\n          'navigator.requestMediaKeySystemAccess is unavailable — EME needs a secure context (https, or localhost)',\n        ),\n      );\n    }\n    return navigator\n      .requestMediaKeySystemAccess(\n        keySystem,\n        withVideoRobustnessFallbacks(keySystem, supportedConfigurations),\n      )\n      .then((access) => ({\n        keySystem: access.keySystem,\n        getConfiguration: () => access.getConfiguration(),\n        createMediaKeys: () => mediaKeysFor(media, access),\n      }));\n  };\n}\n\n// Deliberately no way to drop an element's cached CDM. There is no correct\n// caller: engine teardown runs on every rebuild, which is exactly when the\n// entry must survive, and player teardown does not help either — if the host\n// reuses the element for another player, that player still cannot be given a\n// different CDM. The WeakMap covers the only case that matters, an element\n// going out of scope.\n","/**\n * DRM capability detection — answers \"can protected playback work in this\n * browser at all?\" so a host can show an accurate message instead of a black\n * player (e.g. iOS Chrome/Firefox/Edge: WKWebView exposes the EME API surface\n * but grants no CDM — Widevine does not exist on iOS and WebKit withholds\n * FairPlay from third-party browsers).\n *\n * Design rules (each encodes a real failure mode):\n *  - PROBE, don't UA-sniff: the probe is ground truth for playability. UA\n *    strings are spoofable, iPadOS masquerades as macOS, and in-app WebViews\n *    carry no browser token. Hosts may still use the UA to WORD a message —\n *    never to gate playback.\n *  - `requestMediaKeySystemAccess` resolving is NOT proof: some platforms\n *    resolve access and then fail `createMediaKeys()` — both must succeed.\n *  - Bounded: some WebView builds never settle the access promise; without a\n *    timeout the caller's UI hangs on \"checking…\".\n *  - Lazy by contract: the EME spec allows the call to have user-visible\n *    effects (consent prompts, CDM provisioning downloads). Only call this\n *    once you know the content is DRM-protected — never on page load.\n *  - Per-system configs must mirror what the engine will actually request:\n *    Widevine uses the SW_SECURE_* pins; FairPlay must NOT (WebKit's\n *    supportedRobustnesses() for FPS is { '' } — a Widevine-shaped probe\n *    false-negatives on Safari).\n *  - `SecurityError` is reported separately: a cross-origin iframe missing\n *    allow=\"encrypted-media\" (or an https embed in an http parent) is an\n *    embedding misconfiguration, not an unsupported browser.\n */\nimport type { DrmSystem } from '../types';\nimport { withVideoRobustnessFallbacks } from './cdm';\n\nexport type DrmSupportVerdict =\n  /** At least one probed system produced working MediaKeys. */\n  | 'supported'\n  /** EME exists but no probed system yields a CDM (e.g. iOS third-party browsers). */\n  | 'no-cdm'\n  /** Not a secure context — EME is unavailable by spec (http://<LAN-IP> dev, etc.). */\n  | 'insecure-context'\n  /** SecurityError: permissions-policy / iframe allow=\"encrypted-media\" missing. */\n  | 'blocked-by-policy'\n  /** The access promise never settled within timeoutMs (some WebViews). */\n  | 'probe-timeout';\n\nexport interface DrmSupportResult {\n  verdict: DrmSupportVerdict;\n  /** The first system that probed as working (when verdict is 'supported'). */\n  system?: DrmSystem;\n  /** Per-system outcome, for diagnostics/support bundles. */\n  detail: Partial<Record<DrmSystem, string>>;\n}\n\nexport interface DetectDrmSupportOptions {\n  /** Systems to probe, in order. Default: both. */\n  systems?: DrmSystem[];\n  /** OVERALL budget for the whole call before concluding 'probe-timeout' —\n   *  shared across every probed system/stage, not per probe. Default 5000ms. */\n  timeoutMs?: number;\n}\n\nconst DEFAULT_PROBE_TIMEOUT_MS = 5_000;\n\nconst VIDEO_MP4 = 'video/mp4; codecs=\"avc1.42E01E\"';\nconst AUDIO_MP4 = 'audio/mp4; codecs=\"mp4a.40.2\"';\n\n/** Key-system strings per DRM system. EXACTLY what the hls.js engine\n *  configures — the probe predicts the ENGINE, not abstract capability, so a\n *  key system the engine never requests (e.g. legacy `com.apple.fps.1_0`,\n *  which hls.js has no fallback to) must not produce a 'supported' verdict. */\nconst KEY_SYSTEMS: Record<DrmSystem, string[]> = {\n  widevine: ['com.widevine.alpha'],\n  fairplay: ['com.apple.fps'],\n};\n\n/** One config per system, mirroring the engine's EFFECTIVE access request\n *  (hls.ts drmSystemOptions → hls.js builds a single configuration):\n *  Widevine carries the SW_SECURE_* pins, FairPlay the empty robustness\n *  WebKit requires, and BOTH pin encryptionScheme cbcs exactly as the engine\n *  does. (Not literally byte-for-byte: initDataTypes ordering and the\n *  canonical codec strings differ from hls.js's manifest-derived ones, and\n *  spec-default fields are omitted — none of which can flip a verdict.) A\n *  WEAKER probe config (pin-less, scheme-less) would report 'supported' on\n *  CDMs the engine's stricter request then fails on — the exact\n *  optimistic-message-then-black-player this API exists to prevent. */\nfunction probeConfigs(system: DrmSystem): MediaKeySystemConfiguration[] {\n  if (system === 'fairplay') {\n    return [\n      {\n        initDataTypes: ['sinf', 'skd', 'cenc'],\n        videoCapabilities: [{ contentType: VIDEO_MP4, robustness: '', encryptionScheme: 'cbcs' }],\n        audioCapabilities: [{ contentType: AUDIO_MP4, robustness: '', encryptionScheme: 'cbcs' }],\n      },\n    ];\n  }\n  // Laddered through the same helper the engine's access hook uses, so the\n  // probe keeps mirroring the EFFECTIVE request. Without this the probe would\n  // be STRICTER than the engine and report 'no-cdm' for a device the engine\n  // then plays on — the same false verdict this file exists to prevent, only\n  // inverted.\n  return withVideoRobustnessFallbacks('com.widevine.alpha', [\n    {\n      initDataTypes: ['cenc'],\n      videoCapabilities: [\n        { contentType: VIDEO_MP4, robustness: 'SW_SECURE_DECODE', encryptionScheme: 'cbcs' },\n      ],\n      audioCapabilities: [\n        { contentType: AUDIO_MP4, robustness: 'SW_SECURE_CRYPTO', encryptionScheme: 'cbcs' },\n      ],\n    },\n  ]);\n}\n\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n  return new Promise<T>((resolve, reject) => {\n    const t = setTimeout(() => reject(new Error('probe-timeout')), ms);\n    p.then(\n      (v) => {\n        clearTimeout(t);\n        resolve(v);\n      },\n      (e) => {\n        clearTimeout(t);\n        reject(e);\n      },\n    );\n  });\n}\n\n/**\n * Probe whether DRM playback can work here. Resolves — never rejects.\n *\n * Call it lazily (only for content whose descriptor says `drmEnabled`), CACHE\n * the result yourself (probes are not memoized, and the EME spec allows each\n * call to show consent prompts / trigger CDM downloads), and treat\n * 'supported' as necessary-not-sufficient: the licence server, CORS\n * onboarding and content packaging still have to cooperate.\n */\nexport async function detectDrmSupport(\n  opts: DetectDrmSupportOptions = {},\n): Promise<DrmSupportResult> {\n  const systems = opts.systems ?? (['widevine', 'fairplay'] as DrmSystem[]);\n  const timeoutMs = opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;\n  const detail: Partial<Record<DrmSystem, string>> = {};\n\n  if (typeof window !== 'undefined' && window.isSecureContext === false) {\n    return { verdict: 'insecure-context', detail };\n  }\n  const rmksa =\n    typeof navigator !== 'undefined'\n      ? navigator.requestMediaKeySystemAccess?.bind(navigator)\n      : undefined;\n  if (!rmksa) {\n    // Secure context but no EME entry point at all (very old browser, or an\n    // embedder stripped it) — indistinguishable from no CDM for the caller.\n    return { verdict: 'no-cdm', detail };\n  }\n\n  let sawTimeout = false;\n  let sawSecurityError = false;\n  // One WALL-CLOCK deadline for the whole call: stages consume the remaining\n  // budget, so N systems × 2 stages can never stack into N×2 × timeoutMs of\n  // \"checking…\" UI hang.\n  const deadline = Date.now() + timeoutMs;\n  const remaining = (): number => Math.max(1, deadline - Date.now());\n\n  for (const system of systems) {\n    const keySystems = KEY_SYSTEMS[system];\n    if (!keySystems) {\n      // Untyped callers can pass arbitrary strings — record, don't reject\n      // (the documented contract is \"resolves, never rejects\").\n      detail[system] = 'unknown system';\n      continue;\n    }\n    for (const keySystem of keySystems) {\n      if (Date.now() >= deadline) {\n        sawTimeout = true;\n        detail[system] ??= `${keySystem}: probe-timeout`;\n        break;\n      }\n      try {\n        const access = await withTimeout(rmksa(keySystem, probeConfigs(system)), remaining());\n        // Access alone is not proof — the CDM must actually instantiate.\n        await withTimeout(access.createMediaKeys(), remaining());\n        detail[system] = `ok (${keySystem})`;\n        return { verdict: 'supported', system, detail };\n      } catch (err) {\n        // Read name/message structurally: DOMException is not `instanceof\n        // Error` in every runtime (jsdom, older WebKit) and the distinction\n        // matters — SecurityError must not degrade into 'no-cdm'. Prefer the\n        // message when the name is the generic 'Error' so a timeout is\n        // legible in support bundles.\n        const e = err as { name?: string; message?: string } | undefined;\n        const name = (e?.name && e.name !== 'Error' ? e.name : e?.message) || String(err);\n        detail[system] = `${keySystem}: ${name}`;\n        if (e?.message === 'probe-timeout') sawTimeout = true;\n        if (e?.name === 'SecurityError') sawSecurityError = true;\n        // NotSupportedError (no CDM for this key system) → try the next one.\n      }\n    }\n  }\n\n  if (sawSecurityError) return { verdict: 'blocked-by-policy', detail };\n  if (sawTimeout) return { verdict: 'probe-timeout', detail };\n  return { verdict: 'no-cdm', detail };\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","// Named Oddin environments and the API base URL each one means.\n//\n// WHY THESE ARE PINNED HERE, since pinning an endpoint into a published\n// package is normally the wrong instinct: a hand-written `baseUrl` is\n// invisible when it is wrong. A typo, a stale env var, or the wrong\n// environment produces a player that either fails in a way that looks like an\n// outage, or — worse — works while quietly losing everything derived from the\n// host. The beacons endpoint is derived from `baseUrl` by hostname convention\n// (`feed[-dev].<domain>` → `beacons[-dev].<domain>`), so an off-convention\n// baseUrl means QoE analytics is off; an integrator who selects `env` instead\n// cannot land in that state at all, because the base is a feed host by\n// construction.\n//\n// AND WHY `baseUrl` SURVIVES ANYWAY. Pinning couples the endpoint to a\n// release, and this SDK is consumed by integrators who self-host a pinned\n// build and upgrade rarely. If these constants were the only way to reach the\n// platform, a domain change would strand every one of them. `baseUrl` is\n// therefore not deprecated: it is the escape hatch for that, for a customer\n// whose own CDN or gateway fronts the streams API, and for the CN plane,\n// whose ICP-registered host is not under this domain and rotates.\n//\n// Exactly one of the two is the right answer for any given integration, which\n// is why the callers reject being given both rather than picking a winner.\n\nimport { PlaybackError } from './errors';\n\n/**\n * A named Oddin environment.\n *\n * - `integration` — the shared pre-production stack. Where customers build\n *   and test against real matches.\n * - `production` — the production stack. A separate distribution, not an\n *   alias of integration.\n */\nexport type HavikEnv = 'integration' | 'production';\n\nconst BASE_URLS: Readonly<Record<HavikEnv, string>> = {\n  integration: 'https://feed-dev.oddin-video.gg',\n  production: 'https://feed.oddin-video.gg',\n};\n\n/**\n * The API base URL for a named environment.\n *\n * Mode A takes {@link HavikEnv} directly (`createPlayer({ env: 'production' })`).\n * Mode B and Mode C still take a `baseUrl`, so call this to get one rather\n * than copying a hostname into your own config:\n *\n * ```ts\n * resolveStream({ baseUrl: baseUrlForEnv('production'), matchUrn, credential });\n * ```\n *\n * Throws on an unrecognized value rather than returning undefined — callers\n * reach this from plain JS where the type gives no protection, and a silent\n * undefined becomes a relative URL against the embedding page.\n */\nexport function baseUrlForEnv(env: HavikEnv): string {\n  // Own-property lookup, not a bare index. A bare BASE_URLS['constructor']\n  // resolves an INHERITED Object.prototype property and returns a FUNCTION,\n  // which is truthy — so the guard below would pass it through, createPlayer\n  // would carry a function as its base URL, and joinUrl would die on\n  // `base.replace is not a function`: a TypeError from deep inside the SDK\n  // instead of the typed error naming the two valid values. `toString` and\n  // `__proto__` do the same. Verified before fixing: all three returned\n  // without throwing.\n  //\n  // hasOwnProperty.call, not Object.hasOwn — the global build targets\n  // firefox91/safari15 (vite.global.config.ts) and Object.hasOwn needs\n  // Firefox 92 / Safari 15.4, which Vite downlevels syntax for but does not\n  // polyfill. Same guard and same reasoning as core/environment.ts.\n  const url = Object.prototype.hasOwnProperty.call(BASE_URLS, env) ? BASE_URLS[env] : undefined;\n  if (!url) {\n    throw new PlaybackError(\n      'INTERNAL',\n      0,\n      `unknown env ${JSON.stringify(env)} — expected 'integration' or 'production'`,\n    );\n  }\n  return url;\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  /**\n   * TOTAL frames this session — the denominator dropped frames need, and it\n   * ALREADY INCLUDES the dropped ones (it is\n   * `VideoPlaybackQuality.totalVideoFrames`). So the ratio is\n   * `droppedFrames / decodedFrames`, with nothing added to the bottom.\n   *\n   * The name says \"decoded\" because it is the wire field's name and a\n   * contract rename is not worth a migration (plan D10). Read it as \"total\".\n   */\n  decodedFrames?: 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  /** Cumulative media segment bytes fetched this session (hls.js engine only). */\n  bytesLoaded?: number;\n  /** Cumulative manifest/playlist bytes fetched this session (hls.js engine only). */\n  playlistBytesLoaded?: number;\n  /** Cumulative ABR rendition switches this session (hls.js engine only). */\n  renditionSwitches?: number;\n  /** Bitrate of the current rendition in kbps, when known. */\n  renditionBitrateKbps?: number;\n  /** Duration of the most recent DRM license acquisition in ms (0 = none yet). */\n  licenseTimeMs?: number;\n\n  // ---- Contract v2 (QoE plane). Cumulative where the beacon layer takes\n  // deltas; a snapshot otherwise. Engines fill what they can observe.\n  /** RFC 6381 codec strings of the current rendition. */\n  videoCodec?: string;\n  audioCodec?: string;\n  /** The offered rendition ladder, ascending heights, and its top bitrate. */\n  ladderHeights?: number[];\n  ladderTopBitrateKbps?: number;\n  /** DRM the engine negotiated for the session. */\n  keySystem?: 'widevine' | 'fairplay' | 'none';\n  /** Robustness requested/granted, e.g. Widevine L3 (software) on the web. */\n  drmSecurityLevel?: string;\n  /** The viewer pinned a quality; ABR is not deciding. */\n  qualityPinned?: boolean;\n  /** Cumulative ABR switch direction split (renditionSwitches = both). */\n  upshifts?: number;\n  downshifts?: number;\n  /** Cumulative media requests completed / failed, and the durations of the\n   *  most recent ones (ms, newest last, bounded) for interval percentiles. */\n  requestCount?: number;\n  requestErrors?: number;\n  requestTimesMs?: number[];\n  /** Host of the most recent media request — host only, never the path. */\n  cdnHost?: string;\n}\n\nexport const EMPTY_STATS: PlaybackStats = { droppedFrames: 0 };\n\n/**\n * Host of a media URL, without the path. The beacon plane records WHICH CDN\n * served the session — so region and vendor come from the client with no IP\n * anywhere in the chain — never the path, which on a signed slot carries the\n * token.\n *\n * Shared by both engines on purpose: two copies would be two answers to\n * \"which CDN was this\", and the column is a LowCardinality string where\n * `CDN.example` and `cdn.example` would become two entries for one CDN. The\n * URL parser lowercases for us. `host`, not `hostname`: the port is signal.\n */\nexport function hostOf(url: string): string {\n  try {\n    return new URL(url).host;\n  } catch {\n    return '';\n  }\n}\n\n/** Media URLs, playlists included. The playlist matters more than the\n *  segments here — see mediaHostSince. */\nconst MEDIA_URL = /\\.(m3u8|ts|m4s|mp4|cmfv|cmfa)(\\?|$)/i;\n\n/**\n * The CDN host for an engine that does its own fetching, read back out of\n * Resource Timing.\n *\n * WebKit's native HLS player exposes NO per-segment network detail to script,\n * which is why the native engine reports no request timings. It does however\n * leave the media load in the resource timeline, so the host is recoverable\n * even though the transfer is not.\n *\n * Measured on iOS 26 (real WebKit, cross-origin stream), 2026-09-17:\n *\n *   master.m3u8   1 entry, initiatorType 'video'\n *   segments      0 entries, over 22 s of confirmed playback\n *\n * So this finds the PLAYLIST, not a segment, and two things follow:\n *\n *   - It works cross-origin with no `Timing-Allow-Origin` anywhere. TAO gates\n *     the TIMING fields, never `name` — the entry arrives with responseStart\n *     and transferSize zeroed and its URL intact, and the URL is all we want.\n *   - The host is fixed at session start. hls.js re-reads it on every\n *     fragment precisely to catch a mid-session CDN failover; this engine\n *     cannot see one. Where a deployment serves playlists and segments from\n *     different hostnames, this would name the playlist's.\n *\n * `since` is a performance.now() mark taken when the engine was built, so a\n * stream played EARLIER on the same page cannot donate its host to this\n * session.\n */\nexport function mediaHostSince(since: number): string {\n  if (typeof performance === 'undefined' || typeof performance.getEntriesByType !== 'function') {\n    return '';\n  }\n  let best = '';\n  let bestStart = -1;\n  // getEntriesByType is typed as PerformanceEntry[]; the 'resource' list is\n  // PerformanceResourceTiming, which is where initiatorType lives.\n  const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[];\n  for (const e of entries) {\n    if (e.startTime < since) continue;\n    // initiatorType 'video' is what WebKit attributes the native media load\n    // to; the extension test carries any engine that attributes differently.\n    if (e.initiatorType !== 'video' && e.initiatorType !== 'audio' && !MEDIA_URL.test(e.name)) {\n      continue;\n    }\n    if (e.startTime >= bestStart) {\n      const host = hostOf(e.name);\n      if (host) {\n        best = host;\n        bestStart = e.startTime;\n      }\n    }\n  }\n  return best;\n}\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/**\n * Frame counts via the standard media quality API (engine-agnostic). Both\n * numbers come from ONE call: a drop count without its denominator says\n * nothing — 300 dropped frames is a broken session at 30fps and a rounding\n * error over an hour.\n *\n * 🔑 `totalFrames`, NOT `decoded`. It is `VideoPlaybackQuality.totalVideoFrames`,\n * which the spec defines as every frame that WOULD have been presented —\n * dropped frames included. So `dropped / totalFrames` is already dropped over\n * total, and the denominator needs no correction.\n *\n * The old name was `decoded`, which reads as \"successfully decoded\" and\n * therefore as \"excluding dropped\". That invites a reader to fix a ratio that\n * was never broken, by switching the denominator to `decoded + dropped` and\n * double-counting every dropped frame. The maths was right; only the name was\n * wrong. Pinned by test/frames.test.ts.\n *\n * The WIRE field keeps its name (`decoded_frames_*`): renaming a contract\n * field to fix a comment is not worth a migration.\n */\nexport function readFrameStats(video: HTMLVideoElement): { dropped: number; totalFrames: number } {\n  const q = (\n    video as HTMLVideoElement & { getVideoPlaybackQuality?: () => VideoPlaybackQuality }\n  ).getVideoPlaybackQuality?.();\n  return { dropped: q?.droppedVideoFrames ?? 0, totalFrames: q?.totalVideoFrames ?? 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 { classifyDrmStatus, PlaybackError } from '../errors';\nimport {\n  mediaHostSince,\n  readFrameStats,\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/**\n * Pull the 32-char hex content IV out of Safari's FairPlay initData.\n *\n * WebKit hands `encrypted` the playlist key URI verbatim — `skd://<kid>:<iv>`\n * as UTF-8 — which is also Axinom's documented asset ID. Returns undefined\n * for any other shape so a malformed URI sends no header at all rather than a\n * stale or wrong one.\n */\nfunction extractSkdIV(initData: ArrayBuffer): string | undefined {\n  try {\n    const uri = new TextDecoder('utf-8').decode(new Uint8Array(initData));\n    return /^skd:\\/\\/[0-9a-fA-F-]{36}:([0-9a-fA-F]{32})$/.exec(uri)?.[1];\n  } catch {\n    return undefined;\n  }\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 * True when the media stack is WebKit's — the only one that decrypts FairPlay\n * through native HLS.\n *\n * `nativeHlsSupported` cannot answer this. Chromium returns `\"maybe\"` for\n * `canPlayType('application/vnd.apple.mpegurl')` (measured 2026-08-07) while\n * being unable to play HLS natively at all, so gating the FairPlay engine on\n * it alone routes Chrome into the native engine and fails every playback with\n * an opaque 'native HLS playback error'.\n *\n * Both signals are present on Safari and on iOS third-party browsers (all\n * WebKit), and absent on Chromium — which reports `vendor: \"Google Inc.\"` and\n * exposes neither legacy FairPlay entry point. OR rather than AND so a future\n * WebKit that drops one of them still reaches the engine that works.\n */\nexport function webkitMediaStack(): boolean {\n  if (typeof window === 'undefined') return false;\n  const legacyFairPlay =\n    'WebKitMediaKeys' in window ||\n    (typeof HTMLMediaElement !== 'undefined' && 'webkitSetMediaKeys' in HTMLMediaElement.prototype);\n  const appleVendor = typeof navigator !== 'undefined' && /Apple/i.test(navigator.vendor ?? '');\n  return legacyFairPlay || appleVendor;\n}\n\n/**\n * Native-HLS engine — plays via `video.src`, and drives FairPlay EME itself.\n *\n * This is the FairPlay engine on Apple platforms. The previous note here said\n * FairPlay was \"implemented on the hls.js engine, which 'auto' prefers\n * everywhere hls.js is supported (including modern Safari via\n * ManagedMediaSource)\". That was wrong, and it cost a lot of debugging: the\n * hls.js/MSE route reaches a 'usable' key and still renders nothing, because\n * WebKit binds FairPlay keys through the fragment `sinf` box rather than a\n * synthesized skd asset ID. Measured on macOS Safari 26.5 against live DRM\n * matches (2026-08-06): hls.js+MSE 1 pass / 6 attempts, this engine 4 / 4 at\n * 1280px (Widevine on Chrome 3/3 as the control).\n *\n * Widevine is not available here — that stays on the hls.js engine.\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  private onEncrypted?: (ev: Event) => void;\n  private fairplayLicenseUrl: string | undefined;\n  /** Kept so teardown can close the CDM session. */\n  private keySession: MediaKeySession | null = null;\n  /** Set synchronously on the first `encrypted` event; see armFairPlay. */\n  private fairplayArmed = false;\n  /** Content IV (32-char hex) parsed from Safari's skd:// initData. */\n  private fairplayKeyIV: string | undefined;\n  /** False while a deferLoad engine waits for startLoad(); see load(). */\n  private loadStarted: boolean;\n  /** True once load() rewrote the element's preload; destroy() restores it. */\n  private preloadTouched = false;\n  /** The preload attribute before load() rewrote it (null = not set). */\n  private savedPreload: string | null = null;\n  /** CDN host, recovered from Resource Timing once and kept; see getStats. */\n  private cdnHost = '';\n  /** Resource-timeline mark taken at construction, so a stream played EARLIER\n   *  on this page cannot donate its host to this session. */\n  private readonly builtAt: number;\n\n  constructor(deps: EngineDeps) {\n    this.deps = deps;\n    this.loadStarted = deps.deferLoad !== true;\n    this.builtAt = typeof performance !== 'undefined' ? performance.now() : 0;\n  }\n\n  async load(): Promise<void> {\n    const { video, descriptor, emit } = this.deps;\n\n    // deferLoad (autoplay-off pre-play): preload 'none' keeps the native\n    // pipeline from fetching the manifest, media, or FairPlay keys until the\n    // viewer presses play. 'ready' (loadedmetadata) then arrives after the\n    // first play() starts fetching — the managed player does not wait on it.\n    if (!this.loadStarted) {\n      this.preloadTouched = true;\n      this.savedPreload = video.getAttribute('preload');\n      video.preload = 'none';\n    }\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            'DRM_CLIENT',\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      this.armFairPlay();\n    }\n\n    // deferLoad: the managed layer owns the pre-play 'ready' for this engine\n    // (emitted after analytics/session wiring, a macrotask after commit — an\n    // emit here would fire inside load(), before the analytics session exists\n    // and before the host can hold listeners). loadedmetadata can't fire\n    // until play() starts fetching under preload=\"none\", so the engine-side\n    // emit is suppressed entirely for a deferred instance — it would only\n    // duplicate the managed one after startLoad() + play().\n    const deferred = !this.loadStarted;\n    this.onLoaded = deferred ? undefined : () => 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    if (this.onLoaded) video.addEventListener('loadedmetadata', this.onLoaded, { once: true });\n    video.addEventListener('error', this.onError);\n    video.src = withCmcdSid(descriptor.manifestUrl, descriptor.analytics?.sid);\n    video.load();\n  }\n\n  startLoad(): void {\n    if (this.loadStarted) return;\n    this.loadStarted = true;\n    // Putting the host's own preload back is enough: the play() that follows\n    // drives the fetch regardless of the attribute's value, and forcing\n    // 'auto' would override a host-set \"metadata\" for the rest of the\n    // session. An explicit video.load() here would abort that play()'s\n    // pending promise (the media load algorithm rejects it with AbortError).\n    this.restorePreload();\n  }\n\n  /** Undo the deferLoad preload rewrite so the element reads as the host set\n   *  it. Idempotent; a no-op when load() never rewrote it. */\n  private restorePreload(): void {\n    if (!this.preloadTouched) return;\n    this.preloadTouched = false;\n    const { video } = this.deps;\n    if (this.savedPreload !== null) video.setAttribute('preload', this.savedPreload);\n    else video.removeAttribute('preload');\n  }\n\n  /**\n   * Drive FairPlay EME on the native HLS pipeline.\n   *\n   * This is how FairPlay is meant to be deployed on Apple platforms, and it\n   * is the path that actually decrypts. Safari raises `encrypted` with\n   * initDataType 'skd' and the playlist key URI as initData — the full\n   * `skd://<kid>:<iv>` string, UTF-8, 75 bytes — which is exactly Axinom's\n   * documented asset ID. We never synthesize or reshape it.\n   *\n   * The hls.js/MSE route cannot do this: WebKit binds a FairPlay key to\n   * media through the fragment `sinf` box, so a session built from a\n   * synthesized skd asset ID is never associated with the stream (key\n   * reports 'usable', element sits at waitingforkey, black), and driving it\n   * from the sinf instead loses the IV that only the URI carries. Measured\n   * on macOS Safari 26.5 against live DRM matches (2026-08-06): hls.js+MSE\n   * 1 pass / 6, this path 4 passes / 4 at 1280px, with Widevine on Chrome\n   * 3/3 as the control.\n   *\n   * Requires drm-proxy to put the content IV on every inline key of the\n   * entitlement message (`content_keys_source.inline[].iv`) — Axinom rejects\n   * a FairPlay request without one unless the asset ID supplies it.\n   */\n  private armFairPlay(): void {\n    const { video, descriptor, credential, deviceId, userId, emit } = this.deps;\n    const fp = descriptor.drm?.fairplay;\n    if (!fp) return;\n    this.fairplayLicenseUrl = fp.licenseUrl;\n\n    // Only a server auth/entitlement refusal is UNAUTHORIZED/FORBIDDEN;\n    // certificate/CDM/transport failures are DRM_CLIENT so callers can tell\n    // \"not allowed\" from \"broke on this device\".\n    const fail = (message: string, status: number, fatal: boolean) =>\n      emit({\n        type: 'error',\n        error: new PlaybackError(classifyDrmStatus(status), status, message),\n        fatal,\n      });\n\n    this.onEncrypted = (event: Event) => {\n      const ev = event as MediaEncryptedEvent;\n      // Safari raises `encrypted` several times in quick succession (once per\n      // track / init segment). The guard must flip BEFORE the first await or\n      // every event enters the async block and races to build its own session.\n      if (!ev.initData || this.fairplayArmed) return;\n      this.fairplayArmed = true;\n      // Safari's initData IS the playlist key URI: skd://<kid>:<iv>, UTF-8.\n      // Axinom needs that IV on every inline key of the entitlement message,\n      // and drm-proxy has no other source for it, so pass it along.\n      this.fairplayKeyIV = extractSkdIV(ev.initData);\n      void (async () => {\n        try {\n          const access = await navigator.requestMediaKeySystemAccess('com.apple.fps', [\n            {\n              initDataTypes: [ev.initDataType],\n              videoCapabilities: [{ contentType: 'application/vnd.apple.mpegurl', robustness: '' }],\n              distinctiveIdentifier: 'not-allowed',\n              persistentState: 'not-allowed',\n              sessionTypes: ['temporary'],\n            },\n          ]);\n          const mediaKeys = await access.createMediaKeys();\n          // drm-proxy requires the api key on the certificate GET.\n          const certRes = await fetch(fp.certificateUrl, {\n            headers: { 'x-api-key': credential.apiKey },\n          });\n          if (!certRes.ok) {\n            // Carry the real status so a certificate 401/403 keeps the server\n            // taxonomy instead of collapsing to DRM_CLIENT/0 in the catch below.\n            this.fairplayArmed = false; // allow a retry on the next `encrypted`\n            fail(`FairPlay certificate request failed: ${certRes.status}`, certRes.status, true);\n            return;\n          }\n          await mediaKeys.setServerCertificate(await certRes.arrayBuffer());\n          await video.setMediaKeys(mediaKeys);\n\n          const session = mediaKeys.createSession();\n          this.keySession = session;\n          session.addEventListener('message', (msgEvent) => {\n            void (async () => {\n              try {\n                // Read the URL fresh: setLicenseUrls may have refreshed the\n                // signed URL since the session was created (10-min TTL).\n                const res = await fetch(this.fairplayLicenseUrl ?? fp.licenseUrl, {\n                  method: 'POST',\n                  body: msgEvent.message,\n                  headers: {\n                    'x-api-key': credential.apiKey,\n                    'X-Match-Urn': descriptor.matchUrn,\n                    'X-Device-Id': deviceId,\n                    ...(userId ? { 'X-User-Id': userId } : {}),\n                    ...(this.fairplayKeyIV ? { 'X-Key-Iv': this.fairplayKeyIV } : {}),\n                  },\n                });\n                if (!res.ok) {\n                  fail(`FairPlay license request failed: ${res.status}`, res.status, true);\n                  return;\n                }\n                await session.update(new Uint8Array(await res.arrayBuffer()));\n              } catch (e) {\n                fail(`FairPlay license request failed: ${String(e)}`, 0, true);\n              }\n            })();\n          });\n          await session.generateRequest(ev.initDataType, ev.initData as BufferSource);\n        } catch (e) {\n          this.fairplayArmed = false; // allow a retry on the next `encrypted`\n          fail(`FairPlay key session failed: ${String(e)}`, 0, true);\n        }\n      })();\n    };\n    video.addEventListener('encrypted', this.onEncrypted);\n  }\n\n  setLicenseUrls(urls: { widevine?: string; fairplay?: string }): void {\n    // Keep the refreshed signed FairPlay URL for the next license request;\n    // Widevine is not reachable on this engine.\n    if (urls.fairplay) this.fairplayLicenseUrl = urls.fairplay;\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 frames = readFrameStats(v);\n    const stats: PlaybackStats = {\n      droppedFrames: frames.dropped,\n      decodedFrames: frames.totalFrames,\n    };\n    // Request timing, ABR direction and codecs stay invisible to this engine:\n    // WebKit fetches the media itself and reports no per-segment network\n    // detail to script. That is plan gate G8, not a gap to fill here.\n    //\n    // The CDN host is the exception, and it is worth the exception: without\n    // it every Safari and iOS session lands in the empty cohort of a per-CDN\n    // breakdown, which on a Safari-heavy audience is most of the traffic.\n    // Resource Timing still carries the media load even though the transfer\n    // detail is gone — see mediaHostSince, which documents what WebKit does\n    // and does not expose, and why this names the PLAYLIST's host.\n    //\n    // Cached on first sight: the host cannot change for this engine (there is\n    // no failover to observe), the entry can be evicted once the resource\n    // buffer fills, and re-scanning the timeline on every heartbeat for a\n    // value that never moves is waste.\n    if (!this.cdnHost) this.cdnHost = mediaHostSince(this.builtAt);\n    if (this.cdnHost) stats.cdnHost = this.cdnHost;\n    if (this.deps.descriptor.drmEnabled) stats.keySystem = 'fairplay';\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    if (this.onEncrypted) video.removeEventListener('encrypted', this.onEncrypted);\n    // Release the CDM session; a stale one would keep the key alive past\n    // teardown and block a fresh arm on the next load().\n    void this.keySession?.close().catch(() => {});\n    this.keySession = null;\n    this.fairplayArmed = false;\n    this.fairplayKeyIV = undefined;\n    try {\n      video.pause();\n    } catch {\n      // ignore\n    }\n    video.removeAttribute('src');\n    this.restorePreload(); // a still-deferred engine dies with 'none' applied\n    try {\n      video.load();\n    } catch {\n      // ignore\n    }\n  }\n}\n\n/**\n * Append the CMCD session id to the manifest URL (CTA-5004 query mode).\n * The native pipeline fetches segments itself with no request hook, so the\n * sid can only ride the manifest request — the known partial-attribution\n * cohort of plan gate G8. No-op without a sid.\n */\nfunction withCmcdSid(manifestUrl: string, sid: string | undefined): string {\n  if (!sid) return manifestUrl;\n  const sep = manifestUrl.includes('?') ? '&' : '?';\n  return `${manifestUrl}${sep}CMCD=${encodeURIComponent(`sid=\"${sid}\"`)}`;\n}\n","import Hls, {\n  CapLevelController,\n  type ErrorData,\n  type HlsConfig,\n  type LevelLoadedData,\n} from 'hls.js';\nimport { emeAvailable, reuseMediaKeysFor } from '../drm/cdm';\nimport { classifyDrmStatus, PlaybackError } from '../errors';\nimport { nativeHlsSupported, webkitMediaStack } from './native';\nimport {\n  hostOf,\n  readFrameStats,\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// Stale-playlist chase suspension. A frozen live manifest keeps hls.js's\n// LatencyController extrapolating the live edge with wall clock, so measured\n// latency grows 1s/s; once it crosses liveMaxLatencyDuration — which happens\n// ceiling−target ≈ 9-10s after the freeze with our defaults, BEFORE the 12s\n// watchdog concludes — the controller force-seeks into data that doesn't\n// exist and the viewer sees a repeat-frame/reset loop until 'ended' lands.\n// The server can't cover this window: the SSE ended push may trail the\n// freeze by up to the bridge's 120s ingest-teardown debounce (feed-dies-\n// first path), so the client suspends chasing itself once the playlist has\n// been stale this long, and resumes the moment it advances again (a genuine\n// origin hiccup keeps full drift protection afterwards). Must stay below\n// ceiling−target (10s at the default target, 9s at liveLatencyTarget 3) and\n// above the 3s watch tick + normal playlist cadence.\nconst CHASE_SUSPEND_STALE_MS = 6_000;\n// Pause suspension. hls.js keeps chasing the live edge while the element is\n// PAUSED: synchronizeToLiveEdge runs on every playlist reload with no\n// `media.paused` check, so once the frozen playhead falls liveMaxLatencyDuration\n// (12s here) behind the edge it is reset to the live-sync position and fresh\n// segments load from there — forever. A paused 1080p viewer pulled the stream\n// at live rate (80–105 MB/min, latency sawing 4.5→12s, portal session\n// 2026-09-19). The pre-play case was already closed by deferLoad (AV-202); this\n// closes the post-play one: after this grace the loader is stopped, and the\n// next play() restarts it at the paused position (hls.js startLoad(-1) resumes\n// at lastCurrentTime; its next playlist reload re-syncs to live if far behind —\n// the same seek as before, now only when the viewer asked to play). A pause\n// shorter than the grace resumes from the untouched buffer. Must stay below\n// ceiling−target (10s) so the loader is quiet before the first forced re-sync.\nconst PAUSE_SUSPEND_MS = 5_000;\n// Video-starvation watchdog (live only). Kept as defence in depth: the wedge\n// below is fixed in the pinned 1.7.2 (upstream #7874 moved key loading behind\n// the part/hint selection, and we backported it to the 1.6.x line as #7976),\n// but the detection is generic — it catches any silently-dropped video load,\n// not only this mechanism, and it is the only thing that noticed this one.\n// The original wedge, on hls.js 1.6.x and encrypted LL-HLS: when a part of the\n// forward-looking `fragmentHint` fragment is selected while that\n// fragment's key-loading promise is in flight, `_doFragLoad` reassigns\n// `fragCurrent` to the hint fragment, the key promise's context check then\n// sees a \"changed\" fragment and resolves null, and the load is silently\n// dropped with the state machine parked in FRAG_LOADING (a sibling path parks\n// it in KEY_LOADING). No loader is in flight, so no load-policy timeout ever\n// fires: the level playlist keeps polling, audio keeps playing, and no video\n// fragment is ever requested again. Reproduced 2026-08-12 against feed-dev\n// with the fullscreen/PiP resize storm from Josef's report — the resize makes\n// `capLevelToPlayerSize` flap levels, which corrupts targetBufferTime past the\n// playlist's fragment end and steers loading onto the hint fragment.\n// Detection: the video element starved (readyState < HAVE_FUTURE_DATA) while\n// the live playlist still advances and hls.js has issued no video fragment\n// load for a while — that combination is never a plain rebuffer (rebuffering\n// keeps FRAG_LOADING events coming) and never stream end (the playlist would\n// be stale, which the end watchdog owns). Recovery: stopLoad() + startLoad(-1)\n// rebuilds the stream-controller state machine at the live edge; media stays\n// attached so EME key sessions survive and playback resumes in ~1-2s.\nconst STARVATION_FRAG_QUIET_MS = 4_000;\n/** Consecutive starved watchdog ticks (3s apart) before kicking. */\nconst STARVATION_TICKS = 2;\nconst MAX_STARVATION_KICKS = 5;\n// Escalation, for the wedges the kick provably cannot clear. A kick keeps the\n// media element attached — deliberately, so EME key sessions survive — but that\n// is exactly why it fails against a wedge held in hls.js's KEY-loader cache: a\n// `keyInfo.keyLoadPromise` that never settles is only discarded by detach() or\n// destroy(), so stopLoad()/startLoad() re-await the same dead promise and park\n// again within a second. Observed 2026-09-03 at join, when a client clock\n// running behind the licence server produced a first key status of `expired`:\n// hls.js renewed the key session and orphaned the promise the fragment loads\n// were waiting on, and all five kicks re-parked (the viewer's own recovery was\n// a page reload, which destroys the instance). Fixed at the source in\n// havik-drm (licence start_datetime is back-dated) and structurally in hls.js\n// 1.7.x (the EME path no longer caches that promise) — this is the client-side\n// net for the next key-status hiccup, and for whatever else the kick cannot\n// reach: if the pipeline is still starved this long after a kick, the instance\n// is unrecoverable in place and gets rebuilt, which is what a reload did.\n// Grace after a kick for main-track media to buffer before rebuilding, counted\n// in watchdog ticks rather than wall-clock milliseconds. Ticks only accrue\n// while the tab is visible and the wedge signature still holds, so the grace\n// measures ACTIVE starvation: a tab backgrounded right after a kick cannot burn\n// it through throttled or suspended timers and come back eligible for an\n// immediate rebuild. Same reason STARVATION_TICKS is a tick count.\nconst STARVATION_KICK_GRACE_TICKS = 2;\n/** Rebuilds per healthy-playback window. Two, because a rebuild that doesn't\n *  take won't start taking: a fresh instance re-requests the licence, so the\n *  second attempt covers a transient failure and further ones just churn. */\nconst MAX_STARVATION_REBUILDS = 2;\n// SDK-managed player-size capping (replaces hls.js's `capLevelToPlayerSize`\n// poller). hls.js re-reads the player's pixel box every 1s and applies a new\n// cap IMMEDIATELY, so a fullscreen enter/exit — which animates through\n// intermediate sizes for a few hundred ms — produces 1-2 ABR switches per\n// transition. At the LL live-sync target the forward buffer IS ~2s, and each\n// switch costs a cold rendition-playlist fetch at the CDN edge plus a decoder\n// re-init (slowest through Windows hardware-DRM pipelines), so every switch is\n// a visible ~1s stall right after the transition (the fullscreen-exit stutter\n// follow-up to Josef's freeze report: 1-2 stalls per exit, worst on Edge).\n// The SDK owns the cap instead: identical level selection — hls.js's own\n// CapLevelController.getMaxLevelByMediaSize on the DPR-scaled element box —\n// but a change only applies once the box has been STABLE for a settle window,\n// so mid-animation intermediate sizes never reach ABR. Raises settle briefly:\n// entering fullscreen should still upgrade quickly. (Known trade vs upstream,\n// VOD only: upstream flushed the forward buffer on a cap raise, so deep-buffer\n// VOD upgraded within ~a fragment; here the raise shows once buffered media\n// plays out. On live-LL the flush was a no-op anyway — hls.js skips it below\n// 2x target duration of buffer, which our latency target guarantees.)\nconst CAP_RAISE_SETTLE_MS = 2_000;\n// A shrink needs no switch to look right — the higher rendition simply\n// downscales into the smaller box; a down-cap only saves bandwidth. Applied\n// lazily, so a fullscreen exit never pays a switch stall at the transition and\n// a re-enter inside the window cancels the pending down-cap outright.\nconst CAP_SHRINK_LAZY_MS = 30_000;\n\n/**\n * Default live-edge target (seconds behind the edge) when the caller sets none.\n *\n * 2s, not the bleeding `PART-HOLD-BACK` edge (~1.5s = 3x part duration), so the\n * playhead parks where Tencent's edge has already settled the parts — chasing\n * hold-back on a low-traffic stream hits the cold per-object origin-pull flap\n * (parts/.m3u8 404<->200) and rebuffers continuously.\n *\n * Was 3s. Lowered after the operator Demo Player (havik-ops\n * streams-player.component.ts) ran 2s from 2026-07-22 to 2026-08-03 with no\n * regression, on srt-bridge rc.34+ (encoder-latency pins) where the cold-edge\n * part flap no longer reproduces. 2s keeps ~0.35s of settle margin over\n * hold-back instead of ~1.35s. If low-traffic streams start rebuffering again,\n * that view is the canary — restore 3 there first.\n */\nconst LIVE_SYNC_DURATION_S = 2;\n/** Recovery band above the target; the drift ceiling is target + this (min 12s). */\nconst LIVE_LATENCY_MARGIN_S = 9;\n/**\n * How far past the live-sync position a seek may land before it's clamped back.\n * Slack so hls.js's own forward nudges — the ≤1.5x catch-up overshooting by a\n * frame, a maxBufferHole gap-skip — never trip the clamp; a scrub to the end of\n * the DVR bar overshoots by seconds, not tenths.\n */\nconst OVER_SEEK_TOLERANCE_S = 0.5;\n/** Recent segment download durations kept for the beacon layer's per-interval\n *  percentiles. One heartbeat spans ~7 segments at a 2s target; 64 leaves room\n *  for LL-HLS part-level traffic without unbounded growth. */\nconst MAX_REQUEST_SAMPLES = 64;\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 ~2s 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  deriveAbrReaction(config, config.liveSyncDuration);\n}\n\n/**\n * Scale ABR's DOWN-switch reaction to the live buffer we actually have.\n *\n * hls.js ships these sized for a multi-second forward buffer:\n * `maxStarvationDelay` 4 and `maxLoadingDelay` 4 (the starvation budget alone\n * is larger than our whole buffer, so ABR's own accounting still reads \"fine\"\n * while the media element is already stalling) and `abrEwmaFastLive` 3 (a\n * bandwidth drop takes 3s+ to move the estimate at all). On a live stream the\n * forward buffer IS `liveSyncDuration`, so at our target there is no runway:\n * a viewer whose link degrades rebuffers at the top rung for seconds instead\n * of dropping a rung. (Reported on the ops demo player, same config family:\n * throttled link → 1080p buffering → manual 720p fixed it.)\n *\n * LL-HLS also blunts hls.js's other rescue — `_abandonRulesCheck` aborts an\n * in-flight load and drops a rung, but it projects the overage over the\n * current PART (sub-second of media), so the overage looks small and the abort\n * rarely fires. These budgets are therefore the primary defence.\n *\n * Derived from the resolved target rather than hard-coded so responsiveness\n * stays consistent if the target moves (the 3s → 2s rollout). Caller values\n * always win — this only fills gaps. Skipped for count-based live sync, where\n * the target isn't expressible in seconds (documented escape hatch).\n */\nfunction deriveAbrReaction(config: Partial<HlsConfig>, syncS: number): void {\n  const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\n  // Half the buffer: ABR must decide while there is still media to play.\n  if (config.maxStarvationDelay == null) {\n    config.maxStarvationDelay = clamp(syncS / 2, 0.5, 4);\n  }\n  // A fragment load that eats the whole buffer is already too slow.\n  if (config.maxLoadingDelay == null) {\n    config.maxLoadingDelay = clamp(syncS, 1, 4);\n  }\n  // The estimate has to move inside the runway, not after it.\n  if (config.abrEwmaFastLive == null) {\n    config.abrEwmaFastLive = clamp(syncS / 2, 1, 3);\n  }\n}\n\n/** hls.js adapter — LL-HLS + Widevine/FairPlay 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 Widevine license URL; mutable so it can be refreshed mid-session. */\n  private licenseUrl: string | undefined;\n  /** FairPlay application-certificate URL (public but auth'd — unsigned, stable). */\n  /** Whether the live media playlist actually carries LL-HLS tags (detected, not requested). */\n  private detectedLowLatency = false;\n  // Consumption counters for the analytics heartbeat (plan A2/§3.1): the\n  // beacon layer samples these cumulative totals via getStats() and computes\n  // per-interval deltas itself, so the engine never needs a beacon cadence.\n  private bytesLoaded = 0;\n  private playlistBytesLoaded = 0;\n  private renditionSwitches = 0;\n  private licenseTimeMs = 0;\n  /** A licence request has actually been issued. The descriptor flag only\n   *  says DRM was CONFIGURED; a session that dies before EME does anything\n   *  should not report a key system it never negotiated. */\n  private licenseRequested = false;\n  // Contract v2 counters. Switch DIRECTION splits renditionSwitches (which\n  // stays the total): a session that only ever shifts down is a starved\n  // viewer, one that oscillates is an unstable estimate, and the two are\n  // indistinguishable from the total alone.\n  private upshifts = 0;\n  private downshifts = 0;\n  private lastLevelIndex = -1;\n  // Request timing and outcome. requestTimesMs is a ring of the most recent\n  // durations; the beacon layer takes the tail its interval added.\n  private requestCount = 0;\n  private requestErrors = 0;\n  private readonly requestTimesMs: number[] = [];\n  private cdnHost = '';\n  // The offered ladder, cached at MANIFEST_PARSED — recomputing it on every\n  // 2s stats poll would walk every level for a value that cannot change.\n  private ladderHeights: number[] | undefined;\n  private ladderTopBitrateKbps = 0;\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  /** seeking handler — clamps a seek past the live-sync position back to it. */\n  private onSeeking: (() => void) | null = null;\n  // Pause suspension state (see PAUSE_SUSPEND_MS).\n  private onPause: (() => void) | null = null;\n  private onPlay: (() => void) | null = null;\n  private pauseSuspendTimer: ReturnType<typeof setTimeout> | null = null;\n  /** Loading is stopped for a lingering pause; the next play() restarts it. */\n  private loadSuspended = false;\n  /** stop() was called (end of stream): a later `play` event must not restart loading. */\n  private stopped = false;\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  // Video-starvation watchdog state (see STARVATION_* constants).\n  private lastFragLoadingMs = 0;\n  private starvedTicks = 0;\n  private starvationKicks = 0;\n  /** A kick is awaiting proof it worked; cleared by main-track FRAG_BUFFERED. */\n  private kickPending = false;\n  /** Active starved ticks observed since that kick (see STARVATION_KICK_GRACE_TICKS). */\n  private kickTicks = 0;\n  private starvationRebuilds = 0;\n  // Stale-playlist chase suspension (see CHASE_SUSPEND_STALE_MS).\n  private chaseSuspended = false;\n  private savedLiveMaxLatency: number | undefined;\n  private savedMaxSyncRate: number | undefined;\n  // SDK-managed player-size cap state (see the CAP_* constants).\n  private capObserver: ResizeObserver | null = null;\n  private capTimer: ReturnType<typeof setTimeout> | null = null;\n  /** Size-derived cap currently applied (level index); null = none applied yet. */\n  private sizeCapLevel: number | null = null;\n  /** Cap derived from setMaxBitrate (-1 = none); combined with the size cap via min. */\n  private bitrateCapLevel = -1;\n  /** False when the caller opted back into hls.js's own capLevelToPlayerSize poller. */\n  private sizeCapManaged = true;\n  // DPR watcher: ResizeObserver only tracks the CSS box, so a devicePixelRatio\n  // change with an unchanged layout (window dragged to a different-DPI\n  // monitor, OS scale change) would never re-evaluate the cap. The media query\n  // pins the CURRENT ratio, so any change fires once; the handler re-pins.\n  private dprMedia: MediaQueryList | null = null;\n  private onDprChange: (() => void) | null = null;\n\n  /** False while a deferLoad engine waits for startLoad(); guards the config\n   *  key on a license-retry rebuild and makes startLoad() idempotent. */\n  private loadStarted: boolean;\n\n  constructor(deps: EngineDeps) {\n    this.deps = deps;\n    this.licenseUrl = deps.descriptor.drm?.widevine?.licenseUrl;\n    this.loadStarted = deps.deferLoad !== true;\n  }\n\n  async load(): Promise<void> {\n    const { descriptor, snapToLiveOnRefocus, video, emit } = this.deps;\n\n    // Fail fast on a DRM-required stream with no system USABLE ON THIS\n    // PLATFORM, instead of loading with EME off (or a definitely-rejecting\n    // drmSystems) and surfacing an opaque KEY_SYSTEM error after the manifest\n    // parses. The platform signal is WebKit-ness: WebKit has FairPlay and\n    // never Widevine; everything else hls.js runs on has Widevine and never\n    // FairPlay. So a FairPlay-only descriptor on Chrome AND a Widevine-only\n    // descriptor on Safari both fail here, with a message that names the\n    // actual mismatch.\n    //\n    // WebKit-ness is NOT native-HLS capability, though the two look alike:\n    // Chromium answers canPlayType('application/vnd.apple.mpegurl') with\n    // \"maybe\" (measured 2026-08-07) while playing no HLS natively. Reading\n    // that as WebKit made `widevineUsable` false on every Chrome session\n    // carrying FairPlay material, and this guard then refused a stream\n    // hls.js plays fine over Widevine.\n    //\n    // Known heuristic edge: some OEM/smart-TV browsers report native-HLS\n    // capability AND ship Widevine (Tizen/WebOS builds). There a\n    // Widevine-ONLY descriptor would fail fast although it might have\n    // played. Accepted: havik-streams always emits both systems (a\n    // both-systems descriptor loads fine there — FairPlay rejects at access\n    // and hls.js falls back to Widevine), and those UAs are outside the\n    // supported matrix. Revisit if a widevine-only + smart-TV case appears.\n    const webkit = nativeHlsSupported(video) && webkitMediaStack();\n    const widevineUsable = Boolean(descriptor.drm?.widevine?.licenseUrl) && !webkit;\n    // FairPlay is NOT usable on this engine. WebKit binds FairPlay keys through\n    // the fragment `sinf` box, so an MSE session built from a synthesized skd\n    // asset ID reaches a 'usable' key and still renders nothing (macOS Safari\n    // 26.5, live DRM: 1 pass / 6 attempts vs 4 / 4 on the native engine).\n    // 'auto' routes FairPlay-on-Apple to NativeHlsEngine; only an explicit\n    // engine:'hls' lands here, and it must fail loudly rather than play black.\n    if (descriptor.drmEnabled && !widevineUsable) {\n      const fairplayOnly = Boolean(descriptor.drm?.fairplay?.licenseUrl) && webkit;\n      emit({\n        type: 'error',\n        error: new PlaybackError(\n          'DRM_CLIENT',\n          0,\n          fairplayOnly\n            ? 'FairPlay cannot be played on the hls.js engine (WebKit binds keys via the media sinf box); use engine \"auto\" or \"native\"'\n            : 'DRM is required but none of the stream’s key systems is usable on this platform (WebKit takes FairPlay only; other browsers take Widevine only)',\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    // Over-seek clamp. Registered here (not in startHls) for the same reason as\n    // the visibility listener: a license-retry rebuild must not stack duplicates.\n    this.onSeeking = () => {\n      this.clampOverSeek();\n      // A seek while suspended — the scrubber, the LIVE button, the refocus\n      // snap above — asks for a frame the stopped loader cannot fetch, so the\n      // picture would stay frozen until play(). Load it, then re-arm: five\n      // seconds of loading per seek, not an open tap.\n      if (this.loadSuspended) {\n        this.resumeLoading();\n        this.schedulePauseSuspend();\n      }\n    };\n    video.addEventListener('seeking', this.onSeeking);\n\n    // Pause suspension (see PAUSE_SUSPEND_MS). Same registration rule: once,\n    // here, so a rebuild cannot stack the handlers.\n    this.onPause = () => this.schedulePauseSuspend();\n    this.onPlay = () => this.resumeLoading();\n    video.addEventListener('pause', this.onPause);\n    video.addEventListener('play', this.onPlay);\n\n    // SDK-managed player-size cap (see the CAP_* constants). Registered once\n    // here so a license-retry rebuild doesn't stack observers; each rebuilt\n    // instance gets the cap re-applied by its MANIFEST_PARSED handler. Guarded:\n    // SSR/older engines without ResizeObserver just play uncapped, exactly as\n    // a caller-disabled cap would.\n    if (this.sizeCapManaged && typeof ResizeObserver !== 'undefined') {\n      this.capObserver = new ResizeObserver(() => this.onMediaResize());\n      this.capObserver.observe(video);\n      this.watchDprChanges();\n    }\n  }\n\n  /**\n   * A `pause` event: arm the suspension grace. Only once media loading has\n   * been asked for (a deferLoad engine before its first play() fetches nothing\n   * to suspend), never after stop() (the stream is over; the loader is already\n   * down and must stay down), and never twice.\n   */\n  private schedulePauseSuspend(): void {\n    if (this.pauseSuspendTimer || this.loadSuspended || this.stopped || !this.loadStarted) return;\n    this.pauseSuspendTimer = setTimeout(() => {\n      this.pauseSuspendTimer = null;\n      const { video } = this.deps;\n      // Re-checked at fire time: a play() inside the grace clears the timer,\n      // but a stop() or an `ended` element between arm and fire does not.\n      if (!video.paused || video.ended || this.stopped || !this.hls) return;\n      try {\n        this.hls.stopLoad();\n      } catch {\n        return; // not suspended: the loader is still running, so the watchdogs must keep watching it\n      }\n      this.loadSuspended = true;\n    }, PAUSE_SUSPEND_MS);\n  }\n\n  /**\n   * A `play` event (or a seek while suspended): cancel a pending suspension,\n   * and if the loader was stopped, restart it. startLoad(-1) resumes at\n   * hls.js's lastCurrentTime (the paused or seeked position); the next\n   * playlist reload then re-syncs to live if the position is past the drift\n   * ceiling — the ordinary go-live seek.\n   */\n  private resumeLoading(): void {\n    if (this.pauseSuspendTimer) {\n      clearTimeout(this.pauseSuspendTimer);\n      this.pauseSuspendTimer = null;\n    }\n    if (!this.loadSuspended) return;\n    this.loadSuspended = false;\n    if (this.stopped || !this.hls) return;\n    // The playlist was deliberately not reloaded while suspended: restart the\n    // staleness and starvation clocks so the quiet window never reads as a\n    // dead stream or a wedged pipeline.\n    this.lastAdvanceMs = Date.now();\n    this.lastFragLoadingMs = 0;\n    this.starvedTicks = 0;\n    // A pause/resume restarts the pipeline the same way a kick does, so a\n    // kick still awaiting proof must not escalate to a rebuild on pre-pause\n    // evidence (same reset stop() makes).\n    this.kickPending = false;\n    this.kickTicks = 0;\n    try {\n      this.hls.startLoad(-1);\n    } catch {\n      /* ignore */\n    }\n  }\n\n  startLoad(): void {\n    if (this.loadStarted) return;\n    this.loadStarted = true;\n    // The first play() of an autoplay-off player kicks the loader and then\n    // calls video.play(). If that play is blocked (a gestureless probe) the\n    // element stays paused with the loader running, MANIFEST_PARSED is long\n    // gone and no pause event will ever come — so this is the third entry\n    // point that must arm the grace. A play that goes through clears it.\n    if (this.deps.video.paused) this.schedulePauseSuspend();\n    // -1 = hls.js default start position: the live-sync point on live\n    // playlists (the same target the armed go-live path lands on), 0 on VOD.\n    // The poster card renders before MANIFEST_PARSED, so this often lands\n    // pre-manifest: hls.js then sets its forceStartLoad flag and re-starts at\n    // config.startPosition (hls.js default -1) once the manifest arrives —\n    // identical here, but a caller hlsConfig.startPosition override would be\n    // honored on the pre-manifest path only.\n    this.hls?.startLoad(-1);\n  }\n\n  /**\n   * Re-evaluate the cap when devicePixelRatio changes without a layout change\n   * (see the dprMedia field). Re-armed after every fire because the query pins\n   * the ratio that was current when it was built. Guarded feature-detection:\n   * without matchMedia (SSR) or MQL addEventListener (legacy WebKit) the cap\n   * simply doesn't track monitor moves — the pre-existing ResizeObserver\n   * behavior, never an error.\n   */\n  private watchDprChanges(): void {\n    if (typeof matchMedia !== 'function') return;\n    this.unwatchDprChanges();\n    const dpr = (typeof self !== 'undefined' && self.devicePixelRatio) || 1;\n    const mql = matchMedia(`(resolution: ${dpr}dppx)`);\n    if (typeof mql.addEventListener !== 'function') return;\n    const handler = () => {\n      this.onMediaResize();\n      this.watchDprChanges();\n    };\n    mql.addEventListener('change', handler);\n    this.dprMedia = mql;\n    this.onDprChange = handler;\n  }\n\n  private unwatchDprChanges(): void {\n    if (this.dprMedia && this.onDprChange) {\n      this.dprMedia.removeEventListener('change', this.onDprChange);\n    }\n    this.dprMedia = null;\n    this.onDprChange = null;\n  }\n\n  /**\n   * Pull a seek that landed ahead of the live-sync position back onto it.\n   *\n   * Dragging the scrub bar (or a native control) to the far right lands the\n   * playhead on `seekable.end()` — the bleeding edge of the live playlist, where\n   * latency reads <1s and Tencent's newest parts aren't reliably published yet.\n   * The forward buffer is then permanently empty: each ~0.5s part plays out\n   * before the next exists, so playback advances a few frames, starves,\n   * advances again. hls.js will NOT recover from this on its own — both of its\n   * live-sync mechanisms only correct being too far BEHIND:\n   * `maxLiveSyncPlaybackRate` rebases to 1.0x once `latency - targetLatency`\n   * goes negative and never slows below it, and `liveMaxLatencyDuration`'s\n   * force-seek (stream-controller's synchronizeToLiveEdge) skips out entirely\n   * while `currentTime >= liveSyncPosition`. Nothing pulls the playhead back, so\n   * the stutter persists for the rest of the session. This clamp is that missing\n   * near-side counterpart to the drift ceiling.\n   *\n   * Clamping on `seeking` (not `seeked`) means the bleeding edge is never even\n   * fetched. `liveSyncPosition` is the same spot seekToLive() targets, so an\n   * over-seek reads as \"go live\" rather than as a broken seek.\n   *\n   * Skipped while chase suspension is active: a stale playlist is exactly the\n   * state #39 stopped seeking in (the extrapolated edge points at data that\n   * isn't there), and the policy there is to play out the buffer and hold the\n   * last frame until 'ended' lands. An over-seek costs nothing then — there's no\n   * live edge left to stutter against.\n   *\n   * Live only: hls.js still reports a non-null `liveSyncPosition` on VOD (it's\n   * derived from liveSyncDuration, which has no VOD meaning), so gating on\n   * `details.live` is what keeps this from capping VOD seeks at duration − 3s.\n   *\n   * Terminates without a re-entrancy flag: the clamped position is at or below\n   * the limit, and the limit only moves forward, so the follow-up `seeking`\n   * event this assignment queues returns at the tolerance check.\n   */\n  private clampOverSeek(): void {\n    const hls = this.hls;\n    if (!hls || this.chaseSuspended) return;\n    const level = hls.levels[hls.currentLevel] ?? hls.levels[hls.loadLevel];\n    if (level?.details?.live !== true) return;\n    const limit = hls.liveSyncPosition;\n    if (typeof limit !== 'number' || !Number.isFinite(limit)) return;\n    const { video } = this.deps;\n    if (video.currentTime <= limit + OVER_SEEK_TOLERANCE_S) return;\n    video.currentTime = limit;\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, debug, hlsConfig, emit } =\n      this.deps;\n\n    // Per-instance watchdog state. A rebuilt instance has issued no kick and\n    // has loaded no fragment, so carrying either across would let the first\n    // tick after a rebuild escalate again on the previous instance's evidence.\n    this.kickPending = false;\n    this.kickTicks = 0;\n    this.starvedTicks = 0;\n    this.lastFragLoadingMs = 0;\n    // The end-of-stream stamps are per-instance for a sharper reason: trackEnd\n    // only restamps lastAdvanceMs when endSN CHANGES, so a rebuilt instance\n    // whose first playlist load repeats the endSN the old one last saw would\n    // keep ageing an advance stamp from before the rebuild, and can cross\n    // CHASE_SUSPEND_STALE_MS — or END_STALE_MS, reporting a healthy stream as\n    // ended. Zeroing them gives the new instance a fresh liveness window:\n    // checkEndStale returns early while lastAdvanceMs is 0, and checkStarvation\n    // treats the playlist as not-yet-alive, so both watchdogs stay disarmed\n    // until this instance sees its own first load. The ENDLIST path\n    // (live→false) and the server's SSE push still catch a real end meanwhile.\n    this.lastEndSN = -1;\n    this.lastAdvanceMs = 0;\n\n    const drmEnabled =\n      descriptor.drmEnabled &&\n      Boolean(descriptor.drm?.widevine?.licenseUrl || descriptor.drm?.fairplay?.licenseUrl);\n    // FairPlay is only offered where WebKit can actually take it (Safari /\n    // iOS — the same signal as native-HLS capability). This also scopes\n    // drmSystemOptions below: the option object is GLOBAL in hls.js (applied\n    // to every key system's requestMediaKeySystemAccess), and WebKit's\n    // supportedRobustnesses() for FairPlay is { '' } — handing it the\n    // Widevine SW_SECURE_* pins makes the FairPlay access request reject.\n\n    const config: Partial<HlsConfig> = {\n      // hls.js internal logging (fragment/part scheduling, ABR switches, EME\n      // key sessions + license traffic) — opt-in, for diagnosing a playback\n      // problem with support. The operator Demo Player (havik-ops\n      // streams-player.component.ts) hardcodes this on; it is the one key the\n      // two configs are meant to differ on, which is why it is an option here\n      // rather than a constant.\n      debug: debug === true,\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      // The SDK-managed size cap still bounds the session to the player's\n      // pixel box (below). Caller startLevel / maxBitrate still override.\n      abrEwmaDefaultEstimate: 1_000_000,\n      // Player-size capping is SDK-managed (see the CAP_* constants): hls.js's\n      // own poller applies a new cap the instant the box changes, which turns\n      // every fullscreen enter/exit into immediate ABR switches — each a\n      // visible ~1s stall at LL buffer depth. Explicit false (the hls.js\n      // default) so the two managers never fight over autoLevelCapping; a\n      // caller hlsConfig `capLevelToPlayerSize: true` opts back into the\n      // hls.js poller and stands the SDK manager down.\n      capLevelToPlayerSize: false,\n      // deferLoad (autoplay-off pre-play): parse the manifest (MANIFEST_PARSED\n      // → 'ready', track lists) but fetch no level playlists or fragments —\n      // and so no EME/license traffic — until startLoad(). Keyed off\n      // loadStarted, not deps.deferLoad, so a license-retry rebuild after\n      // playback began loads eagerly again. The hlsConfig escape hatch (spread\n      // below) wins here like everywhere else — an explicit autoStartLoad:\n      // true is the \"warm the buffer, wait for the gesture\" pattern;\n      // loadStarted is re-synced to the final merged config after the spread.\n      ...(this.loadStarted ? {} : { autoStartLoad: false }),\n      ...(drmEnabled\n        ? {\n            emeEnabled: true,\n            drmSystems: {\n              ...(this.licenseUrl ? { 'com.widevine.alpha': { licenseUrl: this.licenseUrl } } : {}),\n            },\n            // FairPlay is NOT configured here — it lives on NativeHlsEngine.\n            // WebKit binds FairPlay keys through the fragment `sinf` box, so an\n            // MSE session built from a synthesized skd asset ID reaches a\n            // 'usable' key and still renders nothing. load() fails fast above\n            // rather than reaching this config.\n            drmSystemOptions: {\n              videoRobustness: 'SW_SECURE_DECODE',\n              audioRobustness: 'SW_SECURE_CRYPTO',\n              videoEncryptionScheme: 'cbcs',\n              audioEncryptionScheme: 'cbcs',\n            },\n            // Reuse this element's CDM across rebuilds. Chrome refuses to\n            // replace OR remove a media element's ContentDecryptionModule once\n            // the element has a player, so every rebuildInstance() below —\n            // starvation, license retry, or a managed reload() building a fresh\n            // engine on the same <video> — would otherwise die on\n            // `setMediaKeys`: \"The existing ContentDecryptionModule object\n            // cannot be removed at this time\", surfaced as a fatal\n            // KEY_SYSTEM_NO_KEYS. hls.js's CDMCleanupPromise cannot help; its\n            // cleanup call is forbidden too. See core/drm/cdm.ts.\n            //\n            // Only when EME exists: with no key-system API at all (a plain-http\n            // embed — EME is secure-context-only), leaving the hook unset lets\n            // hls.js report that cause itself rather than us shadowing it.\n            ...(emeAvailable()\n              ? { requestMediaKeySystemAccessFunc: reuseMediaKeysFor(video) }\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 license URL also picks up a refreshed\n            // signed URL (hls.js keeps passing the construction-time one).\n            // Widevine only — FairPlay lives on the native engine.\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              // License-acquisition timing for the QoE plane — fleet-wide DRM\n              // regressions (WAF license 403s, the canPlayType trap) were\n              // invisible without it.\n              this.licenseRequested = true;\n              const t0 = performance.now();\n              xhr.addEventListener('loadend', () => {\n                this.licenseTimeMs = Math.max(1, Math.round(performance.now() - t0));\n              });\n            },\n          }\n        : {}),\n      // CMCD in query mode (plan D1/D2): the per-session sid rides every CDN\n      // request as the attribution key the edge-log plane joins on. Query\n      // mode, never headers — header mode would force a CORS preflight per\n      // segment. Only when streams minted a sid (server-side analytics flag).\n      ...(descriptor.analytics?.sid\n        ? {\n            cmcd: {\n              sessionId: descriptor.analytics.sid,\n              contentId: descriptor.matchUrn,\n              useHeaders: false,\n            },\n          }\n        : {}),\n      ...hlsConfig,\n    };\n    // The FairPlay certificate GET goes through hls.js's GENERIC loader (not\n    // licenseXhrSetup), and drm-proxy requires the api key on it (its router\n    // test asserts 401 without). Strictly URL-scoped: manifest/segment\n    // requests are public CDN fetches and must not carry auth. hls.js hands\n    // xhrSetup an UNOPENED xhr — open it first or setRequestHeader throws.\n    // COMPOSED with any caller hlsConfig.xhrSetup (applied post-spread): the\n    // escape hatch must not silently disable certificate auth on WebKit.\n    // ORDER MATTERS: the caller runs FIRST — it gets the unopened xhr hls.js\n    // hands out (the documented contract, and many hooks call open()\n    // themselves; the XHR spec EMPTIES author headers on open(), so auth set\n    // before a caller's open() would be silently wiped). Our cert branch then\n    // opens only if still UNSENT and sets the api key on the final, opened xhr.\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    // The escape hatch wins: a caller that re-enabled hls.js's own player-size\n    // poller gets exactly that, and the SDK manager stands down (two writers on\n    // autoLevelCapping would fight, the 1s poller always winning).\n    this.sizeCapManaged = config.capLevelToPlayerSize !== true;\n    // Sync loadStarted with the FINAL config: a caller hlsConfig.autoStartLoad\n    // may have overridden the deferLoad key above, and a stale flag would make\n    // startLoad() re-kick a loader that is already running (or believe a\n    // caller-stopped one had started).\n    this.loadStarted = config.autoStartLoad !== false;\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      const heights = hls.levels.map((l) => l.height).filter((h): h is number => !!h);\n      this.ladderHeights = heights.length ? [...new Set(heights)].sort((a, b) => a - b) : undefined;\n      this.ladderTopBitrateKbps = hls.levels.length\n        ? Math.round(Math.max(...hls.levels.map((l) => l.bitrate)) / 1000)\n        : 0;\n      // Initial size cap, applied immediately (no settle window): the first\n      // fetch of a small/embedded player must not grab 1080p.\n      this.applySizeCap();\n      // An instance that starts paused — autoplay still pending, autoplay\n      // BLOCKED (the play card, with hls.js chasing live behind it forever:\n      // no `pause` event ever fires for an element that never played), or a\n      // rebuild while suspended (the new instance loads regardless) — gets\n      // the same grace as a pause. Pending autoplay resolves long before it\n      // fires, and the fire-time re-check sees the element playing.\n      this.loadSuspended = false;\n      if (video.paused) this.schedulePauseSuspend();\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\n    // protection. Main-track fragments only: the starvation wedge keeps audio\n    // buffering while video is dead, and audio frags must not refund the kicks.\n    hls.on(Hls.Events.FRAG_BUFFERED, (_e, data) => {\n      if (data?.frag?.type !== 'main') return;\n      // Main-track media buffered, so the last kick did its job: cancel the\n      // pending escalation. This is the only place the grace is cleared, which\n      // is what makes the escalation fire on exactly the case it is for — a\n      // kick that produced no video, rather than one that merely took a while.\n      this.kickPending = false;\n      this.kickTicks = 0;\n      if (\n        (this.mediaRecoveries > 0 ||\n          this.networkRecoveries > 0 ||\n          this.licenseRecoveries > 0 ||\n          this.starvationKicks > 0 ||\n          this.starvationRebuilds > 0) &&\n        !this.recoveryResetTimer\n      ) {\n        this.recoveryResetTimer = setTimeout(() => {\n          this.mediaRecoveries = 0;\n          this.networkRecoveries = 0;\n          this.licenseRecoveries = 0;\n          this.starvationKicks = 0;\n          this.starvationRebuilds = 0;\n          this.recoveryResetTimer = null;\n        }, RECOVERY_RESET_MS);\n      }\n    });\n    // Starvation-watchdog liveness input: a healthy or merely rebuffering\n    // pipeline keeps issuing main-track fragment loads; a wedged one goes quiet.\n    hls.on(Hls.Events.FRAG_LOADING, (_e, data) => {\n      if (data?.frag?.type === 'main') this.lastFragLoadingMs = Date.now();\n    });\n    // Consumption accounting: media segment payload bytes (all tracks — the\n    // dashboard-grade half of D3; edge logs stay the billing truth).\n    hls.on(Hls.Events.FRAG_LOADED, (_e, data) => {\n      const stats = data?.frag?.stats;\n      const loaded = stats?.loaded;\n      if (typeof loaded === 'number') this.bytesLoaded += loaded;\n      // Per-request delivery timing: the viewer-side view of how the CDN is\n      // serving this session, which the edge plane's own latency cannot show\n      // (it never sees the last mile).\n      const loading = stats?.loading;\n      if (loading && loading.end > 0 && loading.start > 0) {\n        this.requestCount += 1;\n        this.requestTimesMs.push(Math.max(0, Math.round(loading.end - loading.start)));\n        if (this.requestTimesMs.length > MAX_REQUEST_SAMPLES) this.requestTimesMs.shift();\n      }\n      const url = data?.frag?.url;\n      if (url) {\n        const host = hostOf(url);\n        // Re-read every fragment on purpose: a mid-session CDN failover is\n        // exactly the event this column exists to make visible.\n        if (host) this.cdnHost = host;\n      }\n    });\n    hls.on(Hls.Events.LEVEL_LOADED, (_e, data: LevelLoadedData) => {\n      // Playlist chatter is nontrivial at 2s LL refresh windows — counted\n      // separately from media bytes (plan §3.1).\n      const loaded = data?.stats?.loaded;\n      if (typeof loaded === 'number') this.playlistBytesLoaded += loaded;\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    // startHls() also runs on a rebuild, and assigning over a live handle\n    // leaks the previous interval: both keep firing, so every checkEndStale /\n    // checkStarvation tick doubles up. That already applied to the\n    // license-retry rebuild (up to MAX_LICENSE_RECOVERIES extra intervals);\n    // it would also break the starvation escalation, whose grace is measured\n    // between ticks — a doubled tick rate re-stamps the kick before the grace\n    // can ever elapse.\n    if (this.endWatchdog) clearInterval(this.endWatchdog);\n    this.endWatchdog = setInterval(() => {\n      this.checkEndStale();\n      this.checkStarvation();\n    }, END_WATCH_INTERVAL_MS);\n    hls.on(Hls.Events.LEVEL_SWITCHED, (_e, data) => {\n      // Rendition is only sampled at heartbeats; without this counter, ABR\n      // churn between samples is invisible (plan §3.1).\n      this.renditionSwitches += 1;\n      // Direction by BITRATE, not by index: level order is a property of\n      // the manifest, not a contract, and a reordered reload would invert\n      // every switch this session reports.\n      const from = this.lastLevelIndex >= 0 ? hls.levels[this.lastLevelIndex] : undefined;\n      const to = hls.levels[data.level];\n      if (from && to && to.bitrate !== from.bitrate) {\n        if (to.bitrate > from.bitrate) this.upshifts += 1;\n        else this.downshifts += 1;\n      }\n      this.lastLevelIndex = data.level;\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      this.bitrateCapLevel = -1;\n    } else {\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      this.bitrateCapLevel = cap;\n    }\n    // Routed through pushCap so clearing the bitrate budget no longer wipes the\n    // player-size cap (both used to write autoLevelCapping directly, last one\n    // winning until hls.js's 1s poller re-imposed the size cap).\n    this.pushCap();\n  }\n\n  /**\n   * Level index the player's current pixel box caps ABR to, or null when it\n   * can't be computed (no levels yet, or a zero-size/hidden box — upstream\n   * skips those the same way). Selection is hls.js's own\n   * CapLevelController.getMaxLevelByMediaSize, on the element's bounding box\n   * scaled by the same DPR rules the upstream poller uses\n   * (ignoreDevicePixelRatio / maxDevicePixelRatio from the merged config), so\n   * flipping between the two managers can never change WHICH level is chosen —\n   * only WHEN it is applied.\n   */\n  private computeSizeCapTarget(): number | null {\n    const hls = this.hls;\n    if (!hls || hls.levels.length === 0) return null;\n    const { video } = this.deps;\n    const rect = video.getBoundingClientRect();\n    let width = rect.width;\n    let height = rect.height;\n    if (!width && !height) {\n      // Mirror upstream getDimensions(): a media element that isn't laid out\n      // (equivalent to not being in the DOM) falls back to its width/height\n      // attributes — without this, off-DOM playback would never receive an\n      // initial cap, since ResizeObserver doesn't fire for detached elements.\n      width = video.width || 0;\n      height = video.height || 0;\n    }\n    if (!(width > 0) || !(height > 0)) return null;\n    const cfg: Partial<HlsConfig> = hls.config ?? {};\n    let scale = 1;\n    if (!cfg.ignoreDevicePixelRatio && typeof self !== 'undefined' && self.devicePixelRatio > 0) {\n      scale = self.devicePixelRatio;\n    }\n    scale = Math.min(scale, cfg.maxDevicePixelRatio ?? Number.POSITIVE_INFINITY);\n    return CapLevelController.getMaxLevelByMediaSize(hls.levels, width * scale, height * scale);\n  }\n\n  /**\n   * ResizeObserver callback: debounce cap changes so only a SETTLED box ever\n   * reaches ABR. Every size event restarts the pending window, so a fullscreen\n   * transition animating through intermediate sizes keeps deferring until the\n   * box stops moving — and a box that settles back onto the applied cap (exit\n   * + prompt re-enter) cancels the pending change without ever touching ABR.\n   */\n  private onMediaResize(): void {\n    if (!this.sizeCapManaged) return;\n    const target = this.computeSizeCapTarget();\n    if (target == null) return;\n    if (this.sizeCapLevel == null) {\n      // Nothing applied yet (manifest parsed while the box was hidden/zero):\n      // this is the initial cap, not a change — apply immediately.\n      this.applySizeCap();\n      return;\n    }\n    if (target === this.sizeCapLevel) {\n      if (this.capTimer) {\n        clearTimeout(this.capTimer);\n        this.capTimer = null;\n      }\n      return;\n    }\n    if (this.capTimer) clearTimeout(this.capTimer);\n    const delay = target > this.sizeCapLevel ? CAP_RAISE_SETTLE_MS : CAP_SHRINK_LAZY_MS;\n    this.capTimer = setTimeout(() => {\n      this.capTimer = null;\n      // The box can move between the last observed event and this firing\n      // (ResizeObserver delivery is frame-aligned, timers aren't): apply only\n      // if the box still selects the level this window was scheduled FOR;\n      // otherwise re-debounce in the fresh direction — a raise window must\n      // never be the door a shrink walks through.\n      if (this.computeSizeCapTarget() === target) this.applySizeCap();\n      else this.onMediaResize();\n    }, delay);\n  }\n\n  /** Recompute the size cap from the CURRENT box and apply it (recomputing at\n   *  timer fire, not schedule time, so a stale pending value is never applied). */\n  private applySizeCap(): void {\n    if (!this.sizeCapManaged) return;\n    if (this.capTimer) {\n      clearTimeout(this.capTimer);\n      this.capTimer = null;\n    }\n    const target = this.computeSizeCapTarget();\n    if (target == null) return;\n    this.sizeCapLevel = target;\n    this.pushCap();\n  }\n\n  /** Combine the size cap and the setMaxBitrate cap (min wins) into hls.js's\n   *  single autoLevelCapping knob. */\n  private pushCap(): void {\n    const hls = this.hls;\n    if (!hls) return;\n    const size = this.sizeCapLevel ?? -1;\n    const rate = this.bitrateCapLevel;\n    hls.autoLevelCapping = size < 0 ? rate : rate < 0 ? size : Math.min(size, rate);\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  setLicenseUrls(urls: { widevine?: string; fairplay?: string }): void {\n    // Refresh only what the caller has — licenseXhrSetup reads this field per\n    // request, so an in-flight session picks the new signed URL up on its next\n    // license POST without an instance rebuild. FairPlay is ignored here: it\n    // runs on NativeHlsEngine, which keeps its own refreshed URL.\n    if (urls.widevine) this.licenseUrl = urls.widevine;\n  }\n\n  getStats(): PlaybackStats {\n    const hls = this.hls;\n    const frames = readFrameStats(this.deps.video);\n    const stats: PlaybackStats = {\n      droppedFrames: frames.dropped,\n      decodedFrames: frames.totalFrames,\n      lowLatency: this.detectedLowLatency,\n      bytesLoaded: this.bytesLoaded,\n      playlistBytesLoaded: this.playlistBytesLoaded,\n      renditionSwitches: this.renditionSwitches,\n      licenseTimeMs: this.licenseTimeMs,\n      upshifts: this.upshifts,\n      downshifts: this.downshifts,\n      requestCount: this.requestCount,\n      requestErrors: this.requestErrors,\n      // A copy: PlaybackStats is public surface, and a consumer that\n      // mutated this array would corrupt the engine's own accounting.\n      requestTimesMs: [...this.requestTimesMs],\n      ladderHeights: this.ladderHeights,\n      ladderTopBitrateKbps: this.ladderTopBitrateKbps || undefined,\n    };\n    if (this.cdnHost) stats.cdnHost = this.cdnHost;\n    // Widevine with the robustness this engine requests (SW_SECURE_*) is\n    // software-path by definition: L3. A hardware-backed L1 is not reachable\n    // through hls.js/MSE here, so reporting the requested level is honest.\n    if (this.licenseRequested) {\n      stats.keySystem = 'widevine';\n      stats.drmSecurityLevel = 'L3';\n    }\n    if (hls) {\n      stats.qualityPinned = !hls.autoLevelEnabled;\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 (level?.videoCodec) stats.videoCodec = level.videoCodec;\n      if (level?.audioCodec) stats.audioCodec = level.audioCodec;\n      if (level && typeof level.bitrate === 'number') {\n        stats.renditionBitrateKbps = Math.round(level.bitrate / 1000);\n      }\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        // The playlist is moving again — restore normal live-edge chasing\n        // before the stale window that suspended it is forgotten.\n        if (this.chaseSuspended) this.resumeChasing();\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    // Same re-stamp while loading is suspended for a pause: no playlist is\n    // being reloaded, so \"not advancing\" is by design, not a frozen origin.\n    if (this.loadSuspended) {\n      this.lastAdvanceMs = Date.now();\n      return;\n    }\n    const staleMs = Date.now() - this.lastAdvanceMs;\n    if (!this.chaseSuspended && staleMs > CHASE_SUSPEND_STALE_MS) this.suspendChasing();\n    if (staleMs > END_STALE_MS) this.emitEnded('stalled');\n  }\n\n  /**\n   * Detect and heal a wedged hls.js video pipeline (see the STARVATION_*\n   * constants for the mechanism). All three conditions must hold across\n   * consecutive ticks:\n   *  - the element is starved (readyState < HAVE_FUTURE_DATA) while playing;\n   *  - the live playlist is still advancing (a stale playlist is stream end —\n   *    the end watchdog and chase suspension own that path);\n   *  - hls.js has issued no main-track fragment load recently (a plain\n   *    rebuffer keeps FRAG_LOADING events flowing; the wedge goes silent).\n   * Hidden tabs are skipped for the same timer-throttling reason as\n   * checkEndStale. The kick budget is windowed like the other recoveries\n   * (refunded after RECOVERY_RESET_MS of healthy buffering).\n   */\n  private checkStarvation(): void {\n    const hls = this.hls;\n    const { video } = this.deps;\n    if (!hls || this.endedEmitted || !this.sawLive || this.chaseSuspended) return;\n    if ((typeof document !== 'undefined' && document.hidden) || this.loadSuspended) {\n      this.starvedTicks = 0;\n      return;\n    }\n    const playlistAlive =\n      this.lastAdvanceMs !== 0 && Date.now() - this.lastAdvanceMs < CHASE_SUSPEND_STALE_MS;\n    const fragQuiet =\n      this.lastFragLoadingMs !== 0 &&\n      Date.now() - this.lastFragLoadingMs > STARVATION_FRAG_QUIET_MS;\n    const starved = video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !video.paused;\n    if (!(starved && playlistAlive && fragQuiet)) {\n      this.starvedTicks = 0;\n      return;\n    }\n    // Reached only on a visible tick that still shows the wedge signature, so\n    // this is the grace's clock: active starvation, not elapsed wall time.\n    if (this.kickPending) this.kickTicks += 1;\n    if (++this.starvedTicks < STARVATION_TICKS) return;\n    this.starvedTicks = 0;\n\n    // A kick that has had its grace and still produced no main-track media did\n    // not reach the wedge. Escalate to a rebuild before spending the rest of\n    // the kick budget re-parking the same instance.\n    if (this.kickPending && this.kickTicks >= STARVATION_KICK_GRACE_TICKS) {\n      this.kickPending = false;\n      this.kickTicks = 0;\n      if (this.starvationRebuilds < MAX_STARVATION_REBUILDS) {\n        this.starvationRebuilds += 1;\n        console.warn(\n          'havik-player: restarting the load did not resume video (the wedge is in hls.js state that only a rebuild clears) — rebuilding the player',\n        );\n        this.rebuildInstance('starvation rebuild');\n        return;\n      }\n    }\n\n    if (this.starvationKicks >= MAX_STARVATION_KICKS) return;\n    this.starvationKicks += 1;\n    this.kickPending = true;\n    this.kickTicks = 0;\n    console.warn(\n      'havik-player: video starved while the live playlist advances (wedged hls.js fragment pipeline) — restarting the load',\n    );\n    try {\n      hls.stopLoad();\n      hls.startLoad(-1);\n    } catch (err) {\n      // The kick is best-effort: a throw from a wedged hls.js must not escape\n      // the watchdog interval and take the page down with it.\n      console.warn('havik-player: starvation kick failed', err);\n    }\n  }\n\n  /**\n   * Freeze live-edge chasing on a stale playlist. Both knobs are read\n   * dynamically by hls.js's LatencyController on every tick, so mutating the\n   * live config takes effect immediately: an effectively-infinite latency\n   * ceiling disables the force-seek (which would land in data that doesn't\n   * exist), and a 1.0 sync rate stops the catch-up speedup — the video simply\n   * plays out its buffer and holds the last frame until 'ended' (or fresh\n   * bytes) arrive.\n   */\n  private suspendChasing(): void {\n    const hls = this.hls;\n    if (!hls) return;\n    this.chaseSuspended = true;\n    this.savedLiveMaxLatency = hls.config.liveMaxLatencyDuration;\n    this.savedMaxSyncRate = hls.config.maxLiveSyncPlaybackRate;\n    hls.config.liveMaxLatencyDuration = Number.MAX_SAFE_INTEGER;\n    hls.config.maxLiveSyncPlaybackRate = 1;\n  }\n\n  private resumeChasing(): void {\n    this.chaseSuspended = false;\n    const hls = this.hls;\n    if (!hls) return;\n    // Restore UNCONDITIONALLY — including `undefined`. Count-based caller\n    // configs (liveSyncDurationCount) have their seconds keys deleted by\n    // normalizeLiveSync, so the saved value is legitimately undefined; a\n    // guarded restore would leave the suspension's MAX_SAFE_INTEGER ceiling\n    // in place forever (hls.js prefers the seconds key over the count key),\n    // silently disabling the drift ceiling for the rest of the session.\n    hls.config.liveMaxLatencyDuration = this.savedLiveMaxLatency as number;\n    hls.config.maxLiveSyncPlaybackRate = this.savedMaxSyncRate as number;\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    // A deliberate stop is not a wedge: drop any pending escalation so a\n    // later resume can't rebuild on evidence from before the stop.\n    this.kickPending = false;\n    this.kickTicks = 0;\n    // And it outranks the pause suspension: the `pause` this stop fires must\n    // not arm a grace, and a later `play` on the ended element (the control\n    // bar stays clickable) must not restart the loader.\n    this.stopped = true;\n    this.loadSuspended = false;\n    if (this.pauseSuspendTimer) {\n      clearTimeout(this.pauseSuspendTimer);\n      this.pauseSuspendTimer = 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.capTimer) {\n      clearTimeout(this.capTimer);\n      this.capTimer = null;\n    }\n    if (this.capObserver) {\n      this.capObserver.disconnect();\n      this.capObserver = null;\n    }\n    this.unwatchDprChanges();\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.onSeeking) {\n      this.deps.video.removeEventListener('seeking', this.onSeeking);\n      this.onSeeking = null;\n    }\n    if (this.onPause) {\n      this.deps.video.removeEventListener('pause', this.onPause);\n      this.onPause = null;\n    }\n    if (this.onPlay) {\n      this.deps.video.removeEventListener('play', this.onPlay);\n      this.onPlay = null;\n    }\n    if (this.pauseSuspendTimer) {\n      clearTimeout(this.pauseSuspendTimer);\n      this.pauseSuspendTimer = 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      this.rebuildInstance('license retry'); // fresh key session re-issues the request\n    }, delay);\n    return true;\n  }\n\n  /**\n   * Tear the hls.js instance down and build a fresh one, which is the only\n   * recovery that reaches state the instance owns privately: destroy() runs\n   * KeyLoader.detach(), and that is what discards a cached `keyInfo` whose\n   * key-load promise will never settle (`isCommonEncryption` entries go even\n   * when their session context is null — the wedged shape). Equivalent to the\n   * page reload viewers resort to, minus the page.\n   *\n   * Deliberately not a stopLoad()/startLoad(): those keep the media element\n   * attached and re-await the same cached promise.\n   *\n   * Callers own their own budget — this is the mechanism, not the policy.\n   */\n  private rebuildInstance(reason: string): void {\n    if (!this.hls) return;\n    try {\n      this.hls.destroy();\n    } catch (e) {\n      console.warn(`havik-player: hls.js destroy() threw during ${reason}`, e);\n    }\n    this.hls = null;\n    this.startHls();\n  }\n\n  private onError(data: ErrorData): void {\n    // Counted BEFORE the non-fatal early return: hls.js retries a failed\n    // segment internally, so every request error a viewer actually suffers\n    // is invisible to the QoE plane unless it is tallied here.\n    //\n    // Fragment failures only. requestCount counts fragment loads, so\n    // folding manifest and playlist failures in here would let a\n    // manifest-flapping session report an error ratio above 100% against a\n    // denominator those errors were never part of.\n    if (\n      data.type === Hls.ErrorTypes.NETWORK_ERROR &&\n      String(data.details ?? '').startsWith('frag')\n    ) {\n      this.requestErrors += 1;\n    }\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({\n        type: 'error',\n        error: this.toError(data, 'DRM'),\n        fatal: true,\n        detail: data.details,\n      });\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({\n        type: 'error',\n        error: this.toError(data, 'NETWORK'),\n        fatal: true,\n        detail: data.details,\n      });\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({\n        type: 'error',\n        error: this.toError(data),\n        fatal: true,\n        detail: data.details,\n      });\n      return;\n    }\n    this.deps.emit({ type: 'error', error: this.toError(data), fatal: true, detail: data.details });\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      kind === 'DRM'\n        ? classifyDrmStatus(status)\n        : status === 401\n          ? 'UNAUTHORIZED'\n          : status === 403\n            ? 'FORBIDDEN'\n            : 'INTERNAL';\n    return new PlaybackError(\n      code,\n      status,\n      `${kind} error: ${detail}${reason ? ` (${reason})` : ''}`,\n    );\n  }\n}\n","// QoE beacon emitter — plan A2 (havik-streams docs/plans/havik-analytics-plan.md).\n//\n// One AnalyticsSession per playback session (per sid). Wire format is the\n// proto3 JSON mapping of `havik_analytics.BeaconBatch` (protocol-buffers\n// repo): camelCase keys, enums by NAME, and — the one contract landmine —\n// StateChange uses `fromState`/`toState` (`from` is a Python keyword; the\n// contract renamed the pair for every generator).\n//\n// Privacy posture (plan D11, asserted by test): the sid arrives in the\n// playback response, lives in this object, and dies with it. This module\n// never touches localStorage, sessionStorage, cookies, or IndexedDB.\n//\n// Delivery is fire-and-forget (plan D3/D9): beacons are dashboard-grade, the\n// CDN edge-log plane is the billing truth, so a lost batch is dropped —\n// never retried into a stampede, never allowed to affect playback.\n\nimport { type SessionEnvironment } from './environment';\nimport { type PlaybackStats, type PlayerState } from './stats';\n\n/** Beacon cadence — plan §9 settled at review time to 15s. */\nconst DEFAULT_HEARTBEAT_MS = 15_000;\n/**\n * How long a session may sit PAUSED before it closes itself, and the\n * suspension gap (no timer fired for this long: OS sleep, a frozen tab) after\n * which it does the same on waking.\n *\n * Without this a session had exactly four ends — pagehide, stream end,\n * destroy(), fatal error — so a viewer who paused and forgot the tab, closed\n * the lid, or had the tab discarded left a session \"still open\" for hours\n * (portal, 2026-09-19: paused heartbeats from 16:13 on, no end), and a slept\n * laptop booked its whole sleep as playing time on waking. Ten minutes is\n * longer than any pause that is still \"the same viewing\" (an ad break, a\n * phone call) and shorter than half-time; a viewer who comes back after it\n * starts a NEW session (the player reloads on the next play()), which is\n * what it is. Hidden-but-playing is NOT idle: audio keeps playing in a\n * background tab.\n */\nconst DEFAULT_IDLE_MS = 10 * 60_000;\n/**\n * The suspension detector's floor, whatever idleMs is. A gap between two of\n * our ticks is only a suspension when no legitimate cadence explains it: a\n * hidden tab's throttled timers gap to a minute (Chrome, after 5 min\n * hidden), and a host-configured idleMs below the heartbeat interval is a\n * PAUSE limit, not a licence to close a playing session on its first\n * ordinary heartbeat. Five minutes is above every throttled cadence seen and\n * below any sleep worth closing over.\n */\nconst MIN_SUSPENSION_GAP_MS = 5 * 60_000;\n/** Queue bound: past this the oldest non-lifecycle beacons drop first. */\nconst MAX_PENDING = 100;\n/** Startup stages report >=1ms when reached; 0 means never reached. */\nconst MIN_STAGE_MS = 1;\n/**\n * How long SessionStart waits for the autoplay verdict before going out.\n *\n * The contract puts the autoplay result on SessionStart, but the engine is\n * committed (and this session constructed) BEFORE play() is ever called, so\n * at enqueue time the verdict does not exist yet. Holding the first flush\n * for a beat lets the real value ride the beacon it belongs on; nothing is\n * lost if the verdict misses the window, and any earlier flush (an error,\n * end, pagehide) sends immediately regardless.\n */\nconst FIRST_FLUSH_DELAY_MS = 1_000;\n/** A stall or error this close to the end of a session is what the viewer\n *  left over — beyond it, the two are unrelated events in one session. */\nconst ABANDON_WINDOW_MS = 30_000;\n/**\n * How long after a seek a stall is still the seek's.\n *\n * Was an inline 1_000 in causeOf; named because the rebuffer counters now\n * share it. A seek re-buffers from a new position, so the gap is bounded by\n * a segment fetch rather than by anything the viewer did — one second covers\n * it at any realistic segment duration without swallowing a genuine stall\n * that merely happens to follow a seek.\n */\nconst SEEK_STALL_WINDOW_MS = 1_000;\n\n/** Contract enum names (proto3 JSON encodes enums by name). */\ntype WirePlayer = 'PLAYER_HLSJS' | 'PLAYER_SAFARI_NATIVE';\ntype WireState =\n  | 'PLAYER_STATE_UNSPECIFIED'\n  | 'PLAYER_STATE_IDLE'\n  | 'PLAYER_STATE_WAITING'\n  | 'PLAYER_STATE_LOADING'\n  | 'PLAYER_STATE_PLAYING'\n  | 'PLAYER_STATE_BUFFERING'\n  | 'PLAYER_STATE_PAUSED'\n  | 'PLAYER_STATE_ENDED'\n  | 'PLAYER_STATE_ERROR';\ntype WireKeySystem = 'KEY_SYSTEM_NONE' | 'KEY_SYSTEM_WIDEVINE' | 'KEY_SYSTEM_FAIRPLAY';\ntype WirePresentation = 'PRESENTATION_INLINE' | 'PRESENTATION_FULLSCREEN' | 'PRESENTATION_PIP';\ntype WireCause =\n  | 'TRANSITION_CAUSE_USER'\n  | 'TRANSITION_CAUSE_SEEK'\n  | 'TRANSITION_CAUSE_NETWORK_STALL'\n  | 'TRANSITION_CAUSE_ERROR'\n  | 'TRANSITION_CAUSE_AUTO';\ntype WireErrorStage =\n  | 'ERROR_STAGE_RESOLVE'\n  | 'ERROR_STAGE_MANIFEST'\n  | 'ERROR_STAGE_DRM'\n  | 'ERROR_STAGE_FIRST_SEGMENT'\n  | 'ERROR_STAGE_SEGMENT'\n  | 'ERROR_STAGE_DECODE'\n  | 'ERROR_STAGE_OTHER';\nexport type WireAutoplay =\n  | 'AUTOPLAY_RESULT_ALLOWED'\n  | 'AUTOPLAY_RESULT_ALLOWED_MUTED'\n  | 'AUTOPLAY_RESULT_BLOCKED'\n  | 'AUTOPLAY_RESULT_NOT_ATTEMPTED';\n/** Presentation the viewer is watching in, as the player reports it. */\nexport type Presentation = 'inline' | 'fullscreen' | 'pip';\nconst PRESENTATION_TO_WIRE: Record<Presentation, WirePresentation> = {\n  inline: 'PRESENTATION_INLINE',\n  fullscreen: 'PRESENTATION_FULLSCREEN',\n  pip: 'PRESENTATION_PIP',\n};\n\ntype WireEndReason =\n  | 'END_REASON_ENDED'\n  | 'END_REASON_PAGEHIDE'\n  | 'END_REASON_STOPPED'\n  | 'END_REASON_FATAL_ERROR'\n  | 'END_REASON_IDLE';\n\nconst KEY_SYSTEM_TO_WIRE: Record<'widevine' | 'fairplay' | 'none', WireKeySystem> = {\n  widevine: 'KEY_SYSTEM_WIDEVINE',\n  fairplay: 'KEY_SYSTEM_FAIRPLAY',\n  none: 'KEY_SYSTEM_NONE',\n};\n\n/**\n * Nearest-rank percentile over an ascending slice; 0 for an empty one.\n *\n * Nearest rank, not interpolation: the value returned is one the session\n * actually observed. The rank is ceil(p/100 * N), so p50 of four samples\n * is the second, not the third — worth stating because the two conventions\n * differ only at exact boundaries and a future reader is otherwise likely\n * to \"fix\" one into the other.\n */\nfunction percentile(ascending: number[], p: number): number {\n  if (ascending.length === 0) return 0;\n  const rank = Math.min(ascending.length, Math.max(1, Math.ceil((p / 100) * ascending.length)));\n  return ascending[rank - 1] ?? 0;\n}\n\n/** Codes raised by /v1/playback before any media is fetched. */\nconst RESOLVE_CODES = new Set([\n  'INVALID_URN',\n  'NOT_FOUND',\n  'GONE',\n  'TOO_EARLY',\n  'UNAVAILABLE',\n  'UNAUTHORIZED',\n  'FORBIDDEN',\n  'RATE_LIMITED',\n  'TIMEOUT',\n]);\n\nconst STATE_TO_WIRE: Record<PlayerState, WireState> = {\n  idle: 'PLAYER_STATE_IDLE',\n  waiting: 'PLAYER_STATE_WAITING',\n  loading: 'PLAYER_STATE_LOADING',\n  playing: 'PLAYER_STATE_PLAYING',\n  buffering: 'PLAYER_STATE_BUFFERING',\n  paused: 'PLAYER_STATE_PAUSED',\n  ended: 'PLAYER_STATE_ENDED',\n  error: 'PLAYER_STATE_ERROR',\n};\n\ninterface WireSessionStart extends Partial<SessionEnvironment> {\n  player: WirePlayer;\n  sdkVersion: string;\n  autoplay?: WireAutoplay;\n}\n\ninterface WireBeacon {\n  sid: string;\n  clientTime: string;\n  seq: number;\n  sessionStart?: WireSessionStart;\n  mediaInfo?: {\n    videoCodec?: string;\n    audioCodec?: string;\n    keySystem?: WireKeySystem;\n    drmSecurityLevel?: string;\n    ladderHeights?: number[];\n    ladderTopBitrateKbps?: number;\n    lowLatency?: boolean;\n  };\n  heartbeat?: Record<string, number | string | boolean>;\n  stateChange?: {\n    fromState: WireState;\n    toState: WireState;\n    positionMs?: number;\n    renditionHeight?: number;\n    cause?: WireCause;\n    presentation?: WirePresentation;\n  };\n  error?: {\n    code: string;\n    detail?: string;\n    fatal?: boolean;\n    httpStatus?: number;\n    recoveryAttempts?: number;\n    recovered?: boolean;\n    positionMs?: number;\n    stage?: WireErrorStage;\n    cdnHost?: string;\n  };\n  sessionEnd?: Record<string, number | string>;\n}\n\n/** What the player hands the session at each heartbeat. */\nexport type AnalyticsSample = PlaybackStats & {\n  state: PlayerState;\n  positionMs: number;\n  /** Forward buffer depth, which only the player can measure. */\n  bufferedMs?: number;\n  /** >1 means the engine is catching up toward the live edge. */\n  playbackRate?: number;\n};\n\nexport interface AnalyticsSessionOptions {\n  /** Beacons ingest base URL — POSTs go to `<endpoint>/v1/beacons`. */\n  endpoint: string;\n  /** The per-session id minted by havik-streams at resolve time. */\n  sid: string;\n  player: WirePlayer;\n  sdkVersion: string;\n  /**\n   * Called at each heartbeat for the current cumulative stats; the session\n   * computes per-interval deltas itself.\n   */\n  sample: () => AnalyticsSample;\n  /**\n   * Where the session runs (contract v2): form factor, device model, OS\n   * version, screen, connection, embedding host. Collected once by the\n   * player. Model and OS version come from UA client hints and are absent\n   * on browsers that do not implement them; no IP and no raw user agent\n   * ever leave the client (see core/environment).\n   */\n  environment?: SessionEnvironment;\n  heartbeatMs?: number;\n  /**\n   * Idle limit; see DEFAULT_IDLE_MS. `0` disables the idle close entirely.\n   * The suspension detector never fires below MIN_SUSPENSION_GAP_MS,\n   * whatever this is set to.\n   */\n  idleMs?: number;\n  /**\n   * Called once, after the session ended itself for inactivity\n   * (END_REASON_IDLE) — the player latches \"reload on the next play()\" on it,\n   * because this sid is finished and whatever plays next is a new session.\n   */\n  onIdle?: () => void;\n  /** Test seams. */\n  now?: () => number;\n  sendBeacon?: (url: string, body: string) => boolean;\n  fetchFn?: typeof fetch;\n}\n\n/**\n * Derive the beacons ingest base from the streams API base by the platform\n * hostname convention (`feed[-dev].<domain>` → `beacons[-dev].<domain>`),\n * mirroring how the SSE endpoint derives `events.<domain>`. Returns\n * undefined when the convention doesn't apply — the caller must then pass\n * an explicit endpoint for beacons to flow.\n */\nexport function deriveBeaconsBaseUrl(baseUrl: string): string | undefined {\n  try {\n    const url = new URL(baseUrl);\n    const [first, ...rest] = url.hostname.split('.');\n    if (!first || rest.length === 0) return undefined;\n    if (first !== 'feed' && !first.startsWith('feed-')) return undefined;\n    url.hostname = [first.replace(/^feed/, 'beacons'), ...rest].join('.');\n    url.pathname = '';\n    url.search = '';\n    return url.origin;\n  } catch {\n    return undefined;\n  }\n}\n\n/** Causes already warned about, so each console line is printed once. */\nconst warnedBeaconCauses = new Set<string>();\n\n/** What {@link resolveBeaconsEndpoint} decided, and why. */\nexport interface BeaconsEndpointResolution {\n  /** Where beacons go. Undefined means delivery is OFF for this session. */\n  endpoint?: string;\n  /**\n   * Set whenever delivery is off for a reason the integrator can fix — a\n   * ready-to-surface sentence naming the cause. Always populated in that case\n   * (the caller emits it on every session, so a host that attaches a listener\n   * late still hears about it); the console line it also triggers is deduped\n   * per cause.\n   */\n  disabledReason?: string;\n}\n\n/** Disable delivery with a reason: said once on the console, returned always. */\nfunction beaconsDisabled(cause: string, reason: string): BeaconsEndpointResolution {\n  if (!warnedBeaconCauses.has(cause)) {\n    warnedBeaconCauses.add(cause);\n    console.warn(`havik-player: ${reason}`);\n  }\n  return { disabledReason: reason };\n}\n\n/**\n * Decide where this session's beacons go, and make a silent disable loud.\n *\n * The derivation convention is deliberately narrow (see\n * {@link deriveBeaconsBaseUrl}), so an integrator fronting the streams API\n * with their own hostname — a CDN in front of `feed.<domain>`, say — gets no\n * endpoint and, before this, no beacons and no word of it. That failure is\n * invisible from the client: playback is perfect, only the analytics plane is\n * empty, and it stays empty until someone asks the warehouse why a client has\n * descriptor mints and zero sessions. It cost ~a week of QoE data for one\n * client in Sept 2026.\n *\n * Widening the convention is NOT the fix (a guessed beacons host is a silent\n * 404 instead of a silent nothing) — saying so is.\n */\nexport function resolveBeaconsEndpoint(\n  baseUrl: string,\n  explicitEndpoint?: string,\n): BeaconsEndpointResolution {\n  if (explicitEndpoint) return { endpoint: explicitEndpoint };\n\n  // Supplied but EMPTY — an unset env var, or a template that resolved to\n  // nothing. Test truthiness, not `!== undefined`, but do not fall through to\n  // derivation: the caller asked for a specific endpoint, and quietly\n  // substituting a derived host would start delivery to somewhere they never\n  // named. So this stays disabled, exactly as it was before this function\n  // existed (`explicit ?? derive(...)` kept the '' and failed the `!endpoint`\n  // check). What changes is that it no longer happens in silence.\n  if (explicitEndpoint !== undefined) {\n    return beaconsDisabled(\n      'empty-endpoint',\n      `analytics is disabled: no QoE beacons will be sent. analytics.endpoint ` +\n        `was supplied but empty. Pass a real beacons origin, ` +\n        `analytics: { endpoint: 'https://beacons[-dev].<domain>' }, or omit the ` +\n        `option to derive it from baseUrl (or analytics: false to opt out).`,\n    );\n  }\n\n  const derived = deriveBeaconsBaseUrl(baseUrl);\n  if (derived) return { endpoint: derived };\n\n  // Name the host, not the whole baseUrl: it is the part the convention\n  // rejected, and it cannot carry a query string into the console.\n  let host: string;\n  try {\n    host = new URL(baseUrl).hostname || baseUrl;\n  } catch {\n    host = baseUrl; // not a URL at all — echo it back, that IS the problem\n  }\n  return beaconsDisabled(\n    host,\n    `analytics is disabled: no QoE beacons will be sent. The beacons endpoint ` +\n      `cannot be derived from the baseUrl host \"${host}\" — the convention is ` +\n      `feed[-dev].<domain> → beacons[-dev].<domain>. Pass an explicit endpoint, ` +\n      `analytics: { endpoint: 'https://beacons[-dev].<domain>' }, to createPlayer ` +\n      `(or analytics: false to opt out deliberately).`,\n  );\n}\n\n/**\n * One playback session's QoE beacon emitter. Construct after engine\n * selection (player identity is only truthful then), call lifecycle hooks\n * from the player, and end() exactly once — later calls are no-ops.\n */\nexport class AnalyticsSession {\n  private readonly opts: Required<Pick<AnalyticsSessionOptions, 'heartbeatMs' | 'idleMs'>> &\n    AnalyticsSessionOptions;\n  private readonly url: string;\n  private readonly now: () => number;\n\n  private seq = 0;\n  private pending: WireBeacon[] = [];\n  private timer: ReturnType<typeof setInterval> | null = null;\n  private endedFlag = false;\n\n  // Delta bases for the cumulative counters sampled at each heartbeat.\n  private lastBytes = 0;\n  private lastPlaylistBytes = 0;\n  private lastDropped = 0;\n  private lastSwitches = 0;\n\n  // Wall-clock accounting: playing/hidden time accumulate between their\n  // state flips; totals close out at end().\n  private playingSinceMs: number | null = null;\n  private hiddenSinceMs: number | null = null;\n  private playingTotalMs = 0;\n  private hiddenTotalMs = 0;\n  private lastHbPlayingMs = 0;\n  private lastHbHiddenMs = 0;\n  private lastHbRebufferMs = 0;\n  private lastHbRebufferCount = 0;\n  private lastHbActiveMs = 0;\n\n  // Session totals for SessionEnd.\n  private rebufferCount = 0;\n  private rebufferTotalMs = 0;\n  private bufferingSinceMs: number | null = null;\n  // Rebuffer, like active watch, is a visible-only window: a backgrounded tab\n  // is throttled into 'buffering' and would otherwise book minutes of stall the\n  // viewer never saw. isBuffering is the state; bufferingSinceMs is the open\n  // window, held only while isBuffering && !isHidden (see syncBuffering).\n  private isBuffering = false;\n  private errorCount = 0;\n  private seekCount = 0;\n  private pauseCount = 0;\n\n  // ---- Contract v2 accounting.\n  // Active watch = playing AND visible AND unmuted. Kept as its own window\n  // rather than derived at read time, because the three flip independently\n  // and a server can only ever reconstruct a lower bound from the deltas.\n  private isPlaying = false;\n  private isHidden = false;\n  private isMuted = false;\n  private activeSinceMs: number | null = null;\n  private activeTotalMs = 0;\n  // Muted time is counted only WHILE PLAYING, so it is subtractable from\n  // playing time; a muted paused tab is not watch time to discount.\n  private mutedSinceMs: number | null = null;\n  private mutedTotalMs = 0;\n  private lastHbMutedMs = 0;\n  private volumeChanges = 0;\n  private muteChanges = 0;\n  // Presentation: which surface the viewer watched in, and for how long.\n  private presentation: Presentation = 'inline';\n  private presentationSinceMs: number;\n  private fullscreenTotalMs = 0;\n  private pipTotalMs = 0;\n  // Armed-before-live time: a viewer who waited five minutes for kickoff had\n  // a different session from one who joined a running match.\n  private waitingSinceMs: number | null = null;\n  private waitingTotalMs = 0;\n  // Startup and abandon signals.\n  private firstAudioMs = 0;\n  private timeToFirstStallMs = 0;\n  private lastStallMs = 0;\n  private lastErrorMs = 0;\n  // −1 means \"no seek yet\": 0 is a real seek time when `now` starts at 0.\n  private lastSeekMs = -1;\n  /** True while the CURRENT stall is the seek's rather than the network's. */\n  private stallIsSeekInduced = false;\n  private lastErrorStage: WireErrorStage | null = null;\n  private sawFirstBytes = false;\n  private mediaInfoSignature = '';\n  // Delta bases for the v2 cumulative counters.\n  private lastUpshifts = 0;\n  private lastDownshifts = 0;\n  private lastDecoded = 0;\n  private lastRequestCount = 0;\n  private lastRequestErrors = 0;\n  // The un-flushed SessionStart, held briefly so the autoplay verdict can\n  // land on it (see FIRST_FLUSH_DELAY_MS).\n  private pendingStart: WireSessionStart | null = null;\n  private firstFlushTimer: ReturnType<typeof setTimeout> | null = null;\n  // Idle close (see DEFAULT_IDLE_MS): the pause timer, and the last instant a\n  // timer of ours actually ran — the suspension detector's reference.\n  private idleTimer: ReturnType<typeof setTimeout> | null = null;\n  private lastTickMs = 0;\n\n  // Startup breakdown (0 = stage never reached, plan contract).\n  private readonly bornMs: number;\n  /**\n   * What the startup ladder measures FROM: the moment the viewer asked for\n   * playback, not the moment the player was constructed.\n   *\n   * Defaults to bornMs, which is correct for autoplay — intent and\n   * construction coincide there. It moves once, on the first play(), for the\n   * autoplay-off path, where the player is built with the manifest parsed and\n   * then sits behind a poster for as long as the viewer takes to click. That\n   * dwell was being reported as startup LATENCY: a viewer who read the page\n   * for thirty seconds produced a thirty-second \"time to first frame\", and\n   * ops' own help text has always described the figure this now computes\n   * (\"from the player being asked to play to the first frame\").\n   *\n   * Every stage shares it, or they stop summing: the ladder is\n   * manifest -> first segment -> first frame measured from one origin.\n   */\n  private startupAnchorMs: number;\n  private startupTimeMs = 0;\n  private manifestLoadMs = 0;\n  private firstFragLoadMs = 0;\n\n  private readonly onPagehide = () => this.end('END_REASON_PAGEHIDE');\n  /**\n   * Page Lifecycle `freeze`: the browser is about to stop running this page\n   * (a backgrounded tab after ~5 min in Chrome, ahead of a discard). Nothing\n   * runs after it — a discard fires no pagehide — so what is queued goes out\n   * now, by sendBeacon, the only delivery a stopping page guarantees. Not an\n   * end: the page may `resume`, and the suspension check there decides.\n   */\n  private readonly onFreeze = () => {\n    if (this.endedFlag) return;\n    this.flush(true);\n  };\n  /** Page Lifecycle `resume`: the page runs again; was it away past the idle limit? */\n  private readonly onResume = () => this.checkSuspension(this.now());\n  private readonly onVisibility = () => {\n    if (typeof document === 'undefined') return;\n    if (document.visibilityState === 'hidden') {\n      if (this.hiddenSinceMs === null) this.hiddenSinceMs = this.now();\n      this.isHidden = true;\n    } else {\n      if (this.hiddenSinceMs !== null) {\n        this.hiddenTotalMs += this.now() - this.hiddenSinceMs;\n        this.hiddenSinceMs = null;\n      }\n      this.isHidden = false;\n    }\n    this.syncActive();\n    this.syncBuffering();\n  };\n\n  constructor(opts: AnalyticsSessionOptions) {\n    // ?? at the use point, not object spread: a caller passing an explicit\n    // `heartbeatMs: undefined` must not clobber the default into a 0ms\n    // busy-loop interval.\n    this.opts = {\n      ...opts,\n      heartbeatMs: opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,\n      idleMs: opts.idleMs ?? DEFAULT_IDLE_MS,\n    };\n    this.url = `${opts.endpoint.replace(/\\/$/, '')}/v1/beacons`;\n    this.now = opts.now ?? (() => Date.now());\n    this.bornMs = this.now();\n    // Not a startup stage: presentation time is fullscreen/PiP accounting and\n    // is measured over the session's whole life, so it keeps bornMs.\n    this.presentationSinceMs = this.bornMs;\n    this.startupAnchorMs = this.bornMs;\n  }\n\n  /** Emit SessionStart (always the session's first beacon) and arm timers. */\n  start(): void {\n    if (this.endedFlag) return;\n    const sessionStart: WireSessionStart = {\n      player: this.opts.player,\n      sdkVersion: this.opts.sdkVersion,\n      ...this.opts.environment,\n    };\n    this.pendingStart = sessionStart;\n    this.enqueue({ sessionStart });\n    // Not flushed here: the autoplay verdict does not exist yet. See\n    // FIRST_FLUSH_DELAY_MS — noteAutoplay, an error, a heartbeat or end all\n    // send earlier if they happen first.\n    this.firstFlushTimer = setTimeout(() => this.flush(), FIRST_FLUSH_DELAY_MS);\n    this.lastTickMs = this.now();\n    this.timer = setInterval(() => this.heartbeat(), this.opts.heartbeatMs);\n    if (typeof window !== 'undefined') {\n      window.addEventListener('pagehide', this.onPagehide);\n    }\n    if (typeof document !== 'undefined') {\n      document.addEventListener('visibilitychange', this.onVisibility);\n      document.addEventListener('freeze', this.onFreeze);\n      document.addEventListener('resume', this.onResume);\n      this.onVisibility(); // seed hidden accounting if born in a hidden tab\n    }\n  }\n\n  /**\n   * Did the page stop running for at least the idle limit? Timers do not\n   * fire through OS sleep or a frozen tab, so the gap between two of our own\n   * ticks is the measure: a heartbeat that finds the clock jumped past the\n   * limit is the first tick after a suspension, and the session it wakes\n   * into is over — nothing was watched, and every open accounting window\n   * (playing, active, muted…) would otherwise book the whole gap. Ends as\n   * IDLE; the next play() starts a new session.\n   */\n  private checkSuspension(t: number): boolean {\n    if (this.endedFlag || !this.opts.idleMs) return false;\n    const gap = Math.max(this.opts.idleMs, MIN_SUSPENSION_GAP_MS);\n    if (this.lastTickMs > 0 && t - this.lastTickMs >= gap) {\n      this.endIdle(this.lastTickMs);\n      return true;\n    }\n    this.lastTickMs = t;\n    return false;\n  }\n\n  /** Arm (paused) or disarm (anything else) the idle close. */\n  private syncIdle(state: PlayerState): void {\n    if (state === 'paused' && this.opts.idleMs) {\n      if (this.idleTimer === null) {\n        this.idleTimer = setTimeout(() => {\n          this.idleTimer = null;\n          this.endIdle();\n        }, this.opts.idleMs);\n      }\n    } else if (this.idleTimer !== null) {\n      clearTimeout(this.idleTimer);\n      this.idleTimer = null;\n    }\n  }\n\n  private endIdle(at?: number): void {\n    if (this.endedFlag) return;\n    this.end('END_REASON_IDLE', at);\n    this.opts.onIdle?.();\n  }\n\n  /** Player state transition — also drives rebuffer/watch-time accounting. */\n  noteStateChange(\n    from: PlayerState,\n    to: PlayerState,\n    positionMs: number,\n    renditionHeight?: number,\n  ): void {\n    if (this.endedFlag || from === to) return;\n    const t = this.now();\n\n    if (to === 'playing') {\n      if (this.playingSinceMs === null) this.playingSinceMs = t;\n      if (this.startupTimeMs === 0) {\n        this.startupTimeMs = Math.max(MIN_STAGE_MS, t - this.startupAnchorMs);\n      }\n      if (this.firstAudioMs === 0 && !this.isMuted) {\n        // The closest a browser gets to \"first audible frame\": playback\n        // running with the element neither muted nor at zero volume.\n        this.firstAudioMs = Math.max(MIN_STAGE_MS, t - this.startupAnchorMs);\n      }\n      if (this.isMuted && this.mutedSinceMs === null) this.mutedSinceMs = t;\n    } else if (this.playingSinceMs !== null) {\n      this.playingTotalMs += t - this.playingSinceMs;\n      this.playingSinceMs = null;\n      if (this.mutedSinceMs !== null) {\n        this.mutedTotalMs += t - this.mutedSinceMs;\n        this.mutedSinceMs = null;\n      }\n    }\n    this.isPlaying = to === 'playing';\n    this.syncActive();\n\n    // Time spent armed before the match went live, closed on the way out of\n    // 'waiting' so it never includes the playback that followed.\n    if (to === 'waiting') this.waitingSinceMs = t;\n    else if (this.waitingSinceMs !== null) {\n      this.waitingTotalMs += t - this.waitingSinceMs;\n      this.waitingSinceMs = null;\n    }\n\n    if (to === 'buffering' && from === 'playing') {\n      // Two kinds of stall are not the delivery path's fault and are excluded\n      // from every stall figure:\n      //\n      //   hidden  — a backgrounded tab is throttled into 'buffering'; the\n      //             viewer saw nothing.\n      //   seek    — the buffering that follows a seek is the seek's. The\n      //             state machine is driven by media events that carry no\n      //             reason, so this is inferred, and causeOf has always\n      //             inferred it for the LABEL; now the counters agree.\n      //\n      // (A stall that begins visible and later goes hidden is still one\n      // stall: the count runs once here; syncBuffering only pauses its time.)\n      this.stallIsSeekInduced = this.seekInduced(t);\n      const counts = !this.isHidden && !this.stallIsSeekInduced;\n      if (counts) this.rebufferCount += 1;\n      this.isBuffering = true;\n      this.lastStallMs = t;\n      // Gated identically: \"time to first stall\" is a delivery figure, and a\n      // seek is not a delivery event. Leaving it ungated would let a session\n      // report a first stall it never counted.\n      if (counts && this.timeToFirstStallMs === 0) {\n        // Playback delivered before the first interruption — a session that\n        // stalls at 3s and one that stalls at 30 minutes read identically\n        // through the stall count alone.\n        this.timeToFirstStallMs = Math.max(MIN_STAGE_MS, Math.round(this.playingTotalMs));\n      }\n    } else if (from === 'buffering') {\n      this.isBuffering = false;\n      this.stallIsSeekInduced = false;\n    }\n    // Open/close the rebuffer window against the new buffering state; visibility\n    // changes come through onVisibility, which also calls syncBuffering.\n    this.syncBuffering();\n    if (to === 'paused') this.pauseCount += 1;\n    this.syncIdle(to);\n\n    const change: NonNullable<WireBeacon['stateChange']> = {\n      fromState: STATE_TO_WIRE[from] ?? 'PLAYER_STATE_UNSPECIFIED',\n      toState: STATE_TO_WIRE[to] ?? 'PLAYER_STATE_UNSPECIFIED',\n      presentation: PRESENTATION_TO_WIRE[this.presentation],\n    };\n    const cause = this.causeOf(from, to, t);\n    if (cause) change.cause = cause;\n    if (positionMs > 0) change.positionMs = Math.round(positionMs);\n    if (renditionHeight) change.renditionHeight = renditionHeight;\n    this.enqueue({ stateChange: change });\n  }\n\n  /**\n   * Was the stall starting at `t` the seek's rather than the network's?\n   *\n   * ONE definition, shared by the transition cause and by the rebuffer\n   * accounting, because they are the same question: if the label said SEEK\n   * while the counter said network stall, the beacon would contradict itself\n   * inside a single session.\n   *\n   * The `lastSeekMs >= 0` guard is load-bearing, not defensive. Without it,\n   * any stall within the first second of a clock that starts near zero reads\n   * as seek-induced — true of an injected `now`, and the first stall of a\n   * session is exactly the one a join-time analysis cares about most. The\n   * \"no seek yet\" sentinel is −1 rather than 0 because 0 is a real seek\n   * time on such a clock: a seek at the epoch must still own the\n   * buffering that follows it.\n   */\n  private seekInduced(t: number): boolean {\n    return this.lastSeekMs >= 0 && t - this.lastSeekMs < SEEK_STALL_WINDOW_MS;\n  }\n\n  /**\n   * Why a transition happened, as far as the player can tell. Inferred, not\n   * reported: the state machine is driven by media events that carry no\n   * reason, so buffering right after a seek is the seek's, and buffering\n   * otherwise is the network running dry — which is what the state means.\n   */\n  private causeOf(from: PlayerState, to: PlayerState, t: number): WireCause | undefined {\n    if (to === 'error') return 'TRANSITION_CAUSE_ERROR';\n    if (to === 'buffering') {\n      return this.seekInduced(t) ? 'TRANSITION_CAUSE_SEEK' : 'TRANSITION_CAUSE_NETWORK_STALL';\n    }\n    if (to === 'paused' || (to === 'playing' && from === 'paused')) {\n      return 'TRANSITION_CAUSE_USER';\n    }\n    return undefined;\n  }\n\n  /** Open or close the active-watch window after any of its three inputs moved. */\n  private syncActive(at = this.now()): void {\n    const on = this.isPlaying && !this.isHidden && !this.isMuted;\n    if (on && this.activeSinceMs === null) this.activeSinceMs = at;\n    else if (!on && this.activeSinceMs !== null) {\n      this.activeTotalMs += at - this.activeSinceMs;\n      this.activeSinceMs = null;\n    }\n  }\n\n  /**\n   * Open or close the rebuffer window after buffering or visibility moved.\n   * Mirrors syncActive: a hidden tab is throttled into 'buffering', and that\n   * stall time is not something the viewer experienced, so rebuffer time\n   * accrues only while isBuffering && !isHidden.\n   */\n  private syncBuffering(): void {\n    const on = this.isBuffering && !this.isHidden && !this.stallIsSeekInduced;\n    if (on && this.bufferingSinceMs === null) this.bufferingSinceMs = this.now();\n    else if (!on && this.bufferingSinceMs !== null) {\n      this.rebufferTotalMs += this.now() - this.bufferingSinceMs;\n      this.bufferingSinceMs = null;\n    }\n  }\n\n  /** Bank the time spent in the current presentation and restart the clock. */\n  private closePresentation(t = this.now()): void {\n    const spent = t - this.presentationSinceMs;\n    if (this.presentation === 'fullscreen') this.fullscreenTotalMs += spent;\n    else if (this.presentation === 'pip') this.pipTotalMs += spent;\n    this.presentationSinceMs = t;\n  }\n\n  /**\n   * The autoplay verdict, once play() has resolved (or was never attempted).\n   * Lands on SessionStart when it arrives inside the first-flush window,\n   * which is the normal case; a slow manifest can miss it, and the field is\n   * then simply absent rather than wrong.\n   */\n  noteAutoplay(result: WireAutoplay): void {\n    if (this.pendingStart) this.pendingStart.autoplay = result;\n  }\n\n  /**\n   * Environment facts that only resolve asynchronously (the high-entropy UA\n   * client hints). Same one-shot window as the autoplay verdict: patched\n   * onto SessionStart while it is still held, ignored once it has gone.\n   */\n  noteEnvironment(extra: Partial<SessionEnvironment>): void {\n    if (this.pendingStart) Object.assign(this.pendingStart, extra);\n  }\n\n  /**\n   * The audio state a session STARTS in. Counts no change: a player that\n   * begins muted (the autoplay-friendly default) has had nothing done to\n   * it, and reporting that as a mute change would put a phantom 1 on every\n   * session and destroy the \"did the viewer touch the volume\" signal.\n   */\n  seedVolume(muted: boolean, volume: number): void {\n    if (this.endedFlag) return;\n    this.isMuted = muted || volume === 0;\n    if (this.isMuted && this.isPlaying && this.mutedSinceMs === null) {\n      this.mutedSinceMs = this.now();\n    }\n    this.syncActive();\n  }\n\n  /** Volume or mute changed on the media element. */\n  noteVolume(muted: boolean, volume: number): void {\n    if (this.endedFlag) return;\n    const silent = muted || volume === 0;\n    if (silent === this.isMuted) {\n      // Same audibility, different level: a volume change, not a mute.\n      this.volumeChanges += 1;\n      return;\n    }\n    this.muteChanges += 1;\n    const t = this.now();\n    if (silent) {\n      // Only while playing: a muted pause is not discountable watch time.\n      if (this.isPlaying && this.mutedSinceMs === null) this.mutedSinceMs = t;\n    } else {\n      if (this.mutedSinceMs !== null) {\n        this.mutedTotalMs += t - this.mutedSinceMs;\n        this.mutedSinceMs = null;\n      }\n      // Unmuting mid-playback is when a muted-autoplay session first\n      // becomes audible — the only moment that stage can be observed for\n      // the dominant startup path, which no state change reports.\n      if (this.firstAudioMs === 0 && this.isPlaying) {\n        this.firstAudioMs = Math.max(MIN_STAGE_MS, t - this.startupAnchorMs);\n      }\n    }\n    this.isMuted = silent;\n    this.syncActive();\n  }\n\n  /** Fullscreen / picture-in-picture / inline transition. */\n  notePresentation(next: Presentation): void {\n    if (this.endedFlag || next === this.presentation) return;\n    this.closePresentation();\n    this.presentation = next;\n  }\n\n  /**\n   * The viewer asked for playback. Re-anchors the startup ladder here, once.\n   *\n   * Only the autoplay-off path calls this, and only on the first play(): an\n   * autoplay session already has intent at construction, and a SECOND call\n   * after playback began would rewrite a finished measurement.\n   *\n   * A stage that already closed is re-stamped PRE-PAID rather than left\n   * alone or allowed to go negative. In the deferred path the engine parses\n   * the manifest while fetching no media, so manifestLoadMs is genuinely\n   * already done when the viewer clicks — it cost them nothing, and\n   * MIN_STAGE_MS is how this contract says \"reached, immeasurably fast\".\n   * Leaving the old value would keep the poster dwell inside the ladder by\n   * another route.\n   */\n  notePlayIntent(): void {\n    if (this.endedFlag || this.startupTimeMs > 0) return;\n    this.startupAnchorMs = this.now();\n    if (this.manifestLoadMs > 0) this.manifestLoadMs = MIN_STAGE_MS;\n    if (this.firstFragLoadMs > 0) this.firstFragLoadMs = MIN_STAGE_MS;\n  }\n\n  /** Engine 'ready' — closes the manifest-load startup stage. */\n  noteReady(): void {\n    if (this.manifestLoadMs === 0) {\n      this.manifestLoadMs = Math.max(MIN_STAGE_MS, this.now() - this.startupAnchorMs);\n    }\n  }\n\n  /** First media bytes observed — closes the first-segment startup stage. */\n  noteFirstBytes(): void {\n    this.sawFirstBytes = true;\n    if (this.firstFragLoadMs === 0) {\n      this.firstFragLoadMs = Math.max(MIN_STAGE_MS, this.now() - this.startupAnchorMs);\n    }\n  }\n\n  noteSeek(): void {\n    if (this.endedFlag) return;\n    this.seekCount += 1;\n    // Remembered so the buffering that usually follows is attributed to the\n    // seek rather than reported as a network stall.\n    this.lastSeekMs = this.now();\n  }\n\n  /** Playback error (code from src/core/errors.ts — strings, no lockstep). */\n  noteError(\n    code: string,\n    detail: string | undefined,\n    fatal: boolean,\n    httpStatus: number,\n    positionMs: number,\n    /** The ENGINE's own name for the failure, when it supplied one. */\n    engineDetail?: string,\n  ): void {\n    if (this.endedFlag) return;\n    this.errorCount += 1;\n    this.lastErrorMs = this.now();\n    const err: NonNullable<WireBeacon['error']> = { code };\n    if (detail) err.detail = detail;\n    if (fatal) err.fatal = true;\n    if (httpStatus > 0) err.httpStatus = httpStatus;\n    if (positionMs > 0) err.positionMs = Math.round(positionMs);\n    const stage = this.stageOf(code, detail, engineDetail);\n    err.stage = stage;\n    this.lastErrorStage = stage;\n    const host = this.safeSample()?.cdnHost;\n    if (host) err.cdnHost = host;\n    this.enqueue({ error: err });\n    // Errors shouldn't wait up to 15s to surface fleet-wide.\n    this.flush();\n  }\n\n  /**\n   * Which step of the pipeline an error interrupted. Resolve-time codes come\n   * from the playback API before any media is touched; the rest are read off\n   * the engine's own detail string, which is why the contract carries the\n   * stage as an enum instead of leaving every reader to parse that string.\n   */\n  private stageOf(\n    code: string,\n    detail: string | undefined,\n    engineDetail: string | undefined,\n  ): WireErrorStage {\n    // The ENGINE's own name for the failure when it gave one. The player's\n    // message is composed prose — \"NETWORK error: fragLoadError (...)\" —\n    // and staging by pattern-matching that prose is how the single most\n    // common fatal error, a fragment load giving up, was staged as OTHER:\n    // no anchored pattern can match a string that opens with the error\n    // KIND. It also settles DRM: a licence 401 arrives with the code\n    // UNAUTHORIZED, which is indistinguishable from a resolve failure by\n    // code alone, but the engine calls it keyLoadError.\n    const engine = engineDetail ?? '';\n    if (engine !== '') {\n      // fragLOAD only: a fragment that arrived and would not parse\n      // (fragParsingError, fragDecryptError) is a decode failure, not a\n      // delivery one, and staging it as a segment problem would send\n      // someone to look at the CDN for a codec bug.\n      if (/^fragLoad/i.test(engine)) {\n        return this.sawFirstBytes ? 'ERROR_STAGE_SEGMENT' : 'ERROR_STAGE_FIRST_SEGMENT';\n      }\n      if (/^(manifest|level|audioTrack|subtitleTrack)/i.test(engine)) {\n        return 'ERROR_STAGE_MANIFEST';\n      }\n      if (/key|licen[cs]e/i.test(engine)) return 'ERROR_STAGE_DRM';\n      if (/parsing|append|buffer|remux|decrypt|media/i.test(engine)) return 'ERROR_STAGE_DECODE';\n      return 'ERROR_STAGE_OTHER';\n    }\n\n    // No engine detail: the failure did not come from a running engine, so\n    // the resolve stage is reachable here and nowhere else.\n    if (code === 'DRM_CLIENT') return 'ERROR_STAGE_DRM';\n    if (RESOLVE_CODES.has(code)) return 'ERROR_STAGE_RESOLVE';\n    const d = detail ?? '';\n    if (/frag|segment/i.test(d)) {\n      return this.sawFirstBytes ? 'ERROR_STAGE_SEGMENT' : 'ERROR_STAGE_FIRST_SEGMENT';\n    }\n    if (/manifest|level|playlist/i.test(d)) return 'ERROR_STAGE_MANIFEST';\n    if (/key|licen[cs]e/i.test(d)) return 'ERROR_STAGE_DRM';\n    if (/buffer|decode|media|append/i.test(d)) return 'ERROR_STAGE_DECODE';\n    return 'ERROR_STAGE_OTHER';\n  }\n\n  /** The 'ended' player state maps here with reason ENDED. */\n  noteEnded(): void {\n    this.end('END_REASON_ENDED');\n  }\n\n  /** Player torn down by the embedding page. */\n  noteDestroyed(): void {\n    this.end('END_REASON_STOPPED');\n  }\n\n  /** Fatal error path. */\n  noteFatal(): void {\n    this.end('END_REASON_FATAL_ERROR');\n  }\n\n  private heartbeat(): void {\n    if (this.endedFlag) return;\n    const t = this.now();\n    // The first tick after a suspension ends the session instead of\n    // reporting an interval that spans the whole gap.\n    if (this.checkSuspension(t)) return;\n    const s = this.opts.sample();\n\n    // Close open accounting windows into the totals without ending them.\n    let playing = this.playingTotalMs;\n    if (this.playingSinceMs !== null) playing += t - this.playingSinceMs;\n    let hidden = this.hiddenTotalMs;\n    if (this.hiddenSinceMs !== null) hidden += t - this.hiddenSinceMs;\n    // Stall, per interval. bufferingSinceMs is opened and closed by\n    // syncBuffering, which holds it ONLY while the stall is visible and not\n    // seek-induced — so both exclusions come for free here and, more to the\n    // point, cannot drift from the session totals that share that window.\n    // A stall spanning two heartbeats is split across them and counted once.\n    let rebuffer = this.rebufferTotalMs;\n    if (this.bufferingSinceMs !== null) rebuffer += t - this.bufferingSinceMs;\n    // Active watch (playing AND visible AND unmuted), per interval, off the\n    // window syncActive already maintains. MEASURED, not derived: the server\n    // has been reconstructing this as playing - hidden - muted, and those\n    // three overlap, so an interval that was both hidden and muted is\n    // subtracted twice. The overlap is not recoverable after the fact, which\n    // is why it has to be observed here rather than computed there.\n    let active = this.activeTotalMs;\n    if (this.activeSinceMs !== null) active += t - this.activeSinceMs;\n\n    const hb: Record<string, number | string | boolean> = {};\n    const bytes = s.bytesLoaded ?? 0;\n    if (bytes > this.lastBytes) hb.bytesDownloadedDelta = bytes - this.lastBytes;\n    this.lastBytes = bytes;\n    const pl = s.playlistBytesLoaded ?? 0;\n    if (pl > this.lastPlaylistBytes) hb.playlistBytesDelta = pl - this.lastPlaylistBytes;\n    this.lastPlaylistBytes = pl;\n    const dropped = s.droppedFrames ?? 0;\n    if (dropped > this.lastDropped) hb.droppedFramesDelta = dropped - this.lastDropped;\n    this.lastDropped = dropped;\n    const switches = s.renditionSwitches ?? 0;\n    if (switches > this.lastSwitches) hb.renditionSwitchesDelta = switches - this.lastSwitches;\n    this.lastSwitches = switches;\n    if (playing > this.lastHbPlayingMs)\n      hb.playingMsDelta = Math.round(playing - this.lastHbPlayingMs);\n    this.lastHbPlayingMs = playing;\n    if (hidden > this.lastHbHiddenMs) hb.hiddenMsDelta = Math.round(hidden - this.lastHbHiddenMs);\n    this.lastHbHiddenMs = hidden;\n    if (rebuffer > this.lastHbRebufferMs) {\n      hb.rebufferMsDelta = Math.round(rebuffer - this.lastHbRebufferMs);\n    }\n    this.lastHbRebufferMs = rebuffer;\n    // The COUNT is already gated on the same two exclusions where it is\n    // incremented, so this is a plain watermark diff. A stall that opens in\n    // one interval and closes in a later one counts in the interval it\n    // OPENED, which is where the viewer felt it.\n    if (this.rebufferCount > this.lastHbRebufferCount) {\n      hb.rebufferCountDelta = this.rebufferCount - this.lastHbRebufferCount;\n    }\n    this.lastHbRebufferCount = this.rebufferCount;\n    // Deltas cover FULL heartbeat intervals only — end() emits no final\n    // heartbeat, so a sub-interval tail after the last tick is carried by\n    // the sessionEnd totals alone. sum(activeMsDelta) therefore reconciles\n    // with activeWatchTimeMs through the last tick; the remainder is always\n    // < heartbeatMs. Same policy as every other per-interval figure above.\n    if (active > this.lastHbActiveMs) hb.activeMsDelta = Math.round(active - this.lastHbActiveMs);\n    this.lastHbActiveMs = active;\n\n    if (typeof s.latencySeconds === 'number') hb.latencyMs = Math.round(s.latencySeconds * 1000);\n    if (typeof s.targetLatencySeconds === 'number') {\n      hb.targetLatencyMs = Math.round(s.targetLatencySeconds * 1000);\n    }\n    if (typeof s.bandwidthKbps === 'number') hb.bandwidthEstKbps = s.bandwidthKbps;\n    if (typeof s.levelHeight === 'number') hb.renditionHeight = s.levelHeight;\n    if (typeof s.renditionBitrateKbps === 'number') {\n      hb.renditionBitrateKbps = s.renditionBitrateKbps;\n    }\n    if (s.atLiveEdge === true) hb.atLiveEdge = true;\n    if (typeof s.bufferedMs === 'number' && s.bufferedMs > 0) {\n      hb.bufferedMs = Math.round(s.bufferedMs);\n    }\n    hb.playerState = STATE_TO_WIRE[s.state] ?? 'PLAYER_STATE_UNSPECIFIED';\n\n    // ---- Contract v2.\n    let muted = this.mutedTotalMs;\n    if (this.mutedSinceMs !== null) muted += t - this.mutedSinceMs;\n    if (muted > this.lastHbMutedMs) hb.mutedMsDelta = Math.round(muted - this.lastHbMutedMs);\n    this.lastHbMutedMs = muted;\n\n    const up = s.upshifts ?? 0;\n    if (up > this.lastUpshifts) hb.upshiftsDelta = up - this.lastUpshifts;\n    this.lastUpshifts = up;\n    const down = s.downshifts ?? 0;\n    if (down > this.lastDownshifts) hb.downshiftsDelta = down - this.lastDownshifts;\n    this.lastDownshifts = down;\n    const decoded = s.decodedFrames ?? 0;\n    if (decoded > this.lastDecoded) hb.decodedFramesDelta = decoded - this.lastDecoded;\n    this.lastDecoded = decoded;\n    const reqErrors = s.requestErrors ?? 0;\n    if (reqErrors > this.lastRequestErrors) {\n      hb.requestErrorsDelta = reqErrors - this.lastRequestErrors;\n    }\n    this.lastRequestErrors = reqErrors;\n\n    // Request timing: the engine keeps a ring of recent durations and a\n    // cumulative count, so the tail this interval added is exactly the\n    // slice to summarize — no per-request beacon, no unbounded array.\n    const reqCount = s.requestCount ?? 0;\n    const reqDelta = reqCount - this.lastRequestCount;\n    this.lastRequestCount = reqCount;\n    if (reqDelta > 0) {\n      hb.requestCountDelta = reqDelta;\n      const times = s.requestTimesMs ?? [];\n      const recent = times.slice(Math.max(0, times.length - reqDelta)).sort((a, b) => a - b);\n      if (recent.length > 0) {\n        hb.requestTimeP50Ms = percentile(recent, 50);\n        hb.requestTimeP95Ms = percentile(recent, 95);\n      }\n    }\n\n    if (typeof s.latencySeconds === 'number' && typeof s.targetLatencySeconds === 'number') {\n      // Signed: behind the target is positive, ahead of it negative.\n      hb.driftMs = Math.round((s.latencySeconds - s.targetLatencySeconds) * 1000);\n    }\n    if (typeof s.playbackRate === 'number' && s.playbackRate > 0) {\n      hb.playbackRate = s.playbackRate;\n    }\n    if (s.qualityPinned === true) hb.qualityPinned = true;\n    if (s.cdnHost) hb.cdnHost = s.cdnHost;\n\n    this.maybeEmitMediaInfo(s);\n    this.enqueue({ heartbeat: hb });\n    this.flush();\n\n    if (bytes > 0) this.noteFirstBytes();\n  }\n\n  /**\n   * MediaInfo once per session, as soon as the engine has negotiated what it\n   * will play. SessionStart cannot carry this: it is emitted at engine\n   * selection, before the manifest is parsed or a DRM session exists.\n   */\n  private maybeEmitMediaInfo(s: AnalyticsSample): void {\n    const known = s.videoCodec ?? s.keySystem ?? s.ladderHeights;\n    if (!known) return;\n    const info: NonNullable<WireBeacon['mediaInfo']> = {};\n    if (s.videoCodec) info.videoCodec = s.videoCodec;\n    if (s.audioCodec) info.audioCodec = s.audioCodec;\n    if (s.keySystem) info.keySystem = KEY_SYSTEM_TO_WIRE[s.keySystem];\n    if (s.drmSecurityLevel) info.drmSecurityLevel = s.drmSecurityLevel;\n    if (s.ladderHeights?.length) info.ladderHeights = s.ladderHeights;\n    if (s.ladderTopBitrateKbps) info.ladderTopBitrateKbps = s.ladderTopBitrateKbps;\n    if (s.lowLatency) info.lowLatency = true;\n    // The contract says MediaInfo is emitted again if the negotiated facts\n    // change (an audio-track switch changes the codec; a rebuilt engine can\n    // change the ladder), so this compares rather than firing once. A\n    // session whose media never changes still sends exactly one.\n    const signature = JSON.stringify(info);\n    if (signature === this.mediaInfoSignature) return;\n    this.mediaInfoSignature = signature;\n    this.enqueue({ mediaInfo: info });\n  }\n\n  /**\n   * End the session exactly once: totals beacon + final flush. On pagehide\n   * the flush rides navigator.sendBeacon — the only delivery a closing page\n   * guarantees.\n   */\n  end(reason: WireEndReason, at?: number): void {\n    if (this.endedFlag) return;\n    this.endedFlag = true;\n    // `at`: the instant the session is closed AS OF. Only the suspension\n    // path passes one — the last tick before the page stopped — so a slept\n    // laptop closes every open window (playing, active, muted, hidden) there\n    // and books none of the sleep.\n    const t = at ?? this.now();\n    if (this.playingSinceMs !== null) {\n      this.playingTotalMs += t - this.playingSinceMs;\n      this.playingSinceMs = null;\n    }\n    if (this.hiddenSinceMs !== null) {\n      this.hiddenTotalMs += t - this.hiddenSinceMs;\n      this.hiddenSinceMs = null;\n    }\n    if (this.bufferingSinceMs !== null) {\n      this.rebufferTotalMs += t - this.bufferingSinceMs;\n      this.bufferingSinceMs = null;\n    }\n    if (this.mutedSinceMs !== null) {\n      this.mutedTotalMs += t - this.mutedSinceMs;\n      this.mutedSinceMs = null;\n    }\n    if (this.waitingSinceMs !== null) {\n      this.waitingTotalMs += t - this.waitingSinceMs;\n      this.waitingSinceMs = null;\n    }\n    this.isPlaying = false;\n    // At `t`, like every window above: the active-watch and presentation\n    // windows are the two that read the clock themselves.\n    this.syncActive(t);\n    this.closePresentation(t);\n    if (this.firstFlushTimer) {\n      clearTimeout(this.firstFlushTimer);\n      this.firstFlushTimer = null;\n    }\n    if (this.timer) {\n      clearInterval(this.timer);\n      this.timer = null;\n    }\n    if (this.idleTimer) {\n      clearTimeout(this.idleTimer);\n      this.idleTimer = null;\n    }\n    if (typeof window !== 'undefined') {\n      window.removeEventListener('pagehide', this.onPagehide);\n    }\n    if (typeof document !== 'undefined') {\n      document.removeEventListener('visibilitychange', this.onVisibility);\n      document.removeEventListener('freeze', this.onFreeze);\n      document.removeEventListener('resume', this.onResume);\n    }\n\n    const s = this.safeSample();\n    // A session that ends before its first heartbeat — an early error, a\n    // quick bounce, a short preview — would otherwise never report what it\n    // was playing, even though the engine has known since the manifest\n    // parsed.\n    if (s) this.maybeEmitMediaInfo(s);\n    const endTotals: Record<string, number | string> = {\n      reason,\n    };\n    const bytes = s?.bytesLoaded ?? this.lastBytes;\n    if (bytes > 0) endTotals.bytesDownloadedTotal = bytes;\n    if (this.playingTotalMs > 0) endTotals.watchTimeMs = Math.round(this.playingTotalMs);\n    if (this.rebufferCount > 0) endTotals.rebufferCount = this.rebufferCount;\n    if (this.rebufferTotalMs > 0) endTotals.rebufferTimeMs = Math.round(this.rebufferTotalMs);\n    if (this.startupTimeMs > 0) endTotals.startupTimeMs = Math.round(this.startupTimeMs);\n    if (this.manifestLoadMs > 0) endTotals.manifestLoadMs = Math.round(this.manifestLoadMs);\n    if (this.firstFragLoadMs > 0) endTotals.firstFragLoadMs = Math.round(this.firstFragLoadMs);\n    const license = s?.licenseTimeMs ?? 0;\n    if (license > 0) endTotals.licenseTimeMs = Math.round(license);\n    const dropped = s?.droppedFrames ?? this.lastDropped;\n    if (dropped > 0) endTotals.droppedFramesTotal = dropped;\n    if (this.errorCount > 0) endTotals.errorCount = this.errorCount;\n    if (this.hiddenTotalMs > 0) endTotals.hiddenTimeMs = Math.round(this.hiddenTotalMs);\n    if (this.seekCount > 0) endTotals.seekCount = this.seekCount;\n    if (this.pauseCount > 0) endTotals.pauseCount = this.pauseCount;\n\n    // ---- Contract v2 totals.\n    if (this.firstAudioMs > 0) endTotals.firstAudioMs = Math.round(this.firstAudioMs);\n    if (this.waitingTotalMs > 0) endTotals.waitingForLiveMs = Math.round(this.waitingTotalMs);\n    if (this.timeToFirstStallMs > 0) {\n      endTotals.timeToFirstStallMs = Math.round(this.timeToFirstStallMs);\n    }\n    const exit = s?.positionMs ?? 0;\n    if (exit > 0) endTotals.exitPositionMs = Math.round(exit);\n    if (this.activeTotalMs > 0) endTotals.activeWatchTimeMs = Math.round(this.activeTotalMs);\n    if (this.mutedTotalMs > 0) endTotals.mutedTimeMs = Math.round(this.mutedTotalMs);\n    // Abandon cohorts: only when the session actually ended NEAR the event,\n    // so an unrelated stall an hour earlier never reads as an abandonment.\n    // Not on a clean end: a viewer who stalled, recovered and watched to\n    // the finish has not abandoned anything.\n    const abandoned = reason !== 'END_REASON_ENDED';\n    if (abandoned && this.lastStallMs > 0 && t - this.lastStallMs <= ABANDON_WINDOW_MS) {\n      endTotals.abandonAfterStallMs = Math.max(MIN_STAGE_MS, Math.round(t - this.lastStallMs));\n    }\n    if (abandoned && this.lastErrorMs > 0 && t - this.lastErrorMs <= ABANDON_WINDOW_MS) {\n      endTotals.abandonAfterErrorMs = Math.max(MIN_STAGE_MS, Math.round(t - this.lastErrorMs));\n    }\n    if (this.fullscreenTotalMs > 0) {\n      endTotals.fullscreenTimeMs = Math.round(this.fullscreenTotalMs);\n    }\n    if (this.pipTotalMs > 0) endTotals.pipTimeMs = Math.round(this.pipTotalMs);\n    if (this.volumeChanges > 0) endTotals.volumeChanges = this.volumeChanges;\n    if (this.muteChanges > 0) endTotals.muteChanges = this.muteChanges;\n    const decodedTotal = s?.decodedFrames ?? this.lastDecoded;\n    if (decodedTotal > 0) endTotals.decodedFramesTotal = decodedTotal;\n    // Only for a session that never played: which stage it died at. The last\n    // error's stage when there was one, otherwise the first startup stage\n    // that never closed.\n    if (this.startupTimeMs === 0) {\n      endTotals.failedStage =\n        this.lastErrorStage ??\n        (this.manifestLoadMs === 0\n          ? 'ERROR_STAGE_MANIFEST'\n          : this.firstFragLoadMs === 0\n            ? 'ERROR_STAGE_FIRST_SEGMENT'\n            : 'ERROR_STAGE_OTHER');\n    }\n\n    this.pending.push(this.wrap({ sessionEnd: endTotals }));\n    this.flush(reason === 'END_REASON_PAGEHIDE');\n  }\n\n  private safeSample(): AnalyticsSample | null {\n    try {\n      return this.opts.sample();\n    } catch {\n      return null; // engine already torn down — totals fall back to last-seen\n    }\n  }\n\n  private wrap(event: Omit<WireBeacon, 'sid' | 'clientTime' | 'seq'>): WireBeacon {\n    this.seq += 1;\n    return {\n      sid: this.opts.sid,\n      clientTime: new Date(this.now()).toISOString(),\n      seq: this.seq,\n      ...event,\n    };\n  }\n\n  private enqueue(event: Omit<WireBeacon, 'sid' | 'clientTime' | 'seq'>): void {\n    this.pending.push(this.wrap(event));\n    while (this.pending.length > MAX_PENDING) {\n      // Evict the oldest routine beacon (heartbeat/state change) first;\n      // lifecycle and error beacons carry the session's story and only go\n      // when nothing routine is left to shed.\n      const i = this.pending.findIndex((b) => b.heartbeat || b.stateChange);\n      this.pending.splice(i >= 0 ? i : 0, 1);\n    }\n  }\n\n  private flush(useSendBeacon = false): void {\n    if (this.firstFlushTimer) {\n      clearTimeout(this.firstFlushTimer);\n      this.firstFlushTimer = null;\n    }\n    this.pendingStart = null;\n    if (this.pending.length === 0) return;\n    // The batch carries the beacons and nothing else. No api key rides a\n    // beacon in any form — not as a header, not in the envelope: the\n    // ingest has no origin gate to resolve it against (the origin was\n    // enforced by havik-streams when it minted the sid, and the ingest's\n    // reads join the mints), and a publishable key is never an identity.\n    // What identifies the batch is the sid on every beacon.\n    const body = JSON.stringify({ beacons: this.pending });\n    this.pending = [];\n\n    if (useSendBeacon) {\n      // Deliberately a STRING body: sendBeacon with a string posts\n      // text/plain, which is CORS-safelisted — no preflight, and a\n      // preflight never completes on an unloading page. A Blob typed\n      // application/json would trigger one and lose the final batch. The\n      // ingest parses the body regardless of Content-Type by design (D9).\n      const send =\n        this.opts.sendBeacon ??\n        (typeof navigator !== 'undefined' && navigator.sendBeacon\n          ? navigator.sendBeacon.bind(navigator)\n          : undefined);\n      if (send && send(this.url, body)) return;\n      // sendBeacon refused (quota) — fall through to keepalive fetch, which\n      // may or may not complete on an unloading page; acceptable loss.\n    }\n    const doFetch = this.opts.fetchFn ?? (typeof fetch !== 'undefined' ? fetch : undefined);\n    if (!doFetch) return;\n    void doFetch(this.url, {\n      method: 'POST',\n      headers: { 'content-type': 'application/json' },\n      body,\n      keepalive: true,\n    }).catch(() => {\n      // Fire-and-forget: a failed batch is dropped by design.\n    });\n  }\n}\n","// Session environment for the QoE plane (contract v2, SessionStart): the\n// IP-free facts about where the player runs, read once at session start.\n//\n// Model and OS version come from UA client hints ONLY — the high-entropy\n// values a browser is willing to hand over, on request. The raw user agent\n// string is never read or sent from here: the ingest already derives\n// browser family and major from the transport header and drops it (plan\n// D11), and parsing it a second time in the client would put a\n// fingerprint-grade string on the wire to no benefit. On a browser without\n// client hints (Safari, Firefox) both fields are simply absent.\n\nexport type DeviceClass =\n  | 'DEVICE_CLASS_PHONE'\n  | 'DEVICE_CLASS_TABLET'\n  | 'DEVICE_CLASS_DESKTOP'\n  | 'DEVICE_CLASS_TV'\n  | 'DEVICE_CLASS_OTHER';\nexport type ConnectionType =\n  | 'CONNECTION_TYPE_UNKNOWN'\n  | 'CONNECTION_TYPE_WIFI'\n  | 'CONNECTION_TYPE_CELLULAR'\n  | 'CONNECTION_TYPE_ETHERNET'\n  | 'CONNECTION_TYPE_OTHER';\n/**\n * The generation of the access network, separate from the bearer above.\n *\n * The web can only ever report the ESTIMATED_ half. `effectiveType` is not\n * a radio reading: it names the cellular generation whose typical\n * performance the connection RESEMBLES, derived from observed rtt and\n * downlink. A fast WiFi link reports '4g', which is exactly why these\n * values may not share a value space with the measured ones the native\n * SDKs send — merged, every Chrome-desktop WiFi session would be charted as\n * cellular traffic that does not exist.\n *\n * There is no ESTIMATED_5G: the API is specified to top out at '4g'. That\n * is a ceiling, not a mapping — a congested 5G link reports '3g', because\n * the value follows throughput rather than the radio.\n */\nexport type RadioAccess =\n  | 'RADIO_ACCESS_ESTIMATED_SLOW_2G'\n  | 'RADIO_ACCESS_ESTIMATED_2G'\n  | 'RADIO_ACCESS_ESTIMATED_3G'\n  | 'RADIO_ACCESS_ESTIMATED_4G';\nexport type IntegrationKind = 'INTEGRATION_KIND_SDK' | 'INTEGRATION_KIND_IFRAME';\n\n/**\n * Where the player is embedded, and how.\n *\n * Not simply location.origin: inside an iframe that is the PLAYER's own\n * origin, which is the same constant for every hosted-embed session and\n * answers a question nobody asked. The embedding page is what a customer\n * means by \"where\", so a framed player reports the top-level ancestor —\n * from ancestorOrigins where the browser has it, else the referrer's\n * origin — and says so with INTEGRATION_KIND_IFRAME.\n *\n * A cross-origin embedder that reveals neither leaves the host empty\n * rather than substituting the player's own origin: a wrong attribution\n * is worse than a missing one.\n */\nfunction embedding(): { host?: string; integration: IntegrationKind } {\n  if (typeof window === 'undefined') return { integration: 'INTEGRATION_KIND_SDK' };\n  let framed: boolean;\n  try {\n    framed = window.top !== window.self;\n  } catch {\n    // A cross-origin ancestor can make even the comparison throw, and that\n    // it threw is itself proof of being framed.\n    framed = true;\n  }\n  const own = window.location?.origin;\n  if (!framed) {\n    return { host: own && own !== 'null' ? own : undefined, integration: 'INTEGRATION_KIND_SDK' };\n  }\n  let host: string | undefined;\n  try {\n    const ancestors = (window.location as Location & { ancestorOrigins?: DOMStringList })\n      .ancestorOrigins;\n    // The LAST entry is the top-level document — the page the viewer is\n    // actually on; [0] would be an intermediate wrapper frame.\n    if (ancestors && ancestors.length > 0) host = ancestors[ancestors.length - 1] ?? undefined;\n    if (!host && typeof document !== 'undefined' && document.referrer) {\n      host = new URL(document.referrer).origin;\n    }\n  } catch {\n    host = undefined;\n  }\n  if (host === 'null') host = undefined;\n  return { host, integration: 'INTEGRATION_KIND_IFRAME' };\n}\n\nexport interface SessionEnvironment {\n  deviceClass: DeviceClass;\n  /** Marketing model name, e.g. \"Pixel 8\". Empty on desktop and wherever\n   *  client hints are unavailable. */\n  deviceModel?: string;\n  /** Platform version as the browser reports it, e.g. \"14.0.0\". */\n  osVersion?: string;\n  screenWidth?: number;\n  screenHeight?: number;\n  devicePixelRatio?: number;\n  viewportWidth?: number;\n  viewportHeight?: number;\n  connectionType: ConnectionType;\n  /** The generation, INDEPENDENT of connectionType: Chrome on Android sets\n   *  both, Chrome on desktop sets only this one, Safari and Firefox set\n   *  neither. Absent when the browser reports no effectiveType. */\n  radioAccess?: RadioAccess;\n  downlinkKbps?: number;\n  /** Round-trip estimate the browser reports, beside downlinkKbps. Absent\n   *  when unreported; never sent as 0, which would read as \"measured, and\n   *  instant\". */\n  rttMs?: number;\n  saveData?: boolean;\n  /** The page the player is embedded in: the document's own origin when the\n   *  SDK runs top-level, the TOP-LEVEL ancestor when it runs framed. Absent\n   *  when a cross-origin embedder reveals neither. */\n  host?: string;\n  integration: IntegrationKind;\n}\n\ninterface UAData {\n  mobile?: boolean;\n  platform?: string;\n  getHighEntropyValues?: (hints: string[]) => Promise<{ model?: string; platformVersion?: string }>;\n}\ninterface NetInfo {\n  type?: string;\n  effectiveType?: string;\n  downlink?: number;\n  rtt?: number;\n  saveData?: boolean;\n}\n\n/** Coarse form factor from UA client hints where they exist, else from the viewport. */\nfunction deviceClass(): DeviceClass {\n  if (typeof navigator === 'undefined') return 'DEVICE_CLASS_OTHER';\n  const ua = (navigator as Navigator & { userAgentData?: UAData }).userAgentData;\n  const platform = ua?.platform ?? '';\n  if (/tv|tizen|webos/i.test(platform)) return 'DEVICE_CLASS_TV';\n  const shortSide = typeof screen !== 'undefined' ? Math.min(screen.width, screen.height) : 0;\n  const mobile =\n    ua?.mobile ??\n    (typeof navigator.maxTouchPoints === 'number' &&\n      navigator.maxTouchPoints > 1 &&\n      shortSide > 0 &&\n      shortSide < 900);\n  if (mobile) return shortSide >= 600 ? 'DEVICE_CLASS_TABLET' : 'DEVICE_CLASS_PHONE';\n  if (ua || shortSide >= 900 || /mac|win|linux|chrome os/i.test(platform))\n    return 'DEVICE_CLASS_DESKTOP';\n  return 'DEVICE_CLASS_OTHER';\n}\n\n/**\n * The Network Information API, read as TWO independent facts.\n *\n * `type` is the bearer and `effectiveType` is the generation estimate, and\n * a browser may report either, both, or neither — they are not fallbacks\n * for one another. Chrome on Android sets both; Chrome on DESKTOP sets\n * `effectiveType`, `downlink` and `rtt` but leaves `type` undefined; Safari\n * and Firefox implement none of it. Reading only `type` therefore threw\n * away a populated reading on every Chrome-desktop session and reported\n * CONNECTION_TYPE_UNKNOWN beside a perfectly good downlink figure.\n *\n * The two are mapped into two fields rather than one: a WiFi session whose\n * effectiveType is '4g' is WIFI and ESTIMATED_4G, never 4G. See RadioAccess.\n */\nfunction connection(): Pick<\n  SessionEnvironment,\n  'connectionType' | 'radioAccess' | 'downlinkKbps' | 'rttMs' | 'saveData'\n> {\n  const c =\n    typeof navigator !== 'undefined'\n      ? (navigator as Navigator & { connection?: NetInfo }).connection\n      : undefined;\n  if (!c) return { connectionType: 'CONNECTION_TYPE_UNKNOWN' };\n  const map: Record<string, ConnectionType> = {\n    wifi: 'CONNECTION_TYPE_WIFI',\n    cellular: 'CONNECTION_TYPE_CELLULAR',\n    ethernet: 'CONNECTION_TYPE_ETHERNET',\n  };\n  // The four values the spec defines, and only those: an unrecognised\n  // string is left ABSENT rather than mapped to a nearest neighbour, so a\n  // future spec value cannot silently land in the wrong bucket.\n  const radio: Record<string, RadioAccess> = {\n    'slow-2g': 'RADIO_ACCESS_ESTIMATED_SLOW_2G',\n    '2g': 'RADIO_ACCESS_ESTIMATED_2G',\n    '3g': 'RADIO_ACCESS_ESTIMATED_3G',\n    '4g': 'RADIO_ACCESS_ESTIMATED_4G',\n  };\n  // Own-property lookups, not a bare index. `navigator.connection` is\n  // replaceable by the embedding page, so these strings are not guaranteed\n  // to be the spec's — and a bare `radio['constructor']` resolves to an\n  // INHERITED property of Object.prototype, which is truthy. That would put\n  // a non-enum value on the beacon and break the rule directly above: an\n  // unrecognised reading stays ABSENT.\n  //\n  // hasOwnProperty.call, not Object.hasOwn: the global build targets\n  // firefox91 and safari15 (vite.global.config.ts), and Object.hasOwn needs\n  // Firefox 92 / Safari 15.4. Vite downlevels syntax but does not polyfill\n  // built-in methods, so it would be undefined there — and the page that\n  // defines its own navigator.connection, which is the whole reason for\n  // this guard, is exactly the case that would then throw out of session\n  // start.\n  const own = <T>(table: Record<string, T>, key: string): T | undefined =>\n    Object.prototype.hasOwnProperty.call(table, key) ? table[key] : undefined;\n  const out: ReturnType<typeof connection> = {\n    connectionType: c.type\n      ? (own(map, c.type) ?? 'CONNECTION_TYPE_OTHER')\n      : 'CONNECTION_TYPE_UNKNOWN',\n  };\n  if (c.effectiveType) {\n    const r = own(radio, c.effectiveType);\n    if (r) out.radioAccess = r;\n  }\n  if (typeof c.downlink === 'number' && c.downlink > 0)\n    out.downlinkKbps = Math.round(c.downlink * 1000);\n  // Same guard as downlink: an unreported rtt must stay absent, not become\n  // a measured zero.\n  if (typeof c.rtt === 'number' && c.rtt > 0) out.rttMs = Math.round(c.rtt);\n  if (c.saveData) out.saveData = true;\n  return out;\n}\n\nexport function collectEnvironment(video: HTMLVideoElement): SessionEnvironment {\n  const where = embedding();\n  const env: SessionEnvironment = {\n    deviceClass: deviceClass(),\n    integration: where.integration,\n    ...connection(),\n  };\n  if (where.host) env.host = where.host;\n  if (typeof screen !== 'undefined' && screen.width > 0) {\n    env.screenWidth = screen.width;\n    env.screenHeight = screen.height;\n  }\n  if (typeof window !== 'undefined' && window.devicePixelRatio > 0) {\n    env.devicePixelRatio = Math.round(window.devicePixelRatio * 100) / 100;\n  }\n  const rect = video.getBoundingClientRect?.();\n  if (rect && rect.width > 0) {\n    env.viewportWidth = Math.round(rect.width);\n    env.viewportHeight = Math.round(rect.height);\n  }\n  return env;\n}\n\n/**\n * The two high-entropy hints, which the API only exposes asynchronously.\n * Resolves from data the browser already holds, so it settles in well under\n * the window SessionStart waits before it goes out; a browser that declines\n * or lacks the API yields an empty object and the fields stay absent.\n */\nexport async function collectHighEntropy(): Promise<\n  Pick<SessionEnvironment, 'deviceModel' | 'osVersion'>\n> {\n  const ua =\n    typeof navigator !== 'undefined'\n      ? (navigator as Navigator & { userAgentData?: UAData }).userAgentData\n      : undefined;\n  if (!ua?.getHighEntropyValues) return {};\n  try {\n    const v = await ua.getHighEntropyValues(['model', 'platformVersion']);\n    const out: Pick<SessionEnvironment, 'deviceModel' | 'osVersion'> = {};\n    if (v.model) out.deviceModel = v.model;\n    if (v.platformVersion) out.osVersion = v.platformVersion;\n    return out;\n  } catch {\n    return {};\n  }\n}\n","// SDK version for the analytics SessionStart beacon (build-time injected).\n//\n// `__HAVIK_SDK_VERSION__` is defined by the vite configs from\n// npm_package_version at build time; `typeof` on an undeclared identifier is\n// safe, so test runners and consumers that bypass the define get 'dev'.\n// Fidelity note: the value is only as good as package.json at build time —\n// semantic-release stamps it during the release pipeline, local builds say\n// whatever the placeholder version is.\ndeclare const __HAVIK_SDK_VERSION__: string | undefined;\n\nexport const SDK_VERSION: string =\n  typeof __HAVIK_SDK_VERSION__ === 'string' ? __HAVIK_SDK_VERSION__ : 'dev';\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/* A status card carries its own logo — the corner watermark would duplicate it. */\n.havik-player.havik-has-status .havik-watermark{display:none}\n\n.havik-bar{position:absolute;left:0;right:0;bottom:0;padding:0 12px 8px;\n  opacity:1;transition:opacity .25s;z-index:2}\n.havik-seek{position:relative;height:16px;display:flex;align-items:center;cursor:pointer;margin-bottom:2px}\n.havik-seek__track{position:relative;width:100%;height:4px;border-radius:3px;background:rgba(255,255,255,.25)}\n.havik-seek__buffered{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:rgba(255,255,255,.4)}\n.havik-seek__played{position:absolute;left:0;top:0;height:100%;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-seek__thumb{position:absolute;top:50%;width:13px;height:13px;border-radius:50%;\n  background:var(--havik-accent,#E1B600);transform:translate(-50%,-50%) scale(0);transition:transform .1s}\n.havik-seek:hover .havik-seek__thumb{transform:translate(-50%,-50%) scale(1)}\n\n.havik-controls{display:flex;align-items:center;gap:6px;height:40px}\n.havik-spacer{flex:1}\n.havik-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;\n  border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;border-radius:var(--havik-radius,8px);\n  padding:0;transition:background .12s,color .12s}\n.havik-btn:hover{background:rgba(255,255,255,.12)}\n.havik-btn:focus-visible{outline:2px solid var(--havik-accent,#E1B600);outline-offset:1px}\n.havik-btn svg{width:22px;height:22px}\n\n/* The volume level is filled in accent up to the thumb, like the seek bar's\n   played span — a native range input paints no \"elapsed\" side, which left the\n   level readable only from the dot's position. --havik-vol (0..1) is set by\n   syncVolume(); the track width and thumb size live here as variables because\n   the WebKit fill has to compute the thumb centre from both. */\n.havik-vol{display:flex;align-items:center}\n.havik-vol__slider{--havik-vol-w:70px;--havik-vol-thumb:12px;--havik-vol:1;\n  --havik-vol-pos:calc(var(--havik-vol-thumb) / 2 + var(--havik-vol) * (var(--havik-vol-w) - var(--havik-vol-thumb)));\n  width:0;opacity:0;transition:width .18s,opacity .18s;height:4px;cursor:pointer;\n  appearance:none;-webkit-appearance:none;background:transparent;border-radius:3px;margin:0 4px}\n.havik-vol:hover .havik-vol__slider,.havik-vol:focus-within .havik-vol__slider{width:var(--havik-vol-w);opacity:1}\n/* WebKit/Blink have no progress pseudo-element: paint the fill as a hard-stop\n   gradient that ends at the thumb centre. */\n.havik-vol__slider::-webkit-slider-runnable-track{height:4px;border-radius:3px;\n  background:linear-gradient(to right,var(--havik-accent,#E1B600) var(--havik-vol-pos),\n  rgba(255,255,255,.3) var(--havik-vol-pos))}\n/* Firefox fills natively, and tracks the thumb exactly. */\n.havik-vol__slider::-moz-range-track{height:4px;border-radius:3px;background:rgba(255,255,255,.3)}\n.havik-vol__slider::-moz-range-progress{height:4px;border-radius:3px;background:var(--havik-accent,#E1B600)}\n.havik-vol__slider::-webkit-slider-thumb{-webkit-appearance:none;width:var(--havik-vol-thumb);\n  height:var(--havik-vol-thumb);border-radius:50%;background:var(--havik-accent,#E1B600);\n  margin-top:calc((4px - var(--havik-vol-thumb)) / 2)}\n.havik-vol__slider::-moz-range-thumb{width:var(--havik-vol-thumb);height:var(--havik-vol-thumb);\n  border:0;border-radius:50%;background:var(--havik-accent,#E1B600)}\n\n.havik-time{font-size:12px;color:var(--havik-text-muted,#9aa0ad);\n  font-variant-numeric:tabular-nums;padding:0 6px;white-space:nowrap}\n.havik-live{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;\n  letter-spacing:.4px;padding:0 8px;color:var(--havik-text,#fff)}\n.havik-live svg{width:9px;height:9px;color:var(--havik-text-muted,#9aa0ad)}\n.havik-live.is-live svg{color:var(--havik-accent,#E1B600)}\n.havik-live__go{margin-left:2px;border:0;cursor:pointer;background:transparent;color:var(--havik-text-muted,#9aa0ad);\n  font:inherit;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 6px;border-radius:6px}\n.havik-live.is-live .havik-live__go{display:none}\n.havik-live__go:hover{color:var(--havik-accent,#E1B600)}\n\n.havik-menu{position:absolute;bottom:54px;right:12px;min-width:170px;max-height:230px;overflow:auto;\n  background:var(--havik-surface,#1B1C23);border:1px solid var(--havik-border,rgba(255,255,255,.1));\n  border-radius:var(--havik-radius,8px);box-shadow:0 10px 30px rgba(0,0,0,.5);padding:6px;z-index:3}\n.havik-menu[hidden]{display:none}\n.havik-menu__title{font-size:11px;text-transform:uppercase;letter-spacing:.6px;\n  color:var(--havik-text-muted,#9aa0ad);padding:6px 8px 4px}\n.havik-menu__item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;\n  border:0;background:transparent;color:var(--havik-text,#fff);cursor:pointer;font:inherit;font-size:13px;\n  text-align:left;padding:7px 8px;border-radius:6px}\n.havik-menu__item:hover{background:var(--havik-surface-muted,#2a2c36)}\n.havik-menu__item[aria-checked=\"true\"]{color:var(--havik-accent,#E1B600);font-weight:600}\n.havik-menu__item[aria-checked=\"true\"]::after{content:'✓'}\n`;\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.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.11-.2.06-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z\"/>',\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 {\n  enterVideoFullscreen,\n  exitVideoFullscreen,\n  videoFullscreenActive,\n} from '../core/fullscreen';\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  /**\n   * Message shown INSTEAD of endedMessage when the server says the stream\n   * stopped but the match is still running (`endedReason: 'interrupted'` —\n   * an ingest outage that outlived the bridge's hold). Telling a viewer\n   * mid-match that the stream \"has ended\" is what this exists to prevent.\n   *\n   * Default \"Reconnecting — please stand by\", which matches what the player\n   * does WHEN auto-rejoin is on (`rejoinOnLive`, the default, and it needs\n   * `liveStateEvents` too). A host that turns rejoin off should pass its own\n   * wording here: nothing will re-attach on its own, so the default would\n   * promise the viewer a reconnect that never comes.\n   */\n  interruptedMessage?: 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  /** Copy currently rendered on the card; part of the dedup key (see\n   *  renderStatus) so a same-kind message change still repaints. */\n  private statusMsg: string | 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 interruptedMessage: 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.interruptedMessage = o.interruptedMessage ?? 'Reconnecting — please stand by';\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      // Paint the fill from the input event rather than waiting for the\n      // 'volumechange' round-trip, so it tracks the thumb during a drag.\n      this.setVolumeFill(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    // `ended` can fire a second time with a refined reason (interrupted →\n    // match_ended) WITHOUT a state transition, so statechange alone would\n    // never repaint the card and the viewer would keep being promised a\n    // reconnect for a match that is over. syncState re-reads player.state\n    // (still 'ended') and the copy-aware dedup does the rest.\n    this.offFns.push(this.player.on('ended', () => 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    // Prefixed/native-fullscreen state changes: older Safari fires the\n    // prefixed document event for element fullscreen, and iPhone Safari's\n    // video-native fullscreen (webkitEnterFullscreen, see toggleFullscreen)\n    // reports only through these video-scoped events.\n    this.on(this.doc, 'webkitfullscreenchange', () => this.syncFullscreen());\n    this.on(v, 'webkitbeginfullscreen', () => this.syncFullscreen());\n    this.on(v, 'webkitendfullscreen', () => 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  /** Copy for a card kind. Split out of renderStatus so the dedup below can\n   *  compare the TEXT, not just the kind — an \"ended\" card can change its\n   *  message without changing kind (interrupted → match_ended). */\n  private statusMessageFor(kind: ControlBar['statusKind']): string | null {\n    if (kind === 'ended') {\n      // An \"interrupted\" end is a live match whose stream dropped. With\n      // auto-rejoin on (the default) the player keeps its live-state\n      // subscription open and re-attaches when the bridge returns, so the\n      // default copy promises exactly what it will do; a host that disabled\n      // rejoin overrides interruptedMessage (see the option's docs).\n      return this.player.endedReason === 'interrupted'\n        ? this.interruptedMessage\n        : this.endedMessage;\n    }\n    if (kind === 'waiting') {\n      // A waiting card after playback has run is a mid-session rejoin (ingest\n      // outage → session recreated), not a pre-kickoff wait — \"Starting soon\"\n      // would misdescribe a live match recovering.\n      return this.hasPlayed ? 'Reconnecting — please stand by' : 'Starting soon…';\n    }\n    if (kind === 'error') {\n      return this.errorMessage ?? this.lastError?.message ?? 'Playback unavailable';\n    }\n    return null; // preplay carries a CTA, not copy; null = no card\n  }\n\n  /** Build (or tear down) the status card for `kind`. No-op when neither the\n   *  kind NOR its copy changed, so repeated statechange events don't\n   *  rebuild/flicker the DOM — but a same-kind message change (the\n   *  interrupted → match_ended upgrade) still re-renders, otherwise a viewer\n   *  keeps being promised a reconnect for a match that is over. */\n  private renderStatus(kind: ControlBar['statusKind']): void {\n    const msgText = this.statusMessageFor(kind);\n    if (kind === this.statusKind && msgText === this.statusMsg) return;\n    this.statusKind = kind;\n    this.statusMsg = msgText;\n    this.statusBox?.remove();\n    this.statusBox = null;\n    // The card carries its own logo, so the corner watermark is redundant\n    // (and reads as a duplicate) while one is up.\n    this.root.classList.toggle('havik-has-status', !!kind);\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 = msgText ?? '';\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    const level = muted ? 0 : v.volume;\n    this.volSlider.value = String(level);\n    this.setVolumeFill(level);\n  }\n\n  /** Drives the accent fill on the volume track (see --havik-vol in styles). */\n  private setVolumeFill(level: number): void {\n    this.volSlider.style.setProperty('--havik-vol', String(Math.min(1, Math.max(0, level))));\n  }\n\n  // ---- progress / live ----------------------------------------------------\n\n  /**\n   * The span the scrub bar maps onto, and the window a seek may land in.\n   *\n   * On a live stream the right end is the live-edge TARGET, not `seekable.end()`\n   * — the bleeding edge, where the newest parts aren't published yet and\n   * playback stutters frame-by-frame (see the over-seek clamp in the hls.js\n   * engine for why hls.js can't recover from it). Mapping 100% to the target\n   * means dragging to the far right is \"go live\", the same spot seekToLive()\n   * uses, and the thumb reads full at the live edge instead of parking ~2s short\n   * of an end the player deliberately never reaches.\n   *\n   * `targetLatencySeconds` is engine-reported and hls.js-only, so the native\n   * (Safari) path keeps the raw range — AVPlayer runs its own live-edge policy.\n   */\n  private seekableRange(): { start: number; end: number } | null {\n    const v = this.video;\n    let start: number;\n    let end: number;\n    if (v.seekable && v.seekable.length > 0) {\n      start = v.seekable.start(0);\n      end = v.seekable.end(v.seekable.length - 1);\n    } else if (Number.isFinite(v.duration) && v.duration > 0) {\n      start = 0;\n      end = v.duration;\n    } else {\n      return null;\n    }\n    const stats = this.player.getStats();\n    // isLive !== false = live or not yet known; targetLatencySeconds is only\n    // reported once level details exist, i.e. once isLive is actually known, so\n    // VOD never pulls the end back.\n    if (stats.isLive !== false && stats.targetLatencySeconds && stats.targetLatencySeconds > 0) {\n      end = Math.max(start, end - stats.targetLatencySeconds);\n    }\n    if (end > start) return { start, end };\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  /**\n   * Delegates to the shared capability ladder (core/fullscreen.ts): element\n   * fullscreen on the player root where the API exists (keeps this control\n   * bar on screen), video-native webkitEnterFullscreen on iPhone (NATIVE\n   * controls; the user exits via the native Done button and\n   * webkitendfullscreen re-syncs the icon).\n   */\n  private async toggleFullscreen(): Promise<void> {\n    try {\n      if (videoFullscreenActive(this.video, this.root)) {\n        await exitVideoFullscreen(this.video, this.root);\n      } else {\n        await enterVideoFullscreen(this.video, this.root);\n      }\n    } catch {\n      // fullscreen can be blocked by policy; ignore\n    }\n  }\n\n  private syncFullscreen(): void {\n    const full = videoFullscreenActive(this.video, 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', 'havik-has-status');\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 { baseUrlForEnv, type HavikEnv } from '../core/env';\nimport { resolvePlaybackOnce, resolveStream, type ResolveOptions } from '../core/resolve';\nimport { HlsEngine, hlsSupported } from '../core/engine/hls';\nimport { NativeHlsEngine, nativeHlsSupported, webkitMediaStack } 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 { enterVideoFullscreen, exitVideoFullscreen } from '../core/fullscreen';\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 EndedReason,\n  type LiveStateSubscription,\n} from '../core/events';\nimport { AnalyticsSession, resolveBeaconsEndpoint, type Presentation } from '../core/analytics';\nimport { collectEnvironment, collectHighEntropy } from '../core/environment';\n\n// Re-exported: it names the `ended` event payload and Player.endedReason, so\n// a managed-player consumer must be able to refer to it without reaching into\n// the BYO entry point.\nexport type { EndedReason } from '../core/events';\nimport { SDK_VERSION } from '../core/version';\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  /**\n   * Which Oddin environment to play against. Prefer this over\n   * {@link baseUrl}: the host is then correct by construction, and so is\n   * everything derived from it (the SSE events host, the QoE beacons host).\n   *\n   * Exactly one of `env` or {@link baseUrl} is required. Passing both is an\n   * error rather than a precedence rule — the two disagreeing is precisely\n   * the misconfiguration worth refusing, and silently preferring one would\n   * point a player at an environment nobody chose.\n   */\n  env?: HavikEnv;\n  /**\n   * API base URL, when {@link env} does not name it. Use this for a\n   * deployment the named environments do not cover: your own CDN or gateway\n   * fronting the streams API, the CN plane, or a pinned build that must\n   * outlive a domain change.\n   *\n   * Note that a base which is not a `feed[-dev].<domain>` host derives no QoE\n   * beacons endpoint, so pair it with `analytics: { endpoint }`.\n   */\n  baseUrl?: string;\n  matchUrn: string;\n  credential: CredentialSource;\n  /**\n   * Start playback once ready. Default: true. With false the player resolves\n   * playback, then holds in a pre-play 'paused' state (poster + play card,\n   * no spinner) and defers media loading — segments and DRM license traffic —\n   * until the first play(). The hls.js engine still parses the manifest\n   * pre-play ('ready', track lists); the native engine defers all fetching\n   * and emits 'ready' at attach. A policy-blocked programmatic play()\n   * ('autoplayblocked') leaves the loader warming the buffer — media and DRM\n   * traffic flows from that kick, not from a viewer gesture.\n   */\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: 2 (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' prefers hls.js wherever supported (incl. modern\n   *  Safari/iOS via ManagedMediaSource) and falls back to native HLS. 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  /**\n   * Message shown instead of {@link endedMessage} when the stream stopped but\n   * the match is still running (the server's `interrupted` reason — an ingest\n   * outage that outlived the bridge's reconnect hold). Default:\n   * \"Reconnecting — please stand by\" — a promise the player keeps while\n   * {@link rejoinOnLive} is on (the default; it also needs\n   * {@link liveStateEvents}). Override it if you disable auto-rejoin, since\n   * nothing will then re-attach on its own.\n   */\n  interruptedMessage?: string;\n  /** Message on the fatal-error card. Default: the error's own message. */\n  errorMessage?: string;\n  /**\n   * Turn on hls.js's internal console logging — segment/part scheduling, ABR\n   * switches, and EME key-session + license traffic. Default: false.\n   *\n   * For diagnosing a playback problem with Oddin support; leave it off in\n   * production (it logs on every fragment, and the EME lines include license\n   * request/response metadata). hls.js engine only — a no-op on the native\n   * engine, which has no equivalent. Equivalent to `hlsConfig: { debug: true }`,\n   * which still wins if you set both.\n   */\n  debug?: boolean;\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 re-resolve playback so the signed DRM license URL\n   * the player would use next stays inside its signature lifetime (~10 min).\n   * Default: 8 min. Set 0 to disable.\n   *\n   * This refreshes the URL, not the license. It gives the player a newer URL\n   * for the next license request it makes, and issues none of its own, so it\n   * cannot extend a license the CDM already holds: how long that stays valid\n   * is set when it is issued, server-side. A session that outlasts its license\n   * ends with a fatal `DRM_CLIENT` error whatever this is set to.\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`; a later pushed `live`\n   * rejoins automatically (see {@link rejoinOnLive}). 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   * Rejoin automatically when a stream that ended comes back live. An ingest\n   * outage longer than the bridge's reconnect grace tears the session down\n   * and — when the encoder reconnects — recreates it under a NEW manifest\n   * URL, while the old playlist freezes at HTTP 200 forever. A player that\n   * latched `ended` can therefore only recover by re-resolving playback.\n   * With this on (the default), the live-state subscription stays open after\n   * `ended` (it closes on `gone`, the past-catchup terminal), and a pushed\n   * `live` re-arms and re-attaches automatically through the same jittered\n   * go-live path the pre-live wait uses. Set false to keep `ended` terminal\n   * (the pre-1.13 behavior). No effect when {@link liveStateEvents} is\n   * disabled — without the push channel there is nothing to rejoin on.\n   */\n  rejoinOnLive?: 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   * Extra headers attached to every request the SDK makes to {@link baseUrl}\n   * (playback resolution, live-status polling) — and ONLY there: never to\n   * CDN manifest/segment hosts, DRM license endpoints, the SSE events host,\n   * or the beacons endpoint. For integrations whose API front door wants\n   * its own auth signal on top of the publishable key — an operator console\n   * proving a session to its reverse proxy, or a customer gateway in front\n   * of the streams API. `x-api-key` cannot be overridden through this map.\n   */\n  apiHeaders?: Record<string, string>;\n  /**\n   * QoE beacons + CMCD session attribution. Active only when the playback\n   * response carries an analytics sid (the server-side flag) — without one\n   * this option does nothing. Default: true, with the beacons endpoint\n   * derived from {@link baseUrl} by hostname convention (`feed[-dev].…` →\n   * `beacons[-dev].…`). Pass `false` to opt out entirely, or\n   * `{ endpoint }` to point at a non-conventional beacons host.\n   *\n   * If {@link baseUrl} is NOT a `feed[-dev].<domain>` host — a CDN or gateway\n   * fronting the streams API, say — nothing can be derived and beacons are\n   * disabled. That is reported, not silent: a `console.warn` naming the host\n   * (once per host) plus a non-fatal `warning` event. Supply `{ endpoint }`.\n   *\n   * Privacy: the sid is an opaque per-session id, held in memory only and\n   * never persisted. Beacons carry playback QoE numbers plus the session's\n   * environment (form factor, device model and OS version where the browser\n   * offers client hints, screen, connection, embedding origin) — never an\n   * IP, never the raw user agent, and no identifier beyond the sid.\n   */\n  analytics?:\n    | boolean\n    | {\n        endpoint?: string;\n        /**\n         * How long the player may sit paused before the session closes itself\n         * (`END_REASON_IDLE`), and the suspension gap (OS sleep, a frozen tab)\n         * after which it does the same on waking. Default 10 minutes; `0`\n         * disables the idle close; the suspension gap never goes below five\n         * minutes, whatever this is set to (a hidden tab's throttled timers\n         * legitimately gap to a minute). After an idle close the next play()\n         * reloads the stream — a fresh session, a fresh sid, the live edge.\n         */\n        idleMs?: number;\n      };\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  /**\n   * End of stream. The payload says WHY when the server told us\n   * (`interrupted` = the match is still running and play should resume, so\n   * do NOT say \"ended\"; `match_ended` = a real end); `undefined` means the\n   * end came from the HLS staleness watchdog or a server predating the\n   * field — treat it as a real end, the pre-existing behavior. Fires again\n   * with `match_ended` if an interruption later turns out to be the end.\n   */\n  ended: EndedReason | undefined;\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  /** Why the stream ended, when the server said so; null otherwise. Cleared\n   *  when fresh playback commits (including a rejoin). See EndedReason. */\n  readonly endedReason: EndedReason | 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  /**\n   * Enter fullscreen: element fullscreen on the player container where the\n   * API exists (custom controls stay on screen); on iPhone Safari falls back\n   * to the video-native presentation with NATIVE controls (there is no\n   * element Fullscreen API there at all). Call from a user gesture; on\n   * iPhone it takes effect once media is loaded. Resolves false when no\n   * fullscreen mechanism exists on the platform.\n   */\n  enterFullscreen(): Promise<boolean>;\n  /** Leave fullscreen (element or video-native). Safe when not fullscreen. */\n  exitFullscreen(): 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;\n/**\n * How often to re-resolve playback so the signed license URL the engine would\n * use is younger than havik-streams' DRM_SIGN_TTL (10m).\n *\n * This refreshes the URL, NOT the license: it hands the engine a new string\n * (see refreshLicense → engine.setLicenseUrl), and licenseXhrSetup reads that\n * string when hls.js next POSTs. It issues no license request of its own, so it\n * cannot extend a license the CDM has already taken — the licence's own\n * expiration governs that, and it is set server-side\n * (havik-drm LICENSE_EXPIRATION_DURATION). Do not \"fix\" that by rebuilding the\n * instance on a timer: a rebuild is a visible interruption, and the CDM it\n * would want to re-key is not replaceable anyway (see core/drm/cdm.ts).\n */\nconst DEFAULT_LICENSE_REFRESH_MS = 8 * 60 * 1000;\n\n/**\n * Options with the endpoint decision already made. The player reads\n * `opts.baseUrl` in a dozen places (resolve, SSE derivation, beacons\n * derivation); resolving once at the boundary keeps every one of them a\n * plain string instead of pushing an `env`-or-`baseUrl` branch inward.\n */\ntype ResolvedPlayerOptions = CreatePlayerOptions & { baseUrl: string };\n\n/**\n * Settle `env` / `baseUrl` into the one base URL the player will use, or\n * throw. Kept separate from the other createPlayer guards because it is the\n * only one that has to reject a combination rather than an absence.\n */\nfunction resolveEndpoint(opts: CreatePlayerOptions): string {\n  if (opts.env !== undefined && opts.baseUrl !== undefined) {\n    throw new PlaybackError(\n      'INTERNAL',\n      0,\n      `createPlayer: pass env OR baseUrl, not both (got env=${JSON.stringify(opts.env)} ` +\n        `and baseUrl=${JSON.stringify(opts.baseUrl)})`,\n    );\n  }\n  if (opts.env !== undefined) return baseUrlForEnv(opts.env);\n  if (!opts.baseUrl) {\n    throw new PlaybackError(\n      'INTERNAL',\n      0,\n      \"createPlayer: env or baseUrl is required (env: 'integration' | 'production')\",\n    );\n  }\n  return opts.baseUrl;\n}\n\n/**\n * Endpoint selection, enforced at COMPILE time.\n *\n * `env` and `baseUrl` are both optional on {@link CreatePlayerOptions} so it\n * stays an interface others can `extend` and `Partial<>`. That alone would\n * make \"neither given\" type-check and fail only in the browser, which is a\n * guarantee this SDK used to have while `baseUrl` was required. These two\n * overloads put it back at the call site without changing the interface: one\n * names `env` and forbids `baseUrl`, the other the reverse, so neither and\n * both are both \"no overload matches this call\".\n *\n * resolveEndpoint still enforces the same rule at runtime — the overloads do\n * nothing for a JavaScript caller, and that is most of the embed surface.\n */\nexport function createPlayer(\n  opts: CreatePlayerOptions & { env: HavikEnv; baseUrl?: never },\n): Promise<Player>;\nexport function createPlayer(\n  opts: CreatePlayerOptions & { baseUrl: string; env?: never },\n): Promise<Player>;\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  const baseUrl = resolveEndpoint(opts);\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, baseUrl });\n  await player.start();\n  return player;\n}\n\nclass ManagedPlayer implements Player {\n  private readonly opts: ResolvedPlayerOptions;\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  /** Why the current end happened, when the server said (see EndedReason).\n   *  null = ended without a reason (an older server, or the HLS staleness\n   *  watchdog getting there first) — callers must read that as \"unknown\",\n   *  which is exactly the pre-#191 behavior: assume a real end. */\n  private _endedReason: EndedReason | null = null;\n  /** Pending post-ended rejoin (the jitter timer; see scheduleRejoin). */\n  private rejoinTimer: ReturnType<typeof setTimeout> | null = null;\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  /** Abort handle for the SSE arm's polling fallback — closeArming() (and thus\n   *  reload()/retry()/destroy()) must cancel it, or an orphaned poll keeps\n   *  hitting /v1/playback and emitting stale 'waiting' for a dead generation. */\n  private armFallbackCtrl: AbortController | null = null;\n  /** Last fatal error routed through fail() — start() turns a fatal engine\n   *  load into a createPlayer rejection with it (the emit happens before the\n   *  caller can attach listeners, so the event alone is unobservable there). */\n  private _lastError: PlaybackError | null = null;\n  /** QoE beacon session (plan A2); one per sid, created at engine commit. */\n  private analyticsSession: AnalyticsSession | null = null;\n  private detachMediaListeners?: () => void;\n  /** True once play() was ever asked for (user gesture or API). Ends the\n   *  autoplay-off deferral, and lets a post-play reload/rejoin resume without\n   *  a second gesture — playback the viewer started should come back on its\n   *  own after a recovery re-attach. */\n  private playRequested = false;\n  /**\n   * The beacon session closed itself for inactivity (see the `idleMs`\n   * option): the sid is finished and the engine was stopped, so the next\n   * play() is a rejoin — reload() resolves a new sid and re-plays from the\n   * live edge — not a resume of a session that no longer exists.\n   */\n  private idleClosed = false;\n  /** A deferLoad engine is attached and waits for the first play() to start\n   *  media loading (autoplay-off pre-play). */\n  private pendingStartLoad = false;\n\n  constructor(opts: ResolvedPlayerOptions) {\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  get endedReason(): EndedReason | null {\n    return this._endedReason;\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      if (gen === this.loadGen && this._state === 'error' && this._lastError) {\n        // Fatal engine load (already routed through fail()) — reject so the\n        // first-load caller observes the typed error; the 'error' event fired\n        // before any listener could exist. Unwind FULLY first: the caller\n        // never receives a Player to destroy(), so leaving the mounted\n        // control bar / PiP listeners / video wrapper behind would leak DOM\n        // for the life of the page.\n        const err = this._lastError;\n        this.destroy();\n        throw err;\n      }\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, err.liveStartsAtMs);\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      apiHeaders: this.opts.apiHeaders,\n    };\n  }\n\n  private beginArming(gen: number, liveStartsAtMs?: number, reconnect = false): 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, liveStartsAtMs, reconnect);\n    else this.beginArmingByPolling(gen);\n  }\n\n  /** SSE-disabled path: the original kickoff-aware /v1/playback poll. */\n  private beginArmingByPolling(gen: number): void {\n    resolveStream({\n      ...this.resolveOptions(),\n      signal: this.abort.signal,\n      waitForLive: this.buildWaitConfig(gen),\n    })\n      .then((descriptor) => {\n        // Same staleness guard as the catch: a resolve that SETTLED before a\n        // supersede (reload/destroy bumps loadGen) must not build an engine.\n        if (this.destroyed || this.abort.signal.aborted || gen !== this.loadGen) return;\n        return this.onResolved(descriptor, gen);\n      })\n      .catch((err) => {\n        if (this.destroyed || this.abort.signal.aborted || gen !== this.loadGen) return;\n        // Wrap non-PlaybackErrors (e.g. a rejecting credential function) so\n        // the 'error' event payload honors its documented type.\n        this.fail(\n          err instanceof PlaybackError ? err : new PlaybackError('INTERNAL', 0, String(err)),\n        );\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(\n    gen: number,\n    eventsBaseUrl: string,\n    liveStartsAtMs?: number,\n    reconnect = false,\n  ): void {\n    let committed = false;\n    let fallbackStarted = false;\n    const fallbackCtrl = new AbortController();\n    // No abort-listener chain to this.abort here: every abandonment path\n    // (reload()/retry()/destroy()/commit) runs closeArming(), which aborts\n    // this controller — a per-arm listener on the player-lifetime signal\n    // would just accumulate one captured closure per re-arm cycle.\n    this.armFallbackCtrl = fallbackCtrl;\n    this.setState('waiting');\n    // Kickoff hint precedence mirrors the polling path: the server's 425\n    // liveStartsAt wins, the caller's waitForLive.kickoffAt is the fallback.\n    const wf = this.opts.waitForLive;\n    const kickoffOpt = wf && wf !== true ? wf.kickoffAt : undefined;\n    const kickoffOptMs =\n      typeof kickoffOpt === 'number'\n        ? kickoffOpt\n        : typeof kickoffOpt === 'string'\n          ? Date.parse(kickoffOpt)\n          : NaN;\n    const startsAtMs = liveStartsAtMs ?? (Number.isFinite(kickoffOptMs) ? kickoffOptMs : undefined);\n    // Emit an observable 'waiting' for the SSE arm. The polling path emits\n    // one per poll via buildWaitConfig, but a healthy SSE wait makes no\n    // /v1/playback requests at all — without this, an armed player is\n    // invisible to hosts (the pre-resolve statechange is deduped and\n    // createPlayer resolves already armed; the ops Demo Player had to probe\n    // player.state synchronously to find out). Deferred a MACROTASK: this\n    // runs synchronously inside start(), before createPlayer resolves — a\n    // synchronous (or microtask) emit would fire before the caller can\n    // attach listeners and be missed, recreating the exact gap this closes.\n    // Synthetic-but-honest shape: the next self-check is the SSE liveness\n    // window.\n    // No clearTimeout-on-abort bookkeeping: the callback's committed/\n    // destroyed/gen guard makes a post-teardown firing a no-op, and the\n    // timer is 0 ms — long gone by any later re-arm. (A per-arm abort\n    // listener on the player-lifetime signal would leak one closure per\n    // re-arm cycle.)\n    setTimeout(() => {\n      if (committed || this.destroyed || gen !== this.loadGen) return;\n      this.emit('waiting', {\n        // A rejoin arm is a mid-match reconnect, not a pre-kickoff wait —\n        // 'unavailable' is the phase hosts map to \"temporarily unavailable\"\n        // copy; 'tooEarly' would read as \"starting soon\" while a live match\n        // recovers from an ingest outage.\n        phase: reconnect ? 'unavailable' : 'tooEarly',\n        retryInMs: SSE_ARM_LIVENESS_MS,\n        attempt: 1,\n        elapsedMs: 0,\n        ...(!reconnect && startsAtMs !== undefined && startsAtMs > Date.now()\n          ? { ttkMs: startsAtMs - Date.now() }\n          : {}),\n      });\n    }, 0);\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      try {\n        await this.onResolved(descriptor, gen);\n      } catch (err) {\n        // onResolved can reject (credential mint at go-live, engine\n        // construction). By now committed=true, so the callers' .catch\n        // handlers early-return — an unhandled rejection here would strand\n        // the player in 'waiting' forever with arming already closed.\n        // Surface it: listeners exist by now (this is post-createPlayer).\n        if (stale()) return;\n        this.fail(\n          err instanceof PlaybackError ? err : new PlaybackError('INTERNAL', 0, String(err)),\n        );\n      }\n    };\n\n    const startFallback = (): void => {\n      if (committed || fallbackStarted || stale()) return;\n      fallbackStarted = true;\n      // Route the fallback's success through commit(), NOT straight to\n      // onResolved: SSE can recover and push 'live' while the poll's resolve\n      // has already settled (abort can't retract a settled promise), and two\n      // unguarded onResolved calls for the same generation would build two\n      // engines on one video — the first leaks its watchdog + media\n      // listeners. commit() is idempotent, so whichever side wins, the other\n      // becomes a no-op.\n      resolveStream({\n        ...this.resolveOptions(),\n        signal: fallbackCtrl.signal,\n        waitForLive: this.buildWaitConfig(gen),\n      })\n        .then((descriptor) => commit(descriptor))\n        .catch((err) => {\n          if (committed || stale() || fallbackCtrl.signal.aborted) return;\n          committed = true;\n          this.closeArming();\n          this.fail(\n            err instanceof PlaybackError ? err : new PlaybackError('INTERNAL', 0, String(err)),\n          );\n        });\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/503/transient). We only get here because SSE\n          // PUSHED live/gone, so playback lagging the push is a consistency\n          // race, not \"keep waiting for the next push\": a healthy SSE never\n          // re-pushes an unchanged state, and its heartbeats keep deferring\n          // the staleness fallback — returning here wedged the post-ended\n          // rejoin until the SSE lifetime-cap reconnect replayed the snapshot\n          // (observed live 2026-08-25 on a 10s-delayed stream, where the\n          // recreated session's /v1/playback view lagged the bridge's `live`\n          // flip by ~5s). Hand the wait to the polling ladder instead: it\n          // retries with backoff and commits through the same idempotent\n          // commit(), and a terminal answer still surfaces through its catch.\n          if (err instanceof PlaybackError && isWaitable(err.code)) {\n            startFallback();\n            return;\n          }\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    // Cancel the SSE arm's polling fallback too — reload()/retry()/destroy()\n    // all route through here, and an orphaned poll would keep hitting\n    // /v1/playback and emitting stale 'waiting' for a dead generation.\n    this.armFallbackCtrl?.abort();\n    this.armFallbackCtrl = null;\n    this.armingSub?.close();\n    this.armingSub = null;\n  }\n\n  private buildWaitConfig(gen: number): WaitForLive {\n    const base: WaitForLiveOptions =\n      this.opts.waitForLive && this.opts.waitForLive !== true ? this.opts.waitForLive : {};\n    return {\n      ...base,\n      onState: (s) => {\n        // Generation guard: an orphaned poll (superseded by reload/destroy)\n        // must not flip a newer generation's state back to 'waiting'.\n        if (this.destroyed || gen !== this.loadGen) return;\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    this._endedReason = null;\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    // Autoplay off and no play() yet: attach a deferLoad engine — it parses\n    // the manifest ('ready', track lists) but fetches no media and mints no\n    // DRM license until the first play(). Without this the pre-play player\n    // buffered the live stream forever: state never left 'loading' (spinner,\n    // no poster card) and hls.js force-seeked the paused video to the live\n    // edge every ~12s (the drift ceiling), repainting the frame (AV-202).\n    const deferLoad = this.opts.autoplay === false && !this.playRequested;\n    const engine = this.createEngine(descriptor, credential, deferLoad);\n    const detachMedia = attachMediaStateListeners(this.opts.video, (s) => {\n      // Terminal latch: after 'ended'/'error' the media element stays attached\n      // and its queued events — the 'pause' from engine.stop(), buffer-drain\n      // 'waiting'/'stalled' after a fatal error — would flip the state right\n      // back off the terminal value (a host rendering an overlay from\n      // statechange sees it dismiss itself). Explicit transitions\n      // (retry/reload/load → 'loading', destroy → 'idle') call setState\n      // directly and are unaffected.\n      if (this._state === 'ended' || this._state === 'error') return;\n      this.setState(s);\n    });\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    if (this._state === 'error') {\n      // A FATAL engine error was emitted during load() (e.g. the platform\n      // fail-fast: DRM required but no key system usable here). Do not wire\n      // the dead engine — no stats ticker, no 8-minute license-refresh\n      // resolves, no SSE subscription. start() additionally turns this into\n      // a createPlayer rejection: the emit fired before the caller could\n      // attach listeners, so the event alone is unobservable on first load.\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    this.startAnalytics(descriptor, engine, gen);\n    if (deferLoad) {\n      // Pre-play: 'paused' is what renders the poster + play card (and hides\n      // the spinner — 'loading' means work is happening, and none is). No\n      // media event will fire to move the state until play() starts loading.\n      this.pendingStartLoad = true;\n      this.setState('paused');\n      // Native engine: its 'ready' (loadedmetadata) cannot fire pre-play\n      // under preload=\"none\", and the engine-side emit is suppressed for a\n      // deferred instance — emit it here instead, so host event timing\n      // matches the hls.js engine (ready from MANIFEST_PARSED, which still\n      // loads). Deferred a MACROTASK: this runs synchronously inside\n      // start()/reload(), before createPlayer resolves — a synchronous emit\n      // would fire before the host can attach listeners and before the\n      // analytics session exists to stamp noteReady(). Same pattern as the\n      // SSE arm's 'waiting' emit.\n      if (engine.name === 'native') {\n        setTimeout(() => {\n          if (this.destroyed || gen !== this.loadGen || this.engine !== engine) return;\n          this.analyticsSession?.noteReady();\n          this.emit('ready');\n        }, 0);\n      }\n    }\n  }\n\n  /**\n   * Start the QoE beacon session for a committed engine (plan A2). This is\n   * deliberately AFTER engine selection: player identity is only truthful\n   * once the engine is chosen (the SessionStart contract). A reload/retry\n   * resolves a NEW sid, so the superseded session ends as STOPPED first.\n   */\n  /**\n   * The beacon session ended itself (END_REASON_IDLE). Stop the engine with\n   * it: a paused player has nothing to keep loading, and a player that\n   * slept through the idle limit while playing would otherwise carry on\n   * unbeaconed, miles behind live. stop() pauses the element, so the viewer\n   * sees a paused player and the next play() rejoins (see idleClosed). Only\n   * for the engine the session belonged to — a reload that raced the timer\n   * has already replaced both.\n   */\n  private onAnalyticsIdle(engine: PlaybackEngine): void {\n    if (this.destroyed || this.engine !== engine) return;\n    this.idleClosed = true;\n    this.pendingStartLoad = false; // a deferred engine is stopped too; play() reloads instead\n    engine.stop();\n  }\n\n  private startAnalytics(descriptor: StreamDescriptor, engine: PlaybackEngine, gen: number): void {\n    this.analyticsSession?.noteDestroyed();\n    this.analyticsSession = null;\n    this.detachMediaListeners?.();\n\n    if (this.opts.analytics === false) return;\n    const sid = descriptor.analytics?.sid;\n    if (!sid) return; // server-side analytics flag is off — nothing to do\n    const explicit =\n      typeof this.opts.analytics === 'object' ? this.opts.analytics.endpoint : undefined;\n    const { endpoint, disabledReason } = resolveBeaconsEndpoint(this.opts.baseUrl, explicit);\n    if (!endpoint) {\n      // The server minted a sid — analytics is ON for this client — but we have\n      // nowhere to put the beacons. resolveBeaconsEndpoint has already said so\n      // on the console; mirror it on the 'warning' event so an integrator can\n      // alert on it without reading a browser console. Deferred a MACROTASK for\n      // the same reason the native pre-play 'ready' is: startAnalytics runs\n      // synchronously inside start(), before createPlayer resolves, so a\n      // synchronous emit would fire before the host can attach listeners —\n      // unobservable on exactly the first load that matters.\n      if (disabledReason) {\n        setTimeout(() => {\n          if (this.destroyed || gen !== this.loadGen) return;\n          this.emit('warning', new PlaybackError('INTERNAL', 0, disabledReason));\n        }, 0);\n      }\n      return;\n    }\n\n    const video = this.opts.video;\n    const session = new AnalyticsSession({\n      endpoint,\n      sid,\n      player: engine.name === 'native' ? 'PLAYER_SAFARI_NATIVE' : 'PLAYER_HLSJS',\n      sdkVersion: SDK_VERSION,\n      environment: collectEnvironment(video),\n      idleMs: typeof this.opts.analytics === 'object' ? this.opts.analytics.idleMs : undefined,\n      onIdle: () => this.onAnalyticsIdle(engine),\n      sample: () => {\n        const stats = this.engine?.getStats() ?? EMPTY_STATS;\n        const buffered = video.buffered.length\n          ? Math.max(0, video.buffered.end(video.buffered.length - 1) - video.currentTime)\n          : 0;\n        return {\n          ...stats,\n          bufferedMs: Math.round(buffered * 1000),\n          state: this._state,\n          positionMs: Math.round(video.currentTime * 1000),\n          playbackRate: video.playbackRate,\n        };\n      },\n    });\n    this.analyticsSession = session;\n    const onSeeking = () => session.noteSeek();\n    const onVolume = () => session.noteVolume(video.muted, video.volume);\n    video.addEventListener('seeking', onSeeking);\n    video.addEventListener('volumechange', onVolume);\n    this.detachMediaListeners = () => {\n      video.removeEventListener('seeking', onSeeking);\n      video.removeEventListener('volumechange', onVolume);\n    };\n    session.start();\n    // Seeds the muted/active-watch accounting for a player that starts muted\n    // (the autoplay-friendly default), which fires no volumechange of its\n    // own. seedVolume, not noteVolume: the starting state is not something\n    // the viewer did, and counting it would put a phantom change on every\n    // session.\n    session.seedVolume(video.muted, video.volume);\n    session.notePresentation(this.currentPresentation());\n    // Model and OS version resolve asynchronously; SessionStart is held\n    // briefly for exactly this, and a browser without client hints simply\n    // never patches anything.\n    void collectHighEntropy().then((extra) => session.noteEnvironment(extra));\n    // Autoplay disabled is a verdict this session already has: nothing will\n    // be attempted, so say so rather than leaving the field unset.\n    if (this.opts.autoplay === false) session.noteAutoplay('AUTOPLAY_RESULT_NOT_ATTEMPTED');\n  }\n\n  /** Which surface the video is showing on right now. */\n  private currentPresentation(): Presentation {\n    const video = this.opts.video as HTMLVideoElement & {\n      webkitDisplayingFullscreen?: boolean;\n    };\n    if (typeof document !== 'undefined' && document.pictureInPictureElement === video) return 'pip';\n    const fs = typeof document !== 'undefined' ? document.fullscreenElement : null;\n    if (video.webkitDisplayingFullscreen === true) return 'fullscreen';\n    if (fs && (fs === video || fs.contains(video))) return 'fullscreen';\n    return 'inline';\n  }\n\n  private createEngine(\n    descriptor: StreamDescriptor,\n    credential: Credential,\n    deferLoad = false,\n  ): 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    // (2s) 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      debug: this.opts.debug,\n      deferLoad,\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: FairPlay content on an Apple browser goes to the NATIVE engine.\n    // hls.js/MSE cannot decrypt FairPlay here — WebKit binds the key through\n    // the fragment `sinf` box, so a session built from a synthesized skd asset\n    // ID reports 'usable' and still renders nothing. Measured on macOS Safari\n    // 26.5 against live DRM (2026-08-06): hls.js+MSE 1 pass / 6, native 4 / 4.\n    // Everything else keeps hls.js, which gives Widevine + LL-HLS control.\n    //\n    // webkitMediaStack() is what makes \"on an Apple browser\" true: Chromium\n    // answers canPlayType('application/vnd.apple.mpegurl') with \"maybe\" while\n    // being unable to play HLS natively, so nativeHlsSupported() alone sent\n    // every DRM-enabled Chrome session into the native engine and failed it.\n    const fairplayOnApple =\n      descriptor.drmEnabled &&\n      Boolean(descriptor.drm?.fairplay?.licenseUrl && descriptor.drm.fairplay.certificateUrl) &&\n      nativeHlsSupported(this.opts.video) &&\n      webkitMediaStack();\n    if (fairplayOnApple) return new NativeHlsEngine(deps);\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.analyticsSession?.noteReady();\n        this.emit('ready');\n        // playRequested keeps autoplay-off resume semantics: once the viewer\n        // pressed play, a re-attach (rejoin after an interruption, a play()\n        // during the armed wait, reload) resumes without a second gesture —\n        // otherwise it would sit in 'loading' forever, exactly AV-202 again.\n        if (this.opts.autoplay !== false || this.playRequested) 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        this.analyticsSession?.noteError(\n          ev.error.code,\n          ev.error.message,\n          ev.fatal,\n          ev.error.httpStatus ?? 0,\n          Math.round(this.opts.video.currentTime * 1000),\n          ev.detail,\n        );\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 === 'gone') {\n          // Past the catch-up window — truly terminal, nothing left to rejoin.\n          this.onStreamEnded(true, ev.reason ?? 'match_ended');\n        } else if (ev.state === 'ended') {\n          this.onStreamEnded(false, ev.reason);\n        } else if (ev.state === 'live' && this.endedHandled) {\n          // A live push AFTER end-of-stream means the bridge recreated the\n          // session (encoder reconnected past the grace window) — and it now\n          // serves under a NEW manifest URL, so only a re-resolve gets back.\n          this.scheduleRejoin(gen);\n        }\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        // After end-of-stream the subscription's only remaining job is the\n        // rejoin trigger — end detection already happened, so the degraded-\n        // detection warning below would be misinformation there.\n        if (this.endedHandled) 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   * End-of-stream: stop the engine's buffer/poll loop (keeping the last\n   * frame) and surface `ended` (via setState). Idempotent across the SSE and\n   * HLS-watchdog signals. The live-state subscription closes only when the\n   * end is `terminal` (`gone` — past the catch-up window) or rejoin is off;\n   * otherwise it stays open as the rejoin trigger — a later pushed `live`\n   * means the session was recreated and playback can re-arm (scheduleRejoin).\n   */\n  private onStreamEnded(terminal = false, reason?: EndedReason): void {\n    if (this.destroyed) return;\n    if (terminal || !this.rejoinEnabled()) {\n      this.cancelRejoin();\n      this.closeLiveState();\n    }\n    if (this.endedHandled) {\n      // Already ended, but the SERVER may have refined WHY: an outage that\n      // outlived the bridge's hold reports `interrupted` first and flips to\n      // `match_ended` when the match actually finishes (havik-streams #191\n      // publishes that as a second event — the state never changes, only the\n      // reason). Upgrade the reason and re-render so a viewer stops being\n      // told \"reconnecting\" for a match that is over. Never downgrade: once\n      // we know the match ended, a late `interrupted` cannot un-end it.\n      if (reason === 'match_ended' && this._endedReason !== 'match_ended') {\n        this._endedReason = reason;\n        // Only `ended` — NOT `statechange`. The state does not transition\n        // here (it was, and stays, 'ended'), and `statechange` is documented\n        // as a transition signal: re-emitting it would make an\n        // edge-triggered host run its end-of-stream work twice. `ended`\n        // re-firing with a refined reason IS part of its contract (see\n        // PlayerEventMap), and the built-in control bar re-renders off it.\n        this.emit('ended', reason);\n      }\n      return;\n    }\n    this.endedHandled = true;\n    this._endedReason = reason ?? null;\n    this.stopRefresh();\n    this.pendingStartLoad = false; // the engine is stopped; a later play() must not re-kick it\n    this.engine?.stop();\n    this.setState('ended');\n  }\n\n  private rejoinEnabled(): boolean {\n    return this.opts.rejoinOnLive !== false && this.opts.liveStateEvents !== false;\n  }\n\n  /**\n   * A `live` push arrived after end-of-stream. Jittered like the armed\n   * go-live resolve, so a popular stream's stranded viewers don't all\n   * re-resolve in lockstep when the encoder comes back.\n   */\n  private scheduleRejoin(gen: number): void {\n    if (!this.rejoinEnabled() || this.rejoinTimer) return;\n    this.rejoinTimer = setTimeout(() => {\n      this.rejoinTimer = null;\n      if (this.destroyed || gen !== this.loadGen || this._state !== 'ended') return;\n      this.rejoin();\n    }, Math.random() * SSE_ARM_RESOLVE_JITTER_MS);\n  }\n\n  private cancelRejoin(): void {\n    if (this.rejoinTimer) {\n      clearTimeout(this.rejoinTimer);\n      this.rejoinTimer = null;\n    }\n  }\n\n  /**\n   * Tear down the ended playback and re-enter the armed wait under a fresh\n   * load generation. beginArming (SSE-first) re-resolves off the events\n   * snapshot / `live` push, which keeps this robust to the recreate race: if\n   * the pushed `live` slightly precedes /v1/playback consistency, the arming\n   * ladder keeps waiting through waitable errors instead of failing the\n   * player, and a terminal answer (`gone` → 410) still surfaces as an error.\n   */\n  private rejoin(): void {\n    const gen = ++this.loadGen; // supersedes anything stale for the old playback\n    this.teardownPlayback();\n    this.setState('waiting');\n    this.beginArming(gen, undefined, true);\n  }\n\n  /**\n   * Stop and release the committed-playback plumbing: stats, license refresh,\n   * any pending rejoin, arming, the live-state subscription, media listeners,\n   * and the engine. Callers bump loadGen first so in-flight continuations for\n   * the old playback go stale. destroy() keeps its own sequence — it\n   * interleaves these with controls/PiP/analytics teardown in a load-bearing\n   * order.\n   */\n  private teardownPlayback(): void {\n    this.stopStats();\n    this.stopRefresh();\n    this.cancelRejoin();\n    this.closeArming();\n    this.closeLiveState();\n    this.detachMedia?.();\n    this.engine?.destroy();\n    this.engine = null;\n    this.pendingStartLoad = false; // the deferred engine is gone with it\n    this.idleClosed = false; // and so is the idle-closed session; the next engine starts clean\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      interruptedMessage: this.opts.interruptedMessage,\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) {\n      if (container.parentNode) {\n        container.parentNode.insertBefore(this.opts.video, container);\n        container.remove();\n      } else if (this.opts.video.parentNode === container) {\n        // Mounted while the video was still detached (host creates the\n        // element before inserting it): there is no parent to restore into,\n        // but the video must still be released from the wrapper — otherwise\n        // a destroyed player (or a rejected createPlayer, where the caller\n        // never gets a handle to destroy) leaves an orphan container holding\n        // the element.\n        container.removeChild(this.opts.video);\n      }\n    }\n    this.container = null;\n  }\n\n  private attachPipListeners(): void {\n    const v = this.opts.video;\n    const sync = () => this.analyticsSession?.notePresentation(this.currentPresentation());\n    const onEnter = () => {\n      sync();\n      this.emit('pipchange', true);\n    };\n    const onLeave = () => {\n      sync();\n      this.emit('pipchange', false);\n    };\n    v.addEventListener('enterpictureinpicture', onEnter);\n    v.addEventListener('leavepictureinpicture', onLeave);\n    // Fullscreen is a document-level event and the QoE plane wants the same\n    // answer for both surfaces, so one sync serves them: how long a viewer\n    // watched fullscreen, in PiP, or inline.\n    const doc = typeof document !== 'undefined' ? document : undefined;\n    doc?.addEventListener('fullscreenchange', sync);\n    doc?.addEventListener('webkitfullscreenchange', sync);\n    this.detachPip = () => {\n      v.removeEventListener('enterpictureinpicture', onEnter);\n      v.removeEventListener('leavepictureinpicture', onLeave);\n      doc?.removeEventListener('fullscreenchange', sync);\n      doc?.removeEventListener('webkitfullscreenchange', sync);\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      const stats = this.engine.getStats();\n      // First-segment startup stage at stats granularity (~2s), not the 15s\n      // beacon cadence — first_frag_load_ms would otherwise overreport badly.\n      if ((stats.bytesLoaded ?? 0) > 0) this.analyticsSession?.noteFirstBytes();\n      this.emit('stats', stats);\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 widevine = fresh.drm?.widevine?.licenseUrl;\n      const fairplay = fresh.drm?.fairplay?.licenseUrl;\n      if (widevine || fairplay) {\n        engine.setLicenseUrls({ widevine, fairplay });\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    const firstRequest = !this.playRequested;\n    this.playRequested = true;\n    // After an idle close the session and the engine are gone: rejoin. With\n    // playRequested set, reload() → 'ready' re-enters play() on the fresh\n    // engine, where the ordinary path below runs.\n    // Not from a terminal state: an `ended` push that landed after the idle\n    // close owns the UI exactly as it does below, and a reload would resolve\n    // a finished match into an error card.\n    if (this.idleClosed && this._state !== 'ended' && this._state !== 'error') {\n      this.idleClosed = false;\n      await this.reload();\n      return;\n    }\n    // Terminal states own the UI; a play() arriving there (the control-bar\n    // button stays clickable on the ended card) must not kick the loader and\n    // resurrect 'loading' over an ended/error stream — the pre-PR player\n    // resumed buffered frames there, and a dead session has nothing to load.\n    const kickedDeferred =\n      this.pendingStartLoad && this._state !== 'ended' && this._state !== 'error';\n    // Startup is measured from HERE for the autoplay-off path, not from\n    // load(): the player has been sitting behind a poster with the manifest\n    // already parsed, and the time the viewer spent deciding is theirs.\n    //\n    // Gated on the PLAYER being autoplay-off, not on kickedDeferred, and\n    // that difference is load-bearing. A gestureless play() that the policy\n    // blocks still clears pendingStartLoad, and the blocked path\n    // deliberately leaves the loader warm rather than re-latching — so the\n    // eventual real gesture does NOT take the kicked branch. Anchoring there\n    // would have pinned startup to the failed probe and measured the whole\n    // wait for the gesture as latency, which is the bug this is here to fix.\n    // notePlayIntent's own guard stops a post-playback resume re-anchoring a\n    // startup that already finished; before the first frame, the LAST intent\n    // is the right one. The blocked branch below says the same thing in\n    // words: \"a policy-blocked play() is not viewer intent\".\n    //\n    // autoplay:true never reaches this: its intent is at load(), where the\n    // manifest fetch genuinely is part of startup, so its ladder keeps\n    // measuring from bornMs.\n    if (this.opts.autoplay === false) this.analyticsSession?.notePlayIntent();\n    if (kickedDeferred) {\n      // First play() on an autoplay-off player: start media loading now.\n      // 'loading' is honest again from here — the spinner shows while the\n      // first segments and (when enabled) the DRM license are fetched, until\n      // the media element's own events take over.\n      this.pendingStartLoad = false;\n      this.setState('loading');\n      this.engine?.startLoad();\n    }\n    try {\n      await this.opts.video.play();\n      // The autoplay verdict, which only exists once play() has resolved.\n      // Reported on the FIRST attempt of a session; SessionStart has gone by\n      // the time a later user gesture plays, so a hand-started session can\n      // never overwrite it.\n      if (firstRequest) {\n        this.analyticsSession?.noteAutoplay(\n          this.opts.video.muted ? 'AUTOPLAY_RESULT_ALLOWED_MUTED' : 'AUTOPLAY_RESULT_ALLOWED',\n        );\n      }\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.analyticsSession?.noteAutoplay('AUTOPLAY_RESULT_BLOCKED');\n        // A policy-blocked play() is not viewer intent: nothing ever played.\n        // Un-latch, so a later reload/rejoin does not keep attempting\n        // gestureless auto-plays — but only when THIS call set the latch; a\n        // block after real playback must keep resume semantics.\n        if (firstRequest) this.playRequested = false;\n        // And put the pre-play card back instead of the spinner the blocked\n        // kick left up. The loader is not stopped HERE: the warmed buffer\n        // makes a prompt gesture play() start instantly; the hls.js engine's\n        // own pause suspension stops it after its grace (PAUSE_SUSPEND_MS)\n        // so a play card nobody clicks does not pull the live stream forever.\n        // The state guard keeps a mid-await teardown/reload/ended transition\n        // untouched.\n        if (kickedDeferred && !this.destroyed && this._state === 'loading') {\n          this.setState('paused');\n        }\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  async enterFullscreen(): Promise<boolean> {\n    if (this.destroyed) return false;\n    // The custom-controls wrapper (when mounted) is the element-fullscreen\n    // target so the branded UI stays visible; native/none modes present the\n    // bare video, where the browser's own fullscreen controls appear.\n    return enterVideoFullscreen(this.opts.video, this.container ?? undefined);\n  }\n  async exitFullscreen(): Promise<void> {\n    await exitVideoFullscreen(this.opts.video, this.container ?? undefined);\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.teardownPlayback();\n    try {\n      const descriptor = await resolveStream({\n        ...this.resolveOptions(),\n        waitForLive: this.opts.waitForLive ? this.buildWaitConfig(gen) : undefined,\n      });\n      await this.onResolved(descriptor, gen);\n      if (gen === this.loadGen && this._state === 'error' && this._lastError) {\n        // Fatal engine load — already routed through fail() by the engine\n        // event channel; reject so the awaiting caller observes it too.\n        throw this._lastError;\n      }\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 — unless it already went\n      // through fail() above (double-emitting 'error' would fire host handlers twice).\n      if (err !== this._lastError) {\n        this.fail(\n          err instanceof PlaybackError ? err : new PlaybackError('INTERNAL', 0, String(err)),\n        );\n      }\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.cancelRejoin();\n    this.closeArming();\n    this.closeLiveState();\n    this.detachMedia?.();\n    this.detachPip?.();\n    // End the beacon session BEFORE the engine dies so the final sample can\n    // still read totals; no-op if 'ended'/fatal already closed it.\n    this.analyticsSession?.noteDestroyed();\n    this.analyticsSession = null;\n    this.detachMediaListeners?.();\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._lastError = err;\n    this.setState('error');\n    // After the terminal StateChange: SessionEnd(FATAL_ERROR) closes the\n    // beacon session — further events for this sid are meaningless.\n    this.analyticsSession?.noteFatal();\n    this.emit('error', err);\n  }\n\n  private setState(state: PlayerState): void {\n    if (state === this._state) return;\n    const from = this._state;\n    this._state = state;\n    if (this.analyticsSession) {\n      const video = this.opts.video;\n      this.analyticsSession.noteStateChange(\n        from,\n        state,\n        Math.round(video.currentTime * 1000),\n        this.engine?.getStats().levelHeight,\n      );\n      if (state === 'ended') this.analyticsSession.noteEnded();\n    }\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', this._endedReason ?? undefined);\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-dev.oddin-video.gg/embed/`. */\n  src: string;\n  /**\n   * API base URL, forwarded to the embed page as a query parameter.\n   *\n   * ⚠️ QoE beacons: the beacons endpoint is derived from this host by\n   * convention (`feed[-dev].<domain>` → `beacons[-dev].<domain>`). If your\n   * base is NOT a feed host, nothing derives and analytics is off — and\n   * `mountEmbed` CANNOT fix it, because the only way to name an endpoint is\n   * {@link EmbedConfig.analytics}, which the embed page reads from injected\n   * config and deliberately never from the query string (a link must not be\n   * able to redirect a session's telemetry). Host the embed page yourself\n   * and inject `window.HAVIK_EMBED_CONFIG` with `analytics: { endpoint }`.\n   */\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   * QoE beacons, forwarded verbatim to {@link createPlayer}. Same meaning as\n   * there: `false` opts out, `{ endpoint }` names a beacons host.\n   *\n   * Mode C needs this for the same reason Mode A does, and until now had no\n   * way to say it. An embed whose `baseUrl` is not a `feed[-dev].<domain>`\n   * host — a CDN or gateway fronting the streams API — derives no beacons\n   * endpoint, so analytics is off and stays off. Without this option the\n   * only remedy was to move `baseUrl` back onto a feed host, which is not a\n   * choice an integrator who fronts the API deliberately can make.\n   *\n   * INJECTED CONFIG ONLY: unlike every other field here, this is not read\n   * from the query string. See {@link readConfig}.\n   */\n  analytics?: boolean | { endpoint?: string };\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 * Normalize an injected `analytics` value.\n *\n * The host page writes this object, so it is trusted — but it is plain JS\n * with no type checking at the boundary, and a wrong shape is worse than a\n * missing one here: `analytics: 'yes'` reaches createPlayer as a truthy\n * non-object, reads as `{ endpoint: undefined }`, and silently falls back to\n * derivation. Anything unrecognized becomes undefined instead, so the\n * documented default applies and the SDK's own diagnostics see an absent\n * option rather than a malformed one.\n */\nfunction readAnalytics(v: unknown): EmbedConfig['analytics'] {\n  if (typeof v === 'boolean') return v;\n  if (typeof v === 'object' && v !== null) {\n    const endpoint = (v as { endpoint?: unknown }).endpoint;\n    return typeof endpoint === 'string' ? { endpoint } : {};\n  }\n  return undefined;\n}\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 *\n * `analytics` is deliberately absent from the query-string branch. It names a\n * telemetry sink, and a link is the wrong place to set one: `?analytics.\n * endpoint=` would let any URL that opens this page redirect a session's QoE\n * beacons somewhere else while `baseUrl` still looks legitimate. There is no\n * demo case that needs it — a dev embed points `baseUrl` at a feed host and\n * gets the derived endpoint for free. Injected config only.\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      analytics: readAnalytics(injected.analytics),\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        analytics: cfg.analytics,\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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"}