@scarlett-player/hls 1.11.0 → 1.12.0

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,388 @@
1
+ import { Plugin } from '@scarlett-player/core';
2
+
3
+ /**
4
+ * HLS Plugin Types
5
+ */
6
+
7
+ /** HLS plugin configuration options */
8
+ interface HLSPluginConfig {
9
+ /** Enable debug logging */
10
+ debug?: boolean;
11
+ /** Auto start loading when source is set */
12
+ autoStartLoad?: boolean;
13
+ /** Start position in seconds (-1 for default) */
14
+ startPosition?: number;
15
+ /**
16
+ * Enable low-latency HLS for live streams (default: false).
17
+ *
18
+ * Turning this on does more than set the hls.js flag: it also enables
19
+ * latency catch-up (`maxLiveSyncPlaybackRate`, which hls.js leaves at 1 and
20
+ * therefore disabled). Without it, LL-HLS parses and loads parts but latency
21
+ * settles wherever the buffer lands and is never pulled back. The retry
22
+ * budgets are deliberately left at their standard-live values; see
23
+ * `buildBaseHlsConfig()` for why widening them changes nothing a viewer
24
+ * could see.
25
+ *
26
+ * Opt-in, and it is a REQUEST: the manifest has to carry `EXT-X-PART` or
27
+ * advertise `CAN-BLOCK-RELOAD=YES` for low latency to actually happen. The
28
+ * `lowLatencyMode` state key and the `live:lowlatency` event report what the
29
+ * manifest supports, not what was requested here.
30
+ */
31
+ lowLatencyMode?: boolean;
32
+ /**
33
+ * Target latency for live streams in seconds, overriding the manifest's
34
+ * `PART-HOLD-BACK` / `HOLD-BACK`. Left unset, hls.js derives the target from
35
+ * the manifest, which is what a correctly packaged stream wants.
36
+ */
37
+ liveSyncDuration?: number;
38
+ /**
39
+ * Target latency expressed as a count of target durations (hls.js default:
40
+ * 3). Mutually exclusive with `liveSyncDuration`: hls.js THROWS on a config
41
+ * carrying both, so the plugin forwards one group only and logs which half
42
+ * it dropped.
43
+ */
44
+ liveSyncDurationCount?: number;
45
+ /**
46
+ * Maximum latency in seconds before the player seeks forward to catch up.
47
+ * Left unset, hls.js uses `liveMaxLatencyDurationCount`.
48
+ */
49
+ liveMaxLatencyDuration?: number;
50
+ /**
51
+ * Maximum latency as a count of target durations (hls.js default: Infinity,
52
+ * i.e. never force a catch-up seek).
53
+ */
54
+ liveMaxLatencyDurationCount?: number;
55
+ /**
56
+ * Playback rate ceiling used to catch up to the live edge (hls.js default:
57
+ * 1, which disables catch-up entirely).
58
+ *
59
+ * Defaults to 1.1 when `lowLatencyMode` is true and is left at hls.js's
60
+ * default otherwise, so standard live and VOD are unchanged. 1.1 recovers a
61
+ * second of drift in ten and the pitch shift is barely audible; 1.05 is
62
+ * safer and slower.
63
+ */
64
+ maxLiveSyncPlaybackRate?: number;
65
+ /**
66
+ * Report live streams with an Infinity duration rather than the length of
67
+ * the current sliding window (hls.js default: false).
68
+ */
69
+ liveDurationInfinity?: boolean;
70
+ /** Max buffer length in seconds */
71
+ maxBufferLength?: number;
72
+ /** Max max buffer length in seconds */
73
+ maxMaxBufferLength?: number;
74
+ /** Back buffer length in seconds for DVR */
75
+ backBufferLength?: number;
76
+ /** Enable worker for hls.js (better performance) */
77
+ enableWorker?: boolean;
78
+ /**
79
+ * Max network error retries before giving up (default: 3).
80
+ *
81
+ * Governs both playback branches: hls.js retries the load, and the native
82
+ * (Safari/iOS) path reloads the source and restores position.
83
+ */
84
+ maxNetworkRetries?: number;
85
+ /**
86
+ * Max media error retries before giving up (default: 2).
87
+ *
88
+ * Governs both playback branches: hls.js calls recoverMediaError(), and the
89
+ * native (Safari/iOS) path reloads the source and restores position.
90
+ */
91
+ maxMediaRetries?: number;
92
+ /** Cap quality to player element dimensions (default: true) */
93
+ capLevelToPlayerSize?: boolean;
94
+ /** Override initial bandwidth estimate in bits per second */
95
+ initialBandwidthEstimate?: number;
96
+ /** Base retry delay in milliseconds (default: 1000) */
97
+ retryDelayMs?: number;
98
+ /** Exponential backoff multiplier (default: 2) */
99
+ retryBackoffFactor?: number;
100
+ /**
101
+ * Watchdog for source loading in milliseconds (default: 30000, 0 disables).
102
+ * If the manifest has not parsed within this window the load fails with an
103
+ * error instead of leaving the viewer on a spinner forever.
104
+ */
105
+ loadTimeoutMs?: number;
106
+ /**
107
+ * Automatically attempt to reconnect after a fatal network/media error
108
+ * once the stream had been playing (default: true). Viewers should not
109
+ * have to press anything when a connection blip resolves itself.
110
+ */
111
+ autoReconnect?: boolean;
112
+ /** First auto-reconnect delay in milliseconds (default: 2000) */
113
+ reconnectBaseDelayMs?: number;
114
+ /** Cap for the auto-reconnect backoff in milliseconds (default: 30000) */
115
+ reconnectMaxDelayMs?: number;
116
+ /** Total window to keep auto-reconnecting in milliseconds (default: 300000 = 5 min) */
117
+ reconnectWindowMs?: number;
118
+ /**
119
+ * Validate every playlist response before it reaches the M3U8 parser
120
+ * (default: true). A live refresh that returns an error page, a
121
+ * master-only response, or an empty document becomes a normal network
122
+ * error (bounded retries, then reconnect, then the retry UI) instead of
123
+ * being indexed blindly.
124
+ */
125
+ validatePlaylists?: boolean;
126
+ /** Index signature for PluginConfig compatibility */
127
+ [key: string]: unknown;
128
+ }
129
+ /** Quality level information */
130
+ interface HLSQualityLevel {
131
+ /** Level index in hls.js */
132
+ index: number;
133
+ /** Video width */
134
+ width: number;
135
+ /** Video height */
136
+ height: number;
137
+ /** Bitrate in bits per second */
138
+ bitrate: number;
139
+ /** Human-readable label (e.g., "1080p") */
140
+ label: string;
141
+ /** Codec info */
142
+ codec?: string;
143
+ }
144
+ /** HLS error types */
145
+ type HLSErrorType = 'network' | 'media' | 'mux' | 'other';
146
+ /** HLS error details */
147
+ interface HLSError {
148
+ type: HLSErrorType;
149
+ details: string;
150
+ fatal: boolean;
151
+ url?: string;
152
+ reason?: string;
153
+ response?: {
154
+ code: number;
155
+ text: string;
156
+ };
157
+ }
158
+ /** Live stream info */
159
+ interface HLSLiveInfo {
160
+ /** Whether stream is live */
161
+ isLive: boolean;
162
+ /** Edge latency in seconds */
163
+ latency: number;
164
+ /** Target latency for low latency mode */
165
+ targetLatency: number;
166
+ /** Drift from live edge */
167
+ drift: number;
168
+ /** Position to seek to for live sync (seconds from start), when known */
169
+ liveSyncPosition?: number;
170
+ /**
171
+ * Whether the stream is EFFECTIVELY low latency - the manifest carries
172
+ * `EXT-X-PART` or advertises `CAN-BLOCK-RELOAD=YES` - rather than whether
173
+ * `lowLatencyMode` was requested in the plugin config.
174
+ */
175
+ lowLatency: boolean;
176
+ }
177
+ /** HLS Plugin interface extending base Plugin */
178
+ interface IHLSPlugin extends Plugin<HLSPluginConfig> {
179
+ readonly id: 'hls-provider';
180
+ /** Check if this provider can play a source */
181
+ canPlay(src: string): boolean;
182
+ /** Load and play a source */
183
+ loadSource(src: string): Promise<void>;
184
+ /** Get current quality level index (-1 = auto) */
185
+ getCurrentLevel(): number;
186
+ /** Set quality level (-1 for auto) */
187
+ setLevel(index: number): void;
188
+ /** Get all available quality levels */
189
+ getLevels(): HLSQualityLevel[];
190
+ /** Get the raw hls.js instance (for advanced use) */
191
+ getHlsInstance(): unknown | null;
192
+ /** Check if using native HLS (Safari) */
193
+ isNativeHLS(): boolean;
194
+ /** Get live stream info */
195
+ getLiveInfo(): HLSLiveInfo | null;
196
+ /** Switch from hls.js to native HLS (for AirPlay) */
197
+ switchToNative(): Promise<void>;
198
+ /** Switch from native HLS back to hls.js */
199
+ switchToHlsJs(): Promise<void>;
200
+ }
201
+ /** Type guard for hls.js level */
202
+ interface HlsLevel {
203
+ width: number;
204
+ height: number;
205
+ bitrate: number;
206
+ codecSet?: string;
207
+ name?: string;
208
+ }
209
+ /** Minimal hls.js interface for type safety */
210
+ interface HlsInstance {
211
+ loadSource(src: string): void;
212
+ attachMedia(media: HTMLMediaElement): void;
213
+ detachMedia(): void;
214
+ startLoad(startPosition?: number): void;
215
+ stopLoad(): void;
216
+ recoverMediaError(): void;
217
+ destroy(): void;
218
+ on(event: string, handler: (...args: any[]) => void): void;
219
+ off(event: string, handler: (...args: any[]) => void): void;
220
+ levels: HlsLevel[];
221
+ currentLevel: number;
222
+ autoLevelEnabled: boolean;
223
+ nextLevel: number;
224
+ loadLevel: number;
225
+ latency?: number;
226
+ targetLatency?: number;
227
+ drift?: number;
228
+ liveSyncPosition?: number;
229
+ bandwidthEstimate?: number;
230
+ media: HTMLMediaElement | null;
231
+ /** Alternate audio renditions declared by the manifest. */
232
+ audioTracks: HlsAudioTrack[];
233
+ /** Index into {@link audioTracks} of the rendition currently playing. */
234
+ audioTrack: number;
235
+ }
236
+ /**
237
+ * One alternate audio rendition, as hls.js reports it.
238
+ *
239
+ * Only the fields this plugin reads; hls.js attaches several more.
240
+ */
241
+ interface HlsAudioTrack {
242
+ /** hls.js's own numeric id for the track. */
243
+ id?: number;
244
+ /** Human-readable NAME from the manifest, e.g. "Director Commentary". */
245
+ name?: string;
246
+ /** BCP 47 language tag from the manifest, when it declares one. */
247
+ lang?: string;
248
+ /** Whether the manifest marked this the default rendition. */
249
+ default?: boolean;
250
+ /** Rendition group the track belongs to. */
251
+ groupId?: string;
252
+ }
253
+
254
+ /**
255
+ * Live metrics: the single writer for the player's live/DVR state.
256
+ *
257
+ * Before this module, `liveLatency`, `liveEdge` and `seekableRange` had two
258
+ * writers with different definitions of "latency": `hlsLevelLoaded` computed
259
+ * an edge flag from the level details, and the `timeupdate` handler then
260
+ * recomputed it unconditionally from `video.seekable` as `latency < 10`. Under
261
+ * MSE the second source is wrong (`video.seekable.start(0)` stays 0 instead of
262
+ * following the sliding window), and at a 2-4s low-latency target `< 10s` is
263
+ * always true, so "GO LIVE" could never appear no matter how far a viewer
264
+ * drifted. Every write of those keys now goes through {@link applyLiveMetrics},
265
+ * so there is exactly one definition and no clobber.
266
+ *
267
+ * Latency truth differs by playback path:
268
+ *
269
+ * - **hls.js (MSE):** `hls.latency` is real wall-clock latency, computed from
270
+ * `EXT-X-PROGRAM-DATE-TIME` drift where the manifest carries it, measured
271
+ * against `hls.targetLatency` (which hls.js derives from `PART-HOLD-BACK` /
272
+ * `HOLD-BACK`, or from `liveSyncDuration`* config when set).
273
+ * - **native (Safari/iOS):** there is no latency API. `video.seekable.end` is
274
+ * the best available stand-in for the edge, so latency is the distance to it
275
+ * — a buffer distance, not a measured latency. Treated as an approximation
276
+ * throughout, and the edge threshold is deliberately loose unless a target
277
+ * latency is known from a previous hls.js session on the same source.
278
+ */
279
+
280
+ /**
281
+ * The level-details fields the live path reads, as hls.js reports them on
282
+ * `hlsLevelLoaded`.
283
+ *
284
+ * The LL-HLS members are the ones that decide whether a stream is *effectively*
285
+ * low latency: a host can set `lowLatencyMode: true` against a plain live
286
+ * manifest, and that must not produce an LL badge.
287
+ */
288
+ interface HlsLevelDetails {
289
+ /** Whether the playlist is live (no EXT-X-ENDLIST) */
290
+ live?: boolean;
291
+ /** Total duration of the fragments in the playlist window, in seconds */
292
+ totalduration?: number;
293
+ /** EXT-X-TARGETDURATION, in seconds */
294
+ targetduration?: number;
295
+ /** Start of the sliding window, in seconds on the player timeline */
296
+ fragmentStart?: number;
297
+ /** End of the sliding window (live edge), in seconds on the player timeline */
298
+ edge?: number;
299
+ /** Fragments in the current playlist window */
300
+ fragments?: Array<{
301
+ start?: number;
302
+ }>;
303
+ /** LL-HLS: partial segments in the current window (EXT-X-PART) */
304
+ partList?: unknown[] | null;
305
+ /** LL-HLS: EXT-X-PART-INF PART-TARGET, in seconds */
306
+ partTarget?: number;
307
+ /** LL-HLS: EXT-X-SERVER-CONTROL CAN-BLOCK-RELOAD=YES */
308
+ canBlockReload?: boolean;
309
+ /** LL-HLS: EXT-X-SERVER-CONTROL PART-HOLD-BACK, in seconds */
310
+ partHoldBack?: number;
311
+ /** EXT-X-SERVER-CONTROL HOLD-BACK, in seconds */
312
+ holdBack?: number;
313
+ }
314
+ /** A snapshot of the live state of the stream, from one source of truth. */
315
+ interface LiveMetrics {
316
+ /** Latency behind the live edge, in seconds */
317
+ latency: number;
318
+ /** Latency the stream is aiming for, in seconds */
319
+ targetLatency: number;
320
+ /** Whether the viewer counts as being AT the live edge (see below) */
321
+ atEdge: boolean;
322
+ /** DVR window on the player timeline, when it is known */
323
+ seekableRange: {
324
+ start: number;
325
+ end: number;
326
+ } | null;
327
+ /**
328
+ * Whether low latency is EFFECTIVE: the manifest carries `EXT-X-PART` or
329
+ * advertises `CAN-BLOCK-RELOAD=YES`, AND low latency was requested in the
330
+ * plugin config. Both halves matter. A host that sets `lowLatencyMode`
331
+ * against a plain live manifest gets no LL badge because the stream cannot
332
+ * deliver it; a host that leaves the flag off gets none against an LL
333
+ * manifest either, because hls.js will not load the parts.
334
+ */
335
+ lowLatency: boolean;
336
+ }
337
+ /** Where a {@link LiveMetrics} snapshot is measured from. */
338
+ type LiveMetricsSource = {
339
+ /** hls.js (MSE) path: latency comes from hls.js itself */
340
+ kind: 'hls';
341
+ /** The live hls.js instance */
342
+ hls: HlsInstance;
343
+ /** Most recent level details, when one has been seen */
344
+ details?: HlsLevelDetails | null;
345
+ /**
346
+ * Whether low latency was requested in the plugin config. Defaults to
347
+ * true when omitted, so a caller that only has a manifest still gets the
348
+ * manifest's answer.
349
+ */
350
+ lowLatencyRequested?: boolean;
351
+ } | {
352
+ /** Native path (Safari/iOS): latency is approximated from `seekable` */
353
+ kind: 'media';
354
+ /** The media element playing the live stream */
355
+ media: HTMLMediaElement;
356
+ /**
357
+ * Target latency carried over from an hls.js session on the same source
358
+ * (an AirPlay handoff). Absent on a stream that has only ever played
359
+ * natively, where nothing reveals the manifest's hold-back.
360
+ */
361
+ targetLatency?: number;
362
+ /**
363
+ * Whether to report the stream as effectively low latency. The HLS
364
+ * plugin never sets it on this path: native playback is a handoff, and
365
+ * nothing is loading parts any more.
366
+ */
367
+ lowLatency?: boolean;
368
+ };
369
+ /**
370
+ * Target latency assumed when nothing better is known. Matches hls.js's own
371
+ * fallback and the value `getLiveInfo()` reported before LL-HLS.
372
+ */
373
+ declare const DEFAULT_TARGET_LATENCY = 3;
374
+ /**
375
+ * Measure the live state of the stream from one source.
376
+ *
377
+ * Call it only for live content: for VOD the numbers are meaningless and the
378
+ * caller should not be writing live state at all.
379
+ *
380
+ * A viewer is AT the edge when `latency <= targetLatency + tolerance`, with the
381
+ * tolerance from {@link edgeTolerance}.
382
+ *
383
+ * @param source - hls.js instance (with its latest level details) or media element
384
+ * @returns The snapshot, or null when the source reveals nothing usable
385
+ */
386
+ declare function computeLiveMetrics(source: LiveMetricsSource): LiveMetrics | null;
387
+
388
+ export { DEFAULT_TARGET_LATENCY as D, type HLSPluginConfig as H, type IHLSPlugin as I, type LiveMetrics as L, type HLSQualityLevel as a, type HLSError as b, type HLSLiveInfo as c, computeLiveMetrics as d, type LiveMetricsSource as e, type HlsLevelDetails as f };