@scarlett-player/hls 1.0.2 → 1.1.1
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/{chunk-MLOYPPFI.js → chunk-L73PRXS4.js} +1 -0
- package/dist/index.cjs +217 -13
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +219 -14
- package/dist/light.cjs +1 -0
- package/dist/light.d.cts +2 -2
- package/dist/light.d.ts +2 -2
- package/dist/light.js +1 -1
- package/dist/{types-CRKmNxEW.d.cts → types-DOzoPHQe.d.cts} +19 -1
- package/dist/{types-CRKmNxEW.d.ts → types-DOzoPHQe.d.ts} +19 -1
- package/package.json +3 -3
|
@@ -137,6 +137,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
137
137
|
lastBandwidthUpdate = now;
|
|
138
138
|
api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
|
|
139
139
|
}
|
|
140
|
+
callbacks.onFragLoaded?.();
|
|
140
141
|
});
|
|
141
142
|
addHandler("hlsFragBuffered", () => {
|
|
142
143
|
api.setState("buffering", false);
|
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
default: () => index_default
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_core = require("@scarlett-player/core");
|
|
37
38
|
|
|
38
39
|
// src/hls-loader.ts
|
|
39
40
|
var hlsConstructor = null;
|
|
@@ -226,6 +227,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
226
227
|
lastBandwidthUpdate = now;
|
|
227
228
|
api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
|
|
228
229
|
}
|
|
230
|
+
callbacks.onFragLoaded?.();
|
|
229
231
|
});
|
|
230
232
|
addHandler("hlsFragBuffered", () => {
|
|
231
233
|
api.setState("buffering", false);
|
|
@@ -431,8 +433,20 @@ var DEFAULT_CONFIG = {
|
|
|
431
433
|
maxNetworkRetries: 3,
|
|
432
434
|
maxMediaRetries: 2,
|
|
433
435
|
retryDelayMs: 1e3,
|
|
434
|
-
retryBackoffFactor: 2
|
|
436
|
+
retryBackoffFactor: 2,
|
|
437
|
+
// Load watchdog: never leave the viewer on an endless spinner
|
|
438
|
+
loadTimeoutMs: 3e4,
|
|
439
|
+
// Self-healing: reconnect automatically after fatal errors mid-playback
|
|
440
|
+
autoReconnect: true,
|
|
441
|
+
reconnectBaseDelayMs: 2e3,
|
|
442
|
+
reconnectMaxDelayMs: 3e4,
|
|
443
|
+
reconnectWindowMs: 3e5
|
|
435
444
|
};
|
|
445
|
+
var MANIFEST_PHASE_ERRORS = [
|
|
446
|
+
"manifestLoadError",
|
|
447
|
+
"manifestLoadTimeOut",
|
|
448
|
+
"manifestParsingError"
|
|
449
|
+
];
|
|
436
450
|
function createHLSPlugin(config) {
|
|
437
451
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
438
452
|
let api = null;
|
|
@@ -450,6 +464,12 @@ function createHLSPlugin(config) {
|
|
|
450
464
|
let errorWindowStart = 0;
|
|
451
465
|
const MAX_ERRORS_IN_WINDOW = 10;
|
|
452
466
|
const ERROR_WINDOW_MS = 5e3;
|
|
467
|
+
let hasPlayedContent = false;
|
|
468
|
+
let reconnectTimer = null;
|
|
469
|
+
let reconnectAttempts = 0;
|
|
470
|
+
let reconnectWindowStart = 0;
|
|
471
|
+
let reconnectResumePosition = 0;
|
|
472
|
+
let onlineListener = null;
|
|
453
473
|
const getOrCreateVideo = () => {
|
|
454
474
|
if (video) return video;
|
|
455
475
|
const existing = api?.container.querySelector("video");
|
|
@@ -518,21 +538,33 @@ function createHLSPlugin(config) {
|
|
|
518
538
|
const jitter = delay * (0.7 + Math.random() * 0.3);
|
|
519
539
|
return jitter;
|
|
520
540
|
};
|
|
541
|
+
const mapFatalErrorCode = (error) => {
|
|
542
|
+
switch (error.type) {
|
|
543
|
+
case "network":
|
|
544
|
+
return import_core.ErrorCode.MEDIA_NETWORK_ERROR;
|
|
545
|
+
case "media":
|
|
546
|
+
case "mux":
|
|
547
|
+
return import_core.ErrorCode.MEDIA_DECODE_ERROR;
|
|
548
|
+
default:
|
|
549
|
+
return import_core.ErrorCode.PLAYBACK_FAILED;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
521
552
|
const emitFatalError = (error, retriesExhausted) => {
|
|
522
553
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
523
554
|
api?.logger.error(message, { type: error.type, details: error.details });
|
|
524
555
|
api?.setState("playbackState", "error");
|
|
525
556
|
api?.setState("buffering", false);
|
|
526
557
|
api?.emit("error", {
|
|
527
|
-
code:
|
|
558
|
+
code: mapFatalErrorCode(error),
|
|
528
559
|
message,
|
|
529
560
|
fatal: true,
|
|
530
561
|
timestamp: Date.now()
|
|
531
562
|
});
|
|
563
|
+
maybeScheduleReconnect(error);
|
|
532
564
|
};
|
|
533
565
|
const handleHlsError = (error) => {
|
|
534
566
|
const Hls = getHlsConstructor();
|
|
535
|
-
if (!Hls || !hls) return;
|
|
567
|
+
if (!Hls || !hls) return false;
|
|
536
568
|
const now = Date.now();
|
|
537
569
|
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
538
570
|
errorCount = 1;
|
|
@@ -547,7 +579,7 @@ function createHLSPlugin(config) {
|
|
|
547
579
|
cleanupHlsEvents = null;
|
|
548
580
|
hls.destroy();
|
|
549
581
|
hls = null;
|
|
550
|
-
return;
|
|
582
|
+
return true;
|
|
551
583
|
}
|
|
552
584
|
if (error.fatal) {
|
|
553
585
|
api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
|
|
@@ -557,7 +589,7 @@ function createHLSPlugin(config) {
|
|
|
557
589
|
if (networkRetryCount >= maxRetries) {
|
|
558
590
|
api?.logger.error(`Network error recovery failed after ${networkRetryCount} attempts`);
|
|
559
591
|
emitFatalError(error, true);
|
|
560
|
-
return;
|
|
592
|
+
return true;
|
|
561
593
|
}
|
|
562
594
|
networkRetryCount++;
|
|
563
595
|
const delay = getRetryDelay(networkRetryCount - 1);
|
|
@@ -566,8 +598,12 @@ function createHLSPlugin(config) {
|
|
|
566
598
|
if (retryTimeout) {
|
|
567
599
|
clearTimeout(retryTimeout);
|
|
568
600
|
}
|
|
601
|
+
const isManifestPhase = MANIFEST_PHASE_ERRORS.includes(error.details);
|
|
569
602
|
retryTimeout = setTimeout(() => {
|
|
570
|
-
if (hls)
|
|
603
|
+
if (!hls) return;
|
|
604
|
+
if (isManifestPhase && currentSrc) {
|
|
605
|
+
hls.loadSource(currentSrc);
|
|
606
|
+
} else {
|
|
571
607
|
hls.startLoad();
|
|
572
608
|
}
|
|
573
609
|
}, delay);
|
|
@@ -578,7 +614,7 @@ function createHLSPlugin(config) {
|
|
|
578
614
|
if (mediaRetryCount >= maxRetries) {
|
|
579
615
|
api?.logger.error(`Media error recovery failed after ${mediaRetryCount} attempts`);
|
|
580
616
|
emitFatalError(error, true);
|
|
581
|
-
return;
|
|
617
|
+
return true;
|
|
582
618
|
}
|
|
583
619
|
mediaRetryCount++;
|
|
584
620
|
const delay = getRetryDelay(mediaRetryCount - 1);
|
|
@@ -596,9 +632,10 @@ function createHLSPlugin(config) {
|
|
|
596
632
|
}
|
|
597
633
|
default:
|
|
598
634
|
emitFatalError(error, false);
|
|
599
|
-
|
|
635
|
+
return true;
|
|
600
636
|
}
|
|
601
637
|
}
|
|
638
|
+
return false;
|
|
602
639
|
};
|
|
603
640
|
const loadNative = async (src) => {
|
|
604
641
|
const videoEl = getOrCreateVideo();
|
|
@@ -607,19 +644,50 @@ function createHLSPlugin(config) {
|
|
|
607
644
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
608
645
|
}
|
|
609
646
|
return new Promise((resolve, reject) => {
|
|
610
|
-
|
|
647
|
+
let watchdog = null;
|
|
648
|
+
const settle = () => {
|
|
611
649
|
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
612
650
|
videoEl.removeEventListener("error", onError);
|
|
651
|
+
if (watchdog !== null) {
|
|
652
|
+
clearTimeout(watchdog);
|
|
653
|
+
watchdog = null;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
const onLoaded = () => {
|
|
657
|
+
settle();
|
|
658
|
+
hasPlayedContent = true;
|
|
659
|
+
const onFatalVideoError = () => {
|
|
660
|
+
const media_error = videoEl.error;
|
|
661
|
+
const hls_error = {
|
|
662
|
+
type: media_error?.code === MediaError.MEDIA_ERR_NETWORK ? "network" : "media",
|
|
663
|
+
details: media_error?.message || "Native HLS playback error",
|
|
664
|
+
fatal: true
|
|
665
|
+
};
|
|
666
|
+
emitFatalError(hls_error, false);
|
|
667
|
+
};
|
|
668
|
+
videoEl.addEventListener("error", onFatalVideoError);
|
|
669
|
+
const removeFatalListener = () => videoEl.removeEventListener("error", onFatalVideoError);
|
|
670
|
+
const previous_cleanup = cleanupVideoEvents;
|
|
671
|
+
cleanupVideoEvents = () => {
|
|
672
|
+
removeFatalListener();
|
|
673
|
+
previous_cleanup?.();
|
|
674
|
+
};
|
|
613
675
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
614
676
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
615
677
|
resolve();
|
|
616
678
|
};
|
|
617
679
|
const onError = () => {
|
|
618
|
-
|
|
619
|
-
videoEl.removeEventListener("error", onError);
|
|
680
|
+
settle();
|
|
620
681
|
const error = videoEl.error;
|
|
621
682
|
reject(new Error(error?.message || "Failed to load HLS source"));
|
|
622
683
|
};
|
|
684
|
+
const timeout_ms = mergedConfig.loadTimeoutMs ?? 3e4;
|
|
685
|
+
if (timeout_ms > 0) {
|
|
686
|
+
watchdog = setTimeout(() => {
|
|
687
|
+
settle();
|
|
688
|
+
reject(new Error("Video took too long to load (network timeout)"));
|
|
689
|
+
}, timeout_ms);
|
|
690
|
+
}
|
|
623
691
|
videoEl.addEventListener("loadedmetadata", onLoaded);
|
|
624
692
|
videoEl.addEventListener("error", onError);
|
|
625
693
|
videoEl.src = src;
|
|
@@ -640,10 +708,19 @@ function createHLSPlugin(config) {
|
|
|
640
708
|
return;
|
|
641
709
|
}
|
|
642
710
|
let resolved = false;
|
|
711
|
+
let watchdog = null;
|
|
712
|
+
const clearWatchdog = () => {
|
|
713
|
+
if (watchdog !== null) {
|
|
714
|
+
clearTimeout(watchdog);
|
|
715
|
+
watchdog = null;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
643
718
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
644
719
|
onManifestParsed: () => {
|
|
645
720
|
if (!resolved) {
|
|
646
721
|
resolved = true;
|
|
722
|
+
clearWatchdog();
|
|
723
|
+
hasPlayedContent = true;
|
|
647
724
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
648
725
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
649
726
|
resolve();
|
|
@@ -652,18 +729,127 @@ function createHLSPlugin(config) {
|
|
|
652
729
|
onLevelSwitched: () => {
|
|
653
730
|
},
|
|
654
731
|
onError: (error) => {
|
|
655
|
-
handleHlsError(error);
|
|
656
|
-
if (
|
|
732
|
+
const terminal = handleHlsError(error);
|
|
733
|
+
if (terminal && !resolved) {
|
|
657
734
|
resolved = true;
|
|
735
|
+
clearWatchdog();
|
|
658
736
|
reject(new Error(error.details));
|
|
659
737
|
}
|
|
660
738
|
},
|
|
739
|
+
onFragLoaded: () => {
|
|
740
|
+
if (networkRetryCount > 0 || mediaRetryCount > 0) {
|
|
741
|
+
api?.logger.debug("Playback recovered, resetting retry budgets");
|
|
742
|
+
networkRetryCount = 0;
|
|
743
|
+
mediaRetryCount = 0;
|
|
744
|
+
}
|
|
745
|
+
},
|
|
661
746
|
getIsAutoQuality: () => isAutoQuality
|
|
662
747
|
});
|
|
748
|
+
const timeout_ms = mergedConfig.loadTimeoutMs ?? 3e4;
|
|
749
|
+
if (timeout_ms > 0) {
|
|
750
|
+
watchdog = setTimeout(() => {
|
|
751
|
+
if (resolved) return;
|
|
752
|
+
resolved = true;
|
|
753
|
+
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src });
|
|
754
|
+
if (retryTimeout) {
|
|
755
|
+
clearTimeout(retryTimeout);
|
|
756
|
+
retryTimeout = null;
|
|
757
|
+
}
|
|
758
|
+
cleanupHlsEvents?.();
|
|
759
|
+
cleanupHlsEvents = null;
|
|
760
|
+
hls?.destroy();
|
|
761
|
+
hls = null;
|
|
762
|
+
reject(new Error("Video took too long to load (network timeout)"));
|
|
763
|
+
}, timeout_ms);
|
|
764
|
+
}
|
|
663
765
|
hls.attachMedia(videoEl);
|
|
664
766
|
hls.loadSource(src);
|
|
665
767
|
});
|
|
666
768
|
};
|
|
769
|
+
const cancelReconnect = () => {
|
|
770
|
+
if (reconnectTimer) {
|
|
771
|
+
clearTimeout(reconnectTimer);
|
|
772
|
+
reconnectTimer = null;
|
|
773
|
+
}
|
|
774
|
+
reconnectAttempts = 0;
|
|
775
|
+
reconnectWindowStart = 0;
|
|
776
|
+
reconnectResumePosition = 0;
|
|
777
|
+
};
|
|
778
|
+
const scheduleReconnectAttempt = () => {
|
|
779
|
+
if (reconnectTimer) return;
|
|
780
|
+
const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
781
|
+
if (Date.now() - reconnectWindowStart > window_ms) {
|
|
782
|
+
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
786
|
+
const max_delay = mergedConfig.reconnectMaxDelayMs ?? 3e4;
|
|
787
|
+
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
788
|
+
const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
789
|
+
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
790
|
+
api?.emit("error:reconnecting", { attempt: reconnectAttempts + 1, delayMs: delay });
|
|
791
|
+
reconnectTimer = setTimeout(() => {
|
|
792
|
+
reconnectTimer = null;
|
|
793
|
+
void attemptReconnect();
|
|
794
|
+
}, delay);
|
|
795
|
+
};
|
|
796
|
+
const maybeScheduleReconnect = (error) => {
|
|
797
|
+
if (mergedConfig.autoReconnect === false) return;
|
|
798
|
+
if (!hasPlayedContent || !currentSrc) return;
|
|
799
|
+
if (error.type !== "network" && error.type !== "media") return;
|
|
800
|
+
if (reconnectWindowStart === 0) {
|
|
801
|
+
reconnectWindowStart = Date.now();
|
|
802
|
+
reconnectResumePosition = video?.currentTime ?? 0;
|
|
803
|
+
}
|
|
804
|
+
scheduleReconnectAttempt();
|
|
805
|
+
};
|
|
806
|
+
const attemptReconnect = async () => {
|
|
807
|
+
if (!api || !currentSrc) return;
|
|
808
|
+
reconnectAttempts++;
|
|
809
|
+
const saved_src = currentSrc;
|
|
810
|
+
const was_live = api.getState("live");
|
|
811
|
+
const was_native = isNative;
|
|
812
|
+
const resume_position = reconnectResumePosition;
|
|
813
|
+
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: saved_src });
|
|
814
|
+
try {
|
|
815
|
+
cleanupHlsEvents?.();
|
|
816
|
+
cleanupHlsEvents = null;
|
|
817
|
+
cleanupVideoEvents?.();
|
|
818
|
+
cleanupVideoEvents = null;
|
|
819
|
+
if (retryTimeout) {
|
|
820
|
+
clearTimeout(retryTimeout);
|
|
821
|
+
retryTimeout = null;
|
|
822
|
+
}
|
|
823
|
+
hls?.destroy();
|
|
824
|
+
hls = null;
|
|
825
|
+
networkRetryCount = 0;
|
|
826
|
+
mediaRetryCount = 0;
|
|
827
|
+
errorCount = 0;
|
|
828
|
+
errorWindowStart = 0;
|
|
829
|
+
currentSrc = saved_src;
|
|
830
|
+
api.setState("playbackState", "loading");
|
|
831
|
+
if (was_native && supportsNativeHLS()) {
|
|
832
|
+
await loadNative(saved_src);
|
|
833
|
+
} else {
|
|
834
|
+
await loadWithHlsJs(saved_src);
|
|
835
|
+
}
|
|
836
|
+
if (!was_live && video && resume_position > 0) {
|
|
837
|
+
video.currentTime = resume_position;
|
|
838
|
+
}
|
|
839
|
+
api.setState("playbackState", "ready");
|
|
840
|
+
api.setState("buffering", false);
|
|
841
|
+
api.emit("error:recovered", void 0);
|
|
842
|
+
api.logger.info("Auto-reconnect succeeded");
|
|
843
|
+
cancelReconnect();
|
|
844
|
+
try {
|
|
845
|
+
await video?.play();
|
|
846
|
+
} catch {
|
|
847
|
+
}
|
|
848
|
+
} catch {
|
|
849
|
+
api?.logger.warn(`Auto-reconnect attempt ${reconnectAttempts} failed`);
|
|
850
|
+
scheduleReconnectAttempt();
|
|
851
|
+
}
|
|
852
|
+
};
|
|
667
853
|
const plugin = {
|
|
668
854
|
id: "hls-provider",
|
|
669
855
|
name: "HLS Provider",
|
|
@@ -747,6 +933,17 @@ function createHLSPlugin(config) {
|
|
|
747
933
|
}
|
|
748
934
|
}
|
|
749
935
|
});
|
|
936
|
+
if (typeof window !== "undefined") {
|
|
937
|
+
onlineListener = () => {
|
|
938
|
+
if (reconnectTimer) {
|
|
939
|
+
api?.logger.info("Browser back online, reconnecting immediately");
|
|
940
|
+
clearTimeout(reconnectTimer);
|
|
941
|
+
reconnectTimer = null;
|
|
942
|
+
void attemptReconnect();
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
window.addEventListener("online", onlineListener);
|
|
946
|
+
}
|
|
750
947
|
api.onDestroy(() => {
|
|
751
948
|
unsubPlay();
|
|
752
949
|
unsubPause();
|
|
@@ -759,6 +956,11 @@ function createHLSPlugin(config) {
|
|
|
759
956
|
},
|
|
760
957
|
async destroy() {
|
|
761
958
|
api?.logger.info("HLS plugin destroying");
|
|
959
|
+
cancelReconnect();
|
|
960
|
+
if (onlineListener && typeof window !== "undefined") {
|
|
961
|
+
window.removeEventListener("online", onlineListener);
|
|
962
|
+
onlineListener = null;
|
|
963
|
+
}
|
|
762
964
|
cleanup();
|
|
763
965
|
if (video?.parentNode) {
|
|
764
966
|
video.parentNode.removeChild(video);
|
|
@@ -769,6 +971,8 @@ function createHLSPlugin(config) {
|
|
|
769
971
|
async loadSource(src) {
|
|
770
972
|
if (!api) throw new Error("Plugin not initialized");
|
|
771
973
|
api.logger.info("Loading HLS source", { src });
|
|
974
|
+
cancelReconnect();
|
|
975
|
+
hasPlayedContent = false;
|
|
772
976
|
cleanup();
|
|
773
977
|
currentSrc = src;
|
|
774
978
|
api.setState("playbackState", "loading");
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-
|
|
2
|
-
export {
|
|
1
|
+
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-DOzoPHQe.cjs';
|
|
2
|
+
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-DOzoPHQe.cjs';
|
|
3
3
|
import '@scarlett-player/core';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-
|
|
2
|
-
export {
|
|
1
|
+
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-DOzoPHQe.js';
|
|
2
|
+
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-DOzoPHQe.js';
|
|
3
3
|
import '@scarlett-player/core';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
mapLevels,
|
|
5
5
|
setupHlsEventHandlers,
|
|
6
6
|
setupVideoEventHandlers
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-L73PRXS4.js";
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
import { ErrorCode } from "@scarlett-player/core";
|
|
8
11
|
|
|
9
12
|
// src/hls-loader.ts
|
|
10
13
|
var hlsConstructor = null;
|
|
@@ -73,8 +76,20 @@ var DEFAULT_CONFIG = {
|
|
|
73
76
|
maxNetworkRetries: 3,
|
|
74
77
|
maxMediaRetries: 2,
|
|
75
78
|
retryDelayMs: 1e3,
|
|
76
|
-
retryBackoffFactor: 2
|
|
79
|
+
retryBackoffFactor: 2,
|
|
80
|
+
// Load watchdog: never leave the viewer on an endless spinner
|
|
81
|
+
loadTimeoutMs: 3e4,
|
|
82
|
+
// Self-healing: reconnect automatically after fatal errors mid-playback
|
|
83
|
+
autoReconnect: true,
|
|
84
|
+
reconnectBaseDelayMs: 2e3,
|
|
85
|
+
reconnectMaxDelayMs: 3e4,
|
|
86
|
+
reconnectWindowMs: 3e5
|
|
77
87
|
};
|
|
88
|
+
var MANIFEST_PHASE_ERRORS = [
|
|
89
|
+
"manifestLoadError",
|
|
90
|
+
"manifestLoadTimeOut",
|
|
91
|
+
"manifestParsingError"
|
|
92
|
+
];
|
|
78
93
|
function createHLSPlugin(config) {
|
|
79
94
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
80
95
|
let api = null;
|
|
@@ -92,6 +107,12 @@ function createHLSPlugin(config) {
|
|
|
92
107
|
let errorWindowStart = 0;
|
|
93
108
|
const MAX_ERRORS_IN_WINDOW = 10;
|
|
94
109
|
const ERROR_WINDOW_MS = 5e3;
|
|
110
|
+
let hasPlayedContent = false;
|
|
111
|
+
let reconnectTimer = null;
|
|
112
|
+
let reconnectAttempts = 0;
|
|
113
|
+
let reconnectWindowStart = 0;
|
|
114
|
+
let reconnectResumePosition = 0;
|
|
115
|
+
let onlineListener = null;
|
|
95
116
|
const getOrCreateVideo = () => {
|
|
96
117
|
if (video) return video;
|
|
97
118
|
const existing = api?.container.querySelector("video");
|
|
@@ -160,21 +181,33 @@ function createHLSPlugin(config) {
|
|
|
160
181
|
const jitter = delay * (0.7 + Math.random() * 0.3);
|
|
161
182
|
return jitter;
|
|
162
183
|
};
|
|
184
|
+
const mapFatalErrorCode = (error) => {
|
|
185
|
+
switch (error.type) {
|
|
186
|
+
case "network":
|
|
187
|
+
return ErrorCode.MEDIA_NETWORK_ERROR;
|
|
188
|
+
case "media":
|
|
189
|
+
case "mux":
|
|
190
|
+
return ErrorCode.MEDIA_DECODE_ERROR;
|
|
191
|
+
default:
|
|
192
|
+
return ErrorCode.PLAYBACK_FAILED;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
163
195
|
const emitFatalError = (error, retriesExhausted) => {
|
|
164
196
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
165
197
|
api?.logger.error(message, { type: error.type, details: error.details });
|
|
166
198
|
api?.setState("playbackState", "error");
|
|
167
199
|
api?.setState("buffering", false);
|
|
168
200
|
api?.emit("error", {
|
|
169
|
-
code:
|
|
201
|
+
code: mapFatalErrorCode(error),
|
|
170
202
|
message,
|
|
171
203
|
fatal: true,
|
|
172
204
|
timestamp: Date.now()
|
|
173
205
|
});
|
|
206
|
+
maybeScheduleReconnect(error);
|
|
174
207
|
};
|
|
175
208
|
const handleHlsError = (error) => {
|
|
176
209
|
const Hls = getHlsConstructor();
|
|
177
|
-
if (!Hls || !hls) return;
|
|
210
|
+
if (!Hls || !hls) return false;
|
|
178
211
|
const now = Date.now();
|
|
179
212
|
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
180
213
|
errorCount = 1;
|
|
@@ -189,7 +222,7 @@ function createHLSPlugin(config) {
|
|
|
189
222
|
cleanupHlsEvents = null;
|
|
190
223
|
hls.destroy();
|
|
191
224
|
hls = null;
|
|
192
|
-
return;
|
|
225
|
+
return true;
|
|
193
226
|
}
|
|
194
227
|
if (error.fatal) {
|
|
195
228
|
api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
|
|
@@ -199,7 +232,7 @@ function createHLSPlugin(config) {
|
|
|
199
232
|
if (networkRetryCount >= maxRetries) {
|
|
200
233
|
api?.logger.error(`Network error recovery failed after ${networkRetryCount} attempts`);
|
|
201
234
|
emitFatalError(error, true);
|
|
202
|
-
return;
|
|
235
|
+
return true;
|
|
203
236
|
}
|
|
204
237
|
networkRetryCount++;
|
|
205
238
|
const delay = getRetryDelay(networkRetryCount - 1);
|
|
@@ -208,8 +241,12 @@ function createHLSPlugin(config) {
|
|
|
208
241
|
if (retryTimeout) {
|
|
209
242
|
clearTimeout(retryTimeout);
|
|
210
243
|
}
|
|
244
|
+
const isManifestPhase = MANIFEST_PHASE_ERRORS.includes(error.details);
|
|
211
245
|
retryTimeout = setTimeout(() => {
|
|
212
|
-
if (hls)
|
|
246
|
+
if (!hls) return;
|
|
247
|
+
if (isManifestPhase && currentSrc) {
|
|
248
|
+
hls.loadSource(currentSrc);
|
|
249
|
+
} else {
|
|
213
250
|
hls.startLoad();
|
|
214
251
|
}
|
|
215
252
|
}, delay);
|
|
@@ -220,7 +257,7 @@ function createHLSPlugin(config) {
|
|
|
220
257
|
if (mediaRetryCount >= maxRetries) {
|
|
221
258
|
api?.logger.error(`Media error recovery failed after ${mediaRetryCount} attempts`);
|
|
222
259
|
emitFatalError(error, true);
|
|
223
|
-
return;
|
|
260
|
+
return true;
|
|
224
261
|
}
|
|
225
262
|
mediaRetryCount++;
|
|
226
263
|
const delay = getRetryDelay(mediaRetryCount - 1);
|
|
@@ -238,9 +275,10 @@ function createHLSPlugin(config) {
|
|
|
238
275
|
}
|
|
239
276
|
default:
|
|
240
277
|
emitFatalError(error, false);
|
|
241
|
-
|
|
278
|
+
return true;
|
|
242
279
|
}
|
|
243
280
|
}
|
|
281
|
+
return false;
|
|
244
282
|
};
|
|
245
283
|
const loadNative = async (src) => {
|
|
246
284
|
const videoEl = getOrCreateVideo();
|
|
@@ -249,19 +287,50 @@ function createHLSPlugin(config) {
|
|
|
249
287
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
250
288
|
}
|
|
251
289
|
return new Promise((resolve, reject) => {
|
|
252
|
-
|
|
290
|
+
let watchdog = null;
|
|
291
|
+
const settle = () => {
|
|
253
292
|
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
254
293
|
videoEl.removeEventListener("error", onError);
|
|
294
|
+
if (watchdog !== null) {
|
|
295
|
+
clearTimeout(watchdog);
|
|
296
|
+
watchdog = null;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const onLoaded = () => {
|
|
300
|
+
settle();
|
|
301
|
+
hasPlayedContent = true;
|
|
302
|
+
const onFatalVideoError = () => {
|
|
303
|
+
const media_error = videoEl.error;
|
|
304
|
+
const hls_error = {
|
|
305
|
+
type: media_error?.code === MediaError.MEDIA_ERR_NETWORK ? "network" : "media",
|
|
306
|
+
details: media_error?.message || "Native HLS playback error",
|
|
307
|
+
fatal: true
|
|
308
|
+
};
|
|
309
|
+
emitFatalError(hls_error, false);
|
|
310
|
+
};
|
|
311
|
+
videoEl.addEventListener("error", onFatalVideoError);
|
|
312
|
+
const removeFatalListener = () => videoEl.removeEventListener("error", onFatalVideoError);
|
|
313
|
+
const previous_cleanup = cleanupVideoEvents;
|
|
314
|
+
cleanupVideoEvents = () => {
|
|
315
|
+
removeFatalListener();
|
|
316
|
+
previous_cleanup?.();
|
|
317
|
+
};
|
|
255
318
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
256
319
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
257
320
|
resolve();
|
|
258
321
|
};
|
|
259
322
|
const onError = () => {
|
|
260
|
-
|
|
261
|
-
videoEl.removeEventListener("error", onError);
|
|
323
|
+
settle();
|
|
262
324
|
const error = videoEl.error;
|
|
263
325
|
reject(new Error(error?.message || "Failed to load HLS source"));
|
|
264
326
|
};
|
|
327
|
+
const timeout_ms = mergedConfig.loadTimeoutMs ?? 3e4;
|
|
328
|
+
if (timeout_ms > 0) {
|
|
329
|
+
watchdog = setTimeout(() => {
|
|
330
|
+
settle();
|
|
331
|
+
reject(new Error("Video took too long to load (network timeout)"));
|
|
332
|
+
}, timeout_ms);
|
|
333
|
+
}
|
|
265
334
|
videoEl.addEventListener("loadedmetadata", onLoaded);
|
|
266
335
|
videoEl.addEventListener("error", onError);
|
|
267
336
|
videoEl.src = src;
|
|
@@ -282,10 +351,19 @@ function createHLSPlugin(config) {
|
|
|
282
351
|
return;
|
|
283
352
|
}
|
|
284
353
|
let resolved = false;
|
|
354
|
+
let watchdog = null;
|
|
355
|
+
const clearWatchdog = () => {
|
|
356
|
+
if (watchdog !== null) {
|
|
357
|
+
clearTimeout(watchdog);
|
|
358
|
+
watchdog = null;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
285
361
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
286
362
|
onManifestParsed: () => {
|
|
287
363
|
if (!resolved) {
|
|
288
364
|
resolved = true;
|
|
365
|
+
clearWatchdog();
|
|
366
|
+
hasPlayedContent = true;
|
|
289
367
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
290
368
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
291
369
|
resolve();
|
|
@@ -294,18 +372,127 @@ function createHLSPlugin(config) {
|
|
|
294
372
|
onLevelSwitched: () => {
|
|
295
373
|
},
|
|
296
374
|
onError: (error) => {
|
|
297
|
-
handleHlsError(error);
|
|
298
|
-
if (
|
|
375
|
+
const terminal = handleHlsError(error);
|
|
376
|
+
if (terminal && !resolved) {
|
|
299
377
|
resolved = true;
|
|
378
|
+
clearWatchdog();
|
|
300
379
|
reject(new Error(error.details));
|
|
301
380
|
}
|
|
302
381
|
},
|
|
382
|
+
onFragLoaded: () => {
|
|
383
|
+
if (networkRetryCount > 0 || mediaRetryCount > 0) {
|
|
384
|
+
api?.logger.debug("Playback recovered, resetting retry budgets");
|
|
385
|
+
networkRetryCount = 0;
|
|
386
|
+
mediaRetryCount = 0;
|
|
387
|
+
}
|
|
388
|
+
},
|
|
303
389
|
getIsAutoQuality: () => isAutoQuality
|
|
304
390
|
});
|
|
391
|
+
const timeout_ms = mergedConfig.loadTimeoutMs ?? 3e4;
|
|
392
|
+
if (timeout_ms > 0) {
|
|
393
|
+
watchdog = setTimeout(() => {
|
|
394
|
+
if (resolved) return;
|
|
395
|
+
resolved = true;
|
|
396
|
+
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src });
|
|
397
|
+
if (retryTimeout) {
|
|
398
|
+
clearTimeout(retryTimeout);
|
|
399
|
+
retryTimeout = null;
|
|
400
|
+
}
|
|
401
|
+
cleanupHlsEvents?.();
|
|
402
|
+
cleanupHlsEvents = null;
|
|
403
|
+
hls?.destroy();
|
|
404
|
+
hls = null;
|
|
405
|
+
reject(new Error("Video took too long to load (network timeout)"));
|
|
406
|
+
}, timeout_ms);
|
|
407
|
+
}
|
|
305
408
|
hls.attachMedia(videoEl);
|
|
306
409
|
hls.loadSource(src);
|
|
307
410
|
});
|
|
308
411
|
};
|
|
412
|
+
const cancelReconnect = () => {
|
|
413
|
+
if (reconnectTimer) {
|
|
414
|
+
clearTimeout(reconnectTimer);
|
|
415
|
+
reconnectTimer = null;
|
|
416
|
+
}
|
|
417
|
+
reconnectAttempts = 0;
|
|
418
|
+
reconnectWindowStart = 0;
|
|
419
|
+
reconnectResumePosition = 0;
|
|
420
|
+
};
|
|
421
|
+
const scheduleReconnectAttempt = () => {
|
|
422
|
+
if (reconnectTimer) return;
|
|
423
|
+
const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
424
|
+
if (Date.now() - reconnectWindowStart > window_ms) {
|
|
425
|
+
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
429
|
+
const max_delay = mergedConfig.reconnectMaxDelayMs ?? 3e4;
|
|
430
|
+
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
431
|
+
const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
432
|
+
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
433
|
+
api?.emit("error:reconnecting", { attempt: reconnectAttempts + 1, delayMs: delay });
|
|
434
|
+
reconnectTimer = setTimeout(() => {
|
|
435
|
+
reconnectTimer = null;
|
|
436
|
+
void attemptReconnect();
|
|
437
|
+
}, delay);
|
|
438
|
+
};
|
|
439
|
+
const maybeScheduleReconnect = (error) => {
|
|
440
|
+
if (mergedConfig.autoReconnect === false) return;
|
|
441
|
+
if (!hasPlayedContent || !currentSrc) return;
|
|
442
|
+
if (error.type !== "network" && error.type !== "media") return;
|
|
443
|
+
if (reconnectWindowStart === 0) {
|
|
444
|
+
reconnectWindowStart = Date.now();
|
|
445
|
+
reconnectResumePosition = video?.currentTime ?? 0;
|
|
446
|
+
}
|
|
447
|
+
scheduleReconnectAttempt();
|
|
448
|
+
};
|
|
449
|
+
const attemptReconnect = async () => {
|
|
450
|
+
if (!api || !currentSrc) return;
|
|
451
|
+
reconnectAttempts++;
|
|
452
|
+
const saved_src = currentSrc;
|
|
453
|
+
const was_live = api.getState("live");
|
|
454
|
+
const was_native = isNative;
|
|
455
|
+
const resume_position = reconnectResumePosition;
|
|
456
|
+
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: saved_src });
|
|
457
|
+
try {
|
|
458
|
+
cleanupHlsEvents?.();
|
|
459
|
+
cleanupHlsEvents = null;
|
|
460
|
+
cleanupVideoEvents?.();
|
|
461
|
+
cleanupVideoEvents = null;
|
|
462
|
+
if (retryTimeout) {
|
|
463
|
+
clearTimeout(retryTimeout);
|
|
464
|
+
retryTimeout = null;
|
|
465
|
+
}
|
|
466
|
+
hls?.destroy();
|
|
467
|
+
hls = null;
|
|
468
|
+
networkRetryCount = 0;
|
|
469
|
+
mediaRetryCount = 0;
|
|
470
|
+
errorCount = 0;
|
|
471
|
+
errorWindowStart = 0;
|
|
472
|
+
currentSrc = saved_src;
|
|
473
|
+
api.setState("playbackState", "loading");
|
|
474
|
+
if (was_native && supportsNativeHLS()) {
|
|
475
|
+
await loadNative(saved_src);
|
|
476
|
+
} else {
|
|
477
|
+
await loadWithHlsJs(saved_src);
|
|
478
|
+
}
|
|
479
|
+
if (!was_live && video && resume_position > 0) {
|
|
480
|
+
video.currentTime = resume_position;
|
|
481
|
+
}
|
|
482
|
+
api.setState("playbackState", "ready");
|
|
483
|
+
api.setState("buffering", false);
|
|
484
|
+
api.emit("error:recovered", void 0);
|
|
485
|
+
api.logger.info("Auto-reconnect succeeded");
|
|
486
|
+
cancelReconnect();
|
|
487
|
+
try {
|
|
488
|
+
await video?.play();
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
api?.logger.warn(`Auto-reconnect attempt ${reconnectAttempts} failed`);
|
|
493
|
+
scheduleReconnectAttempt();
|
|
494
|
+
}
|
|
495
|
+
};
|
|
309
496
|
const plugin = {
|
|
310
497
|
id: "hls-provider",
|
|
311
498
|
name: "HLS Provider",
|
|
@@ -389,6 +576,17 @@ function createHLSPlugin(config) {
|
|
|
389
576
|
}
|
|
390
577
|
}
|
|
391
578
|
});
|
|
579
|
+
if (typeof window !== "undefined") {
|
|
580
|
+
onlineListener = () => {
|
|
581
|
+
if (reconnectTimer) {
|
|
582
|
+
api?.logger.info("Browser back online, reconnecting immediately");
|
|
583
|
+
clearTimeout(reconnectTimer);
|
|
584
|
+
reconnectTimer = null;
|
|
585
|
+
void attemptReconnect();
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
window.addEventListener("online", onlineListener);
|
|
589
|
+
}
|
|
392
590
|
api.onDestroy(() => {
|
|
393
591
|
unsubPlay();
|
|
394
592
|
unsubPause();
|
|
@@ -401,6 +599,11 @@ function createHLSPlugin(config) {
|
|
|
401
599
|
},
|
|
402
600
|
async destroy() {
|
|
403
601
|
api?.logger.info("HLS plugin destroying");
|
|
602
|
+
cancelReconnect();
|
|
603
|
+
if (onlineListener && typeof window !== "undefined") {
|
|
604
|
+
window.removeEventListener("online", onlineListener);
|
|
605
|
+
onlineListener = null;
|
|
606
|
+
}
|
|
404
607
|
cleanup();
|
|
405
608
|
if (video?.parentNode) {
|
|
406
609
|
video.parentNode.removeChild(video);
|
|
@@ -411,6 +614,8 @@ function createHLSPlugin(config) {
|
|
|
411
614
|
async loadSource(src) {
|
|
412
615
|
if (!api) throw new Error("Plugin not initialized");
|
|
413
616
|
api.logger.info("Loading HLS source", { src });
|
|
617
|
+
cancelReconnect();
|
|
618
|
+
hasPlayedContent = false;
|
|
414
619
|
cleanup();
|
|
415
620
|
currentSrc = src;
|
|
416
621
|
api.setState("playbackState", "loading");
|
package/dist/light.cjs
CHANGED
|
@@ -214,6 +214,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
214
214
|
lastBandwidthUpdate = now;
|
|
215
215
|
api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
|
|
216
216
|
}
|
|
217
|
+
callbacks.onFragLoaded?.();
|
|
217
218
|
});
|
|
218
219
|
addHandler("hlsFragBuffered", () => {
|
|
219
220
|
api.setState("buffering", false);
|
package/dist/light.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-
|
|
2
|
-
export {
|
|
1
|
+
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-DOzoPHQe.cjs';
|
|
2
|
+
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-DOzoPHQe.cjs';
|
|
3
3
|
import '@scarlett-player/core';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/light.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-
|
|
2
|
-
export {
|
|
1
|
+
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-DOzoPHQe.js';
|
|
2
|
+
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-DOzoPHQe.js';
|
|
3
3
|
import '@scarlett-player/core';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/light.js
CHANGED
|
@@ -34,6 +34,24 @@ 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;
|
|
37
55
|
/** Index signature for PluginConfig compatibility */
|
|
38
56
|
[key: string]: unknown;
|
|
39
57
|
}
|
|
@@ -102,4 +120,4 @@ interface IHLSPlugin extends Plugin<HLSPluginConfig> {
|
|
|
102
120
|
switchToHlsJs(): Promise<void>;
|
|
103
121
|
}
|
|
104
122
|
|
|
105
|
-
export type { HLSPluginConfig as H, IHLSPlugin as I,
|
|
123
|
+
export type { HLSPluginConfig as H, IHLSPlugin as I, HLSQualityLevel as a, HLSError as b, HLSLiveInfo as c };
|
|
@@ -34,6 +34,24 @@ 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;
|
|
37
55
|
/** Index signature for PluginConfig compatibility */
|
|
38
56
|
[key: string]: unknown;
|
|
39
57
|
}
|
|
@@ -102,4 +120,4 @@ interface IHLSPlugin extends Plugin<HLSPluginConfig> {
|
|
|
102
120
|
switchToHlsJs(): Promise<void>;
|
|
103
121
|
}
|
|
104
122
|
|
|
105
|
-
export type { HLSPluginConfig as H, IHLSPlugin as I,
|
|
123
|
+
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.
|
|
3
|
+
"version": "1.1.1",
|
|
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.
|
|
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.
|
|
40
|
+
"@scarlett-player/core": "1.1.1"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"video",
|