@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/index.d.cts CHANGED
@@ -1,17 +1,15 @@
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 for Scarlett Player
7
7
  *
8
8
  * Provides HLS playback using hls.js or native HLS (Safari).
9
- * Features:
10
- * - Lazy loading of hls.js
11
- * - Native HLS fallback for Safari
12
- * - Quality level management
13
- * - Error recovery
14
- * - Live stream support
9
+ * Thin wrapper around the shared factory in `create-hls-plugin.ts`; the
10
+ * light entry (`light.ts`) wraps the same factory with the hls.js/light
11
+ * loader, so both builds always carry identical error handling, retry,
12
+ * reconnect, and playlist-validation machinery.
15
13
  */
16
14
 
17
15
  /**
package/dist/index.d.ts CHANGED
@@ -1,17 +1,15 @@
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 for Scarlett Player
7
7
  *
8
8
  * Provides HLS playback using hls.js or native HLS (Safari).
9
- * Features:
10
- * - Lazy loading of hls.js
11
- * - Native HLS fallback for Safari
12
- * - Quality level management
13
- * - Error recovery
14
- * - Live stream support
9
+ * Thin wrapper around the shared factory in `create-hls-plugin.ts`; the
10
+ * light entry (`light.ts`) wraps the same factory with the hls.js/light
11
+ * loader, so both builds always carry identical error handling, retry,
12
+ * reconnect, and playlist-validation machinery.
15
13
  */
16
14
 
