@scarlett-player/hls 1.0.2 → 1.2.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.
package/dist/light.d.cts CHANGED
@@ -1,12 +1,19 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-CRKmNxEW.cjs';
2
- export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-CRKmNxEW.cjs';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './types-D-HW4lmM.cjs';
2
+ export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-D-HW4lmM.cjs';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
6
6
  * HLS Provider Plugin - Light Build
7
7
  *
8
- * Same functionality as the main plugin but uses hls.js/light for smaller bundle.
9
- * The light build excludes: subtitles, ID3 tags, and DRM/EME support.
8
+ * Same functionality as the main plugin but uses hls.js/light for smaller
9
+ * bundle. The light build of hls.js excludes: subtitles, ID3 tags, and
10
+ * DRM/EME support.
11
+ *
12
+ * Thin wrapper around the shared factory in `create-hls-plugin.ts`, so
13
+ * the light entry always carries the same error handling, retry,
14
+ * auto-reconnect, load watchdog, and playlist-validation machinery as the
15
+ * full build (before 1.2.0 this file was a hand-maintained fork that had
16
+ * silently missed all of that hardening).
10
17
  *
11
18
  * @packageDocumentation
12
19
  */
package/dist/light.d.ts CHANGED
@@ -1,12 +1,19 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-CRKmNxEW.js';
2
- export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-CRKmNxEW.js';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './types-D-HW4lmM.js';
2
+ export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-D-HW4lmM.js';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
6
6
  * HLS Provider Plugin - Light Build
7
7
  *
8
- * Same functionality as the main plugin but uses hls.js/light for smaller bundle.
9
- * The light build excludes: subtitles, ID3 tags, and DRM/EME support.
8
+ * Same functionality as the main plugin but uses hls.js/light for smaller
9
+ * bundle. The light build of hls.js excludes: subtitles, ID3 tags, and
10
+ * DRM/EME support.
11
+ *
12
+ * Thin wrapper around the shared factory in `create-hls-plugin.ts`, so
13
+ * the light entry always carries the same error handling, retry,
14
+ * auto-reconnect, load watchdog, and playlist-validation machinery as the
15
+ * full build (before 1.2.0 this file was a hand-maintained fork that had
16
+ * silently missed all of that hardening).
10
17
  *
11
18
  * @packageDocumentation
12
19
  */
package/dist/light.js CHANGED
@@ -1,11 +1,20 @@
1
1
  import {
2
- formatLevel,
3
- mapLevels,
4
- setupHlsEventHandlers,
5
- setupVideoEventHandlers
6
- } from "./chunk-MLOYPPFI.js";
2
+ __export,
3
+ createHLSPluginWith
4
+ } from "./chunk-IFUZZQQE.js";
7
5
 
8
6
  // src/hls-loader-light.ts
7
+ var hls_loader_light_exports = {};
8
+ __export(hls_loader_light_exports, {
9
+ createHlsInstance: () => createHlsInstance,
10
+ getHlsConstructor: () => getHlsConstructor,
11
+ isHLSSupported: () => isHLSSupported,
12
+ isHlsJsSupported: () => isHlsJsSupported,
13
+ loadHlsJs: () => loadHlsJs,
14
+ resetLoader: () => resetLoader,
15
+ shouldPreferNativeHLS: () => shouldPreferNativeHLS,
16
+ supportsNativeHLS: () => supportsNativeHLS
17
+ });
9
18
  var hlsConstructor = null;
10
19
  var loadingPromise = null;
