@scarlett-player/hls 1.0.1 → 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-OEPNMGNQ.js → chunk-L73PRXS4.js} +6 -0
- package/dist/index.cjs +222 -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 +6 -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);
|
|
@@ -282,6 +283,11 @@ function setupVideoEventHandlers(video, api) {
|
|
|
282
283
|
api.setState("duration", video.duration);
|
|
283
284
|
api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
|
|
284
285
|
});
|
|
286
|
+
addHandler("loadeddata", () => {
|
|
287
|
+
if (video.videoWidth > 0) {
|
|
288
|
+
api.setState("mediaType", "video");
|
|
289
|
+
}
|
|
290
|
+
});
|
|
285
291
|
addHandler("error", () => {
|
|
286
292
|
const error = video.error;
|
|
287
293
|
if (error) {
|
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);
|
|
@@ -371,6 +373,11 @@ function setupVideoEventHandlers(video, api) {
|
|
|
371
373
|
api.setState("duration", video.duration);
|
|
372
374
|
api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
|
|
373
375
|
});
|
|
376
|
+
addHandler("loadeddata", () => {
|
|
377
|
+
if (video.videoWidth > 0) {
|
|
378
|
+
api.setState("mediaType", "video");
|
|
379
|
+
}
|
|
380
|
+
});
|
|
374
381
|
addHandler("error", () => {
|
|
375
382
|
const error = video.error;
|
|
376
383
|
if (error) {
|
|
@@ -426,8 +433,20 @@ var DEFAULT_CONFIG = {
|
|
|
426
433
|
maxNetworkRetries: 3,
|
|
427
434
|
maxMediaRetries: 2,
|
|
428
435
|
retryDelayMs: 1e3,
|
|
429
|
-
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
|
|
430
444
|
};
|
|
445
|
+
var MANIFEST_PHASE_ERRORS = [
|
|
446
|
+
"manifestLoadError",
|
|
447
|
+
"manifestLoadTimeOut",
|
|
448
|
+
"manifestParsingError"
|
|
449
|
+
];
|
|
431
450
|
function createHLSPlugin(config) {
|
|
432
451
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
433
452
|
let api = null;
|
|
@@ -445,6 +464,12 @@ function createHLSPlugin(config) {
|
|
|
445
464
|
let errorWindowStart = 0;
|
|
446
465
|
const MAX_ERRORS_IN_WINDOW = 10;
|
|
447
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;
|
|
448
473
|
const getOrCreateVideo = () => {
|
|
449
474
|
if (video) return video;
|
|
450
475
|
const existing = api?.container.querySelector("video");
|
|
@@ -513,21 +538,33 @@ function createHLSPlugin(config) {
|
|
|
513
538
|
const jitter = delay * (0.7 + Math.random() * 0.3);
|
|
514
539
|
return jitter;
|
|
515
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
|
+
};
|
|
516
552
|
const emitFatalError = (error, retriesExhausted) => {
|
|
517
553
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
518
554
|
api?.logger.error(message, { type: error.type, details: error.details });
|
|
519
555
|
api?.setState("playbackState", "error");
|
|
520
556
|
api?.setState("buffering", false);
|
|
521
557
|
api?.emit("error", {
|
|
522
|
-
code:
|
|
558
|
+
code: mapFatalErrorCode(error),
|
|
523
559
|
message,
|
|
524
560
|
fatal: true,
|
|
525
561
|
timestamp: Date.now()
|
|
526
562
|
});
|
|
563
|
+
maybeScheduleReconnect(error);
|
|
527
564
|
};
|
|
528
565
|
const handleHlsError = (error) => {
|
|
529
566
|
const Hls = getHlsConstructor();
|
|
530
|
-
if (!Hls || !hls) return;
|
|
567
|
+
if (!Hls || !hls) return false;
|
|
531
568
|
const now = Date.now();
|
|
532
569
|
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
533
570
|
errorCount = 1;
|
|
@@ -542,7 +579,7 @@ function createHLSPlugin(config) {
|
|
|
542
579
|
cleanupHlsEvents = null;
|
|
543
580
|
hls.destroy();
|
|
544
581
|
hls = null;
|
|
545
|
-
return;
|
|
582
|
+
return true;
|
|
546
583
|
}
|
|
547
584
|
if (error.fatal) {
|
|
548
585
|
api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
|
|
@@ -552,7 +589,7 @@ function createHLSPlugin(config) {
|
|
|
552
589
|
if (networkRetryCount >= maxRetries) {
|
|
553
590
|
api?.logger.error(`Network error recovery failed after ${networkRetryCount} attempts`);
|
|
554
591
|
emitFatalError(error, true);
|
|
555
|
-
return;
|
|
592
|
+
return true;
|
|
556
593
|
}
|
|
557
594
|
networkRetryCount++;
|
|
558
595
|
const delay = getRetryDelay(networkRetryCount - 1);
|
|
@@ -561,8 +598,12 @@ function createHLSPlugin(config) {
|
|
|
561
598
|
if (retryTimeout) {
|
|
562
599
|
clearTimeout(retryTimeout);
|
|
563
600
|
}
|
|
601
|
+
const isManifestPhase = MANIFEST_PHASE_ERRORS.includes(error.details);
|
|
564
602
|
retryTimeout = setTimeout(() => {
|
|
565
|
-
if (hls)
|
|
603
|
+
if (!hls) return;
|
|
604
|
+
if (isManifestPhase && currentSrc) {
|
|
605
|
+
hls.loadSource(currentSrc);
|
|
606
|
+
} else {
|
|
566
607
|
hls.startLoad();
|
|
567
608
|
}
|
|
568
609
|
}, delay);
|
|
@@ -573,7 +614,7 @@ function createHLSPlugin(config) {
|
|
|
573
614
|
if (mediaRetryCount >= maxRetries) {
|
|
574
615
|
api?.logger.error(`Media error recovery failed after ${mediaRetryCount} attempts`);
|
|
575
616
|
emitFatalError(error, true);
|
|
576
|
-
return;
|
|
617
|
+
return true;
|
|
577
618
|
}
|
|
578
619
|
mediaRetryCount++;
|
|
579
620
|
const delay = getRetryDelay(mediaRetryCount - 1);
|
|
@@ -591,9 +632,10 @@ function createHLSPlugin(config) {
|
|
|
591
632
|
}
|
|
592
633
|
default:
|
|
593
634
|
emitFatalError(error, false);
|
|
594
|
-
|
|
635
|
+
return true;
|
|
595
636
|
}
|
|
596
637
|
}
|
|
638
|
+
return false;
|
|
597
639
|
};
|
|
598
640
|
const loadNative = async (src) => {
|
|
599
641
|
const videoEl = getOrCreateVideo();
|
|
@@ -602,19 +644,50 @@ function createHLSPlugin(config) {
|
|
|
602
644
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
603
645
|
}
|
|
604
646
|
return new Promise((resolve, reject) => {
|
|
605
|
-
|
|
647
|
+
let watchdog = null;
|
|
648
|
+
const settle = () => {
|
|
606
649
|
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
607
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
|
+
};
|
|
608
675
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
609
676
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
610
677
|
resolve();
|
|
611
678
|
};
|
|
612
679
|
const onError = () => {
|
|
613
|
-
|
|
614
|
-
videoEl.removeEventListener("error", onError);
|
|
680
|
+
settle();
|
|
615
681
|
const error = videoEl.error;
|
|
616
682
|
reject(new Error(error?.message || "Failed to load HLS source"));
|
|
617
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
|
+
}
|
|
618
691
|
videoEl.addEventListener("loadedmetadata", onLoaded);
|
|
619
692
|
videoEl.addEventListener("error", onError);
|
|
620
693
|
videoEl.src = src;
|
|
@@ -635,10 +708,19 @@ function createHLSPlugin(config) {
|
|
|
635
708
|
return;
|
|
636
709
|
}
|
|
637
710
|
let resolved = false;
|
|
711
|
+
let watchdog = null;
|
|
712
|
+
const clearWatchdog = () => {
|
|
713
|
+
if (watchdog !== null) {
|
|
714
|
+
clearTimeout(watchdog);
|
|
715
|
+
watchdog = null;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
638
718
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
639
719
|
onManifestParsed: () => {
|
|
640
720
|
if (!resolved) {
|
|
641
721
|
resolved = true;
|
|
722
|
+
clearWatchdog();
|
|
723
|
+
hasPlayedContent = true;
|
|
642
724
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
643
725
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
644
726
|
resolve();
|
|
@@ -647,18 +729,127 @@ function createHLSPlugin(config) {
|
|
|
647
729
|
onLevelSwitched: () => {
|
|
648
730
|
},
|
|
649
731
|
onError: (error) => {
|
|
650
|
-
handleHlsError(error);
|
|
651
|
-
if (
|
|
732
|
+
const terminal = handleHlsError(error);
|
|
733
|
+
if (terminal && !resolved) {
|
|
652
734
|
resolved = true;
|
|
735
|
+
clearWatchdog();
|
|
653
736
|
reject(new Error(error.details));
|
|
654
737
|
}
|
|
655
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
|
+
},
|
|
656
746
|
getIsAutoQuality: () => isAutoQuality
|
|
657
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
|
+
}
|
|
658
765
|
hls.attachMedia(videoEl);
|
|
659
766
|
hls.loadSource(src);
|
|
660
767
|
});
|
|
661
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
|
+
};
|
|
662
853
|
const plugin = {
|
|
663
854
|
id: "hls-provider",
|
|
664
855
|
name: "HLS Provider",
|
|
@@ -742,6 +933,17 @@ function createHLSPlugin(config) {
|
|
|
742
933
|
}
|
|
743
934
|
}
|
|
744
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
|
+
}
|
|
745
947
|
api.onDestroy(() => {
|
|
746
948
|
unsubPlay();
|
|
747
949
|
unsubPause();
|
|
@@ -754,6 +956,11 @@ function createHLSPlugin(config) {
|
|
|
754
956
|
},
|
|
755
957
|
async destroy() {
|
|
756
958
|
api?.logger.info("HLS plugin destroying");
|
|
959
|
+
cancelReconnect();
|
|
960
|
+
if (onlineListener && typeof window !== "undefined") {
|
|
961
|
+
window.removeEventListener("online", onlineListener);
|
|
962
|
+
onlineListener = null;
|
|
963
|
+
}
|
|
757
964
|
cleanup();
|
|
758
965
|
if (video?.parentNode) {
|
|
759
966
|
video.parentNode.removeChild(video);
|
|
@@ -764,6 +971,8 @@ function createHLSPlugin(config) {
|
|
|
764
971
|
async loadSource(src) {
|
|
765
972
|
if (!api) throw new Error("Plugin not initialized");
|
|
766
973
|
api.logger.info("Loading HLS source", { src });
|
|
974
|
+
cancelReconnect();
|
|
975
|
+
hasPlayedContent = false;
|
|
767
976
|
cleanup();
|
|
768
977
|
currentSrc = src;
|
|
769
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);
|
|
@@ -359,6 +360,11 @@ function setupVideoEventHandlers(video, api) {
|
|
|
359
360
|
api.setState("duration", video.duration);
|
|
360
361
|
api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
|
|
361
362
|
});
|
|
363
|
+
addHandler("loadeddata", () => {
|
|
364
|
+
if (video.videoWidth > 0) {
|
|
365
|
+
api.setState("mediaType", "video");
|
|
366
|
+
}
|
|
367
|
+
});
|
|
362
368
|
addHandler("error", () => {
|
|
363
369
|
const error = video.error;
|
|
364
370
|
if (error) {
|
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",
|