17
15
  /**
package/dist/index.js CHANGED
@@ -1,12 +1,20 @@
1
1
  import {
2
- formatLevel,
3
- getInitialBandwidthEstimate,
4
- mapLevels,
5
- setupHlsEventHandlers,
6
- setupVideoEventHandlers
7
- } from "./chunk-MLOYPPFI.js";
2
+ __export,
3
+ createHLSPluginWith
4
+ } from "./chunk-IFUZZQQE.js";
8
5
 
9
6
  // src/hls-loader.ts
7
+ var hls_loader_exports = {};
8
+ __export(hls_loader_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
+ });
10
18
  var hlsConstructor = null;
11
19
  var loadingPromise = null;
12
20
  function supportsNativeHLS() {
@@ -14,6 +22,13 @@ function supportsNativeHLS() {
14
22
  const video = document.createElement("video");
15
23
  return video.canPlayType("application/vnd.apple.mpegurl") !== "";
16
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
+ }
17
32
  function isHlsJsSupported() {
18
33
  if (hlsConstructor) {
19
34
  return hlsConstructor.isSupported();
@@ -57,490 +72,23 @@ function createHlsInstance(config) {
57
72
  function getHlsConstructor() {
58
73
  return hlsConstructor;
59
74
  }
75
+ function resetLoader() {
76
+ hlsConstructor = null;
77
+ loadingPromise = null;
78
+ }
60
79
 
61
80
  // src/index.ts
62
- var DEFAULT_CONFIG = {
63
- debug: false,
64
- autoStartLoad: true,
65
- startPosition: -1,
66
- lowLatencyMode: false,
67
- maxBufferLength: 30,
68
- maxMaxBufferLength: 600,
69
- backBufferLength: 30,
70
- enableWorker: true,
71
- capLevelToPlayerSize: true,
72
- // Error recovery settings
73
- maxNetworkRetries: 3,
74
- maxMediaRetries: 2,
75
- retryDelayMs: 1e3,
76
- retryBackoffFactor: 2
77
- };
78
81
  function createHLSPlugin(config) {
79
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
80
- let api = null;
81
- let hls = null;
82
- let video = null;
83
- let isNative = false;
84
- let currentSrc = null;
85
- let cleanupHlsEvents = null;
86
- let cleanupVideoEvents = null;
87
- let isAutoQuality = true;
88
- let networkRetryCount = 0;
89
- let mediaRetryCount = 0;
90
- let retryTimeout = null;
91
- let errorCount = 0;
92
- let errorWindowStart = 0;
93
- const MAX_ERRORS_IN_WINDOW = 10;
94
- const ERROR_WINDOW_MS = 5e3;
95
- const getOrCreateVideo = () => {
96
- if (video) return video;
97
- const existing = api?.container.querySelector("video");
98
- if (existing) {
99
- video = existing;
100
- return video;
101
- }
102
- video = document.createElement("video");
103
- video.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;display:block;object-fit:contain;background:#000";
104
- video.preload = "metadata";
105
- video.controls = false;
106
- video.playsInline = true;
107
- const poster = api?.getState("poster");
108
- if (poster) {
109
- video.poster = poster;
110
- }
111
- api?.container.appendChild(video);
112
- return video;
113
- };
114
- const cleanup = () => {
115
- cleanupHlsEvents?.();
116
- cleanupHlsEvents = null;
117
- cleanupVideoEvents?.();
118
- cleanupVideoEvents = null;
119
- if (retryTimeout) {
120
- clearTimeout(retryTimeout);
121
- retryTimeout = null;
122
- }
123
- if (hls) {
124
- hls.destroy();
125
- hls = null;
126
- }
127
- currentSrc = null;
128
- isNative = false;
129
- isAutoQuality = true;
130
- networkRetryCount = 0;
131
- mediaRetryCount = 0;
132
- errorCount = 0;
133
- errorWindowStart = 0;
134
- };
135
- const buildHlsConfig = () => ({
136
- debug: mergedConfig.debug,
137
- autoStartLoad: mergedConfig.autoStartLoad,
138
- startPosition: mergedConfig.startPosition,
139
- startLevel: -1,
140
- // Auto quality selection (ABR)
141
- abrEwmaDefaultEstimate: getInitialBandwidthEstimate(mergedConfig.initialBandwidthEstimate),
142
- lowLatencyMode: mergedConfig.lowLatencyMode,
143
- maxBufferLength: mergedConfig.maxBufferLength,
144
- maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
145
- backBufferLength: mergedConfig.backBufferLength,
146
- enableWorker: mergedConfig.enableWorker,
147
- capLevelToPlayerSize: mergedConfig.capLevelToPlayerSize,
148
- // Minimize hls.js internal retries - we handle retries ourselves
149
- fragLoadingMaxRetry: 1,
150
- manifestLoadingMaxRetry: 1,
151
- levelLoadingMaxRetry: 1,
152
- fragLoadingRetryDelay: 500,
153
- manifestLoadingRetryDelay: 500,
154
- levelLoadingRetryDelay: 500
155
- });
156
- const getRetryDelay = (retryCount) => {
157
- const baseDelay = mergedConfig.retryDelayMs ?? 1e3;
158
- const backoffFactor = mergedConfig.retryBackoffFactor ?? 2;
159
- const delay = baseDelay * Math.pow(backoffFactor, retryCount);
160
- const jitter = delay * (0.7 + Math.random() * 0.3);
161
- return jitter;
162
- };
163
- const emitFatalError = (error, retriesExhausted) => {
164
- const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
165
- api?.logger.error(message, { type: error.type, details: error.details });
166
- api?.setState("playbackState", "error");
167
- api?.setState("buffering", false);
168
- api?.emit("error", {
169
- code: "MEDIA_ERROR",
170
- message,
171
- fatal: true,
172
- timestamp: Date.now()
173
- });
174
- };
175
- const handleHlsError = (error) => {
176
- const Hls = getHlsConstructor();
177
- if (!Hls || !hls) return;
178
- const now = Date.now();
179
- if (now - errorWindowStart > ERROR_WINDOW_MS) {
180
- errorCount = 1;
181
- errorWindowStart = now;
182
- } else {
183
- errorCount++;
184
- }
185
- if (errorCount >= MAX_ERRORS_IN_WINDOW) {
186
- api?.logger.error(`Too many errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
187
- emitFatalError(error, true);
188
- cleanupHlsEvents?.();
189
- cleanupHlsEvents = null;
190
- hls.destroy();
191
- hls = null;
192
- return;
193
- }
194
- if (error.fatal) {
195
- api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
196
- switch (error.type) {
197
- case "network": {
198
- const maxRetries = mergedConfig.maxNetworkRetries ?? 3;
199
- if (networkRetryCount >= maxRetries) {
200
- api?.logger.error(`Network error recovery failed after ${networkRetryCount} attempts`);
201
- emitFatalError(error, true);
202
- return;
203
- }
204
- networkRetryCount++;
205
- const delay = getRetryDelay(networkRetryCount - 1);
206
- api?.logger.info(`Attempting network error recovery (attempt ${networkRetryCount}/${maxRetries}) in ${delay}ms`);
207
- api?.emit("error:network", { error: new Error(error.details) });
208
- if (retryTimeout) {
209
- clearTimeout(retryTimeout);
210
- }
211
- retryTimeout = setTimeout(() => {
212
- if (hls) {
213
- hls.startLoad();
214
- }
215
- }, delay);
216
- break;
217
- }
218
- case "media": {
219
- const maxRetries = mergedConfig.maxMediaRetries ?? 2;
220
- if (mediaRetryCount >= maxRetries) {
221
- api?.logger.error(`Media error recovery failed after ${mediaRetryCount} attempts`);
222
- emitFatalError(error, true);
223
- return;
224
- }
225
- mediaRetryCount++;
226
- const delay = getRetryDelay(mediaRetryCount - 1);
227
- api?.logger.info(`Attempting media error recovery (attempt ${mediaRetryCount}/${maxRetries}) in ${delay}ms`);
228
- api?.emit("error:media", { error: new Error(error.details) });
229
- if (retryTimeout) {
230
- clearTimeout(retryTimeout);
231
- }
232
- retryTimeout = setTimeout(() => {
233
- if (hls) {
234
- hls.recoverMediaError();
235
- }
236
- }, delay);
237
- break;
238
- }
239
- default:
240
- emitFatalError(error, false);
241
- break;
242
- }
243
- }
244
- };
245
- const loadNative = async (src) => {
246
- const videoEl = getOrCreateVideo();
247
- isNative = true;
248
- if (api) {
249
- cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
250
- }
251
- return new Promise((resolve, reject) => {
252
- const onLoaded = () => {
253
- videoEl.removeEventListener("loadedmetadata", onLoaded);
254
- videoEl.removeEventListener("error", onError);
255
- api?.setState("source", { src, type: "application/x-mpegURL" });
256
- api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
257
- resolve();
258
- };
259
- const onError = () => {
260
- videoEl.removeEventListener("loadedmetadata", onLoaded);
261
- videoEl.removeEventListener("error", onError);
262
- const error = videoEl.error;
263
- reject(new Error(error?.message || "Failed to load HLS source"));
264
- };
265
- videoEl.addEventListener("loadedmetadata", onLoaded);
266
- videoEl.addEventListener("error", onError);
267
- videoEl.src = src;
268
- videoEl.load();
269
- });
270
- };
271
- const loadWithHlsJs = async (src) => {
272
- await loadHlsJs();
273
- const videoEl = getOrCreateVideo();
274
- isNative = false;
275
- hls = createHlsInstance(buildHlsConfig());
276
- if (api) {
277
- cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
278
- }
279
- return new Promise((resolve, reject) => {
280
- if (!hls || !api) {
281
- reject(new Error("HLS not initialized"));
282
- return;
283
- }
284
- let resolved = false;
285
- cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
286
- onManifestParsed: () => {
287
- if (!resolved) {
288
- resolved = true;
289
- api?.setState("source", { src, type: "application/x-mpegURL" });
290
- api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
291
- resolve();
292
- }
293
- },
294
- onLevelSwitched: () => {
295
- },
296
- onError: (error) => {
297
- handleHlsError(error);
298
- if (error.fatal && !resolved && error.type !== "network" && error.type !== "media") {
299
- resolved = true;
300
- reject(new Error(error.details));
301
- }
302
- },
303
- getIsAutoQuality: () => isAutoQuality
304
- });
305
- hls.attachMedia(videoEl);
306
- hls.loadSource(src);
307
- });
308
- };
309
- const plugin = {
310
- id: "hls-provider",
311
- name: "HLS Provider",
312
- version: "1.0.0",
313
- type: "provider",
314
- description: "HLS playback provider using hls.js",
315
- canPlay(src) {
316
- if (!isHLSSupported()) return false;
317
- const url = src.toLowerCase();
318
- const urlWithoutQuery = url.split("?")[0].split("#")[0];
319
- if (urlWithoutQuery.endsWith(".m3u8")) return true;
320
- if (url.includes("application/x-mpegurl")) return true;
321
- if (url.includes("application/vnd.apple.mpegurl")) return true;
322
- return false;
323
- },
324
- async init(pluginApi) {
325
- api = pluginApi;
326
- api.logger.info("HLS plugin initialized");
327
- const unsubPlay = api.on("playback:play", async () => {
328
- if (!video) return;
329
- try {
330
- await video.play();
331
- } catch (e) {
332
- api?.logger.error("Play failed", e);
333
- }
334
- });
335
- const unsubPause = api.on("playback:pause", () => {
336
- video?.pause();
337
- });
338
- const unsubSeek = api.on("playback:seeking", ({ time }) => {
339
- if (!video) return;
340
- const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
341
- video.currentTime = clampedTime;
342
- });
343
- const unsubVolume = api.on("volume:change", ({ volume }) => {
344
- if (video) video.volume = volume;
345
- });
346
- const unsubMute = api.on("volume:mute", ({ muted }) => {
347
- if (video) video.muted = muted;
348
- });
349
- const unsubRate = api.on("playback:ratechange", ({ rate }) => {
350
- if (video) video.playbackRate = rate;
351
- });
352
- const unsubQuality = api.on("quality:select", ({ quality, auto }) => {
353
- if (!hls || isNative) {
354
- api?.logger.warn("Quality selection not available");
355
- return;
356
- }
357
- if (auto || quality === "auto") {
358
- isAutoQuality = true;
359
- hls.currentLevel = -1;
360
- api?.logger.debug("Quality: auto selection enabled");
361
- api?.setState("currentQuality", {
362
- id: "auto",
363
- label: "Auto",
364
- width: 0,
365
- height: 0,
366
- bitrate: 0,
367
- active: true
368
- });
369
- } else {
370
- isAutoQuality = false;
371
- const levelIndex = parseInt(quality.replace("level-", ""), 10);
372
- if (!isNaN(levelIndex) && levelIndex >= 0 && levelIndex < hls.levels.length) {
373
- hls.nextLevel = levelIndex;
374
- api?.logger.debug(`Quality: queued switch to level ${levelIndex}`);
375
- const targetLevel = hls.levels[levelIndex];
376
- if (targetLevel) {
377
- const label = formatLevel(targetLevel);
378
- api?.setState("currentQuality", {
379
- id: `level-${levelIndex}`,
380
- label: `${label}...`,
381
- // Ellipsis indicates switching in progress
382
- width: targetLevel.width,
383
- height: targetLevel.height,
384
- bitrate: targetLevel.bitrate,
385
- active: false
386
- // Not yet active
387
- });
388
- }
389
- }
390
- }
391
- });
392
- api.onDestroy(() => {
393
- unsubPlay();
394
- unsubPause();
395
- unsubSeek();
396
- unsubVolume();
397
- unsubMute();
398
- unsubRate();
399
- unsubQuality();
400
- });
401
- },
402
- async destroy() {
403
- api?.logger.info("HLS plugin destroying");
404
- cleanup();
405
- if (video?.parentNode) {
406
- video.parentNode.removeChild(video);
407
- }
408
- video = null;
409
- api = null;
410
- },
411
- async loadSource(src) {
412
- if (!api) throw new Error("Plugin not initialized");
413
- api.logger.info("Loading HLS source", { src });
414
- cleanup();
415
- currentSrc = src;
416
- api.setState("playbackState", "loading");
417
- api.setState("buffering", true);
418
- if (api.getState("airplayActive") && supportsNativeHLS()) {
419
- api.logger.info("Using native HLS (AirPlay active)");
420
- await loadNative(src);
421
- } else if (isHlsJsSupported()) {
422
- api.logger.info("Using hls.js for HLS playback");
423
- await loadWithHlsJs(src);
424
- } else if (supportsNativeHLS()) {
425
- api.logger.info("Using native HLS playback (hls.js not supported)");
426
- await loadNative(src);
427
- } else {
428
- throw new Error("HLS playback not supported in this browser");
429
- }
430
- if (video) {
431
- const muted = api.getState("muted");
432
- const volume = api.getState("volume");
433
- if (muted !== void 0) video.muted = muted;
434
- if (volume !== void 0) video.volume = volume;
435
- }
436
- api.setState("playbackState", "ready");
437
- api.setState("buffering", false);
82
+ return createHLSPluginWith(
83
+ hls_loader_exports,
84
+ {
85
+ name: "HLS Provider",
86
+ description: "HLS playback provider using hls.js",
87
+ logSuffix: "",
88
+ engineLabel: "hls.js"
438
89
  },
439
- getCurrentLevel() {
440
- if (isNative || !hls) return -1;
441
- return hls.currentLevel;
442
- },
443
- setLevel(index) {
444
- if (isNative || !hls) {
445
- api?.logger.warn("Quality selection not available in native HLS mode");
446
- return;
447
- }
448
- hls.currentLevel = index;
449
- },
450
- getLevels() {
451
- if (isNative || !hls) return [];
452
- return mapLevels(hls.levels, hls.currentLevel);
453
- },
454
- getHlsInstance() {
455
- return hls;
456
- },
457
- isNativeHLS() {
458
- return isNative;
459
- },
460
- getLiveInfo() {
461
- if (isNative || !hls) return null;
462
- const live = api?.getState("live") || false;
463
- if (!live) return null;
464
- return {
465
- isLive: true,
466
- latency: hls.latency || 0,
467
- targetLatency: hls.targetLatency || 3,
468
- drift: hls.drift || 0
469
- };
470
- },
471
- /**
472
- * Switch from hls.js to native HLS playback.
473
- * Used for AirPlay compatibility in Safari.
474
- * Preserves current playback position.
475
- */
476
- async switchToNative() {
477
- if (isNative) {
478
- api?.logger.debug("Already using native HLS");
479
- return;
480
- }
481
- if (!supportsNativeHLS()) {
482
- api?.logger.warn("Native HLS not supported in this browser");
483
- return;
484
- }
485
- if (!currentSrc) {
486
- api?.logger.warn("No source loaded");
487
- return;
488
- }
489
- api?.logger.info("Switching to native HLS for AirPlay");
490
- const wasPlaying = api?.getState("playing") || false;
491
- const currentTime = video?.currentTime || 0;
492
- const savedSrc = currentSrc;
493
- cleanup();
494
- await loadNative(savedSrc);
495
- if (video && currentTime > 0) {
496
- video.currentTime = currentTime;
497
- }
498
- if (wasPlaying && video) {
499
- try {
500
- await video.play();
501
- } catch (e) {
502
- api?.logger.debug("Could not auto-resume after switch");
503
- }
504
- }
505
- api?.logger.info("Switched to native HLS");
506
- },
507
- /**
508
- * Switch from native HLS back to hls.js.
509
- * Restores quality control after AirPlay session ends.
510
- */
511
- async switchToHlsJs() {
512
- if (!isNative) {
513
- api?.logger.debug("Already using hls.js");
514
- return;
515
- }
516
- if (!isHlsJsSupported()) {
517
- api?.logger.warn("hls.js not supported in this browser");
518
- return;
519
- }
520
- if (!currentSrc) {
521
- api?.logger.warn("No source loaded");
522
- return;
523
- }
524
- api?.logger.info("Switching back to hls.js");
525
- const wasPlaying = api?.getState("playing") || false;
526
- const currentTime = video?.currentTime || 0;
527
- const savedSrc = currentSrc;
528
- cleanup();
529
- await loadWithHlsJs(savedSrc);
530
- if (video && currentTime > 0) {
531
- video.currentTime = currentTime;
532
- }
533
- if (wasPlaying && video) {
534
- try {
535
- await video.play();
536
- } catch (e) {
537
- api?.logger.debug("Could not auto-resume after switch");
538
- }
539
- }
540
- api?.logger.info("Switched to hls.js");
541
- }
542
- };
543
- return plugin;
90
+ config
91
+ );
544
92
  }
545
93
  var index_default = createHLSPlugin;
546
94
  export {