11
20
  function supportsNativeHLS() {
@@ -13,6 +22,13 @@ function supportsNativeHLS() {
13
22
  const video = document.createElement("video");
14
23
  return video.canPlayType("application/vnd.apple.mpegurl") !== "";
15
24
  }
25
+ function shouldPreferNativeHLS() {
26
+ if (!supportsNativeHLS()) return false;
27
+ if (typeof navigator === "undefined") return false;
28
+ const ua = navigator.userAgent;
29
+ const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua) && !/CriOS/.test(ua);
30
+ return isSafari;
31
+ }
16
32
  function isHlsJsSupported() {
17
33
  if (hlsConstructor) {
18
34
  return hlsConstructor.isSupported();
@@ -56,466 +72,23 @@ function createHlsInstance(config) {
56
72
  function getHlsConstructor() {
57
73
  return hlsConstructor;
58
74
  }
75
+ function resetLoader() {
76
+ hlsConstructor = null;
77
+ loadingPromise = null;
78
+ }
59
79
 
60
80
  // src/light.ts
61
- var DEFAULT_CONFIG = {
62
- debug: false,
63
- autoStartLoad: true,
64
- startPosition: -1,
65
- lowLatencyMode: false,
66
- maxBufferLength: 30,
67
- maxMaxBufferLength: 600,
68
- backBufferLength: 30,
69
- enableWorker: true,
70
- maxNetworkRetries: 3,
71
- maxMediaRetries: 2,
72
- retryDelayMs: 1e3,
73
- retryBackoffFactor: 2
74
- };
75
81
  function createHLSPlugin(config) {
76
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
77
- let api = null;
78
- let hls = null;
79
- let video = null;
80
- let isNative = false;
81
- let currentSrc = null;
82
- let cleanupHlsEvents = null;
83
- let cleanupVideoEvents = null;
84
- let isAutoQuality = true;
85
- let networkRetryCount = 0;
86
- let mediaRetryCount = 0;
87
- let retryTimeout = null;
88
- let errorCount = 0;
89
- let errorWindowStart = 0;
90
- const MAX_ERRORS_IN_WINDOW = 10;
91
- const ERROR_WINDOW_MS = 5e3;
92
- const getOrCreateVideo = () => {
93
- if (video) return video;
94
- const existing = api?.container.querySelector("video");
95
- if (existing) {
96
- video = existing;
97
- return video;
98
- }
99
- video = document.createElement("video");
100
- video.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;display:block;object-fit:contain;background:#000";
101
- video.preload = "metadata";
102
- video.controls = false;
103
- video.playsInline = true;
104
- api?.container.appendChild(video);
105
- return video;
106
- };
107
- const cleanup = () => {
108
- cleanupHlsEvents?.();
109
- cleanupHlsEvents = null;
110
- cleanupVideoEvents?.();
111
- cleanupVideoEvents = null;
112
- if (retryTimeout) {
113
- clearTimeout(retryTimeout);
114
- retryTimeout = null;
115
- }
116
- if (hls) {
117
- hls.destroy();
118
- hls = null;
119
- }
120
- currentSrc = null;
121
- isNative = false;
122
- isAutoQuality = true;
123
- networkRetryCount = 0;
124
- mediaRetryCount = 0;
125
- errorCount = 0;
126
- errorWindowStart = 0;
127
- };
128
- const buildHlsConfig = () => ({
129
- debug: mergedConfig.debug,
130
- autoStartLoad: mergedConfig.autoStartLoad,
131
- startPosition: mergedConfig.startPosition,
132
- startLevel: -1,
133
- lowLatencyMode: mergedConfig.lowLatencyMode,
134
- maxBufferLength: mergedConfig.maxBufferLength,
135
- maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
136
- backBufferLength: mergedConfig.backBufferLength,
137
- enableWorker: mergedConfig.enableWorker,
138
- fragLoadingMaxRetry: 1,
139
- manifestLoadingMaxRetry: 1,
140
- levelLoadingMaxRetry: 1,
141
- fragLoadingRetryDelay: 500,
142
- manifestLoadingRetryDelay: 500,
143
- levelLoadingRetryDelay: 500
144
- });
145
- const getRetryDelay = (retryCount) => {
146
- const baseDelay = mergedConfig.retryDelayMs ?? 1e3;
147
- const backoffFactor = mergedConfig.retryBackoffFactor ?? 2;
148
- return baseDelay * Math.pow(backoffFactor, retryCount);
149
- };
150
- const emitFatalError = (error, retriesExhausted) => {
151
- const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
152
- api?.logger.error(message, { type: error.type, details: error.details });
153
- api?.setState("playbackState", "error");
154
- api?.setState("buffering", false);
155
- api?.emit("error", {
156
- code: "MEDIA_ERROR",
157
- message,
158
- fatal: true,
159
- timestamp: Date.now()
160
- });
161
- };
162
- const handleHlsError = (error) => {
163
- const Hls = getHlsConstructor();
164
- if (!Hls || !hls) return;
165
- const now = Date.now();
166
- if (now - errorWindowStart > ERROR_WINDOW_MS) {
167
- errorCount = 1;
168
- errorWindowStart = now;
169
- } else {
170
- errorCount++;
171
- }
172
- if (errorCount >= MAX_ERRORS_IN_WINDOW) {
173
- api?.logger.error(`Too many errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
174
- emitFatalError(error, true);
175
- cleanupHlsEvents?.();
176
- cleanupHlsEvents = null;
177
- hls.destroy();
178
- hls = null;
179
- return;
180
- }
181
- if (error.fatal) {
182
- api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
183
- switch (error.type) {
184
- case "network": {
185
- const maxRetries = mergedConfig.maxNetworkRetries ?? 3;
186
- if (networkRetryCount >= maxRetries) {
187
- api?.logger.error(`Network error recovery failed after ${networkRetryCount} attempts`);
188
- emitFatalError(error, true);
189
- return;
190
- }
191
- networkRetryCount++;
192
- const delay = getRetryDelay(networkRetryCount - 1);
193
- api?.logger.info(`Attempting network error recovery (attempt ${networkRetryCount}/${maxRetries}) in ${delay}ms`);
194
- api?.emit("error:network", { error: new Error(error.details) });
195
- if (retryTimeout) {
196
- clearTimeout(retryTimeout);
197
- }
198
- retryTimeout = setTimeout(() => {
199
- if (hls) {
200
- hls.startLoad();
201
- }
202
- }, delay);
203
- break;
204
- }
205
- case "media": {
206
- const maxRetries = mergedConfig.maxMediaRetries ?? 2;
207
- if (mediaRetryCount >= maxRetries) {
208
- api?.logger.error(`Media error recovery failed after ${mediaRetryCount} attempts`);
209
- emitFatalError(error, true);
210
- return;
211
- }
212
- mediaRetryCount++;
213
- const delay = getRetryDelay(mediaRetryCount - 1);
214
- api?.logger.info(`Attempting media error recovery (attempt ${mediaRetryCount}/${maxRetries}) in ${delay}ms`);
215
- api?.emit("error:media", { error: new Error(error.details) });
216
- if (retryTimeout) {
217
- clearTimeout(retryTimeout);
218
- }
219
- retryTimeout = setTimeout(() => {
220
- if (hls) {
221
- hls.recoverMediaError();
222
- }
223
- }, delay);
224
- break;
225
- }
226
- default:
227
- emitFatalError(error, false);
228
- break;
229
- }
230
- }
231
- };
232
- const loadNative = async (src) => {
233
- const videoEl = getOrCreateVideo();
234
- isNative = true;
235
- if (api) {
236
- cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
237
- }
238
- return new Promise((resolve, reject) => {
239
- const onLoaded = () => {
240
- videoEl.removeEventListener("loadedmetadata", onLoaded);
241
- videoEl.removeEventListener("error", onError);
242
- api?.setState("source", { src, type: "application/x-mpegURL" });
243
- api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
244
- resolve();
245
- };
246
- const onError = () => {
247
- videoEl.removeEventListener("loadedmetadata", onLoaded);
248
- videoEl.removeEventListener("error", onError);
249
- const error = videoEl.error;
250
- reject(new Error(error?.message || "Failed to load HLS source"));
251
- };
252
- videoEl.addEventListener("loadedmetadata", onLoaded);
253
- videoEl.addEventListener("error", onError);
254
- videoEl.src = src;
255
- videoEl.load();
256
- });
257
- };
258
- const loadWithHlsJs = async (src) => {
259
- await loadHlsJs();
260
- const videoEl = getOrCreateVideo();
261
- isNative = false;
262
- hls = createHlsInstance(buildHlsConfig());
263
- if (api) {
264
- cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
265
- }
266
- return new Promise((resolve, reject) => {
267
- if (!hls || !api) {
268
- reject(new Error("HLS not initialized"));
269
- return;
270
- }
271
- let resolved = false;
272
- cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
273
- onManifestParsed: () => {
274
- if (!resolved) {
275
- resolved = true;
276
- api?.setState("source", { src, type: "application/x-mpegURL" });
277
- api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
278
- resolve();
279
- }
280
- },
281
- onLevelSwitched: () => {
282
- },
283
- onError: (error) => {
284
- handleHlsError(error);
285
- if (error.fatal && !resolved && error.type !== "network" && error.type !== "media") {
286
- resolved = true;
287
- reject(new Error(error.details));
288
- }
289
- },
290
- getIsAutoQuality: () => isAutoQuality
291
- });
292
- hls.attachMedia(videoEl);
293
- hls.loadSource(src);
294
- });
295
- };
296
- const plugin = {
297
- id: "hls-provider",
298
- name: "HLS Provider (Light)",
299
- version: "1.0.0",
300
- type: "provider",
301
- description: "HLS playback provider using hls.js/light (smaller bundle)",
302
- canPlay(src) {
303
- if (!isHLSSupported()) return false;
304
- const url = src.toLowerCase();
305
- const urlWithoutQuery = url.split("?")[0].split("#")[0];
306
- if (urlWithoutQuery.endsWith(".m3u8")) return true;
307
- if (url.includes("application/x-mpegurl")) return true;
308
- if (url.includes("application/vnd.apple.mpegurl")) return true;
309
- return false;
310
- },
311
- async init(pluginApi) {
312
- api = pluginApi;
313
- api.logger.info("HLS plugin (light) initialized");
314
- const unsubPlay = api.on("playback:play", async () => {
315
- if (!video) return;
316
- try {
317
- await video.play();
318
- } catch (e) {
319
- api?.logger.error("Play failed", e);
320
- }
321
- });
322
- const unsubPause = api.on("playback:pause", () => {
323
- video?.pause();
324
- });
325
- const unsubSeek = api.on("playback:seeking", ({ time }) => {
326
- if (!video) return;
327
- const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
328
- video.currentTime = clampedTime;
329
- });
330
- const unsubVolume = api.on("volume:change", ({ volume }) => {
331
- if (video) video.volume = volume;
332
- });
333
- const unsubMute = api.on("volume:mute", ({ muted }) => {
334
- if (video) video.muted = muted;
335
- });
336
- const unsubRate = api.on("playback:ratechange", ({ rate }) => {
337
- if (video) video.playbackRate = rate;
338
- });
339
- const unsubQuality = api.on("quality:select", ({ quality, auto }) => {
340
- if (!hls || isNative) {
341
- api?.logger.warn("Quality selection not available");
342
- return;
343
- }
344
- if (auto || quality === "auto") {
345
- isAutoQuality = true;
346
- hls.currentLevel = -1;
347
- api?.logger.debug("Quality: auto selection enabled");
348
- api?.setState("currentQuality", {
349
- id: "auto",
350
- label: "Auto",
351
- width: 0,
352
- height: 0,
353
- bitrate: 0,
354
- active: true
355
- });
356
- } else {
357
- isAutoQuality = false;
358
- const levelIndex = parseInt(quality.replace("level-", ""), 10);
359
- if (!isNaN(levelIndex) && levelIndex >= 0 && levelIndex < hls.levels.length) {
360
- hls.nextLevel = levelIndex;
361
- api?.logger.debug(`Quality: queued switch to level ${levelIndex}`);
362
- const targetLevel = hls.levels[levelIndex];
363
- if (targetLevel) {
364
- const label = formatLevel(targetLevel);
365
- api?.setState("currentQuality", {
366
- id: `level-${levelIndex}`,
367
- label: `${label}...`,
368
- // Ellipsis indicates switching in progress
369
- width: targetLevel.width,
370
- height: targetLevel.height,
371
- bitrate: targetLevel.bitrate,
372
- active: false
373
- // Not yet active
374
- });
375
- }
376
- }
377
- }
378
- });
379
- api.onDestroy(() => {
380
- unsubPlay();
381
- unsubPause();
382
- unsubSeek();
383
- unsubVolume();
384
- unsubMute();
385
- unsubRate();
386
- unsubQuality();
387
- });
388
- },
389
- async destroy() {
390
- api?.logger.info("HLS plugin (light) destroying");
391
- cleanup();
392
- if (video?.parentNode) {
393
- video.parentNode.removeChild(video);
394
- }
395
- video = null;
396
- api = null;
397
- },
398
- async loadSource(src) {
399
- if (!api) throw new Error("Plugin not initialized");
400
- api.logger.info("Loading HLS source (light)", { src });
401
- cleanup();
402
- currentSrc = src;
403
- api.setState("playbackState", "loading");
404
- api.setState("buffering", true);
405
- if (isHlsJsSupported()) {
406
- api.logger.info("Using hls.js/light for HLS playback");
407
- await loadWithHlsJs(src);
408
- } else if (supportsNativeHLS()) {
409
- api.logger.info("Using native HLS playback (hls.js not supported)");
410
- await loadNative(src);
411
- } else {
412
- throw new Error("HLS playback not supported in this browser");
413
- }
414
- if (video) {
415
- const muted = api.getState("muted");
416
- const volume = api.getState("volume");
417
- if (muted !== void 0) video.muted = muted;
418
- if (volume !== void 0) video.volume = volume;
419
- }
420
- api.setState("playbackState", "ready");
421
- api.setState("buffering", false);
82
+ return createHLSPluginWith(
83
+ hls_loader_light_exports,
84
+ {
85
+ name: "HLS Provider (Light)",
86
+ description: "HLS playback provider using hls.js/light (smaller bundle)",
87
+ logSuffix: " (light)",
88
+ engineLabel: "hls.js/light"
422
89
  },
423
- getCurrentLevel() {
424
- if (isNative || !hls) return -1;
425
- return hls.currentLevel;
426
- },
427
- setLevel(index) {
428
- if (isNative || !hls) {
429
- api?.logger.warn("Quality selection not available in native HLS mode");
430
- return;
431
- }
432
- hls.currentLevel = index;
433
- },
434
- getLevels() {
435
- if (isNative || !hls) return [];
436
- return mapLevels(hls.levels, hls.currentLevel);
437
- },
438
- getHlsInstance() {
439
- return hls;
440
- },
441
- isNativeHLS() {
442
- return isNative;
443
- },
444
- getLiveInfo() {
445
- if (isNative || !hls) return null;
446
- const live = api?.getState("live") || false;
447
- if (!live) return null;
448
- return {
449
- isLive: true,
450
- latency: hls.latency || 0,
451
- targetLatency: hls.targetLatency || 3,
452
- drift: hls.drift || 0
453
- };
454
- },
455
- async switchToNative() {
456
- if (isNative) {
457
- api?.logger.debug("Already using native HLS");
458
- return;
459
- }
460
- if (!supportsNativeHLS()) {
461
- api?.logger.warn("Native HLS not supported in this browser");
462
- return;
463
- }
464
- if (!currentSrc) {
465
- api?.logger.warn("No source loaded");
466
- return;
467
- }
468
- api?.logger.info("Switching to native HLS for AirPlay");
469
- const wasPlaying = api?.getState("playing") || false;
470
- const currentTime = video?.currentTime || 0;
471
- const savedSrc = currentSrc;
472
- cleanup();
473
- await loadNative(savedSrc);
474
- if (video && currentTime > 0) {
475
- video.currentTime = currentTime;
476
- }
477
- if (wasPlaying && video) {
478
- try {
479
- await video.play();
480
- } catch (e) {
481
- api?.logger.debug("Could not auto-resume after switch");
482
- }
483
- }
484
- api?.logger.info("Switched to native HLS");
485
- },
486
- async switchToHlsJs() {
487
- if (!isNative) {
488
- api?.logger.debug("Already using hls.js");
489
- return;
490
- }
491
- if (!isHlsJsSupported()) {
492
- api?.logger.warn("hls.js not supported in this browser");
493
- return;
494
- }
495
- if (!currentSrc) {
496
- api?.logger.warn("No source loaded");
497
- return;
498
- }
499
- api?.logger.info("Switching back to hls.js");
500
- const wasPlaying = api?.getState("playing") || false;
501
- const currentTime = video?.currentTime || 0;
502
- const savedSrc = currentSrc;
503
- cleanup();
504
- await loadWithHlsJs(savedSrc);
505
- if (video && currentTime > 0) {
506
- video.currentTime = currentTime;
507
- }
508
- if (wasPlaying && video) {
509
- try {
510
- await video.play();
511
- } catch (e) {
512
- api?.logger.debug("Could not auto-resume after switch");
513
- }
514
- }
515
- api?.logger.info("Switched to hls.js");
516
- }
517
- };
518
- return plugin;
90
+ config
91
+ );
519
92
  }
520
93
  var light_default = createHLSPlugin;
521
94
  export {
@@ -34,6 +34,32 @@ interface HLSPluginConfig {
34
34
  retryDelayMs?: number;
35
35
  /** Exponential backoff multiplier (default: 2) */
36
36
  retryBackoffFactor?: number;
37
+ /**
38
+ * Watchdog for source loading in milliseconds (default: 30000, 0 disables).
39
+ * If the manifest has not parsed within this window the load fails with an
40
+ * error instead of leaving the viewer on a spinner forever.
41
+ */
42
+ loadTimeoutMs?: number;
43
+ /**
44
+ * Automatically attempt to reconnect after a fatal network/media error
45
+ * once the stream had been playing (default: true). Viewers should not
46
+ * have to press anything when a connection blip resolves itself.
47
+ */
48
+ autoReconnect?: boolean;
49
+ /** First auto-reconnect delay in milliseconds (default: 2000) */
50
+ reconnectBaseDelayMs?: number;
51
+ /** Cap for the auto-reconnect backoff in milliseconds (default: 30000) */
52
+ reconnectMaxDelayMs?: number;
53
+ /** Total window to keep auto-reconnecting in milliseconds (default: 300000 = 5 min) */
54
+ reconnectWindowMs?: number;
55
+ /**
56
+ * Validate every playlist response before it reaches the M3U8 parser
57
+ * (default: true). A live refresh that returns an error page, a
58
+ * master-only response, or an empty document becomes a normal network
59
+ * error (bounded retries, then reconnect, then the retry UI) instead of
60
+ * being indexed blindly.
61
+ */
62
+ validatePlaylists?: boolean;
37
63
  /** Index signature for PluginConfig compatibility */
38
64
  [key: string]: unknown;
39
65
  }
@@ -102,4 +128,4 @@ interface IHLSPlugin extends Plugin<HLSPluginConfig> {
102
128
  switchToHlsJs(): Promise<void>;
103
129
  }
104
130
 
105
- export type { HLSPluginConfig as H, IHLSPlugin as I, HLSError as a, HLSLiveInfo as b, HLSQualityLevel as c };
131
+ export type { HLSPluginConfig as H, IHLSPlugin as I, HLSQualityLevel as a, HLSError as b, HLSLiveInfo as c };
@@ -34,6 +34,32 @@ interface HLSPluginConfig {
34
34
  retryDelayMs?: number;
35
35
  /** Exponential backoff multiplier (default: 2) */
36
36
  retryBackoffFactor?: number;
37
+ /**
38
+ * Watchdog for source loading in milliseconds (default: 30000, 0 disables).
39
+ * If the manifest has not parsed within this window the load fails with an
40
+ * error instead of leaving the viewer on a spinner forever.
41
+ */
42
+ loadTimeoutMs?: number;
43
+ /**
44
+ * Automatically attempt to reconnect after a fatal network/media error
45
+ * once the stream had been playing (default: true). Viewers should not
46
+ * have to press anything when a connection blip resolves itself.
47
+ */
48
+ autoReconnect?: boolean;
49
+ /** First auto-reconnect delay in milliseconds (default: 2000) */
50
+ reconnectBaseDelayMs?: number;
51
+ /** Cap for the auto-reconnect backoff in milliseconds (default: 30000) */
52
+ reconnectMaxDelayMs?: number;
53
+ /** Total window to keep auto-reconnecting in milliseconds (default: 300000 = 5 min) */
54
+ reconnectWindowMs?: number;
55
+ /**
56
+ * Validate every playlist response before it reaches the M3U8 parser
57
+ * (default: true). A live refresh that returns an error page, a
58
+ * master-only response, or an empty document becomes a normal network
59
+ * error (bounded retries, then reconnect, then the retry UI) instead of
60
+ * being indexed blindly.
61
+ */
62
+ validatePlaylists?: boolean;
37
63
  /** Index signature for PluginConfig compatibility */
38
64
  [key: string]: unknown;
39
65
  }
@@ -102,4 +128,4 @@ interface IHLSPlugin extends Plugin<HLSPluginConfig> {
102
128
  switchToHlsJs(): Promise<void>;
103
129
  }
104
130
 
105
- export type { HLSPluginConfig as H, IHLSPlugin as I, HLSError as a, HLSLiveInfo as b, HLSQualityLevel as c };
131
+ export type { HLSPluginConfig as H, IHLSPlugin as I, HLSQualityLevel as a, HLSError as b, HLSLiveInfo as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/hls",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "HLS Provider Plugin for Scarlett Player",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,7 +22,7 @@
22
22
  "dist"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@scarlett-player/core": "^1.0.2",
25
+ "@scarlett-player/core": "^1.0.3",
26
26
  "hls.js": "^1.5.0"
27
27
  },
28
28
  "peerDependenciesMeta": {
@@ -37,7 +37,7 @@
37
37
  "vitest": "^1.6.0",
38
38
  "@vitest/coverage-v8": "^1.6.0",
39
39
  "jsdom": "^24.0.0",
40
- "@scarlett-player/core": "1.0.2"
40
+ "@scarlett-player/core": "1.2.0"
41
41
  },
42
42
  "keywords": [
43
43
  "video",