@oddin-gg/havik-player 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +300 -0
- package/THIRD_PARTY_NOTICES.txt +59 -0
- package/dist/havik-player.global.js +147 -0
- package/dist/havik-player.global.js.map +1 -0
- package/dist/index.cjs +108 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +795 -0
- package/dist/index.d.ts +795 -0
- package/dist/index.js +1919 -0
- package/dist/index.js.map +1 -0
- package/docs/API.md +1115 -0
- package/docs/STABILITY.md +56 -0
- package/embed/index.html +26 -0
- package/examples/README.md +17 -0
- package/examples/Vue.vue +37 -0
- package/examples/react.tsx +47 -0
- package/examples/vanilla.html +41 -0
- package/package.json +101 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|