@scarlett-player/hls 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,16 +11,14 @@ npm install @scarlett-player/core @scarlett-player/hls
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
- import { ScarlettPlayer } from '@scarlett-player/core';
14
+ import { createPlayer } from '@scarlett-player/core';
15
15
  import { createHLSPlugin } from '@scarlett-player/hls';
16
16
 
17
- const player = new ScarlettPlayer({
17
+ const player = await createPlayer({
18
18
  container: document.getElementById('player'),
19
+ src: 'https://example.com/video.m3u8',
19
20
  plugins: [createHLSPlugin()],
20
21
  });
21
-
22
- await player.init();
23
- await player.load('https://example.com/video.m3u8');
24
22
  ```
25
23
 
26
24
  ## Features
@@ -4,6 +4,17 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // src/sanitize-url.ts
8
+ function sanitizeUrl(url) {
9
+ if (!url) return void 0;
10
+ try {
11
+ const parsed = new URL(url);
12
+ return `${parsed.origin}${parsed.pathname}`;
13
+ } catch {
14
+ return void 0;
15
+ }
16
+ }
17
+
7
18
  // src/create-hls-plugin.ts
8
19
  import { ErrorCode } from "@scarlett-player/core";
9
20
 
@@ -211,8 +222,14 @@ function setupVideoEventHandlers(video, api) {
211
222
  video.addEventListener(event, handler);
212
223
  handlers.push({ event, handler });
213
224
  };
225
+ const syncEndedFromElement = () => {
226
+ if (video.ended || !api.getState("ended")) return;
227
+ api.setState("ended", false);
228
+ api.setState("playbackState", video.paused ? "paused" : "playing");
229
+ };
214
230
  addHandler("play", () => {
215
231
  api.setState("paused", false);
232
+ syncEndedFromElement();
216
233
  });
217
234
  addHandler("playing", () => {
218
235
  api.setState("playing", true);
@@ -220,6 +237,7 @@ function setupVideoEventHandlers(video, api) {
220
237
  api.setState("waiting", false);
221
238
  api.setState("buffering", false);
222
239
  api.setState("playbackState", "playing");
240
+ syncEndedFromElement();
223
241
  });
224
242
  addHandler("pause", () => {
225
243
  api.setState("playing", false);
@@ -274,6 +292,7 @@ function setupVideoEventHandlers(video, api) {
274
292
  });
275
293
  addHandler("seeking", () => {
276
294
  api.setState("seeking", true);
295
+ syncEndedFromElement();
277
296
  });
278
297
  addHandler("seeked", () => {
279
298
  api.setState("seeking", false);
@@ -381,6 +400,9 @@ function createValidatingPlaylistLoader(Hls) {
381
400
  };
382
401
  }
383
402
 
403
+ // src/version.ts
404
+ var PKG_VERSION = true ? "1.7.1" : "0.0.0-dev";
405
+
384
406
  // src/create-hls-plugin.ts
385
407
  var DEFAULT_CONFIG = {
386
408
  debug: false,
@@ -437,6 +459,12 @@ function createHLSPluginWith(loader, variant, config) {
437
459
  let reconnectWindowStart = 0;
438
460
  let reconnectResumePosition = 0;
439
461
  let onlineListener = null;
462
+ let reconnectTriggerError = null;
463
+ let reconnectExhausted = false;
464
+ const applyPoster = () => {
465
+ if (!video) return;
466
+ video.poster = api?.getState("poster") || "";
467
+ };
440
468
  const getOrCreateVideo = () => {
441
469
  if (video) return video;
442
470
  const existing = api?.container.querySelector("video");
@@ -449,10 +477,7 @@ function createHLSPluginWith(loader, variant, config) {
449
477
  video.preload = "metadata";
450
478
  video.controls = false;
451
479
  video.playsInline = true;
452
- const poster = api?.getState("poster");
453
- if (poster) {
454
- video.poster = poster;
455
- }
480
+ applyPoster();
456
481
  api?.container.appendChild(video);
457
482
  return video;
458
483
  };
@@ -545,6 +570,22 @@ function createHLSPluginWith(loader, variant, config) {
545
570
  return ErrorCode.PLAYBACK_FAILED;
546
571
  }
547
572
  };
573
+ const buildErrorDetail = (error, retriesExhausted) => {
574
+ const attempts = error.type === "network" ? networkRetryCount : error.type === "media" ? mediaRetryCount : 0;
575
+ const detail = {
576
+ type: error.type,
577
+ retriesExhausted,
578
+ attempts
579
+ };
580
+ if (typeof error.response?.code === "number" && error.response.code > 0) {
581
+ detail.httpStatus = error.response.code;
582
+ }
583
+ const url = sanitizeUrl(error.url);
584
+ if (url) {
585
+ detail.url = url;
586
+ }
587
+ return detail;
588
+ };
548
589
  const emitFatalError = (error, retriesExhausted) => {
549
590
  const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
550
591
  api?.logger.error(message, { type: error.type, details: error.details });
@@ -554,7 +595,8 @@ function createHLSPluginWith(loader, variant, config) {
554
595
  code: mapFatalErrorCode(error),
555
596
  message,
556
597
  fatal: true,
557
- timestamp: Date.now()
598
+ timestamp: Date.now(),
599
+ detail: buildErrorDetail(error, retriesExhausted)
558
600
  });
559
601
  maybeScheduleReconnect(error);
560
602
  };
@@ -631,6 +673,62 @@ function createHLSPluginWith(loader, variant, config) {
631
673
  }
632
674
  return false;
633
675
  };
676
+ const handleNativeFatalError = (error, resumePosition) => {
677
+ const is_network = error.type === "network";
678
+ const max_retries = is_network ? mergedConfig.maxNetworkRetries ?? 3 : mergedConfig.maxMediaRetries ?? 2;
679
+ const used = is_network ? networkRetryCount : mediaRetryCount;
680
+ if (!currentSrc || used >= max_retries) {
681
+ emitFatalError(error, used >= max_retries);
682
+ return;
683
+ }
684
+ const resume_position = resumePosition ?? video?.currentTime ?? 0;
685
+ if (is_network) {
686
+ networkRetryCount++;
687
+ } else {
688
+ mediaRetryCount++;
689
+ }
690
+ const attempt = used + 1;
691
+ const delay = getRetryDelay(attempt - 1);
692
+ api?.logger.info(
693
+ `Attempting native ${error.type} error recovery (attempt ${attempt}/${max_retries}) in ${delay}ms`
694
+ );
695
+ api?.emit(is_network ? "error:network" : "error:media", {
696
+ error: new Error(error.details)
697
+ });
698
+ if (retryTimeout) {
699
+ clearTimeout(retryTimeout);
700
+ }
701
+ const retry_session = loadSession;
702
+ retryTimeout = setTimeout(() => {
703
+ if (retry_session !== loadSession) return;
704
+ void recoverNative(error, resume_position);
705
+ }, delay);
706
+ };
707
+ const recoverNative = async (error, resumePosition) => {
708
+ if (!currentSrc) return;
709
+ const session = ++loadSession;
710
+ const saved_src = currentSrc;
711
+ const was_live = api?.getState("live") ?? false;
712
+ try {
713
+ teardownPipeline(new Error("HLS load cancelled: native error recovery"));
714
+ api?.setState("playbackState", "loading");
715
+ await loadNative(saved_src);
716
+ if (session !== loadSession) return;
717
+ if (!was_live && video && resumePosition > 0) {
718
+ video.currentTime = resumePosition;
719
+ }
720
+ api?.setState("playbackState", "ready");
721
+ api?.setState("buffering", false);
722
+ try {
723
+ await video?.play();
724
+ } catch {
725
+ }
726
+ } catch {
727
+ if (session !== loadSession) return;
728
+ api?.logger.warn("Native error recovery attempt failed");
729
+ handleNativeFatalError(error, resumePosition);
730
+ }
731
+ };
634
732
  const loadNative = async (src) => {
635
733
  const session = loadSession;
636
734
  const videoEl = getOrCreateVideo();
@@ -675,10 +773,21 @@ function createHLSPluginWith(loader, variant, config) {
675
773
  details: media_error?.message || "Native HLS playback error",
676
774
  fatal: true
677
775
  };
678
- emitFatalError(hls_error, false);
776
+ handleNativeFatalError(hls_error);
679
777
  };
680
778
  videoEl.addEventListener("error", onFatalVideoError);
681
- const removeFatalListener = () => videoEl.removeEventListener("error", onFatalVideoError);
779
+ const onPlayingResetBudget = () => {
780
+ if (networkRetryCount > 0 || mediaRetryCount > 0) {
781
+ api?.logger.debug("Native playback recovered, resetting retry budgets");
782
+ networkRetryCount = 0;
783
+ mediaRetryCount = 0;
784
+ }
785
+ };
786
+ videoEl.addEventListener("playing", onPlayingResetBudget);
787
+ const removeFatalListener = () => {
788
+ videoEl.removeEventListener("error", onFatalVideoError);
789
+ videoEl.removeEventListener("playing", onPlayingResetBudget);
790
+ };
682
791
  const previous_cleanup = cleanupVideoEvents;
683
792
  cleanupVideoEvents = () => {
684
793
  removeFatalListener();
@@ -803,12 +912,38 @@ function createHLSPluginWith(loader, variant, config) {
803
912
  reconnectAttempts = 0;
804
913
  reconnectWindowStart = 0;
805
914
  reconnectResumePosition = 0;
915
+ reconnectTriggerError = null;
916
+ reconnectExhausted = false;
917
+ };
918
+ const emitReconnectExhausted = (elapsedMs, windowMs) => {
919
+ if (reconnectExhausted) return;
920
+ reconnectExhausted = true;
921
+ const attempts = reconnectAttempts;
922
+ const trigger = reconnectTriggerError;
923
+ api?.emit("error:reconnect-exhausted", { attempts, elapsedMs, windowMs });
924
+ api?.setState("playbackState", "error");
925
+ api?.setState("buffering", false);
926
+ api?.emit("error", {
927
+ code: trigger ? mapFatalErrorCode(trigger) : ErrorCode.PLAYBACK_FAILED,
928
+ message: `HLS auto-reconnect gave up after ${attempts} attempts over ${Math.round(elapsedMs / 1e3)}s`,
929
+ fatal: true,
930
+ timestamp: Date.now(),
931
+ detail: {
932
+ type: trigger?.type ?? "other",
933
+ retriesExhausted: true,
934
+ attempts,
935
+ reconnectExhausted: true
936
+ }
937
+ });
806
938
  };
807
939
  const scheduleReconnectAttempt = () => {
940
+ if (reconnectExhausted) return;
808
941
  if (reconnectTimer) return;
809
942
  const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
810
- if (Date.now() - reconnectWindowStart > window_ms) {
943
+ const elapsed_ms = Date.now() - reconnectWindowStart;
944
+ if (elapsed_ms > window_ms) {
811
945
  api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
946
+ emitReconnectExhausted(elapsed_ms, window_ms);
812
947
  return;
813
948
  }
814
949
  const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
@@ -816,7 +951,12 @@ function createHLSPluginWith(loader, variant, config) {
816
951
  const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
817
952
  const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
818
953
  api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
819
- api?.emit("error:reconnecting", { attempt: reconnectAttempts + 1, delayMs: delay });
954
+ api?.emit("error:reconnecting", {
955
+ attempt: reconnectAttempts + 1,
956
+ delayMs: delay,
957
+ elapsedMs: elapsed_ms,
958
+ windowMs: window_ms
959
+ });
820
960
  reconnectTimer = setTimeout(() => {
821
961
  reconnectTimer = null;
822
962
  void attemptReconnect();
@@ -829,6 +969,7 @@ function createHLSPluginWith(loader, variant, config) {
829
969
  if (reconnectWindowStart === 0) {
830
970
  reconnectWindowStart = Date.now();
831
971
  reconnectResumePosition = video?.currentTime ?? 0;
972
+ reconnectTriggerError = error;
832
973
  }
833
974
  scheduleReconnectAttempt();
834
975
  };
@@ -860,7 +1001,10 @@ function createHLSPluginWith(loader, variant, config) {
860
1001
  }
861
1002
  api.setState("playbackState", "ready");
862
1003
  api.setState("buffering", false);
863
- api.emit("error:recovered", void 0);
1004
+ api.emit("error:recovered", {
1005
+ attempt: reconnectAttempts,
1006
+ elapsedMs: Date.now() - reconnectWindowStart
1007
+ });
864
1008
  api.logger.info("Auto-reconnect succeeded");
865
1009
  cancelReconnect();
866
1010
  try {
@@ -876,7 +1020,7 @@ function createHLSPluginWith(loader, variant, config) {
876
1020
  const plugin = {
877
1021
  id: "hls-provider",
878
1022
  name: variant.name,
879
- version: "1.0.0",
1023
+ version: PKG_VERSION,
880
1024
  type: "provider",
881
1025
  description: variant.description,
882
1026
  canPlay(src) {
@@ -967,6 +1111,9 @@ function createHLSPluginWith(loader, variant, config) {
967
1111
  };
968
1112
  window.addEventListener("online", onlineListener);
969
1113
  }
1114
+ const unsubPoster = api.subscribeToState((event) => {
1115
+ if (event.key === "poster") applyPoster();
1116
+ });
970
1117
  api.onDestroy(() => {
971
1118
  unsubPlay();
972
1119
  unsubPause();
@@ -975,6 +1122,7 @@ function createHLSPluginWith(loader, variant, config) {
975
1122
  unsubMute();
976
1123
  unsubRate();
977
1124
  unsubQuality();
1125
+ unsubPoster();
978
1126
  });
979
1127
  },
980
1128
  async destroy() {
@@ -1000,6 +1148,7 @@ function createHLSPluginWith(loader, variant, config) {
1000
1148
  hasPlayedContent = false;
1001
1149
  cleanup(new Error("HLS load cancelled: superseded by a new load"));
1002
1150
  currentSrc = src;
1151
+ applyPoster();
1003
1152
  api.setState("playbackState", "loading");
1004
1153
  api.setState("buffering", true);
1005
1154
  if (api.getState("airplayActive") && loader.supportsNativeHLS()) {
@@ -1137,5 +1286,6 @@ function createHLSPluginWith(loader, variant, config) {
1137
1286
 
1138
1287
  export {
1139
1288
  __export,
1289
+ sanitizeUrl,
1140
1290
  createHLSPluginWith
1141
1291
  };
package/dist/index.cjs CHANGED
@@ -31,7 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  createHLSPlugin: () => createHLSPlugin,
34
- default: () => index_default
34
+ default: () => index_default,
35
+ sanitizeUrl: () => sanitizeUrl
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
 
@@ -316,8 +317,14 @@ function setupVideoEventHandlers(video, api) {
316
317
  video.addEventListener(event, handler);
317
318
  handlers.push({ event, handler });
318
319
  };
320
+ const syncEndedFromElement = () => {
321
+ if (video.ended || !api.getState("ended")) return;
322
+ api.setState("ended", false);
323
+ api.setState("playbackState", video.paused ? "paused" : "playing");
324
+ };
319
325
  addHandler("play", () => {
320
326
  api.setState("paused", false);
327
+ syncEndedFromElement();
321
328
  });
322
329
  addHandler("playing", () => {
323
330
  api.setState("playing", true);
@@ -325,6 +332,7 @@ function setupVideoEventHandlers(video, api) {
325
332
  api.setState("waiting", false);
326
333
  api.setState("buffering", false);
327
334
  api.setState("playbackState", "playing");
335
+ syncEndedFromElement();
328
336
  });
329
337
  addHandler("pause", () => {
330
338
  api.setState("playing", false);
@@ -379,6 +387,7 @@ function setupVideoEventHandlers(video, api) {
379
387
  });
380
388
  addHandler("seeking", () => {
381
389
  api.setState("seeking", true);
390
+ syncEndedFromElement();
382
391
  });
383
392
  addHandler("seeked", () => {
384
393
  api.setState("seeking", false);
@@ -486,6 +495,20 @@ function createValidatingPlaylistLoader(Hls) {
486
495
  };
487
496
  }
488
497
 
498
+ // src/sanitize-url.ts
499
+ function sanitizeUrl(url) {
500
+ if (!url) return void 0;
501
+ try {
502
+ const parsed = new URL(url);
503
+ return `${parsed.origin}${parsed.pathname}`;
504
+ } catch {
505
+ return void 0;
506
+ }
507
+ }
508
+
509
+ // src/version.ts
510
+ var PKG_VERSION = true ? "1.7.1" : "0.0.0-dev";
511
+
489
512
  // src/create-hls-plugin.ts
490
513
  var DEFAULT_CONFIG = {
491
514
  debug: false,
@@ -542,6 +565,12 @@ function createHLSPluginWith(loader, variant, config) {
542
565
  let reconnectWindowStart = 0;
543
566
  let reconnectResumePosition = 0;
544
567
  let onlineListener = null;
568
+ let reconnectTriggerError = null;
569
+ let reconnectExhausted = false;
570
+ const applyPoster = () => {
571
+ if (!video) return;
572
+ video.poster = api?.getState("poster") || "";
573
+ };
545
574
  const getOrCreateVideo = () => {
546
575
  if (video) return video;
547
576
  const existing = api?.container.querySelector("video");
@@ -554,10 +583,7 @@ function createHLSPluginWith(loader, variant, config) {
554
583
  video.preload = "metadata";
555
584
  video.controls = false;
556
585
  video.playsInline = true;
557
- const poster = api?.getState("poster");
558
- if (poster) {
559
- video.poster = poster;
560
- }
586
+ applyPoster();
561
587
  api?.container.appendChild(video);
562
588
  return video;
563
589
  };
@@ -650,6 +676,22 @@ function createHLSPluginWith(loader, variant, config) {
650
676
  return import_core.ErrorCode.PLAYBACK_FAILED;
651
677
  }
652
678
  };
679
+ const buildErrorDetail = (error, retriesExhausted) => {
680
+ const attempts = error.type === "network" ? networkRetryCount : error.type === "media" ? mediaRetryCount : 0;
681
+ const detail = {
682
+ type: error.type,
683
+ retriesExhausted,
684
+ attempts
685
+ };
686
+ if (typeof error.response?.code === "number" && error.response.code > 0) {
687
+ detail.httpStatus = error.response.code;
688
+ }
689
+ const url = sanitizeUrl(error.url);
690
+ if (url) {
691
+ detail.url = url;
692
+ }
693
+ return detail;
694
+ };
653
695
  const emitFatalError = (error, retriesExhausted) => {
654
696
  const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
655
697
  api?.logger.error(message, { type: error.type, details: error.details });
@@ -659,7 +701,8 @@ function createHLSPluginWith(loader, variant, config) {
659
701
  code: mapFatalErrorCode(error),
660
702
  message,
661
703
  fatal: true,
662
- timestamp: Date.now()
704
+ timestamp: Date.now(),
705
+ detail: buildErrorDetail(error, retriesExhausted)
663
706
  });
664
707
  maybeScheduleReconnect(error);
665
708
  };
@@ -736,6 +779,62 @@ function createHLSPluginWith(loader, variant, config) {
736
779
  }
737
780
  return false;
738
781
  };
782
+ const handleNativeFatalError = (error, resumePosition) => {
783
+ const is_network = error.type === "network";
784
+ const max_retries = is_network ? mergedConfig.maxNetworkRetries ?? 3 : mergedConfig.maxMediaRetries ?? 2;
785
+ const used = is_network ? networkRetryCount : mediaRetryCount;
786
+ if (!currentSrc || used >= max_retries) {
787
+ emitFatalError(error, used >= max_retries);
788
+ return;
789
+ }
790
+ const resume_position = resumePosition ?? video?.currentTime ?? 0;
791
+ if (is_network) {
792
+ networkRetryCount++;
793
+ } else {
794
+ mediaRetryCount++;
795
+ }
796
+ const attempt = used + 1;
797
+ const delay = getRetryDelay(attempt - 1);
798
+ api?.logger.info(
799
+ `Attempting native ${error.type} error recovery (attempt ${attempt}/${max_retries}) in ${delay}ms`
800
+ );
801
+ api?.emit(is_network ? "error:network" : "error:media", {
802
+ error: new Error(error.details)
803
+ });
804
+ if (retryTimeout) {
805
+ clearTimeout(retryTimeout);
806
+ }
807
+ const retry_session = loadSession;
808
+ retryTimeout = setTimeout(() => {
809
+ if (retry_session !== loadSession) return;
810
+ void recoverNative(error, resume_position);
811
+ }, delay);
812
+ };
813
+ const recoverNative = async (error, resumePosition) => {
814
+ if (!currentSrc) return;
815
+ const session = ++loadSession;
816
+ const saved_src = currentSrc;
817
+ const was_live = api?.getState("live") ?? false;
818
+ try {
819
+ teardownPipeline(new Error("HLS load cancelled: native error recovery"));
820
+ api?.setState("playbackState", "loading");
821
+ await loadNative(saved_src);
822
+ if (session !== loadSession) return;
823
+ if (!was_live && video && resumePosition > 0) {
824
+ video.currentTime = resumePosition;
825
+ }
826
+ api?.setState("playbackState", "ready");
827
+ api?.setState("buffering", false);
828
+ try {
829
+ await video?.play();
830
+ } catch {
831
+ }
832
+ } catch {
833
+ if (session !== loadSession) return;
834
+ api?.logger.warn("Native error recovery attempt failed");
835
+ handleNativeFatalError(error, resumePosition);
836
+ }
837
+ };
739
838
  const loadNative = async (src) => {
740
839
  const session = loadSession;
741
840
  const videoEl = getOrCreateVideo();
@@ -780,10 +879,21 @@ function createHLSPluginWith(loader, variant, config) {
780
879
  details: media_error?.message || "Native HLS playback error",
781
880
  fatal: true
782
881
  };
783
- emitFatalError(hls_error, false);
882
+ handleNativeFatalError(hls_error);
784
883
  };
785
884
  videoEl.addEventListener("error", onFatalVideoError);
786
- const removeFatalListener = () => videoEl.removeEventListener("error", onFatalVideoError);
885
+ const onPlayingResetBudget = () => {
886
+ if (networkRetryCount > 0 || mediaRetryCount > 0) {
887
+ api?.logger.debug("Native playback recovered, resetting retry budgets");
888
+ networkRetryCount = 0;
889
+ mediaRetryCount = 0;
890
+ }
891
+ };
892
+ videoEl.addEventListener("playing", onPlayingResetBudget);
893
+ const removeFatalListener = () => {
894
+ videoEl.removeEventListener("error", onFatalVideoError);
895
+ videoEl.removeEventListener("playing", onPlayingResetBudget);
896
+ };
787
897
  const previous_cleanup = cleanupVideoEvents;
788
898
  cleanupVideoEvents = () => {
789
899
  removeFatalListener();
@@ -908,12 +1018,38 @@ function createHLSPluginWith(loader, variant, config) {
908
1018
  reconnectAttempts = 0;
909
1019
  reconnectWindowStart = 0;
910
1020
  reconnectResumePosition = 0;
1021
+ reconnectTriggerError = null;
1022
+ reconnectExhausted = false;
1023
+ };
1024
+ const emitReconnectExhausted = (elapsedMs, windowMs) => {
1025
+ if (reconnectExhausted) return;
1026
+ reconnectExhausted = true;
1027
+ const attempts = reconnectAttempts;
1028
+ const trigger = reconnectTriggerError;
1029
+ api?.emit("error:reconnect-exhausted", { attempts, elapsedMs, windowMs });
1030
+ api?.setState("playbackState", "error");
1031
+ api?.setState("buffering", false);
1032
+ api?.emit("error", {
1033
+ code: trigger ? mapFatalErrorCode(trigger) : import_core.ErrorCode.PLAYBACK_FAILED,
1034
+ message: `HLS auto-reconnect gave up after ${attempts} attempts over ${Math.round(elapsedMs / 1e3)}s`,
1035
+ fatal: true,
1036
+ timestamp: Date.now(),
1037
+ detail: {
1038
+ type: trigger?.type ?? "other",
1039
+ retriesExhausted: true,
1040
+ attempts,
1041
+ reconnectExhausted: true
1042
+ }
1043
+ });
911
1044
  };
912
1045
  const scheduleReconnectAttempt = () => {
1046
+ if (reconnectExhausted) return;
913
1047
  if (reconnectTimer) return;
914
1048
  const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
915
- if (Date.now() - reconnectWindowStart > window_ms) {
1049
+ const elapsed_ms = Date.now() - reconnectWindowStart;
1050
+ if (elapsed_ms > window_ms) {
916
1051
  api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
1052
+ emitReconnectExhausted(elapsed_ms, window_ms);
917
1053
  return;
918
1054
  }
919
1055
  const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
@@ -921,7 +1057,12 @@ function createHLSPluginWith(loader, variant, config) {
921
1057
  const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
922
1058
  const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
923
1059
  api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
924
- api?.emit("error:reconnecting", { attempt: reconnectAttempts + 1, delayMs: delay });
1060
+ api?.emit("error:reconnecting", {
1061
+ attempt: reconnectAttempts + 1,
1062
+ delayMs: delay,
1063
+ elapsedMs: elapsed_ms,
1064
+ windowMs: window_ms
1065
+ });
925
1066
  reconnectTimer = setTimeout(() => {
926
1067
  reconnectTimer = null;
927
1068
  void attemptReconnect();
@@ -934,6 +1075,7 @@ function createHLSPluginWith(loader, variant, config) {
934
1075
  if (reconnectWindowStart === 0) {
935
1076
  reconnectWindowStart = Date.now();
936
1077
  reconnectResumePosition = video?.currentTime ?? 0;
1078
+ reconnectTriggerError = error;
937
1079
  }
938
1080
  scheduleReconnectAttempt();
939
1081
  };
@@ -965,7 +1107,10 @@ function createHLSPluginWith(loader, variant, config) {
965
1107
  }
966
1108
  api.setState("playbackState", "ready");
967
1109
  api.setState("buffering", false);
968
- api.emit("error:recovered", void 0);
1110
+ api.emit("error:recovered", {
1111
+ attempt: reconnectAttempts,
1112
+ elapsedMs: Date.now() - reconnectWindowStart
1113
+ });
969
1114
  api.logger.info("Auto-reconnect succeeded");
970
1115
  cancelReconnect();
971
1116
  try {
@@ -981,7 +1126,7 @@ function createHLSPluginWith(loader, variant, config) {
981
1126
  const plugin = {
982
1127
  id: "hls-provider",
983
1128
  name: variant.name,
984
- version: "1.0.0",
1129
+ version: PKG_VERSION,
985
1130
  type: "provider",
986
1131
  description: variant.description,
987
1132
  canPlay(src) {
@@ -1072,6 +1217,9 @@ function createHLSPluginWith(loader, variant, config) {
1072
1217
  };
1073
1218
  window.addEventListener("online", onlineListener);
1074
1219
  }
1220
+ const unsubPoster = api.subscribeToState((event) => {
1221
+ if (event.key === "poster") applyPoster();
1222
+ });
1075
1223
  api.onDestroy(() => {
1076
1224
  unsubPlay();
1077
1225
  unsubPause();
@@ -1080,6 +1228,7 @@ function createHLSPluginWith(loader, variant, config) {
1080
1228
  unsubMute();
1081
1229
  unsubRate();
1082
1230
  unsubQuality();
1231
+ unsubPoster();
1083
1232
  });
1084
1233
  },
1085
1234
  async destroy() {
@@ -1105,6 +1254,7 @@ function createHLSPluginWith(loader, variant, config) {
1105
1254
  hasPlayedContent = false;
1106
1255
  cleanup(new Error("HLS load cancelled: superseded by a new load"));
1107
1256
  currentSrc = src;
1257
+ applyPoster();
1108
1258
  api.setState("playbackState", "loading");
1109
1259
  api.setState("buffering", true);
1110
1260
  if (api.getState("airplayActive") && loader.supportsNativeHLS()) {
@@ -1256,5 +1406,6 @@ function createHLSPlugin(config) {
1256
1406
  var index_default = createHLSPlugin;
1257
1407
  // Annotate the CommonJS export names for ESM import in node:
1258
1408
  0 && (module.exports = {
1259
- createHLSPlugin
1409
+ createHLSPlugin,
1410
+ sanitizeUrl
1260
1411
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-D-HW4lmM.cjs';
2
- export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-D-HW4lmM.cjs';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './sanitize-url-DcZXD_K-.cjs';
2
+ export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel, s as sanitizeUrl } from './sanitize-url-DcZXD_K-.cjs';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
@@ -20,9 +20,10 @@ import '@scarlett-player/core';
20
20
  *
21
21
  * @example
22
22
  * ```ts
23
+ * import { createPlayer } from '@scarlett-player/core';
23
24
  * import { createHLSPlugin } from '@scarlett-player/hls';
24
25
  *
25
- * const player = new ScarlettPlayer({
26
+ * const player = await createPlayer({
26
27
  * container: document.getElementById('player'),
27
28
  * plugins: [createHLSPlugin()],
28
29
  * });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-D-HW4lmM.js';
2
- export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-D-HW4lmM.js';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './sanitize-url-DcZXD_K-.js';
2
+ export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel, s as sanitizeUrl } from './sanitize-url-DcZXD_K-.js';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
@@ -20,9 +20,10 @@ import '@scarlett-player/core';
20
20
  *
21
21
  * @example
22
22
  * ```ts
23
+ * import { createPlayer } from '@scarlett-player/core';
23
24
  * import { createHLSPlugin } from '@scarlett-player/hls';
24
25
  *
25
- * const player = new ScarlettPlayer({
26
+ * const player = await createPlayer({
26
27
  * container: document.getElementById('player'),
27
28
  * plugins: [createHLSPlugin()],
28
29
  * });
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  __export,
3
- createHLSPluginWith
4
- } from "./chunk-IFUZZQQE.js";
3
+ createHLSPluginWith,
4
+ sanitizeUrl
5
+ } from "./chunk-FN6QUW42.js";
5
6
 
6
7
  // src/hls-loader.ts
7
8
  var hls_loader_exports = {};
@@ -93,5 +94,6 @@ function createHLSPlugin(config) {
93
94
  var index_default = createHLSPlugin;
94
95
  export {
95
96
  createHLSPlugin,
96
- index_default as default
97
+ index_default as default,
98
+ sanitizeUrl
97
99
  };
package/dist/light.cjs CHANGED
@@ -31,7 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var light_exports = {};
32
32
  __export(light_exports, {
33
33
  createHLSPlugin: () => createHLSPlugin,
34
- default: () => light_default
34
+ default: () => light_default,
35
+ sanitizeUrl: () => sanitizeUrl
35
36
  });
36
37
  module.exports = __toCommonJS(light_exports);
37
38
 
@@ -316,8 +317,14 @@ function setupVideoEventHandlers(video, api) {
316
317
  video.addEventListener(event, handler);
317
318
  handlers.push({ event, handler });
318
319
  };
320
+ const syncEndedFromElement = () => {
321
+ if (video.ended || !api.getState("ended")) return;
322
+ api.setState("ended", false);
323
+ api.setState("playbackState", video.paused ? "paused" : "playing");
324
+ };
319
325
  addHandler("play", () => {
320
326
  api.setState("paused", false);
327
+ syncEndedFromElement();
321
328
  });
322
329
  addHandler("playing", () => {
323
330
  api.setState("playing", true);
@@ -325,6 +332,7 @@ function setupVideoEventHandlers(video, api) {
325
332
  api.setState("waiting", false);
326
333
  api.setState("buffering", false);
327
334
  api.setState("playbackState", "playing");
335
+ syncEndedFromElement();
328
336
  });
329
337
  addHandler("pause", () => {
330
338
  api.setState("playing", false);
@@ -379,6 +387,7 @@ function setupVideoEventHandlers(video, api) {
379
387
  });
380
388
  addHandler("seeking", () => {
381
389
  api.setState("seeking", true);
390
+ syncEndedFromElement();
382
391
  });
383
392
  addHandler("seeked", () => {
384
393
  api.setState("seeking", false);
@@ -486,6 +495,20 @@ function createValidatingPlaylistLoader(Hls) {
486
495
  };
487
496
  }
488
497
 
498
+ // src/sanitize-url.ts
499
+ function sanitizeUrl(url) {
500
+ if (!url) return void 0;
501
+ try {
502
+ const parsed = new URL(url);
503
+ return `${parsed.origin}${parsed.pathname}`;
504
+ } catch {
505
+ return void 0;
506
+ }
507
+ }
508
+
509
+ // src/version.ts
510
+ var PKG_VERSION = true ? "1.7.1" : "0.0.0-dev";
511
+
489
512
  // src/create-hls-plugin.ts
490
513
  var DEFAULT_CONFIG = {
491
514
  debug: false,
@@ -542,6 +565,12 @@ function createHLSPluginWith(loader, variant, config) {
542
565
  let reconnectWindowStart = 0;
543
566
  let reconnectResumePosition = 0;
544
567
  let onlineListener = null;
568
+ let reconnectTriggerError = null;
569
+ let reconnectExhausted = false;
570
+ const applyPoster = () => {
571
+ if (!video) return;
572
+ video.poster = api?.getState("poster") || "";
573
+ };
545
574
  const getOrCreateVideo = () => {
546
575
  if (video) return video;
547
576
  const existing = api?.container.querySelector("video");
@@ -554,10 +583,7 @@ function createHLSPluginWith(loader, variant, config) {
554
583
  video.preload = "metadata";
555
584
  video.controls = false;
556
585
  video.playsInline = true;
557
- const poster = api?.getState("poster");
558
- if (poster) {
559
- video.poster = poster;
560
- }
586
+ applyPoster();
561
587
  api?.container.appendChild(video);
562
588
  return video;
563
589
  };
@@ -650,6 +676,22 @@ function createHLSPluginWith(loader, variant, config) {
650
676
  return import_core.ErrorCode.PLAYBACK_FAILED;
651
677
  }
652
678
  };
679
+ const buildErrorDetail = (error, retriesExhausted) => {
680
+ const attempts = error.type === "network" ? networkRetryCount : error.type === "media" ? mediaRetryCount : 0;
681
+ const detail = {
682
+ type: error.type,
683
+ retriesExhausted,
684
+ attempts
685
+ };
686
+ if (typeof error.response?.code === "number" && error.response.code > 0) {
687
+ detail.httpStatus = error.response.code;
688
+ }
689
+ const url = sanitizeUrl(error.url);
690
+ if (url) {
691
+ detail.url = url;
692
+ }
693
+ return detail;
694
+ };
653
695
  const emitFatalError = (error, retriesExhausted) => {
654
696
  const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
655
697
  api?.logger.error(message, { type: error.type, details: error.details });
@@ -659,7 +701,8 @@ function createHLSPluginWith(loader, variant, config) {
659
701
  code: mapFatalErrorCode(error),
660
702
  message,
661
703
  fatal: true,
662
- timestamp: Date.now()
704
+ timestamp: Date.now(),
705
+ detail: buildErrorDetail(error, retriesExhausted)
663
706
  });
664
707
  maybeScheduleReconnect(error);
665
708
  };
@@ -736,6 +779,62 @@ function createHLSPluginWith(loader, variant, config) {
736
779
  }
737
780
  return false;
738
781
  };
782
+ const handleNativeFatalError = (error, resumePosition) => {
783
+ const is_network = error.type === "network";
784
+ const max_retries = is_network ? mergedConfig.maxNetworkRetries ?? 3 : mergedConfig.maxMediaRetries ?? 2;
785
+ const used = is_network ? networkRetryCount : mediaRetryCount;
786
+ if (!currentSrc || used >= max_retries) {
787
+ emitFatalError(error, used >= max_retries);
788
+ return;
789
+ }
790
+ const resume_position = resumePosition ?? video?.currentTime ?? 0;
791
+ if (is_network) {
792
+ networkRetryCount++;
793
+ } else {
794
+ mediaRetryCount++;
795
+ }
796
+ const attempt = used + 1;
797
+ const delay = getRetryDelay(attempt - 1);
798
+ api?.logger.info(
799
+ `Attempting native ${error.type} error recovery (attempt ${attempt}/${max_retries}) in ${delay}ms`
800
+ );
801
+ api?.emit(is_network ? "error:network" : "error:media", {
802
+ error: new Error(error.details)
803
+ });
804
+ if (retryTimeout) {
805
+ clearTimeout(retryTimeout);
806
+ }
807
+ const retry_session = loadSession;
808
+ retryTimeout = setTimeout(() => {
809
+ if (retry_session !== loadSession) return;
810
+ void recoverNative(error, resume_position);
811
+ }, delay);
812
+ };
813
+ const recoverNative = async (error, resumePosition) => {
814
+ if (!currentSrc) return;
815
+ const session = ++loadSession;
816
+ const saved_src = currentSrc;
817
+ const was_live = api?.getState("live") ?? false;
818
+ try {
819
+ teardownPipeline(new Error("HLS load cancelled: native error recovery"));
820
+ api?.setState("playbackState", "loading");
821
+ await loadNative(saved_src);
822
+ if (session !== loadSession) return;
823
+ if (!was_live && video && resumePosition > 0) {
824
+ video.currentTime = resumePosition;
825
+ }
826
+ api?.setState("playbackState", "ready");
827
+ api?.setState("buffering", false);
828
+ try {
829
+ await video?.play();
830
+ } catch {
831
+ }
832
+ } catch {
833
+ if (session !== loadSession) return;
834
+ api?.logger.warn("Native error recovery attempt failed");
835
+ handleNativeFatalError(error, resumePosition);
836
+ }
837
+ };
739
838
  const loadNative = async (src) => {
740
839
  const session = loadSession;
741
840
  const videoEl = getOrCreateVideo();
@@ -780,10 +879,21 @@ function createHLSPluginWith(loader, variant, config) {
780
879
  details: media_error?.message || "Native HLS playback error",
781
880
  fatal: true
782
881
  };
783
- emitFatalError(hls_error, false);
882
+ handleNativeFatalError(hls_error);
784
883
  };
785
884
  videoEl.addEventListener("error", onFatalVideoError);
786
- const removeFatalListener = () => videoEl.removeEventListener("error", onFatalVideoError);
885
+ const onPlayingResetBudget = () => {
886
+ if (networkRetryCount > 0 || mediaRetryCount > 0) {
887
+ api?.logger.debug("Native playback recovered, resetting retry budgets");
888
+ networkRetryCount = 0;
889
+ mediaRetryCount = 0;
890
+ }
891
+ };
892
+ videoEl.addEventListener("playing", onPlayingResetBudget);
893
+ const removeFatalListener = () => {
894
+ videoEl.removeEventListener("error", onFatalVideoError);
895
+ videoEl.removeEventListener("playing", onPlayingResetBudget);
896
+ };
787
897
  const previous_cleanup = cleanupVideoEvents;
788
898
  cleanupVideoEvents = () => {
789
899
  removeFatalListener();
@@ -908,12 +1018,38 @@ function createHLSPluginWith(loader, variant, config) {
908
1018
  reconnectAttempts = 0;
909
1019
  reconnectWindowStart = 0;
910
1020
  reconnectResumePosition = 0;
1021
+ reconnectTriggerError = null;
1022
+ reconnectExhausted = false;
1023
+ };
1024
+ const emitReconnectExhausted = (elapsedMs, windowMs) => {
1025
+ if (reconnectExhausted) return;
1026
+ reconnectExhausted = true;
1027
+ const attempts = reconnectAttempts;
1028
+ const trigger = reconnectTriggerError;
1029
+ api?.emit("error:reconnect-exhausted", { attempts, elapsedMs, windowMs });
1030
+ api?.setState("playbackState", "error");
1031
+ api?.setState("buffering", false);
1032
+ api?.emit("error", {
1033
+ code: trigger ? mapFatalErrorCode(trigger) : import_core.ErrorCode.PLAYBACK_FAILED,
1034
+ message: `HLS auto-reconnect gave up after ${attempts} attempts over ${Math.round(elapsedMs / 1e3)}s`,
1035
+ fatal: true,
1036
+ timestamp: Date.now(),
1037
+ detail: {
1038
+ type: trigger?.type ?? "other",
1039
+ retriesExhausted: true,
1040
+ attempts,
1041
+ reconnectExhausted: true
1042
+ }
1043
+ });
911
1044
  };
912
1045
  const scheduleReconnectAttempt = () => {
1046
+ if (reconnectExhausted) return;
913
1047
  if (reconnectTimer) return;
914
1048
  const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
915
- if (Date.now() - reconnectWindowStart > window_ms) {
1049
+ const elapsed_ms = Date.now() - reconnectWindowStart;
1050
+ if (elapsed_ms > window_ms) {
916
1051
  api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
1052
+ emitReconnectExhausted(elapsed_ms, window_ms);
917
1053
  return;
918
1054
  }
919
1055
  const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
@@ -921,7 +1057,12 @@ function createHLSPluginWith(loader, variant, config) {
921
1057
  const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
922
1058
  const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
923
1059
  api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
924
- api?.emit("error:reconnecting", { attempt: reconnectAttempts + 1, delayMs: delay });
1060
+ api?.emit("error:reconnecting", {
1061
+ attempt: reconnectAttempts + 1,
1062
+ delayMs: delay,
1063
+ elapsedMs: elapsed_ms,
1064
+ windowMs: window_ms
1065
+ });
925
1066
  reconnectTimer = setTimeout(() => {
926
1067
  reconnectTimer = null;
927
1068
  void attemptReconnect();
@@ -934,6 +1075,7 @@ function createHLSPluginWith(loader, variant, config) {
934
1075
  if (reconnectWindowStart === 0) {
935
1076
  reconnectWindowStart = Date.now();
936
1077
  reconnectResumePosition = video?.currentTime ?? 0;
1078
+ reconnectTriggerError = error;
937
1079
  }
938
1080
  scheduleReconnectAttempt();
939
1081
  };
@@ -965,7 +1107,10 @@ function createHLSPluginWith(loader, variant, config) {
965
1107
  }
966
1108
  api.setState("playbackState", "ready");
967
1109
  api.setState("buffering", false);
968
- api.emit("error:recovered", void 0);
1110
+ api.emit("error:recovered", {
1111
+ attempt: reconnectAttempts,
1112
+ elapsedMs: Date.now() - reconnectWindowStart
1113
+ });
969
1114
  api.logger.info("Auto-reconnect succeeded");
970
1115
  cancelReconnect();
971
1116
  try {
@@ -981,7 +1126,7 @@ function createHLSPluginWith(loader, variant, config) {
981
1126
  const plugin = {
982
1127
  id: "hls-provider",
983
1128
  name: variant.name,
984
- version: "1.0.0",
1129
+ version: PKG_VERSION,
985
1130
  type: "provider",
986
1131
  description: variant.description,
987
1132
  canPlay(src) {
@@ -1072,6 +1217,9 @@ function createHLSPluginWith(loader, variant, config) {
1072
1217
  };
1073
1218
  window.addEventListener("online", onlineListener);
1074
1219
  }
1220
+ const unsubPoster = api.subscribeToState((event) => {
1221
+ if (event.key === "poster") applyPoster();
1222
+ });
1075
1223
  api.onDestroy(() => {
1076
1224
  unsubPlay();
1077
1225
  unsubPause();
@@ -1080,6 +1228,7 @@ function createHLSPluginWith(loader, variant, config) {
1080
1228
  unsubMute();
1081
1229
  unsubRate();
1082
1230
  unsubQuality();
1231
+ unsubPoster();
1083
1232
  });
1084
1233
  },
1085
1234
  async destroy() {
@@ -1105,6 +1254,7 @@ function createHLSPluginWith(loader, variant, config) {
1105
1254
  hasPlayedContent = false;
1106
1255
  cleanup(new Error("HLS load cancelled: superseded by a new load"));
1107
1256
  currentSrc = src;
1257
+ applyPoster();
1108
1258
  api.setState("playbackState", "loading");
1109
1259
  api.setState("buffering", true);
1110
1260
  if (api.getState("airplayActive") && loader.supportsNativeHLS()) {
@@ -1256,5 +1406,6 @@ function createHLSPlugin(config) {
1256
1406
  var light_default = createHLSPlugin;
1257
1407
  // Annotate the CommonJS export names for ESM import in node:
1258
1408
  0 && (module.exports = {
1259
- createHLSPlugin
1409
+ createHLSPlugin,
1410
+ sanitizeUrl
1260
1411
  });
package/dist/light.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-D-HW4lmM.cjs';
2
- export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-D-HW4lmM.cjs';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './sanitize-url-DcZXD_K-.cjs';
2
+ export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel, s as sanitizeUrl } from './sanitize-url-DcZXD_K-.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-D-HW4lmM.js';
2
- export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-D-HW4lmM.js';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './sanitize-url-DcZXD_K-.js';
2
+ export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel, s as sanitizeUrl } from './sanitize-url-DcZXD_K-.js';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
package/dist/light.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  __export,
3
- createHLSPluginWith
4
- } from "./chunk-IFUZZQQE.js";
3
+ createHLSPluginWith,
4
+ sanitizeUrl
5
+ } from "./chunk-FN6QUW42.js";
5
6
 
6
7
  // src/hls-loader-light.ts
7
8
  var hls_loader_light_exports = {};
@@ -93,5 +94,6 @@ function createHLSPlugin(config) {
93
94
  var light_default = createHLSPlugin;
94
95
  export {
95
96
  createHLSPlugin,
96
- light_default as default
97
+ light_default as default,
98
+ sanitizeUrl
97
99
  };
@@ -22,9 +22,19 @@ interface HLSPluginConfig {
22
22
  backBufferLength?: number;
23
23
  /** Enable worker for hls.js (better performance) */
24
24
  enableWorker?: boolean;
25
- /** Max network error retries before giving up (default: 3) */
25
+ /**
26
+ * Max network error retries before giving up (default: 3).
27
+ *
28
+ * Governs both playback branches: hls.js retries the load, and the native
29
+ * (Safari/iOS) path reloads the source and restores position.
30
+ */
26
31
  maxNetworkRetries?: number;
27
- /** Max media error retries before giving up (default: 2) */
32
+ /**
33
+ * Max media error retries before giving up (default: 2).
34
+ *
35
+ * Governs both playback branches: hls.js calls recoverMediaError(), and the
36
+ * native (Safari/iOS) path reloads the source and restores position.
37
+ */
28
38
  maxMediaRetries?: number;
29
39
  /** Cap quality to player element dimensions (default: true) */
30
40
  capLevelToPlayerSize?: boolean;
@@ -128,4 +138,29 @@ interface IHLSPlugin extends Plugin<HLSPluginConfig> {
128
138
  switchToHlsJs(): Promise<void>;
129
139
  }
130
140
 
131
- export type { HLSPluginConfig as H, IHLSPlugin as I, HLSQualityLevel as a, HLSError as b, HLSLiveInfo as c };
141
+ /**
142
+ * URL sanitizer for error telemetry.
143
+ *
144
+ * Segment and playlist URLs are the single most useful field when
145
+ * diagnosing a dead stream, and the single most dangerous one to hand to a
146
+ * consumer's telemetry: signed-URL tokens, HMAC signatures, and session ids
147
+ * all live in the query string. Stripping the whole query (and fragment) is
148
+ * the privacy-safe default for EVERY consumer; anyone who genuinely needs
149
+ * the parameters can subscribe to hls.js directly.
150
+ */
151
+ /**
152
+ * Reduce a URL to origin + pathname, dropping the query string and fragment.
153
+ *
154
+ * @param url - Raw URL from a provider error, if any
155
+ * @returns Sanitized `origin + pathname`, or undefined when there is
156
+ * nothing usable to report (absent or unparseable URL)
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * sanitizeUrl('https://cdn.example.com/live/x.m3u8?token=secret#frag');
161
+ * // 'https://cdn.example.com/live/x.m3u8'
162
+ * ```
163
+ */
164
+ declare function sanitizeUrl(url: string | undefined | null): string | undefined;
165
+
166
+ export { type HLSPluginConfig as H, type IHLSPlugin as I, type HLSQualityLevel as a, type HLSError as b, type HLSLiveInfo as c, sanitizeUrl as s };
@@ -22,9 +22,19 @@ interface HLSPluginConfig {
22
22
  backBufferLength?: number;
23
23
  /** Enable worker for hls.js (better performance) */
24
24
  enableWorker?: boolean;
25
- /** Max network error retries before giving up (default: 3) */
25
+ /**
26
+ * Max network error retries before giving up (default: 3).
27
+ *
28
+ * Governs both playback branches: hls.js retries the load, and the native
29
+ * (Safari/iOS) path reloads the source and restores position.
30
+ */
26
31
  maxNetworkRetries?: number;
27
- /** Max media error retries before giving up (default: 2) */
32
+ /**
33
+ * Max media error retries before giving up (default: 2).
34
+ *
35
+ * Governs both playback branches: hls.js calls recoverMediaError(), and the
36
+ * native (Safari/iOS) path reloads the source and restores position.
37
+ */
28
38
  maxMediaRetries?: number;
29
39
  /** Cap quality to player element dimensions (default: true) */
30
40
  capLevelToPlayerSize?: boolean;
@@ -128,4 +138,29 @@ interface IHLSPlugin extends Plugin<HLSPluginConfig> {
128
138
  switchToHlsJs(): Promise<void>;
129
139
  }
130
140
 
131
- export type { HLSPluginConfig as H, IHLSPlugin as I, HLSQualityLevel as a, HLSError as b, HLSLiveInfo as c };
141
+ /**
142
+ * URL sanitizer for error telemetry.
143
+ *
144
+ * Segment and playlist URLs are the single most useful field when
145
+ * diagnosing a dead stream, and the single most dangerous one to hand to a
146
+ * consumer's telemetry: signed-URL tokens, HMAC signatures, and session ids
147
+ * all live in the query string. Stripping the whole query (and fragment) is
148
+ * the privacy-safe default for EVERY consumer; anyone who genuinely needs
149
+ * the parameters can subscribe to hls.js directly.
150
+ */
151
+ /**
152
+ * Reduce a URL to origin + pathname, dropping the query string and fragment.
153
+ *
154
+ * @param url - Raw URL from a provider error, if any
155
+ * @returns Sanitized `origin + pathname`, or undefined when there is
156
+ * nothing usable to report (absent or unparseable URL)
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * sanitizeUrl('https://cdn.example.com/live/x.m3u8?token=secret#frag');
161
+ * // 'https://cdn.example.com/live/x.m3u8'
162
+ * ```
163
+ */
164
+ declare function sanitizeUrl(url: string | undefined | null): string | undefined;
165
+
166
+ export { type HLSPluginConfig as H, type IHLSPlugin as I, type HLSQualityLevel as a, type HLSError as b, type HLSLiveInfo as c, sanitizeUrl as s };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/hls",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "HLS Provider Plugin for Scarlett Player",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,8 +22,8 @@
22
22
  "dist"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@scarlett-player/core": "^1.0.3",
26
- "hls.js": "^1.5.0"
25
+ "@scarlett-player/core": "^1.7.0",
26
+ "hls.js": "^1.6.0"
27
27
  },
28
28
  "peerDependenciesMeta": {
29
29
  "hls.js": {
@@ -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.6.0"
40
+ "@scarlett-player/core": "1.8.0"
41
41
  },
42
42
  "keywords": [
43
43
  "video",
@@ -58,10 +58,11 @@
58
58
  },
59
59
  "homepage": "https://scarlettplayer.com",
60
60
  "scripts": {
61
- "build": "tsup src/index.ts src/light.ts --format esm,cjs --dts",
62
- "dev": "tsup src/index.ts src/light.ts --format esm,cjs --dts --watch",
61
+ "build": "tsup",
62
+ "dev": "tsup --watch",
63
63
  "test": "vitest --run",
64
64
  "test:watch": "vitest",
65
- "test:coverage": "vitest --coverage"
65
+ "test:coverage": "vitest --coverage",
66
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
66
67
  }
67
68
  }