@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/README.md +59 -11
- package/dist/chunk-IFUZZQQE.js +1141 -0
- package/dist/index.cjs +396 -43
- package/dist/index.d.cts +6 -8
- package/dist/index.d.ts +6 -8
- package/dist/index.js +34 -486
- package/dist/light.cjs +432 -43
- package/dist/light.d.cts +11 -4
- package/dist/light.d.ts +11 -4
- package/dist/light.js +34 -461
- package/dist/{types-CRKmNxEW.d.cts → types-D-HW4lmM.d.cts} +27 -1
- package/dist/{types-CRKmNxEW.d.ts → types-D-HW4lmM.d.ts} +27 -1
- package/package.json +3 -3
- package/dist/chunk-MLOYPPFI.js +0 -336
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,17 @@ __export(index_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
38
|
// src/hls-loader.ts
|
|
39
|
+
var hls_loader_exports = {};
|
|
40
|
+
__export(hls_loader_exports, {
|
|
41
|
+
createHlsInstance: () => createHlsInstance,
|
|
42
|
+
getHlsConstructor: () => getHlsConstructor,
|
|
43
|
+
isHLSSupported: () => isHLSSupported,
|
|
44
|
+
isHlsJsSupported: () => isHlsJsSupported,
|
|
45
|
+
loadHlsJs: () => loadHlsJs,
|
|
46
|
+
resetLoader: () => resetLoader,
|
|
47
|
+
shouldPreferNativeHLS: () => shouldPreferNativeHLS,
|
|
48
|
+
supportsNativeHLS: () => supportsNativeHLS
|
|
49
|
+
});
|
|
39
50
|
var hlsConstructor = null;
|
|
40
51
|
var loadingPromise = null;
|
|
41
52
|
function supportsNativeHLS() {
|
|
@@ -43,6 +54,13 @@ function supportsNativeHLS() {
|
|
|
43
54
|
const video = document.createElement("video");
|
|
44
55
|
return video.canPlayType("application/vnd.apple.mpegurl") !== "";
|
|
45
56
|
}
|
|
57
|
+
function shouldPreferNativeHLS() {
|
|
58
|
+
if (!supportsNativeHLS()) return false;
|
|
59
|
+
if (typeof navigator === "undefined") return false;
|
|
60
|
+
const ua = navigator.userAgent;
|
|
61
|
+
const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua) && !/CriOS/.test(ua);
|
|
62
|
+
return isSafari;
|
|
63
|
+
}
|
|
46
64
|
function isHlsJsSupported() {
|
|
47
65
|
if (hlsConstructor) {
|
|
48
66
|
return hlsConstructor.isSupported();
|
|
@@ -86,6 +104,13 @@ function createHlsInstance(config) {
|
|
|
86
104
|
function getHlsConstructor() {
|
|
87
105
|
return hlsConstructor;
|
|
88
106
|
}
|
|
107
|
+
function resetLoader() {
|
|
108
|
+
hlsConstructor = null;
|
|
109
|
+
loadingPromise = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/create-hls-plugin.ts
|
|
113
|
+
var import_core = require("@scarlett-player/core");
|
|
89
114
|
|
|
90
115
|
// src/quality.ts
|
|
91
116
|
function formatLevel(level) {
|
|
@@ -226,6 +251,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
226
251
|
lastBandwidthUpdate = now;
|
|
227
252
|
api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
|
|
228
253
|
}
|
|
254
|
+
callbacks.onFragLoaded?.();
|
|
229
255
|
});
|
|
230
256
|
addHandler("hlsFragBuffered", () => {
|
|
231
257
|
api.setState("buffering", false);
|
|
@@ -416,7 +442,51 @@ function setupVideoEventHandlers(video, api) {
|
|
|
416
442
|
};
|
|
417
443
|
}
|
|
418
444
|
|
|
419
|
-
// src/
|
|
445
|
+
// src/playlist-validation.ts
|
|
446
|
+
var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
|
|
447
|
+
var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
|
|
448
|
+
function isValidPlaylistDocument(data, contextType) {
|
|
449
|
+
if (typeof data !== "string" || data.length === 0) return false;
|
|
450
|
+
const text = data.trimStart();
|
|
451
|
+
if (!text.startsWith("#EXTM3U")) return false;
|
|
452
|
+
if (contextType && MEDIA_PLAYLIST_CONTEXTS.includes(contextType)) {
|
|
453
|
+
return /^#EXT(?:INF|-X-TARGETDURATION):/m.test(text);
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
function createValidatingPlaylistLoader(Hls) {
|
|
458
|
+
const BaseLoader = Hls.DefaultConfig.loader;
|
|
459
|
+
return class ValidatingPlaylistLoader extends BaseLoader {
|
|
460
|
+
/**
|
|
461
|
+
* Load a playlist, validating the response document before it reaches
|
|
462
|
+
* the M3U8 parser.
|
|
463
|
+
*
|
|
464
|
+
* @param context - hls.js loader context
|
|
465
|
+
* @param config - hls.js loader config
|
|
466
|
+
* @param callbacks - hls.js loader callbacks
|
|
467
|
+
*/
|
|
468
|
+
load(context, config, callbacks) {
|
|
469
|
+
const wrapped = {
|
|
470
|
+
...callbacks,
|
|
471
|
+
onSuccess: (response, stats, ctx, networkDetails) => {
|
|
472
|
+
if (!isValidPlaylistDocument(response?.data, ctx?.type)) {
|
|
473
|
+
callbacks.onError(
|
|
474
|
+
{ code: 0, text: PLAYLIST_INVALID_TEXT },
|
|
475
|
+
ctx,
|
|
476
|
+
networkDetails,
|
|
477
|
+
stats
|
|
478
|
+
);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
callbacks.onSuccess(response, stats, ctx, networkDetails);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
super.load(context, config, wrapped);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/create-hls-plugin.ts
|
|
420
490
|
var DEFAULT_CONFIG = {
|
|
421
491
|
debug: false,
|
|
422
492
|
autoStartLoad: true,
|
|
@@ -431,9 +501,23 @@ var DEFAULT_CONFIG = {
|
|
|
431
501
|
maxNetworkRetries: 3,
|
|
432
502
|
maxMediaRetries: 2,
|
|
433
503
|
retryDelayMs: 1e3,
|
|
434
|
-
retryBackoffFactor: 2
|
|
504
|
+
retryBackoffFactor: 2,
|
|
505
|
+
// Load watchdog: never leave the viewer on an endless spinner
|
|
506
|
+
loadTimeoutMs: 3e4,
|
|
507
|
+
// Self-healing: reconnect automatically after fatal errors mid-playback
|
|
508
|
+
autoReconnect: true,
|
|
509
|
+
reconnectBaseDelayMs: 2e3,
|
|
510
|
+
reconnectMaxDelayMs: 3e4,
|
|
511
|
+
reconnectWindowMs: 3e5,
|
|
512
|
+
// Never index a malformed live playlist refresh blindly
|
|
513
|
+
validatePlaylists: true
|
|
435
514
|
};
|
|
436
|
-
|
|
515
|
+
var MANIFEST_PHASE_ERRORS = [
|
|
516
|
+
"manifestLoadError",
|
|
517
|
+
"manifestLoadTimeOut",
|
|
518
|
+
"manifestParsingError"
|
|
519
|
+
];
|
|
520
|
+
function createHLSPluginWith(loader, variant, config) {
|
|
437
521
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
438
522
|
let api = null;
|
|
439
523
|
let hls = null;
|
|
@@ -443,6 +527,8 @@ function createHLSPlugin(config) {
|
|
|
443
527
|
let cleanupHlsEvents = null;
|
|
444
528
|
let cleanupVideoEvents = null;
|
|
445
529
|
let isAutoQuality = true;
|
|
530
|
+
let loadSession = 0;
|
|
531
|
+
let abortPendingLoad = null;
|
|
446
532
|
let networkRetryCount = 0;
|
|
447
533
|
let mediaRetryCount = 0;
|
|
448
534
|
let retryTimeout = null;
|
|
@@ -450,6 +536,12 @@ function createHLSPlugin(config) {
|
|
|
450
536
|
let errorWindowStart = 0;
|
|
451
537
|
const MAX_ERRORS_IN_WINDOW = 10;
|
|
452
538
|
const ERROR_WINDOW_MS = 5e3;
|
|
539
|
+
let hasPlayedContent = false;
|
|
540
|
+
let reconnectTimer = null;
|
|
541
|
+
let reconnectAttempts = 0;
|
|
542
|
+
let reconnectWindowStart = 0;
|
|
543
|
+
let reconnectResumePosition = 0;
|
|
544
|
+
let onlineListener = null;
|
|
453
545
|
const getOrCreateVideo = () => {
|
|
454
546
|
if (video) return video;
|
|
455
547
|
const existing = api?.container.querySelector("video");
|
|
@@ -469,7 +561,9 @@ function createHLSPlugin(config) {
|
|
|
469
561
|
api?.container.appendChild(video);
|
|
470
562
|
return video;
|
|
471
563
|
};
|
|
472
|
-
const
|
|
564
|
+
const teardownPipeline = (reason) => {
|
|
565
|
+
abortPendingLoad?.(reason ?? new Error("HLS load cancelled"));
|
|
566
|
+
abortPendingLoad = null;
|
|
473
567
|
cleanupHlsEvents?.();
|
|
474
568
|
cleanupHlsEvents = null;
|
|
475
569
|
cleanupVideoEvents?.();
|
|
@@ -482,6 +576,9 @@ function createHLSPlugin(config) {
|
|
|
482
576
|
hls.destroy();
|
|
483
577
|
hls = null;
|
|
484
578
|
}
|
|
579
|
+
};
|
|
580
|
+
const cleanup = (reason) => {
|
|
581
|
+
teardownPipeline(reason);
|
|
485
582
|
currentSrc = null;
|
|
486
583
|
isNative = false;
|
|
487
584
|
isAutoQuality = true;
|
|
@@ -490,7 +587,17 @@ function createHLSPlugin(config) {
|
|
|
490
587
|
errorCount = 0;
|
|
491
588
|
errorWindowStart = 0;
|
|
492
589
|
};
|
|
493
|
-
const buildHlsConfig = () =>
|
|
590
|
+
const buildHlsConfig = () => {
|
|
591
|
+
const config2 = buildBaseHlsConfig();
|
|
592
|
+
if (mergedConfig.validatePlaylists !== false) {
|
|
593
|
+
const Hls = loader.getHlsConstructor();
|
|
594
|
+
if (Hls && Hls.DefaultConfig?.loader) {
|
|
595
|
+
config2.pLoader = createValidatingPlaylistLoader(Hls);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return config2;
|
|
599
|
+
};
|
|
600
|
+
const buildBaseHlsConfig = () => ({
|
|
494
601
|
debug: mergedConfig.debug,
|
|
495
602
|
autoStartLoad: mergedConfig.autoStartLoad,
|
|
496
603
|
startPosition: mergedConfig.startPosition,
|
|
@@ -518,21 +625,47 @@ function createHLSPlugin(config) {
|
|
|
518
625
|
const jitter = delay * (0.7 + Math.random() * 0.3);
|
|
519
626
|
return jitter;
|
|
520
627
|
};
|
|
628
|
+
const APPEND_ERROR_DETAILS = [
|
|
629
|
+
"bufferAppendError",
|
|
630
|
+
"bufferAppendingError",
|
|
631
|
+
"bufferAddCodecError"
|
|
632
|
+
];
|
|
633
|
+
const mapFatalErrorCode = (error) => {
|
|
634
|
+
if (error.response?.text === PLAYLIST_INVALID_TEXT) {
|
|
635
|
+
return import_core.ErrorCode.PLAYLIST_INVALID;
|
|
636
|
+
}
|
|
637
|
+
if (error.details === "bufferFullError") {
|
|
638
|
+
return import_core.ErrorCode.MEDIA_BUFFER_FULL;
|
|
639
|
+
}
|
|
640
|
+
if (APPEND_ERROR_DETAILS.includes(error.details)) {
|
|
641
|
+
return import_core.ErrorCode.MEDIA_APPEND_ERROR;
|
|
642
|
+
}
|
|
643
|
+
switch (error.type) {
|
|
644
|
+
case "network":
|
|
645
|
+
return import_core.ErrorCode.MEDIA_NETWORK_ERROR;
|
|
646
|
+
case "media":
|
|
647
|
+
case "mux":
|
|
648
|
+
return import_core.ErrorCode.MEDIA_DECODE_ERROR;
|
|
649
|
+
default:
|
|
650
|
+
return import_core.ErrorCode.PLAYBACK_FAILED;
|
|
651
|
+
}
|
|
652
|
+
};
|
|
521
653
|
const emitFatalError = (error, retriesExhausted) => {
|
|
522
654
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
523
655
|
api?.logger.error(message, { type: error.type, details: error.details });
|
|
524
656
|
api?.setState("playbackState", "error");
|
|
525
657
|
api?.setState("buffering", false);
|
|
526
658
|
api?.emit("error", {
|
|
527
|
-
code:
|
|
659
|
+
code: mapFatalErrorCode(error),
|
|
528
660
|
message,
|
|
529
661
|
fatal: true,
|
|
530
662
|
timestamp: Date.now()
|
|
531
663
|
});
|
|
664
|
+
maybeScheduleReconnect(error);
|
|
532
665
|
};
|
|
533
666
|
const handleHlsError = (error) => {
|
|
534
|
-
const Hls = getHlsConstructor();
|
|
535
|
-
if (!Hls || !hls) return;
|
|
667
|
+
const Hls = loader.getHlsConstructor();
|
|
668
|
+
if (!Hls || !hls) return false;
|
|
536
669
|
const now = Date.now();
|
|
537
670
|
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
538
671
|
errorCount = 1;
|
|
@@ -543,11 +676,8 @@ function createHLSPlugin(config) {
|
|
|
543
676
|
if (errorCount >= MAX_ERRORS_IN_WINDOW) {
|
|
544
677
|
api?.logger.error(`Too many errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
|
|
545
678
|
emitFatalError(error, true);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
hls.destroy();
|
|
549
|
-
hls = null;
|
|
550
|
-
return;
|
|
679
|
+
teardownPipeline(new Error(error.details));
|
|
680
|
+
return true;
|
|
551
681
|
}
|
|
552
682
|
if (error.fatal) {
|
|
553
683
|
api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
|
|
@@ -557,7 +687,7 @@ function createHLSPlugin(config) {
|
|
|
557
687
|
if (networkRetryCount >= maxRetries) {
|
|
558
688
|
api?.logger.error(`Network error recovery failed after ${networkRetryCount} attempts`);
|
|
559
689
|
emitFatalError(error, true);
|
|
560
|
-
return;
|
|
690
|
+
return true;
|
|
561
691
|
}
|
|
562
692
|
networkRetryCount++;
|
|
563
693
|
const delay = getRetryDelay(networkRetryCount - 1);
|
|
@@ -566,8 +696,13 @@ function createHLSPlugin(config) {
|
|
|
566
696
|
if (retryTimeout) {
|
|
567
697
|
clearTimeout(retryTimeout);
|
|
568
698
|
}
|
|
699
|
+
const isManifestPhase = MANIFEST_PHASE_ERRORS.includes(error.details);
|
|
700
|
+
const retry_session = loadSession;
|
|
569
701
|
retryTimeout = setTimeout(() => {
|
|
570
|
-
if (hls)
|
|
702
|
+
if (retry_session !== loadSession || !hls) return;
|
|
703
|
+
if (isManifestPhase && currentSrc) {
|
|
704
|
+
hls.loadSource(currentSrc);
|
|
705
|
+
} else {
|
|
571
706
|
hls.startLoad();
|
|
572
707
|
}
|
|
573
708
|
}, delay);
|
|
@@ -578,7 +713,7 @@ function createHLSPlugin(config) {
|
|
|
578
713
|
if (mediaRetryCount >= maxRetries) {
|
|
579
714
|
api?.logger.error(`Media error recovery failed after ${mediaRetryCount} attempts`);
|
|
580
715
|
emitFatalError(error, true);
|
|
581
|
-
return;
|
|
716
|
+
return true;
|
|
582
717
|
}
|
|
583
718
|
mediaRetryCount++;
|
|
584
719
|
const delay = getRetryDelay(mediaRetryCount - 1);
|
|
@@ -587,39 +722,91 @@ function createHLSPlugin(config) {
|
|
|
587
722
|
if (retryTimeout) {
|
|
588
723
|
clearTimeout(retryTimeout);
|
|
589
724
|
}
|
|
725
|
+
const retry_session = loadSession;
|
|
590
726
|
retryTimeout = setTimeout(() => {
|
|
591
|
-
if (hls)
|
|
592
|
-
|
|
593
|
-
}
|
|
727
|
+
if (retry_session !== loadSession || !hls) return;
|
|
728
|
+
hls.recoverMediaError();
|
|
594
729
|
}, delay);
|
|
595
730
|
break;
|
|
596
731
|
}
|
|
597
732
|
default:
|
|
598
733
|
emitFatalError(error, false);
|
|
599
|
-
|
|
734
|
+
return true;
|
|
600
735
|
}
|
|
601
736
|
}
|
|
737
|
+
return false;
|
|
602
738
|
};
|
|
603
739
|
const loadNative = async (src) => {
|
|
740
|
+
const session = loadSession;
|
|
604
741
|
const videoEl = getOrCreateVideo();
|
|
605
742
|
isNative = true;
|
|
606
743
|
if (api) {
|
|
607
744
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
608
745
|
}
|
|
609
746
|
return new Promise((resolve, reject) => {
|
|
610
|
-
|
|
747
|
+
let watchdog = null;
|
|
748
|
+
let settled = false;
|
|
749
|
+
const settle = () => {
|
|
750
|
+
settled = true;
|
|
751
|
+
if (abortPendingLoad === abort) {
|
|
752
|
+
abortPendingLoad = null;
|
|
753
|
+
}
|
|
611
754
|
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
612
755
|
videoEl.removeEventListener("error", onError);
|
|
756
|
+
if (watchdog !== null) {
|
|
757
|
+
clearTimeout(watchdog);
|
|
758
|
+
watchdog = null;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
const abort = (reason) => {
|
|
762
|
+
if (settled) return;
|
|
763
|
+
settle();
|
|
764
|
+
reject(reason);
|
|
765
|
+
};
|
|
766
|
+
abortPendingLoad = abort;
|
|
767
|
+
const onLoaded = () => {
|
|
768
|
+
if (settled) return;
|
|
769
|
+
if (session !== loadSession) {
|
|
770
|
+
settle();
|
|
771
|
+
reject(new Error("HLS load cancelled"));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
settle();
|
|
775
|
+
hasPlayedContent = true;
|
|
776
|
+
const onFatalVideoError = () => {
|
|
777
|
+
const media_error = videoEl.error;
|
|
778
|
+
const hls_error = {
|
|
779
|
+
type: media_error?.code === MediaError.MEDIA_ERR_NETWORK ? "network" : "media",
|
|
780
|
+
details: media_error?.message || "Native HLS playback error",
|
|
781
|
+
fatal: true
|
|
782
|
+
};
|
|
783
|
+
emitFatalError(hls_error, false);
|
|
784
|
+
};
|
|
785
|
+
videoEl.addEventListener("error", onFatalVideoError);
|
|
786
|
+
const removeFatalListener = () => videoEl.removeEventListener("error", onFatalVideoError);
|
|
787
|
+
const previous_cleanup = cleanupVideoEvents;
|
|
788
|
+
cleanupVideoEvents = () => {
|
|
789
|
+
removeFatalListener();
|
|
790
|
+
previous_cleanup?.();
|
|
791
|
+
};
|
|
613
792
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
614
793
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
615
794
|
resolve();
|
|
616
795
|
};
|
|
617
796
|
const onError = () => {
|
|
618
|
-
|
|
619
|
-
|
|
797
|
+
if (settled) return;
|
|
798
|
+
settle();
|
|
620
799
|
const error = videoEl.error;
|
|
621
800
|
reject(new Error(error?.message || "Failed to load HLS source"));
|
|
622
801
|
};
|
|
802
|
+
const timeout_ms = mergedConfig.loadTimeoutMs ?? 3e4;
|
|
803
|
+
if (timeout_ms > 0) {
|
|
804
|
+
watchdog = setTimeout(() => {
|
|
805
|
+
if (settled || session !== loadSession) return;
|
|
806
|
+
settle();
|
|
807
|
+
reject(new Error("Video took too long to load (network timeout)"));
|
|
808
|
+
}, timeout_ms);
|
|
809
|
+
}
|
|
623
810
|
videoEl.addEventListener("loadedmetadata", onLoaded);
|
|
624
811
|
videoEl.addEventListener("error", onError);
|
|
625
812
|
videoEl.src = src;
|
|
@@ -627,10 +814,14 @@ function createHLSPlugin(config) {
|
|
|
627
814
|
});
|
|
628
815
|
};
|
|
629
816
|
const loadWithHlsJs = async (src) => {
|
|
630
|
-
|
|
817
|
+
const session = loadSession;
|
|
818
|
+
await loader.loadHlsJs();
|
|
819
|
+
if (session !== loadSession) {
|
|
820
|
+
throw new Error("HLS load cancelled");
|
|
821
|
+
}
|
|
631
822
|
const videoEl = getOrCreateVideo();
|
|
632
823
|
isNative = false;
|
|
633
|
-
hls = createHlsInstance(buildHlsConfig());
|
|
824
|
+
hls = loader.createHlsInstance(buildHlsConfig());
|
|
634
825
|
if (api) {
|
|
635
826
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
636
827
|
}
|
|
@@ -640,10 +831,33 @@ function createHLSPlugin(config) {
|
|
|
640
831
|
return;
|
|
641
832
|
}
|
|
642
833
|
let resolved = false;
|
|
834
|
+
let watchdog = null;
|
|
835
|
+
const clearWatchdog = () => {
|
|
836
|
+
if (watchdog !== null) {
|
|
837
|
+
clearTimeout(watchdog);
|
|
838
|
+
watchdog = null;
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
const abort = (reason) => {
|
|
842
|
+
if (resolved) return;
|
|
843
|
+
resolved = true;
|
|
844
|
+
clearWatchdog();
|
|
845
|
+
reject(reason);
|
|
846
|
+
};
|
|
847
|
+
abortPendingLoad = abort;
|
|
848
|
+
const releaseAbort = () => {
|
|
849
|
+
if (abortPendingLoad === abort) {
|
|
850
|
+
abortPendingLoad = null;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
643
853
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
644
854
|
onManifestParsed: () => {
|
|
855
|
+
if (session !== loadSession) return;
|
|
645
856
|
if (!resolved) {
|
|
646
857
|
resolved = true;
|
|
858
|
+
releaseAbort();
|
|
859
|
+
clearWatchdog();
|
|
860
|
+
hasPlayedContent = true;
|
|
647
861
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
648
862
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
649
863
|
resolve();
|
|
@@ -652,26 +866,126 @@ function createHLSPlugin(config) {
|
|
|
652
866
|
onLevelSwitched: () => {
|
|
653
867
|
},
|
|
654
868
|
onError: (error) => {
|
|
655
|
-
|
|
656
|
-
|
|
869
|
+
if (session !== loadSession) return;
|
|
870
|
+
const terminal = handleHlsError(error);
|
|
871
|
+
if (terminal && !resolved) {
|
|
657
872
|
resolved = true;
|
|
873
|
+
releaseAbort();
|
|
874
|
+
clearWatchdog();
|
|
658
875
|
reject(new Error(error.details));
|
|
659
876
|
}
|
|
660
877
|
},
|
|
878
|
+
onFragLoaded: () => {
|
|
879
|
+
if (session !== loadSession) return;
|
|
880
|
+
if (networkRetryCount > 0 || mediaRetryCount > 0) {
|
|
881
|
+
api?.logger.debug("Playback recovered, resetting retry budgets");
|
|
882
|
+
networkRetryCount = 0;
|
|
883
|
+
mediaRetryCount = 0;
|
|
884
|
+
}
|
|
885
|
+
},
|
|
661
886
|
getIsAutoQuality: () => isAutoQuality
|
|
662
887
|
});
|
|
888
|
+
const timeout_ms = mergedConfig.loadTimeoutMs ?? 3e4;
|
|
889
|
+
if (timeout_ms > 0) {
|
|
890
|
+
watchdog = setTimeout(() => {
|
|
891
|
+
if (resolved || session !== loadSession) return;
|
|
892
|
+
resolved = true;
|
|
893
|
+
releaseAbort();
|
|
894
|
+
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src });
|
|
895
|
+
teardownPipeline();
|
|
896
|
+
reject(new Error("Video took too long to load (network timeout)"));
|
|
897
|
+
}, timeout_ms);
|
|
898
|
+
}
|
|
663
899
|
hls.attachMedia(videoEl);
|
|
664
900
|
hls.loadSource(src);
|
|
665
901
|
});
|
|
666
902
|
};
|
|
903
|
+
const cancelReconnect = () => {
|
|
904
|
+
if (reconnectTimer) {
|
|
905
|
+
clearTimeout(reconnectTimer);
|
|
906
|
+
reconnectTimer = null;
|
|
907
|
+
}
|
|
908
|
+
reconnectAttempts = 0;
|
|
909
|
+
reconnectWindowStart = 0;
|
|
910
|
+
reconnectResumePosition = 0;
|
|
911
|
+
};
|
|
912
|
+
const scheduleReconnectAttempt = () => {
|
|
913
|
+
if (reconnectTimer) return;
|
|
914
|
+
const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
915
|
+
if (Date.now() - reconnectWindowStart > window_ms) {
|
|
916
|
+
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
920
|
+
const max_delay = mergedConfig.reconnectMaxDelayMs ?? 3e4;
|
|
921
|
+
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
922
|
+
const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
923
|
+
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
924
|
+
api?.emit("error:reconnecting", { attempt: reconnectAttempts + 1, delayMs: delay });
|
|
925
|
+
reconnectTimer = setTimeout(() => {
|
|
926
|
+
reconnectTimer = null;
|
|
927
|
+
void attemptReconnect();
|
|
928
|
+
}, delay);
|
|
929
|
+
};
|
|
930
|
+
const maybeScheduleReconnect = (error) => {
|
|
931
|
+
if (mergedConfig.autoReconnect === false) return;
|
|
932
|
+
if (!hasPlayedContent || !currentSrc) return;
|
|
933
|
+
if (error.type !== "network" && error.type !== "media") return;
|
|
934
|
+
if (reconnectWindowStart === 0) {
|
|
935
|
+
reconnectWindowStart = Date.now();
|
|
936
|
+
reconnectResumePosition = video?.currentTime ?? 0;
|
|
937
|
+
}
|
|
938
|
+
scheduleReconnectAttempt();
|
|
939
|
+
};
|
|
940
|
+
const attemptReconnect = async () => {
|
|
941
|
+
if (!api || !currentSrc) return;
|
|
942
|
+
const session = ++loadSession;
|
|
943
|
+
reconnectAttempts++;
|
|
944
|
+
const saved_src = currentSrc;
|
|
945
|
+
const was_live = api.getState("live");
|
|
946
|
+
const was_native = isNative;
|
|
947
|
+
const resume_position = reconnectResumePosition;
|
|
948
|
+
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: saved_src });
|
|
949
|
+
try {
|
|
950
|
+
teardownPipeline(new Error("HLS load cancelled: reconnecting"));
|
|
951
|
+
networkRetryCount = 0;
|
|
952
|
+
mediaRetryCount = 0;
|
|
953
|
+
errorCount = 0;
|
|
954
|
+
errorWindowStart = 0;
|
|
955
|
+
currentSrc = saved_src;
|
|
956
|
+
api.setState("playbackState", "loading");
|
|
957
|
+
if (was_native && loader.supportsNativeHLS()) {
|
|
958
|
+
await loadNative(saved_src);
|
|
959
|
+
} else {
|
|
960
|
+
await loadWithHlsJs(saved_src);
|
|
961
|
+
}
|
|
962
|
+
if (session !== loadSession) return;
|
|
963
|
+
if (!was_live && video && resume_position > 0) {
|
|
964
|
+
video.currentTime = resume_position;
|
|
965
|
+
}
|
|
966
|
+
api.setState("playbackState", "ready");
|
|
967
|
+
api.setState("buffering", false);
|
|
968
|
+
api.emit("error:recovered", void 0);
|
|
969
|
+
api.logger.info("Auto-reconnect succeeded");
|
|
970
|
+
cancelReconnect();
|
|
971
|
+
try {
|
|
972
|
+
await video?.play();
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
} catch {
|
|
976
|
+
if (session !== loadSession) return;
|
|
977
|
+
api?.logger.warn(`Auto-reconnect attempt ${reconnectAttempts} failed`);
|
|
978
|
+
scheduleReconnectAttempt();
|
|
979
|
+
}
|
|
980
|
+
};
|
|
667
981
|
const plugin = {
|
|
668
982
|
id: "hls-provider",
|
|
669
|
-
name:
|
|
983
|
+
name: variant.name,
|
|
670
984
|
version: "1.0.0",
|
|
671
985
|
type: "provider",
|
|
672
|
-
description:
|
|
986
|
+
description: variant.description,
|
|
673
987
|
canPlay(src) {
|
|
674
|
-
if (!isHLSSupported()) return false;
|
|
988
|
+
if (!loader.isHLSSupported()) return false;
|
|
675
989
|
const url = src.toLowerCase();
|
|
676
990
|
const urlWithoutQuery = url.split("?")[0].split("#")[0];
|
|
677
991
|
if (urlWithoutQuery.endsWith(".m3u8")) return true;
|
|
@@ -681,7 +995,7 @@ function createHLSPlugin(config) {
|
|
|
681
995
|
},
|
|
682
996
|
async init(pluginApi) {
|
|
683
997
|
api = pluginApi;
|
|
684
|
-
api.logger.info(
|
|
998
|
+
api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
|
|
685
999
|
const unsubPlay = api.on("playback:play", async () => {
|
|
686
1000
|
if (!video) return;
|
|
687
1001
|
try {
|
|
@@ -747,6 +1061,17 @@ function createHLSPlugin(config) {
|
|
|
747
1061
|
}
|
|
748
1062
|
}
|
|
749
1063
|
});
|
|
1064
|
+
if (typeof window !== "undefined") {
|
|
1065
|
+
onlineListener = () => {
|
|
1066
|
+
if (reconnectTimer) {
|
|
1067
|
+
api?.logger.info("Browser back online, reconnecting immediately");
|
|
1068
|
+
clearTimeout(reconnectTimer);
|
|
1069
|
+
reconnectTimer = null;
|
|
1070
|
+
void attemptReconnect();
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
window.addEventListener("online", onlineListener);
|
|
1074
|
+
}
|
|
750
1075
|
api.onDestroy(() => {
|
|
751
1076
|
unsubPlay();
|
|
752
1077
|
unsubPause();
|
|
@@ -758,8 +1083,14 @@ function createHLSPlugin(config) {
|
|
|
758
1083
|
});
|
|
759
1084
|
},
|
|
760
1085
|
async destroy() {
|
|
761
|
-
api?.logger.info(
|
|
762
|
-
|
|
1086
|
+
api?.logger.info(`HLS plugin${variant.logSuffix} destroying`);
|
|
1087
|
+
loadSession++;
|
|
1088
|
+
cancelReconnect();
|
|
1089
|
+
if (onlineListener && typeof window !== "undefined") {
|
|
1090
|
+
window.removeEventListener("online", onlineListener);
|
|
1091
|
+
onlineListener = null;
|
|
1092
|
+
}
|
|
1093
|
+
cleanup(new Error("HLS load cancelled: player destroyed"));
|
|
763
1094
|
if (video?.parentNode) {
|
|
764
1095
|
video.parentNode.removeChild(video);
|
|
765
1096
|
}
|
|
@@ -768,23 +1099,27 @@ function createHLSPlugin(config) {
|
|
|
768
1099
|
},
|
|
769
1100
|
async loadSource(src) {
|
|
770
1101
|
if (!api) throw new Error("Plugin not initialized");
|
|
771
|
-
api.logger.info(
|
|
772
|
-
|
|
1102
|
+
api.logger.info(`Loading HLS source${variant.logSuffix}`, { src });
|
|
1103
|
+
const session = ++loadSession;
|
|
1104
|
+
cancelReconnect();
|
|
1105
|
+
hasPlayedContent = false;
|
|
1106
|
+
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
773
1107
|
currentSrc = src;
|
|
774
1108
|
api.setState("playbackState", "loading");
|
|
775
1109
|
api.setState("buffering", true);
|
|
776
|
-
if (api.getState("airplayActive") && supportsNativeHLS()) {
|
|
1110
|
+
if (api.getState("airplayActive") && loader.supportsNativeHLS()) {
|
|
777
1111
|
api.logger.info("Using native HLS (AirPlay active)");
|
|
778
1112
|
await loadNative(src);
|
|
779
|
-
} else if (isHlsJsSupported()) {
|
|
780
|
-
api.logger.info(
|
|
1113
|
+
} else if (loader.isHlsJsSupported()) {
|
|
1114
|
+
api.logger.info(`Using ${variant.engineLabel} for HLS playback`);
|
|
781
1115
|
await loadWithHlsJs(src);
|
|
782
|
-
} else if (supportsNativeHLS()) {
|
|
1116
|
+
} else if (loader.supportsNativeHLS()) {
|
|
783
1117
|
api.logger.info("Using native HLS playback (hls.js not supported)");
|
|
784
1118
|
await loadNative(src);
|
|
785
1119
|
} else {
|
|
786
1120
|
throw new Error("HLS playback not supported in this browser");
|
|
787
1121
|
}
|
|
1122
|
+
if (session !== loadSession) return;
|
|
788
1123
|
if (video) {
|
|
789
1124
|
const muted = api.getState("muted");
|
|
790
1125
|
const volume = api.getState("volume");
|
|
@@ -836,7 +1171,7 @@ function createHLSPlugin(config) {
|
|
|
836
1171
|
api?.logger.debug("Already using native HLS");
|
|
837
1172
|
return;
|
|
838
1173
|
}
|
|
839
|
-
if (!supportsNativeHLS()) {
|
|
1174
|
+
if (!loader.supportsNativeHLS()) {
|
|
840
1175
|
api?.logger.warn("Native HLS not supported in this browser");
|
|
841
1176
|
return;
|
|
842
1177
|
}
|
|
@@ -848,8 +1183,10 @@ function createHLSPlugin(config) {
|
|
|
848
1183
|
const wasPlaying = api?.getState("playing") || false;
|
|
849
1184
|
const currentTime = video?.currentTime || 0;
|
|
850
1185
|
const savedSrc = currentSrc;
|
|
851
|
-
|
|
1186
|
+
const session = ++loadSession;
|
|
1187
|
+
cleanup(new Error("HLS load cancelled: switching to native HLS"));
|
|
852
1188
|
await loadNative(savedSrc);
|
|
1189
|
+
if (session !== loadSession) return;
|
|
853
1190
|
if (video && currentTime > 0) {
|
|
854
1191
|
video.currentTime = currentTime;
|
|
855
1192
|
}
|
|
@@ -871,7 +1208,7 @@ function createHLSPlugin(config) {
|
|
|
871
1208
|
api?.logger.debug("Already using hls.js");
|
|
872
1209
|
return;
|
|
873
1210
|
}
|
|
874
|
-
if (!isHlsJsSupported()) {
|
|
1211
|
+
if (!loader.isHlsJsSupported()) {
|
|
875
1212
|
api?.logger.warn("hls.js not supported in this browser");
|
|
876
1213
|
return;
|
|
877
1214
|
}
|
|
@@ -883,8 +1220,10 @@ function createHLSPlugin(config) {
|
|
|
883
1220
|
const wasPlaying = api?.getState("playing") || false;
|
|
884
1221
|
const currentTime = video?.currentTime || 0;
|
|
885
1222
|
const savedSrc = currentSrc;
|
|
886
|
-
|
|
1223
|
+
const session = ++loadSession;
|
|
1224
|
+
cleanup(new Error("HLS load cancelled: switching to hls.js"));
|
|
887
1225
|
await loadWithHlsJs(savedSrc);
|
|
1226
|
+
if (session !== loadSession) return;
|
|
888
1227
|
if (video && currentTime > 0) {
|
|
889
1228
|
video.currentTime = currentTime;
|
|
890
1229
|
}
|
|
@@ -900,6 +1239,20 @@ function createHLSPlugin(config) {
|
|
|
900
1239
|
};
|
|
901
1240
|
return plugin;
|
|
902
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
// src/index.ts
|
|
1244
|
+
function createHLSPlugin(config) {
|
|
1245
|
+
return createHLSPluginWith(
|
|
1246
|
+
hls_loader_exports,
|
|
1247
|
+
{
|
|
1248
|
+
name: "HLS Provider",
|
|
1249
|
+
description: "HLS playback provider using hls.js",
|
|
1250
|
+
logSuffix: "",
|
|
1251
|
+
engineLabel: "hls.js"
|
|
1252
|
+
},
|
|
1253
|
+
config
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
903
1256
|
var index_default = createHLSPlugin;
|
|
904
1257
|
// Annotate the CommonJS export names for ESM import in node:
|
|
905
1258
|
0 && (module.exports = {
|