@oddin-gg/havik-player 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,795 @@
1
+ import { HlsConfig } from 'hls.js';
2
+
3
+ /** Apply a (possibly partial) theme to a player root element as CSS variables. */
4
+ export declare function applyTheme(root: HTMLElement, theme?: Partial<HavikTheme>): void;
5
+
6
+ /** A selectable audio rendition. */
7
+ export declare interface AudioTrackInfo {
8
+ id: number;
9
+ name: string;
10
+ lang?: string;
11
+ default: boolean;
12
+ }
13
+
14
+ /**
15
+ * Boots the iframe-side player and speaks the postMessage protocol with the host.
16
+ * The api-key is NEVER accepted over postMessage. The bridge fails CLOSED: with no
17
+ * trusted parentOrigin it neither accepts commands nor broadcasts events.
18
+ */
19
+ export declare function bootEmbed(): Promise<void>;
20
+
21
+ export declare interface Catalog {
22
+ tournaments: CatalogTournament[];
23
+ /** ETag for a cheap conditional refresh next time. */
24
+ etag?: string;
25
+ /** True when the server returned 304 (the prior etag is still fresh); tournaments is empty. */
26
+ notModified: boolean;
27
+ }
28
+
29
+ export declare interface CatalogMatch {
30
+ matchUrn: string;
31
+ matchName: string;
32
+ status: MatchLiveStatus;
33
+ datePlannedStart?: string;
34
+ tournamentUrn: string;
35
+ tournamentName: string;
36
+ sport: string;
37
+ }
38
+
39
+ export declare interface CatalogTournament {
40
+ urn: string;
41
+ name: string;
42
+ sport: string;
43
+ isOffline: boolean;
44
+ matches: CatalogMatch[];
45
+ }
46
+
47
+ /**
48
+ * Clear the persisted device id (localStorage + in-memory cache). The next
49
+ * getDeviceId() generates a fresh one. Provide this to viewers as a privacy /
50
+ * "reset my device identifier" control — the device id is sent as X-Device-Id
51
+ * on every DRM license request and persists across sessions.
52
+ */
53
+ export declare function clearDeviceId(): void;
54
+
55
+ /**
56
+ * Mode A — managed player. Owns the <video> and a bundled hls.js, wiring
57
+ * LL-HLS + DRM automatically. With `waitForLive`, arms on an upcoming match and
58
+ * auto-plays the instant it goes live. Resolves to a Player as soon as it is
59
+ * armed/attached — terminal errors (404/410/401/...) reject; the live wait does
60
+ * not block and is surfaced via 'waiting' events.
61
+ */
62
+ export declare function createPlayer(opts: CreatePlayerOptions): Promise<Player>;
63
+
64
+ export declare interface CreatePlayerOptions {
65
+ /** The <video> element the SDK will own. */
66
+ video: HTMLVideoElement;
67
+ baseUrl: string;
68
+ matchUrn: string;
69
+ credential: CredentialSource;
70
+ /** Start playback once ready. Default: true. */
71
+ autoplay?: boolean;
72
+ /** Mute the element (required for reliable autoplay with sound policies). Default: false. */
73
+ muted?: boolean;
74
+ /** Arm on an upcoming match and auto-attach at go-live. See WaitForLive. */
75
+ waitForLive?: WaitForLive;
76
+ /** 'auto' lets hls.js decide from the manifest (safe default). Default: 'auto'. */
77
+ lowLatency?: 'auto' | boolean;
78
+ /** Poster image shown before playback starts. */
79
+ poster?: string;
80
+ /** Initial rendition index (-1 = auto, the default). */
81
+ startLevel?: number;
82
+ /** Cap ABR to renditions at/below this bitrate (bps). */
83
+ maxBitrate?: number;
84
+ /**
85
+ * Target live-edge latency in seconds (maps to hls.js liveSyncDuration).
86
+ * Default: 3 (a balance the engine ships — close to live but back from the
87
+ * bleeding PART-HOLD-BACK edge, where parts aren't reliably published yet).
88
+ * Lower = closer to live but more rebuffer-prone; higher = steadier. The
89
+ * engine derives a matching latency ceiling internally, so this is the only
90
+ * live-latency knob you need. Live/LL only.
91
+ */
92
+ liveLatencyTarget?: number;
93
+ /**
94
+ * Snap straight to the live edge when a backgrounded tab is refocused and
95
+ * playback has fallen well behind. Default: true. Live/LL only.
96
+ */
97
+ snapToLiveOnRefocus?: boolean;
98
+ /** Force an engine; 'auto' picks native HLS on Safari, hls.js elsewhere. Default: 'auto'. */
99
+ engine?: 'auto' | 'hls' | 'native';
100
+ /** Optional viewer id (X-User-Id on the license POST). */
101
+ userId?: string;
102
+ /**
103
+ * Which controls to render:
104
+ * - 'custom' (default): the Oddin-branded, skinnable control bar.
105
+ * - 'native': the browser's built-in `<video controls>` UI.
106
+ * - 'none': no controls (drive the player via its API).
107
+ */
108
+ controls?: 'custom' | 'native' | 'none';
109
+ /**
110
+ * Re-skin the custom control bar. Merged over the Oddin default; any omitted
111
+ * key keeps the brand value. Equivalent to overriding the `--havik-*` CSS
112
+ * variables on the player element. Ignored unless controls is 'custom'.
113
+ */
114
+ theme?: Partial<HavikTheme>;
115
+ /**
116
+ * Show branded full-bleed cards for the stopped/pre-play states — ended
117
+ * ("This stream has ended"), fatal error (message + Retry when retryable),
118
+ * pre-play (poster + play CTA), and armed-waiting ("Starting soon…") — instead
119
+ * of a frozen frame + a dead play button. Themed via the control-bar tokens +
120
+ * `theme.logoUrl`. Default: true. Set false if you render your own end/error
121
+ * UI from the player events (`ended`, `error`). Ignored unless controls is
122
+ * 'custom'.
123
+ */
124
+ statusOverlays?: boolean;
125
+ /** Message on the end-of-stream card. Default: "This stream has ended". */
126
+ endedMessage?: string;
127
+ /** Message on the fatal-error card. Default: the error's own message. */
128
+ errorMessage?: string;
129
+ /** Escape hatch for low-level hls.js tuning. */
130
+ hlsConfig?: Partial<HlsConfig>;
131
+ /** Stats sampling cadence. Default: 2000ms. */
132
+ statsIntervalMs?: number;
133
+ /**
134
+ * How often to silently refresh the signed DRM license URL for long live
135
+ * sessions (the HMAC expires ~10min). Default: 8min. Set 0 to disable.
136
+ */
137
+ licenseRefreshMs?: number;
138
+ /**
139
+ * Subscribe to push-based live-state over SSE (`events.<domain>/v1/events/{urn}`)
140
+ * to detect end-of-stream the instant the bridge flips, instead of waiting on
141
+ * the frozen Tencent manifest (which never emits #EXT-X-ENDLIST). On `ended`/
142
+ * `gone` the player stops buffering and emits `ended`. Default: true — and
143
+ * fully graceful: if the endpoint is unreachable the subscription retries
144
+ * quietly in the background while the HLS staleness watchdog remains the
145
+ * layer-fallback, so playback is never affected. Set false to disable.
146
+ */
147
+ liveStateEvents?: boolean;
148
+ /**
149
+ * Override the SSE endpoint base URL. Default: derived from {@link baseUrl} by
150
+ * prefixing the host with `events.` (e.g. `feed.<domain>` →
151
+ * `events.<domain>`, served ALB-direct). Set this if your events subdomain
152
+ * differs from that convention.
153
+ */
154
+ eventsBaseUrl?: string;
155
+ }
156
+
157
+ declare interface Credential_2 {
158
+ /** A publishable api-key, e.g. `pk_live_…` / `pk_test_…`. */
159
+ apiKey: string;
160
+ }
161
+ export { Credential_2 as Credential }
162
+
163
+ /**
164
+ * A credential, or a function that produces one. The function form lets a host
165
+ * plug in a future short-lived-token mint service without an API change; it is
166
+ * invoked per request so refresh works transparently.
167
+ */
168
+ export declare type CredentialSource = Credential_2 | (() => Credential_2 | Promise<Credential_2>);
169
+
170
+ /**
171
+ * Derive the SSE endpoint base from the API base by prefixing the host with
172
+ * `events.` — matching the deployed convention (`feed.<domain>` →
173
+ * `events.<domain>`, served ALB-direct because CloudFront can't stream SSE).
174
+ * Returns `undefined` if `baseUrl` can't be parsed.
175
+ */
176
+ export declare function deriveEventsBaseUrl(baseUrl: string): string | undefined;
177
+
178
+ export declare interface DrmInfo {
179
+ widevine?: WidevineInfo;
180
+ fairplay?: FairPlayInfo;
181
+ }
182
+
183
+ export declare type DrmSystem = 'widevine' | 'fairplay';
184
+
185
+ export declare interface EmbedConfig {
186
+ baseUrl: string;
187
+ matchUrn: string;
188
+ /** DEV/DEMO ONLY when read from the query string — in production inject server-side. */
189
+ apiKey: string;
190
+ /** Origin to post events to and accept commands from. Required for the bridge. */
191
+ parentOrigin?: string;
192
+ autoplay?: boolean;
193
+ muted?: boolean;
194
+ waitForLive?: WaitForLive;
195
+ }
196
+
197
+ export declare interface EmbedHandle {
198
+ readonly iframe: HTMLIFrameElement;
199
+ play(): void;
200
+ pause(): void;
201
+ setMuted(muted: boolean): void;
202
+ setVolume(volume: number): void;
203
+ setQuality(index: number): void;
204
+ setMaxBitrate(bitrate: number | null): void;
205
+ setAudioTrack(id: number): void;
206
+ setTextTrack(id: number): void;
207
+ seekToLive(): void;
208
+ enterPip(): void;
209
+ exitPip(): void;
210
+ retry(): void;
211
+ load(matchUrn: string): void;
212
+ on(cb: (msg: FrameToHost) => void): () => void;
213
+ destroy(): void;
214
+ }
215
+
216
+ export declare interface FairPlayInfo {
217
+ licenseUrl: string;
218
+ certificateUrl: string;
219
+ }
220
+
221
+ /** GET /v1/catalog — metadata only (no manifest/DRM). Supports conditional refresh. */
222
+ export declare function fetchCatalog(opts: FetchCatalogOptions): Promise<Catalog>;
223
+
224
+ export declare interface FetchCatalogOptions {
225
+ baseUrl: string;
226
+ credential: CredentialSource;
227
+ signal?: AbortSignal;
228
+ /** Prior ETag for a conditional GET (304 when unchanged). */
229
+ etag?: string;
230
+ status?: MatchLiveStatus | MatchLiveStatus[];
231
+ sport?: string;
232
+ }
233
+
234
+ /**
235
+ * `GET /v1/tournaments/{urn}` — single-tournament metadata (matches included,
236
+ * never any signed/per-viewer URLs; those live on `/v1/playback/{urn}`).
237
+ *
238
+ * Returns `notFound: true` on 404 (unknown URN OR caller-not-entitled — the
239
+ * server intentionally collapses both so existence isn't leaked). Other waitable
240
+ * statuses (5xx) reject as a typed `PlaybackError`. Supports ETag/304.
241
+ */
242
+ export declare function fetchTournament(opts: FetchTournamentOptions): Promise<TournamentResult>;
243
+
244
+ export declare interface FetchTournamentOptions {
245
+ baseUrl: string;
246
+ credential: CredentialSource;
247
+ /** Tournament URN, e.g. `od:tournament:42`. */
248
+ urn: string;
249
+ signal?: AbortSignal;
250
+ /** Prior ETag for a conditional GET (304 when unchanged) — only effective once havik-streams ETags `/v1/tournaments/{urn}` (parity with `/v1/matches/{urn}`). */
251
+ etag?: string;
252
+ }
253
+
254
+ /** Flatten a Catalog to a single match list (handy for grids). */
255
+ export declare function flattenMatches(catalog: Catalog): CatalogMatch[];
256
+
257
+ /** Events the iframe sends up to the host page. */
258
+ export declare type FrameToHost = {
259
+ type: 'oddin:ready';
260
+ protocol: number;
261
+ } | {
262
+ type: 'oddin:autoplayblocked';
263
+ } | {
264
+ type: 'oddin:state';
265
+ state: PlayerState;
266
+ } | {
267
+ type: 'oddin:waiting';
268
+ retryInMs: number;
269
+ attempt: number;
270
+ phase: string;
271
+ } | {
272
+ type: 'oddin:tracks';
273
+ quality: QualityLevel[];
274
+ audio: AudioTrackInfo[];
275
+ text: TextTrackInfo[];
276
+ } | {
277
+ type: 'oddin:qualitychange';
278
+ index: number;
279
+ } | {
280
+ type: 'oddin:audiotrackchange';
281
+ id: number;
282
+ } | {
283
+ type: 'oddin:texttrackchange';
284
+ id: number;
285
+ } | {
286
+ type: 'oddin:stats';
287
+ droppedFrames: number;
288
+ latencySeconds?: number;
289
+ bandwidthKbps?: number;
290
+ levelHeight?: number;
291
+ lowLatency?: boolean;
292
+ isLive?: boolean;
293
+ atLiveEdge?: boolean;
294
+ } | {
295
+ type: 'oddin:error';
296
+ code: PlaybackErrorCode;
297
+ message: string;
298
+ };
299
+
300
+ export declare function getDeviceId(): string;
301
+
302
+ export declare interface HavikTheme {
303
+ /** Primary accent (play button, progress fill, live dot, focus). */
304
+ accent: string;
305
+ /** Text/icon color that sits ON the accent (e.g. on the play button). */
306
+ accentText: string;
307
+ /** Letterbox / behind-video background. */
308
+ background: string;
309
+ /** Control bar + menu surface color. */
310
+ surface: string;
311
+ /** Hover/active surface color. */
312
+ surfaceMuted: string;
313
+ /** Primary text/icon color. */
314
+ text: string;
315
+ /** Secondary text color (timestamps, inactive). */
316
+ textMuted: string;
317
+ /** Hairline borders. */
318
+ border: string;
319
+ /** Corner radius for buttons/menus. */
320
+ radius: string;
321
+ /** Font stack for control-bar text. */
322
+ fontFamily: string;
323
+ /** Optional logo shown as a small corner watermark (URL). */
324
+ logoUrl?: string;
325
+ }
326
+
327
+ /** Commands the host page sends down to the iframe. */
328
+ export declare type HostToFrame = {
329
+ type: 'oddin:play';
330
+ } | {
331
+ type: 'oddin:pause';
332
+ } | {
333
+ type: 'oddin:setMuted';
334
+ muted: boolean;
335
+ } | {
336
+ type: 'oddin:setVolume';
337
+ volume: number;
338
+ } | {
339
+ type: 'oddin:setQuality';
340
+ index: number;
341
+ } | {
342
+ type: 'oddin:setMaxBitrate';
343
+ bitrate: number | null;
344
+ } | {
345
+ type: 'oddin:setAudioTrack';
346
+ id: number;
347
+ } | {
348
+ type: 'oddin:setTextTrack';
349
+ id: number;
350
+ } | {
351
+ type: 'oddin:seekToLive';
352
+ } | {
353
+ type: 'oddin:enterPip';
354
+ } | {
355
+ type: 'oddin:exitPip';
356
+ } | {
357
+ type: 'oddin:retry';
358
+ } | {
359
+ type: 'oddin:load';
360
+ matchUrn: string;
361
+ } | {
362
+ type: 'oddin:destroy';
363
+ };
364
+
365
+ export declare function isLowLatencyManifest(playlistText: string): boolean;
366
+
367
+ export declare function isOddinMessage(data: unknown): data is {
368
+ type: string;
369
+ };
370
+
371
+ export declare interface LicenseHeaderOptions {
372
+ /** Override the persistent device id (defaults to getDeviceId()). */
373
+ deviceId?: string;
374
+ /** Optional viewer id, forwarded as X-User-Id for billing/tracking. */
375
+ userId?: string;
376
+ }
377
+
378
+ /**
379
+ * Headers a host player must attach to the DRM license POST. The license URL
380
+ * itself must be sent VERBATIM (its signed query string is the token) — these
381
+ * headers are the rest of the contract:
382
+ * - x-api-key : the SAME key used to resolve playback (cid binding)
383
+ * - X-Device-Id : required, persistent
384
+ * - X-Match-Urn : advisory (the signed `m` param is authoritative)
385
+ * - X-User-Id : optional
386
+ * Body must be the raw EME challenge bytes (application/octet-stream).
387
+ */
388
+ export declare function licenseRequestHeaders(descriptor: StreamDescriptor, credential: Credential_2, opts?: LicenseHeaderOptions): Record<string, string>;
389
+
390
+ /**
391
+ * Coarse, player-facing live state pushed by havik-streams over SSE. Mirrors
392
+ * the `/v1/playback` outcome ladder so a pushed event and a follow-up
393
+ * `/v1/playback` call always agree:
394
+ * live ↔ 200 OK (a serveable session exists — play now)
395
+ * upcoming ↔ 425 Too Early (scheduled, not serving yet)
396
+ * ended ↔ 503 (was live; media stopped, still in catchup)
397
+ * gone ↔ 410 Gone (ended past the catchup window)
398
+ */
399
+ export declare type LiveState = 'live' | 'upcoming' | 'ended' | 'gone';
400
+
401
+ export declare interface LiveStateEvent {
402
+ matchUrn: string;
403
+ state: LiveState;
404
+ /** RFC3339 server timestamp the transition was stamped at. */
405
+ serverTime: string;
406
+ }
407
+
408
+ export declare interface LiveStateSubscription {
409
+ /** Stop the subscription and abort the in-flight request. Idempotent. */
410
+ close(): void;
411
+ }
412
+
413
+ export declare type MatchLiveStatus = 'upcoming' | 'live' | 'ended';
414
+
415
+ export declare interface MatchStatus {
416
+ matchUrn: string;
417
+ matchName?: string;
418
+ status: MatchLiveStatus;
419
+ datePlannedStart?: string;
420
+ }
421
+
422
+ /**
423
+ * Mode C — mount the hosted iframe player and bridge it over postMessage. Both
424
+ * ends verify event.origin on every message; commands are posted only to the
425
+ * embed page's origin.
426
+ */
427
+ export declare function mountEmbed(opts: MountEmbedOptions): EmbedHandle;
428
+
429
+ export declare interface MountEmbedOptions {
430
+ /** Element to mount the iframe into. */
431
+ container: HTMLElement;
432
+ /** URL of the hosted embed page, e.g. https://player.oddin.gg/embed/. */
433
+ src: string;
434
+ baseUrl: string;
435
+ matchUrn: string;
436
+ /**
437
+ * DEV/DEMO ONLY: appended as a query param so the embed page can read it.
438
+ * In production the embed page is hosted by you and injects the key
439
+ * server-side — do not pass it here.
440
+ */
441
+ apiKey?: string;
442
+ /** Origin of the embed page used to verify its messages. Defaults to new URL(src).origin. */
443
+ allowedFrameOrigin?: string;
444
+ muted?: boolean;
445
+ /**
446
+ * iframe sandbox attribute. Default keeps playback + EME + fullscreen working
447
+ * while blocking top-frame navigation and popups. Override only if you know
448
+ * what you're doing.
449
+ */
450
+ sandbox?: string;
451
+ /** Convenience callback for every event from the iframe. */
452
+ onEvent?: (msg: FrameToHost) => void;
453
+ }
454
+
455
+ /**
456
+ * Default skin — Oddin.gg brand identity: gold accent on dark navy, white text.
457
+ */
458
+ export declare const ODDIN_THEME: HavikTheme;
459
+
460
+ export declare class PlaybackError extends Error {
461
+ readonly code: PlaybackErrorCode;
462
+ readonly httpStatus: number;
463
+ /** Present for TOO_EARLY / RATE_LIMITED / UNAVAILABLE when the server sent Retry-After. */
464
+ readonly retryAfterMs?: number;
465
+ /** Server request id, when surfaced — useful for correlating opaque 403/503 in server logs. */
466
+ readonly requestId?: string;
467
+ /** Raw server error code (e.g. "TOO_EARLY"), preserved even if `code` maps it to a known enum. */
468
+ readonly serverCode?: string;
469
+ /**
470
+ * Scheduled kickoff time (epoch ms), if the server carried `liveStartsAt` on
471
+ * a TOO_EARLY body. `pollToLive` reads this to widen poll cadence for
472
+ * far-future matches.
473
+ */
474
+ readonly liveStartsAtMs?: number;
475
+ constructor(code: PlaybackErrorCode, httpStatus: number, message: string, init?: PlaybackErrorInit);
476
+ }
477
+
478
+ export declare type PlaybackErrorCode = 'INVALID_URN' | 'NOT_FOUND' | 'GONE' | 'TOO_EARLY' | 'UNAVAILABLE' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'RATE_LIMITED' | 'INTERNAL' | 'NETWORK' | 'TIMEOUT' | 'ABORTED';
479
+
480
+ export declare interface PlaybackErrorInit {
481
+ retryAfterMs?: number;
482
+ requestId?: string;
483
+ /** The raw server-provided error code string, if any — forward-compatible with new server codes. */
484
+ serverCode?: string;
485
+ /**
486
+ * Scheduled kickoff time (epoch ms), if the server included it on a TOO_EARLY
487
+ * response body. The SDK uses this to widen poll cadence for far-future
488
+ * matches without a separate catalog call.
489
+ */
490
+ liveStartsAtMs?: number;
491
+ cause?: unknown;
492
+ }
493
+
494
+ export declare interface PlaybackStats {
495
+ droppedFrames: number;
496
+ /** Live-edge latency in seconds (hls.js engine only). */
497
+ latencySeconds?: number;
498
+ /** The live-sync target latency hls.js is converging to, in seconds. Compare
499
+ * against latencySeconds to see drift. */
500
+ targetLatencySeconds?: number;
501
+ /** Estimated bandwidth in kbps (hls.js engine only). */
502
+ bandwidthKbps?: number;
503
+ /** Height of the current rendition, when known. */
504
+ levelHeight?: number;
505
+ /** Whether the stream is being played in low-latency mode. */
506
+ lowLatency?: boolean;
507
+ /** Whether the current media playlist is live. */
508
+ isLive?: boolean;
509
+ /** Whether playback is at/near the live edge (within the live sync window). */
510
+ atLiveEdge?: boolean;
511
+ }
512
+
513
+ export declare interface Player {
514
+ readonly state: PlayerState;
515
+ readonly descriptor: StreamDescriptor | null;
516
+ play(): Promise<void>;
517
+ pause(): void;
518
+ setMuted(muted: boolean): void;
519
+ setVolume(volume: number): void;
520
+ getStats(): PlaybackStats;
521
+ /** Available renditions (empty on the native engine, which does ABR internally). */
522
+ getQualityLevels(): QualityLevel[];
523
+ /** Current rendition index, or -1 when ABR is choosing automatically. */
524
+ getCurrentQuality(): number;
525
+ /** Pick a rendition by index, or 'auto'/-1 to re-enable ABR. */
526
+ setQuality(index: number | 'auto'): void;
527
+ /** Cap ABR to renditions at/below this bitrate (bps), or null to clear. */
528
+ setMaxBitrate(bitrate: number | null): void;
529
+ getAudioTracks(): AudioTrackInfo[];
530
+ setAudioTrack(id: number): void;
531
+ getTextTracks(): TextTrackInfo[];
532
+ /** Select a text track by id, or -1 to disable captions. */
533
+ setTextTrack(id: number): void;
534
+ /** Jump to the live edge (live streams). */
535
+ seekToLive(): void;
536
+ enterPictureInPicture(): Promise<void>;
537
+ exitPictureInPicture(): Promise<void>;
538
+ /** Retry playback after an error (re-resolves and re-attaches). */
539
+ retry(): Promise<void>;
540
+ /** Re-resolve playback (refreshes the signed license URL) and re-attach. */
541
+ reload(): Promise<void>;
542
+ on<E extends PlayerEvent>(event: E, cb: (payload: PlayerEventMap[E]) => void): () => void;
543
+ destroy(): void;
544
+ }
545
+
546
+ export declare type PlayerEvent = 'ready' | 'waiting' | 'playing' | 'buffering' | 'paused' | 'ended' | 'error' | 'warning' | 'autoplayblocked' | 'qualitychange' | 'audiotrackchange' | 'texttrackchange' | 'pipchange' | 'stats' | 'statechange';
547
+
548
+ export declare interface PlayerEventMap {
549
+ ready: void;
550
+ waiting: WaitState;
551
+ playing: void;
552
+ buffering: void;
553
+ paused: void;
554
+ ended: void;
555
+ error: PlaybackError;
556
+ /** Non-fatal condition (e.g. FairPlay-not-implemented passthrough, a failed license refresh). Playback continues. */
557
+ warning: PlaybackError;
558
+ /** The browser blocked autoplay (NotAllowedError). Prompt a tap-to-play / tap-to-unmute affordance. */
559
+ autoplayblocked: void;
560
+ /** Active rendition changed; payload is the level index (-1 = auto). */
561
+ qualitychange: number;
562
+ /** Active audio track changed; payload is the track id. */
563
+ audiotrackchange: number;
564
+ /** Active text track changed; payload is the track id (-1 = off). */
565
+ texttrackchange: number;
566
+ /** Picture-in-Picture entered (true) or exited (false). */
567
+ pipchange: boolean;
568
+ stats: PlaybackStats;
569
+ statechange: PlayerState;
570
+ }
571
+
572
+ export declare type PlayerState = 'idle' | 'waiting' | 'loading' | 'playing' | 'buffering' | 'paused' | 'ended' | 'error';
573
+
574
+ /**
575
+ * Poll `attempt` until it resolves, retrying on waitable PlaybackErrors
576
+ * (TOO_EARLY / UNAVAILABLE / RATE_LIMITED / NETWORK). Each retry's delay is
577
+ * computed by {@link nextDelayMs} from (a) the server's `Retry-After` (always a
578
+ * floor) and (b) the time-to-kickoff from `kickoffAt` and/or
579
+ * `PlaybackError.liveStartsAtMs`. Far from kickoff the cadence widens (to
580
+ * `~ttk/10`, capped at `sanityFloorMs`) so the SDK does not hammer the
581
+ * per-viewer `/v1/playback` endpoint when a viewer is armed hours early; inside
582
+ * the imminent window it collapses to the tight `[floorMs, ceilMs]` band so a
583
+ * 200 — i.e. the instant the match goes live — is caught fast. Terminal errors
584
+ * (404 / 410 / 400 / 401 / 403 / INTERNAL) reject straight away.
585
+ */
586
+ export declare function pollToLive<T>(attempt: () => Promise<T>, cfg: WaitForLive, signal?: AbortSignal): Promise<T>;
587
+
588
+ /** A selectable video rendition. */
589
+ export declare interface QualityLevel {
590
+ /** Index into getQualityLevels() — pass to setQuality(). */
591
+ index: number;
592
+ width?: number;
593
+ height?: number;
594
+ bitrate: number;
595
+ codecs?: string;
596
+ }
597
+
598
+ export declare interface ResolveOptions {
599
+ /** Base URL of the havik-streams API, e.g. https://streams.oddin.gg. */
600
+ baseUrl: string;
601
+ matchUrn: string;
602
+ credential: CredentialSource;
603
+ /**
604
+ * When set, resolve retries internally on 425 TOO_EARLY honoring Retry-After
605
+ * and resolves the instant the stream is live. See WaitForLive.
606
+ */
607
+ waitForLive?: WaitForLive;
608
+ signal?: AbortSignal;
609
+ }
610
+
611
+ /**
612
+ * Resolve a match into a playback descriptor (manifest URL + DRM config). With
613
+ * `waitForLive`, blocks (politely, server-paced) until the stream is live.
614
+ */
615
+ export declare function resolveStream(opts: ResolveOptions): Promise<StreamDescriptor>;
616
+
617
+ export declare interface StatusWatcher {
618
+ stop(): void;
619
+ }
620
+
621
+ /**
622
+ * The per-match playback credential returned by `GET /v1/playback/{urn}`,
623
+ * normalized for the SDK. `manifestUrl` is an unsigned HLS `.m3u8`; the DRM
624
+ * license URLs are pre-signed by havik-streams (the signed query string IS the
625
+ * token — never rewrite them).
626
+ */
627
+ export declare interface StreamDescriptor {
628
+ matchUrn: string;
629
+ protocol: 'hls';
630
+ /** Producer-set DRM decision. When false, init no EME/CDM at all. */
631
+ drmEnabled: boolean;
632
+ manifestUrl: string;
633
+ /** Present only when drmEnabled. */
634
+ drm?: DrmInfo;
635
+ serverTime: string;
636
+ /** Present only while the match is upcoming. */
637
+ liveStartsAt?: string;
638
+ /** Filled by the player once a media playlist with EXT-X-PART-INF is seen. */
639
+ isLowLatency?: boolean;
640
+ /** Any unrecognized top-level fields from the playback response (forward-compat). */
641
+ extensions?: Record<string, unknown>;
642
+ }
643
+
644
+ /**
645
+ * Subscribe to a match's live-state stream (`GET /v1/events/{urn}`).
646
+ *
647
+ * Uses `fetch` rather than the native `EventSource` because the endpoint
648
+ * authenticates with the `x-api-key` HEADER, which `EventSource` cannot set.
649
+ * The body is a `text/event-stream` we parse incrementally. The connection is
650
+ * long-lived (the server caps it at ~30min and sends heartbeats); on any
651
+ * close/error the client reconnects with exponential backoff and re-syncs from
652
+ * the initial snapshot the server replays on connect. A subscription failure
653
+ * is always non-fatal to playback — callers keep the HLS staleness watchdog as
654
+ * the layer-fallback.
655
+ */
656
+ export declare function subscribeLiveState(opts: SubscribeLiveStateOptions): LiveStateSubscription;
657
+
658
+ export declare interface SubscribeLiveStateOptions {
659
+ /** Base URL of the SSE endpoint, e.g. `https://events.feed.oddin-video.gg`. */
660
+ eventsBaseUrl: string;
661
+ matchUrn: string;
662
+ credential: CredentialSource;
663
+ /** Fired for the initial snapshot on connect and every subsequent transition. */
664
+ onState: (ev: LiveStateEvent) => void;
665
+ /**
666
+ * Fired on a non-fatal subscription problem (network drop, HTTP error,
667
+ * server lifetime-cap close). The client keeps retrying with backoff; this
668
+ * is observational only — it never tears playback down.
669
+ */
670
+ onError?: (err: unknown) => void;
671
+ /**
672
+ * Fired on EVERY received frame, including heartbeat comments — a liveness
673
+ * tick. Lets a caller treat the subscription as healthy (e.g. suspend
674
+ * polling) while frames keep arriving, and fall back when they stop.
675
+ */
676
+ onAlive?: () => void;
677
+ /** Reconnect backoff floor / ceiling (ms). Defaults: 1000 / 15000. */
678
+ minBackoffMs?: number;
679
+ maxBackoffMs?: number;
680
+ }
681
+
682
+ /** A selectable text (caption/subtitle) track. */
683
+ export declare interface TextTrackInfo {
684
+ id: number;
685
+ name: string;
686
+ lang?: string;
687
+ }
688
+
689
+ export declare interface TournamentResult {
690
+ /** Present unless the tournament is unknown (404) or the server returned 304. */
691
+ tournament?: CatalogTournament;
692
+ /** ETag for a cheap conditional refresh next time, when the server emits one. */
693
+ etag?: string;
694
+ /** True when the server returned 304 (the prior etag is still fresh); `tournament` is undefined. */
695
+ notModified: boolean;
696
+ /** True when the URN is not in the catalog (or unknown to the caller's brand). */
697
+ notFound: boolean;
698
+ }
699
+
700
+ export declare type WaitForLive = boolean | WaitForLiveOptions;
701
+
702
+ export declare interface WaitForLiveOptions {
703
+ /** Overall budget before giving up with a TIMEOUT error. Default: unbounded. */
704
+ timeoutMs?: number;
705
+ /** Minimum delay between polls. Floored at 1000ms (the server's min / limiter). Default: 1000. */
706
+ floorMs?: number;
707
+ /**
708
+ * Maximum delay between polls in the IMMINENT window (≤ {@link imminentMs}
709
+ * before kickoff). Far-from-kickoff polls are widened automatically using
710
+ * `kickoffAt` (see below) up to {@link sanityFloorMs}, regardless of this
711
+ * value — so this knob really controls only the near-kickoff cadence. Default: 30000.
712
+ */
713
+ ceilMs?: number;
714
+ /**
715
+ * Caller-supplied scheduled kickoff time (epoch ms, or an ISO string), used to
716
+ * widen the long-tail poll cadence so the SDK does not hammer the per-viewer
717
+ * `/v1/playback` endpoint when a viewer is armed hours early.
718
+ *
719
+ * Far-from-kickoff (time-to-kickoff > {@link imminentMs}) the poll interval is
720
+ * stretched to `min(ttk / 10, sanityFloorMs)` (never tighter than the server's
721
+ * `Retry-After`, never longer than {@link sanityFloorMs}, never longer than
722
+ * the time remaining to kickoff). Inside the imminent window the cadence
723
+ * collapses to the tight `[floorMs, ceilMs]` range so a 200 is caught fast.
724
+ *
725
+ * When omitted, the SDK also reads `liveStartsAt` off the 425 body if the
726
+ * server returns it (`PlaybackError.liveStartsAtMs`), so newer servers tune
727
+ * automatically without any client change.
728
+ */
729
+ kickoffAt?: number | string;
730
+ /**
731
+ * Threshold (ms) for "imminent kickoff" — inside this window the cadence
732
+ * collapses to the tight `[floorMs, ceilMs]` range. Default: 10 minutes.
733
+ */
734
+ imminentMs?: number;
735
+ /**
736
+ * Absolute hard ceiling on any single wait, even when far from kickoff or
737
+ * when the server returns a very large `Retry-After`. Sanity floor so an
738
+ * early go-live or a schedule change is caught within bounded latency.
739
+ * Default: 5 minutes (300_000ms).
740
+ */
741
+ sanityFloorMs?: number;
742
+ /** Add up to +15% jitter to each delay (never reduces below the server's ask). Default: true. */
743
+ jitter?: boolean;
744
+ /** Called before each wait with the upcoming retry. */
745
+ onState?: (state: WaitState) => void;
746
+ }
747
+
748
+ export declare interface WaitState {
749
+ phase: 'tooEarly' | 'unavailable' | 'rateLimited' | 'network';
750
+ /** How long until the next poll, in ms. */
751
+ retryInMs: number;
752
+ /** 1-based retry count. */
753
+ attempt: number;
754
+ /** Wall-clock ms since the wait started. */
755
+ elapsedMs: number;
756
+ /**
757
+ * Time-to-kickoff (ms) used to compute this delay, if known. `undefined` when
758
+ * no kickoff hint is available (caller didn't pass `kickoffAt` and the server
759
+ * didn't carry `liveStartsAt` on the error).
760
+ */
761
+ ttkMs?: number;
762
+ }
763
+
764
+ /**
765
+ * Poll `GET /v1/matches/{urn}` for the live status (there is no push channel),
766
+ * firing onChange on transitions. Uses ETag/If-None-Match so unchanged polls
767
+ * are cheap 304s. This drives discovery; actual readiness is the /v1/playback
768
+ * 200 (see resolveStream + waitForLive).
769
+ */
770
+ export declare function watchStatus(opts: WatchStatusOptions): StatusWatcher;
771
+
772
+ export declare interface WatchStatusOptions {
773
+ baseUrl: string;
774
+ matchUrn: string;
775
+ credential: CredentialSource;
776
+ /** Fired whenever the status changes (and once on first read). */
777
+ onChange: (status: MatchStatus) => void;
778
+ /** Fired on a poll error; polling continues. */
779
+ onError?: (err: PlaybackError) => void;
780
+ /** Poll interval in ms. Floored at 2000. Default: 10000 (catalog max-age). */
781
+ intervalMs?: number;
782
+ }
783
+
784
+ export declare interface WidevineInfo {
785
+ licenseUrl: string;
786
+ }
787
+
788
+ export { }
789
+
790
+
791
+ declare global {
792
+ interface Window {
793
+ HAVIK_EMBED_CONFIG?: Partial<EmbedConfig>;
794
+ }
795
+ }