@scarlett-player/hls 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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/index.ts
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
- function createHLSPlugin(config) {
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 cleanup = () => {
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: "MEDIA_ERROR",
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
- cleanupHlsEvents?.();
547
- cleanupHlsEvents = null;
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
- hls.recoverMediaError();
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
- break;
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
- const onLoaded = () => {
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
- videoEl.removeEventListener("loadedmetadata", onLoaded);
619
- videoEl.removeEventListener("error", onError);
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
- await loadHlsJs();
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
- handleHlsError(error);
656
- if (error.fatal && !resolved && error.type !== "network" && error.type !== "media") {
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: "HLS Provider",
983
+ name: variant.name,
670
984
  version: "1.0.0",
671
985
  type: "provider",
672
- description: "HLS playback provider using hls.js",
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("HLS plugin initialized");
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("HLS plugin destroying");
762
- cleanup();
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("Loading HLS source", { src });
772
- cleanup();
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("Using hls.js for HLS playback");
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
- cleanup();
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
- cleanup();
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 = {