@codenkay/video-nsgplayer-core 3.0.0 → 3.0.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/index.d.cts CHANGED
@@ -151,6 +151,12 @@ type StreamingConfig = {
151
151
  * Use when the BFF cannot reach Appextech (Cloudflare challenge on server IP).
152
152
  */
153
153
  getKeyAccessToken?: () => string | null | undefined;
154
+ /**
155
+ * Max same-origin key-proxy failures before the session permanently switches
156
+ * to browser Bearer / signed-query (default 2). Prevents hls.js from hammering
157
+ * a Cloudflare-blocked BFF (9–10× 502s).
158
+ */
159
+ keyProxyMaxFailures?: number;
154
160
  };
155
161
  type LoggingConfig = {
156
162
  enabled?: boolean;
@@ -198,6 +204,7 @@ type PlayerConfig = Required<{
198
204
  hls: Required<NonNullable<StreamingConfig["hls"]>>;
199
205
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
200
206
  getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
207
+ keyProxyMaxFailures?: StreamingConfig["keyProxyMaxFailures"];
201
208
  };
202
209
  logging: Required<LoggingConfig>;
203
210
  debug: Required<DebugConfig>;
@@ -279,7 +286,7 @@ type PlayerEventMap = {
279
286
  };
280
287
 
281
288
  declare const PLAYBACK_RATES: readonly [0.5, 0.75, 1, 1.25, 1.5, 2];
282
- declare const PACKAGE_VERSION = "2.0.1";
289
+ declare const PACKAGE_VERSION = "3.0.1";
283
290
  declare const DEFAULT_PLAYER_BEHAVIOR: Required<PlayerBehaviorConfig>;
284
291
  declare const DEFAULT_CONTROLS: Required<ControlsConfig>;
285
292
  declare const DEFAULT_THEME: Required<ThemeConfig>;
@@ -312,6 +319,7 @@ type PlayerConfigPartial = Partial<{
312
319
  hls?: Partial<PlayerConfig["streaming"]["hls"]>;
313
320
  keyProxyUrlBuilder?: PlayerConfig["streaming"]["keyProxyUrlBuilder"];
314
321
  getKeyAccessToken?: PlayerConfig["streaming"]["getKeyAccessToken"];
322
+ keyProxyMaxFailures?: PlayerConfig["streaming"]["keyProxyMaxFailures"];
315
323
  };
316
324
  logging: Partial<PlayerConfig["logging"]>;
317
325
  debug: Partial<PlayerConfig["debug"]>;
@@ -431,6 +439,7 @@ type HlsEngineOptions = {
431
439
  videoId?: string;
432
440
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
433
441
  getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
442
+ keyProxyMaxFailures?: StreamingConfig["keyProxyMaxFailures"];
434
443
  hlsConfig: {
435
444
  enableWorker: boolean;
436
445
  lowLatencyMode: boolean;
@@ -646,15 +655,21 @@ declare class RefreshManager {
646
655
 
647
656
  type HlsLoaderInstance = {
648
657
  stats: LoaderStats;
658
+ context: LoaderContext | null;
649
659
  load(context: LoaderContext, config: LoaderConfiguration, callbacks: LoaderCallbacks<LoaderContext>): void;
650
660
  destroy(): void;
651
- abort?: () => void;
661
+ abort(): void;
652
662
  };
653
- type HlsLoader = new (config: object) => HlsLoaderInstance;
663
+ /** Constructor shape compatible with `Hls.DefaultConfig.loader`. */
664
+ type HlsLoaderConstructor = new (config: object) => HlsLoaderInstance;
665
+ /** Max same-origin BFF key attempts before the session circuit opens. */
666
+ declare const KEY_PROXY_MAX_FAILURES = 2;
654
667
  type AuthParamsLoaderOptions = {
655
668
  videoId?: string;
656
669
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
657
670
  getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
671
+ /** Override max BFF key failures before permanent browser fallback (default 2). */
672
+ keyProxyMaxFailures?: number;
658
673
  };
659
674
  declare function isEncryptionKeyUrl(rawUrl: string, baseOrigin?: string): boolean;
660
675
  /**
@@ -664,26 +679,24 @@ declare function isEncryptionKeyUrl(rawUrl: string, baseOrigin?: string): boolea
664
679
  * custom wrapper, `extends` / nested `inner.load()` chains blow Firefox's
665
680
  * stack (`InternalError: too much recursion`).
666
681
  */
667
- declare function resolveBuiltinHlsLoader(HlsClass: typeof Hls): HlsLoader;
682
+ declare function resolveBuiltinHlsLoader(HlsClass: typeof Hls): HlsLoaderConstructor;
668
683
  /**
669
684
  * Rewrites an HLS request URL so signed auth query params are present.
670
685
  *
671
686
  * AES `enc.key` handling (enterprise priority order):
672
687
  * 1. `keyProxyUrlBuilder` — same-origin BFF (production; server IP must pass CDN/WAF)
673
688
  * 2. Otherwise keep CDN/API URL and apply signed `token`/`expires` (and related) query
674
- * params — same auth model as segments on Appextech CDN hosts
675
689
  * 3. Optional `getKeyAccessToken` is applied at load-time (Bearer), not in this rewrite
676
690
  */
677
691
  declare function rewriteHlsRequestUrl(rawUrl: string, store: AuthParamsStore, options?: AuthParamsLoaderOptions, baseOrigin?: string): string;
678
692
  /**
679
693
  * hls.js loader that:
680
694
  * - Re-applies signed auth query params on every media request
681
- * - Loads AES `enc.key` via BFF / optional Bearer fetch / signed-query XHR
682
- *
683
- * Uses composition (not `extends DefaultConfig.loader`) so repeated mounts,
684
- * HMR, or a polluted DefaultConfig cannot nest `super.load()` into recursion.
695
+ * - Loads AES `enc.key` via controlled BFF (≤2) → Bearer → query XHR
696
+ * with a **session circuit breaker** so a dead Cloudflare BFF is not
697
+ * hammered by hls.js retries (which previously caused 9–10× 502s).
685
698
  */
686
- declare function createAuthParamsLoader(HlsClass: typeof Hls, store: AuthParamsStore, options?: AuthParamsLoaderOptions): HlsLoader;
699
+ declare function createAuthParamsLoader(HlsClass: typeof Hls, store: AuthParamsStore, options?: AuthParamsLoaderOptions): HlsLoaderConstructor;
687
700
 
688
701
  declare function redactValue(key: string, value: unknown, seen?: WeakSet<object>, depth?: number): unknown;
689
702
  /**
@@ -692,4 +705,4 @@ declare function redactValue(key: string, value: unknown, seen?: WeakSet<object>
692
705
  */
693
706
  declare function redactObject(obj: Record<string, unknown>, seen?: WeakSet<object>, depth?: number): Record<string, unknown>;
694
707
 
695
- export { type AccessibilityConfig, type AnalyticsConfig, type AuthConfig, AuthManager, AuthParamsStore, type AuthToken, type AuthTokenProviderContext, type BufferedRange, type ControlsConfig, type CreateVideoPlayerOptions, DEFAULT_CONTROLS, DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS, DEFAULT_PLAYER_BEHAVIOR, DEFAULT_PLAYER_CONFIG, DEFAULT_THEME, DEFAULT_WATERMARK, type DebugConfig, EventEmitter, type HlsQualityLevel, type HlsSupportMode, type LiveMediaSnapshot, type LoggingConfig, PACKAGE_VERSION, PLAYBACK_RATES, type PlayIconPreset, type PlaybackCredentialConfig, type PlaybackRate, type PlaybackSource, type PlaybackSourceContext, PlaybackSourceManager, type PlayerAudioTrack, type PlayerBehaviorConfig, type PlayerConfig, type PlayerDiagnostics, type PlayerError, PlayerErrorCode, type PlayerEventMap, type PlayerSkipSeconds, type PlayerState, type PlayerTextTrack, type PlayerZoomLevel, type QualitySelection, RefreshManager, SECRET_SCAN_MAX_DEPTH, type SeekToLiveResult, type StreamingConfig, type TextTrackSelection, type ThemeConfig, type VideoPlayer, type WatermarkConfig, assertNoSecretsInConfig, createAuthParamsLoader, createPlayerError, createVideoPlayer, getLiveEdgeTime, isAtLiveEdge, isEncryptionKeyUrl, isPlainObject, mergePlayerConfig, redactObject, redactSensitive, redactValue, resolveBuiltinHlsLoader, rewriteHlsRequestUrl, seekToLiveEdge, themeToCssVars };
708
+ export { type AccessibilityConfig, type AnalyticsConfig, type AuthConfig, AuthManager, AuthParamsStore, type AuthToken, type AuthTokenProviderContext, type BufferedRange, type ControlsConfig, type CreateVideoPlayerOptions, DEFAULT_CONTROLS, DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS, DEFAULT_PLAYER_BEHAVIOR, DEFAULT_PLAYER_CONFIG, DEFAULT_THEME, DEFAULT_WATERMARK, type DebugConfig, EventEmitter, type HlsQualityLevel, type HlsSupportMode, KEY_PROXY_MAX_FAILURES, type LiveMediaSnapshot, type LoggingConfig, PACKAGE_VERSION, PLAYBACK_RATES, type PlayIconPreset, type PlaybackCredentialConfig, type PlaybackRate, type PlaybackSource, type PlaybackSourceContext, PlaybackSourceManager, type PlayerAudioTrack, type PlayerBehaviorConfig, type PlayerConfig, type PlayerDiagnostics, type PlayerError, PlayerErrorCode, type PlayerEventMap, type PlayerSkipSeconds, type PlayerState, type PlayerTextTrack, type PlayerZoomLevel, type QualitySelection, RefreshManager, SECRET_SCAN_MAX_DEPTH, type SeekToLiveResult, type StreamingConfig, type TextTrackSelection, type ThemeConfig, type VideoPlayer, type WatermarkConfig, assertNoSecretsInConfig, createAuthParamsLoader, createPlayerError, createVideoPlayer, getLiveEdgeTime, isAtLiveEdge, isEncryptionKeyUrl, isPlainObject, mergePlayerConfig, redactObject, redactSensitive, redactValue, resolveBuiltinHlsLoader, rewriteHlsRequestUrl, seekToLiveEdge, themeToCssVars };
package/dist/index.d.ts CHANGED
@@ -151,6 +151,12 @@ type StreamingConfig = {
151
151
  * Use when the BFF cannot reach Appextech (Cloudflare challenge on server IP).
152
152
  */
153
153
  getKeyAccessToken?: () => string | null | undefined;
154
+ /**
155
+ * Max same-origin key-proxy failures before the session permanently switches
156
+ * to browser Bearer / signed-query (default 2). Prevents hls.js from hammering
157
+ * a Cloudflare-blocked BFF (9–10× 502s).
158
+ */
159
+ keyProxyMaxFailures?: number;
154
160
  };
155
161
  type LoggingConfig = {
156
162
  enabled?: boolean;
@@ -198,6 +204,7 @@ type PlayerConfig = Required<{
198
204
  hls: Required<NonNullable<StreamingConfig["hls"]>>;
199
205
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
200
206
  getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
207
+ keyProxyMaxFailures?: StreamingConfig["keyProxyMaxFailures"];
201
208
  };
202
209
  logging: Required<LoggingConfig>;
203
210
  debug: Required<DebugConfig>;
@@ -279,7 +286,7 @@ type PlayerEventMap = {
279
286
  };
280
287
 
281
288
  declare const PLAYBACK_RATES: readonly [0.5, 0.75, 1, 1.25, 1.5, 2];
282
- declare const PACKAGE_VERSION = "2.0.1";
289
+ declare const PACKAGE_VERSION = "3.0.1";
283
290
  declare const DEFAULT_PLAYER_BEHAVIOR: Required<PlayerBehaviorConfig>;
284
291
  declare const DEFAULT_CONTROLS: Required<ControlsConfig>;
285
292
  declare const DEFAULT_THEME: Required<ThemeConfig>;
@@ -312,6 +319,7 @@ type PlayerConfigPartial = Partial<{
312
319
  hls?: Partial<PlayerConfig["streaming"]["hls"]>;
313
320
  keyProxyUrlBuilder?: PlayerConfig["streaming"]["keyProxyUrlBuilder"];
314
321
  getKeyAccessToken?: PlayerConfig["streaming"]["getKeyAccessToken"];
322
+ keyProxyMaxFailures?: PlayerConfig["streaming"]["keyProxyMaxFailures"];
315
323
  };
316
324
  logging: Partial<PlayerConfig["logging"]>;
317
325
  debug: Partial<PlayerConfig["debug"]>;
@@ -431,6 +439,7 @@ type HlsEngineOptions = {
431
439
  videoId?: string;
432
440
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
433
441
  getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
442
+ keyProxyMaxFailures?: StreamingConfig["keyProxyMaxFailures"];
434
443
  hlsConfig: {
435
444
  enableWorker: boolean;
436
445
  lowLatencyMode: boolean;
@@ -646,15 +655,21 @@ declare class RefreshManager {
646
655
 
647
656
  type HlsLoaderInstance = {
648
657
  stats: LoaderStats;
658
+ context: LoaderContext | null;
649
659
  load(context: LoaderContext, config: LoaderConfiguration, callbacks: LoaderCallbacks<LoaderContext>): void;
650
660
  destroy(): void;
651
- abort?: () => void;
661
+ abort(): void;
652
662
  };
653
- type HlsLoader = new (config: object) => HlsLoaderInstance;
663
+ /** Constructor shape compatible with `Hls.DefaultConfig.loader`. */
664
+ type HlsLoaderConstructor = new (config: object) => HlsLoaderInstance;
665
+ /** Max same-origin BFF key attempts before the session circuit opens. */
666
+ declare const KEY_PROXY_MAX_FAILURES = 2;
654
667
  type AuthParamsLoaderOptions = {
655
668
  videoId?: string;
656
669
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
657
670
  getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
671
+ /** Override max BFF key failures before permanent browser fallback (default 2). */
672
+ keyProxyMaxFailures?: number;
658
673
  };
659
674
  declare function isEncryptionKeyUrl(rawUrl: string, baseOrigin?: string): boolean;
660
675
  /**
@@ -664,26 +679,24 @@ declare function isEncryptionKeyUrl(rawUrl: string, baseOrigin?: string): boolea
664
679
  * custom wrapper, `extends` / nested `inner.load()` chains blow Firefox's
665
680
  * stack (`InternalError: too much recursion`).
666
681
  */
667
- declare function resolveBuiltinHlsLoader(HlsClass: typeof Hls): HlsLoader;
682
+ declare function resolveBuiltinHlsLoader(HlsClass: typeof Hls): HlsLoaderConstructor;
668
683
  /**
669
684
  * Rewrites an HLS request URL so signed auth query params are present.
670
685
  *
671
686
  * AES `enc.key` handling (enterprise priority order):
672
687
  * 1. `keyProxyUrlBuilder` — same-origin BFF (production; server IP must pass CDN/WAF)
673
688
  * 2. Otherwise keep CDN/API URL and apply signed `token`/`expires` (and related) query
674
- * params — same auth model as segments on Appextech CDN hosts
675
689
  * 3. Optional `getKeyAccessToken` is applied at load-time (Bearer), not in this rewrite
676
690
  */
677
691
  declare function rewriteHlsRequestUrl(rawUrl: string, store: AuthParamsStore, options?: AuthParamsLoaderOptions, baseOrigin?: string): string;
678
692
  /**
679
693
  * hls.js loader that:
680
694
  * - Re-applies signed auth query params on every media request
681
- * - Loads AES `enc.key` via BFF / optional Bearer fetch / signed-query XHR
682
- *
683
- * Uses composition (not `extends DefaultConfig.loader`) so repeated mounts,
684
- * HMR, or a polluted DefaultConfig cannot nest `super.load()` into recursion.
695
+ * - Loads AES `enc.key` via controlled BFF (≤2) → Bearer → query XHR
696
+ * with a **session circuit breaker** so a dead Cloudflare BFF is not
697
+ * hammered by hls.js retries (which previously caused 9–10× 502s).
685
698
  */
686
- declare function createAuthParamsLoader(HlsClass: typeof Hls, store: AuthParamsStore, options?: AuthParamsLoaderOptions): HlsLoader;
699
+ declare function createAuthParamsLoader(HlsClass: typeof Hls, store: AuthParamsStore, options?: AuthParamsLoaderOptions): HlsLoaderConstructor;
687
700
 
688
701
  declare function redactValue(key: string, value: unknown, seen?: WeakSet<object>, depth?: number): unknown;
689
702
  /**
@@ -692,4 +705,4 @@ declare function redactValue(key: string, value: unknown, seen?: WeakSet<object>
692
705
  */
693
706
  declare function redactObject(obj: Record<string, unknown>, seen?: WeakSet<object>, depth?: number): Record<string, unknown>;
694
707
 
695
- export { type AccessibilityConfig, type AnalyticsConfig, type AuthConfig, AuthManager, AuthParamsStore, type AuthToken, type AuthTokenProviderContext, type BufferedRange, type ControlsConfig, type CreateVideoPlayerOptions, DEFAULT_CONTROLS, DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS, DEFAULT_PLAYER_BEHAVIOR, DEFAULT_PLAYER_CONFIG, DEFAULT_THEME, DEFAULT_WATERMARK, type DebugConfig, EventEmitter, type HlsQualityLevel, type HlsSupportMode, type LiveMediaSnapshot, type LoggingConfig, PACKAGE_VERSION, PLAYBACK_RATES, type PlayIconPreset, type PlaybackCredentialConfig, type PlaybackRate, type PlaybackSource, type PlaybackSourceContext, PlaybackSourceManager, type PlayerAudioTrack, type PlayerBehaviorConfig, type PlayerConfig, type PlayerDiagnostics, type PlayerError, PlayerErrorCode, type PlayerEventMap, type PlayerSkipSeconds, type PlayerState, type PlayerTextTrack, type PlayerZoomLevel, type QualitySelection, RefreshManager, SECRET_SCAN_MAX_DEPTH, type SeekToLiveResult, type StreamingConfig, type TextTrackSelection, type ThemeConfig, type VideoPlayer, type WatermarkConfig, assertNoSecretsInConfig, createAuthParamsLoader, createPlayerError, createVideoPlayer, getLiveEdgeTime, isAtLiveEdge, isEncryptionKeyUrl, isPlainObject, mergePlayerConfig, redactObject, redactSensitive, redactValue, resolveBuiltinHlsLoader, rewriteHlsRequestUrl, seekToLiveEdge, themeToCssVars };
708
+ export { type AccessibilityConfig, type AnalyticsConfig, type AuthConfig, AuthManager, AuthParamsStore, type AuthToken, type AuthTokenProviderContext, type BufferedRange, type ControlsConfig, type CreateVideoPlayerOptions, DEFAULT_CONTROLS, DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS, DEFAULT_PLAYER_BEHAVIOR, DEFAULT_PLAYER_CONFIG, DEFAULT_THEME, DEFAULT_WATERMARK, type DebugConfig, EventEmitter, type HlsQualityLevel, type HlsSupportMode, KEY_PROXY_MAX_FAILURES, type LiveMediaSnapshot, type LoggingConfig, PACKAGE_VERSION, PLAYBACK_RATES, type PlayIconPreset, type PlaybackCredentialConfig, type PlaybackRate, type PlaybackSource, type PlaybackSourceContext, PlaybackSourceManager, type PlayerAudioTrack, type PlayerBehaviorConfig, type PlayerConfig, type PlayerDiagnostics, type PlayerError, PlayerErrorCode, type PlayerEventMap, type PlayerSkipSeconds, type PlayerState, type PlayerTextTrack, type PlayerZoomLevel, type QualitySelection, RefreshManager, SECRET_SCAN_MAX_DEPTH, type SeekToLiveResult, type StreamingConfig, type TextTrackSelection, type ThemeConfig, type VideoPlayer, type WatermarkConfig, assertNoSecretsInConfig, createAuthParamsLoader, createPlayerError, createVideoPlayer, getLiveEdgeTime, isAtLiveEdge, isEncryptionKeyUrl, isPlainObject, mergePlayerConfig, redactObject, redactSensitive, redactValue, resolveBuiltinHlsLoader, rewriteHlsRequestUrl, seekToLiveEdge, themeToCssVars };
package/dist/index.js CHANGED
@@ -146,7 +146,7 @@ var AuthManager = class {
146
146
 
147
147
  // src/config/defaults.ts
148
148
  var PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
149
- var PACKAGE_VERSION = "2.0.1";
149
+ var PACKAGE_VERSION = "3.0.1";
150
150
  var DEFAULT_PLAYER_BEHAVIOR = {
151
151
  autoplay: true,
152
152
  startMuted: true,
@@ -325,7 +325,8 @@ function mergePlayerConfig(partial) {
325
325
  ...partial?.streaming?.hls
326
326
  },
327
327
  keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder,
328
- getKeyAccessToken: partial?.streaming?.getKeyAccessToken
328
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken,
329
+ keyProxyMaxFailures: partial?.streaming?.keyProxyMaxFailures
329
330
  },
330
331
  logging: { ...DEFAULT_PLAYER_CONFIG.logging, ...partial?.logging },
331
332
  debug: { ...DEFAULT_PLAYER_CONFIG.debug, ...partial?.debug },
@@ -768,6 +769,7 @@ function seekToLiveEdge(media, options) {
768
769
  import { XhrLoader as BuiltinXhrLoader } from "hls.js";
769
770
  var ENC_KEY_PATH_RE = /enc\.key$/i;
770
771
  var BUILTIN_LOADER_NAMES = /* @__PURE__ */ new Set(["XhrLoader", "FetchLoader"]);
772
+ var KEY_PROXY_MAX_FAILURES = 2;
771
773
  function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
772
774
  try {
773
775
  return ENC_KEY_PATH_RE.test(new URL(rawUrl, baseOrigin).pathname);
@@ -828,6 +830,20 @@ function createLoaderStats() {
828
830
  buffering: { start: 0, first: 0, end: 0 }
829
831
  };
830
832
  }
833
+ function isProxyUpstreamBlocked(status, contentType, bodyHead, errorText) {
834
+ if (status === 0 || status === 502 || status === 503 || status === 504) {
835
+ return true;
836
+ }
837
+ if (status === 403 || status === 401) return true;
838
+ if (contentType?.includes("text/html")) return true;
839
+ if (/<!DOCTYPE|<html|Just a moment|Cloudflare|blocked/i.test(bodyHead)) {
840
+ return true;
841
+ }
842
+ if (errorText && /Cloudflare|HTML|DOCTYPE|blocked|502/i.test(errorText)) {
843
+ return true;
844
+ }
845
+ return false;
846
+ }
831
847
  async function loadEncryptionKeyWithFetch(context, callbacks, accessToken, signal) {
832
848
  const stats = createLoaderStats();
833
849
  stats.loading.first = performance.now();
@@ -886,10 +902,122 @@ async function loadEncryptionKeyWithFetch(context, callbacks, accessToken, signa
886
902
  response
887
903
  );
888
904
  }
905
+ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state) {
906
+ const stats = createLoaderStats();
907
+ stats.loading.first = performance.now();
908
+ const canTryProxy = Boolean(plan.proxyUrl) && !state.circuitOpen && state.proxyFailures < plan.maxProxyFailures;
909
+ if (canTryProxy && plan.proxyUrl) {
910
+ while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures && !signal.aborted) {
911
+ state.proxyFailures += 1;
912
+ try {
913
+ const response = await fetch(plan.proxyUrl, {
914
+ method: "GET",
915
+ headers: { Accept: "*/*" },
916
+ credentials: "same-origin",
917
+ cache: "no-store",
918
+ signal
919
+ });
920
+ const contentType = response.headers.get("content-type");
921
+ const data = await response.arrayBuffer();
922
+ const head = new TextDecoder().decode(data.slice(0, 64));
923
+ if (response.ok && !isProxyUpstreamBlocked(response.status, contentType, head)) {
924
+ const end = performance.now();
925
+ stats.loading.end = end;
926
+ stats.loaded = data.byteLength;
927
+ stats.total = data.byteLength;
928
+ callbacks.onSuccess(
929
+ {
930
+ url: response.url || plan.proxyUrl,
931
+ data,
932
+ code: response.status
933
+ },
934
+ stats,
935
+ { ...context, url: plan.proxyUrl },
936
+ response
937
+ );
938
+ return "ok";
939
+ }
940
+ if (isProxyUpstreamBlocked(
941
+ response.status,
942
+ contentType,
943
+ head,
944
+ response.statusText
945
+ )) {
946
+ if (response.status === 502 || contentType?.includes("text/html") || /Cloudflare|DOCTYPE|blocked/i.test(head)) {
947
+ state.circuitOpen = true;
948
+ break;
949
+ }
950
+ continue;
951
+ }
952
+ callbacks.onError(
953
+ {
954
+ code: response.status,
955
+ text: `enc.key proxy HTTP ${response.status}`
956
+ },
957
+ { ...context, url: plan.proxyUrl },
958
+ response,
959
+ stats
960
+ );
961
+ return "ok";
962
+ } catch (error) {
963
+ if (error instanceof DOMException && error.name === "AbortError") {
964
+ callbacks.onError(
965
+ { code: 0, text: "enc.key proxy aborted/timeout" },
966
+ context,
967
+ null,
968
+ stats
969
+ );
970
+ return "ok";
971
+ }
972
+ state.circuitOpen = true;
973
+ break;
974
+ }
975
+ }
976
+ state.circuitOpen = true;
977
+ }
978
+ if (plan.accessToken) {
979
+ try {
980
+ await loadEncryptionKeyWithFetch(
981
+ { ...context, url: plan.signedKeyUrl },
982
+ callbacks,
983
+ plan.accessToken,
984
+ signal
985
+ );
986
+ return "ok";
987
+ } catch (error) {
988
+ if (error instanceof DOMException && error.name === "AbortError") {
989
+ callbacks.onError(
990
+ { code: 0, text: "enc.key fetch aborted/timeout" },
991
+ { ...context, url: plan.signedKeyUrl },
992
+ null,
993
+ stats
994
+ );
995
+ return "ok";
996
+ }
997
+ if (error instanceof TypeError || error instanceof Error && /Failed to fetch|NetworkError|CORS/i.test(error.message)) {
998
+ return "try_xhr";
999
+ }
1000
+ callbacks.onError(
1001
+ {
1002
+ code: 0,
1003
+ text: error instanceof Error ? error.message : "enc.key Bearer fetch failed"
1004
+ },
1005
+ { ...context, url: plan.signedKeyUrl },
1006
+ null,
1007
+ stats
1008
+ );
1009
+ return "ok";
1010
+ }
1011
+ }
1012
+ return "try_xhr";
1013
+ }
889
1014
  function createAuthParamsLoader(HlsClass, store, options = {}) {
890
1015
  const BaseLoader = resolveBuiltinHlsLoader(HlsClass);
1016
+ const maxProxyFailures = typeof options.keyProxyMaxFailures === "number" && options.keyProxyMaxFailures >= 0 ? options.keyProxyMaxFailures : KEY_PROXY_MAX_FAILURES;
1017
+ const keyProxyState = { proxyFailures: 0, circuitOpen: false };
891
1018
  return class AuthParamsLoader {
892
1019
  constructor(config) {
1020
+ this.context = null;
893
1021
  this.keyAbort = null;
894
1022
  this.inner = new BaseLoader(config);
895
1023
  this.stats = this.inner.stats;
@@ -897,14 +1025,16 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
897
1025
  abort() {
898
1026
  this.keyAbort?.abort();
899
1027
  this.keyAbort = null;
900
- this.inner.abort?.();
1028
+ this.inner.abort();
901
1029
  }
902
1030
  destroy() {
903
1031
  this.keyAbort?.abort();
904
1032
  this.keyAbort = null;
1033
+ this.context = null;
905
1034
  this.inner.destroy();
906
1035
  }
907
1036
  load(context, config, callbacks) {
1037
+ this.context = context;
908
1038
  if (!context.url) {
909
1039
  this.inner.load(context, config, callbacks);
910
1040
  return;
@@ -912,87 +1042,59 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
912
1042
  try {
913
1043
  const originalUrl = context.url;
914
1044
  const isKey = isEncryptionKeyUrl(originalUrl);
915
- const usingKeyProxy = Boolean(
916
- isKey && options.keyProxyUrlBuilder && options.videoId
917
- );
918
- const signedKeyUrl = isKey ? rewriteHlsRequestUrl(originalUrl, store, {
919
- ...options,
920
- keyProxyUrlBuilder: void 0
921
- }) : context.url;
922
- context.url = rewriteHlsRequestUrl(originalUrl, store, options);
923
- if (usingKeyProxy) {
924
- const accessToken = options.getKeyAccessToken?.() ?? null;
925
- if (!accessToken) {
926
- this.inner.load(context, config, callbacks);
927
- return;
1045
+ if (!isKey) {
1046
+ if (store.hasParams()) {
1047
+ context.url = rewriteHlsRequestUrl(originalUrl, store, {
1048
+ ...options,
1049
+ keyProxyUrlBuilder: void 0
1050
+ });
928
1051
  }
929
- const proxyCallbacks = {
930
- ...callbacks,
931
- onError: (error, errContext, networkDetails, stats) => {
932
- const code = error?.code;
933
- const retryable = code === 0 || code === 502 || code === 403 || code === 401 || /Cloudflare|HTML|DOCTYPE|blocked/i.test(error?.text ?? "");
934
- if (!retryable) {
935
- callbacks.onError(error, errContext, networkDetails, stats);
936
- return;
937
- }
938
- this.keyAbort?.abort();
939
- this.keyAbort = new AbortController();
940
- const bearerContext = { ...context, url: signedKeyUrl };
941
- void loadEncryptionKeyWithFetch(
942
- bearerContext,
943
- callbacks,
944
- accessToken,
945
- this.keyAbort.signal
946
- ).catch((fetchError) => {
947
- callbacks.onError(
948
- {
949
- code: 0,
950
- text: fetchError instanceof Error ? fetchError.message : error.text || "enc.key proxy and Bearer fetch failed"
951
- },
952
- bearerContext,
953
- null,
954
- stats
955
- );
956
- });
957
- }
958
- };
959
- this.inner.load(context, config, proxyCallbacks);
1052
+ this.inner.load(context, config, callbacks);
960
1053
  return;
961
1054
  }
962
- if (isKey && typeof options.getKeyAccessToken === "function") {
963
- const accessToken = options.getKeyAccessToken() ?? null;
964
- if (accessToken) {
965
- this.keyAbort?.abort();
966
- this.keyAbort = new AbortController();
967
- const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
968
- const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
969
- void loadEncryptionKeyWithFetch(
970
- context,
971
- callbacks,
972
- accessToken,
973
- this.keyAbort.signal
974
- ).catch((error) => {
975
- const stats = createLoaderStats();
976
- const aborted = error instanceof DOMException && error.name === "AbortError";
977
- if (!aborted && (error instanceof TypeError || error instanceof Error && /Failed to fetch|NetworkError|CORS/i.test(error.message))) {
978
- this.inner.load(context, config, callbacks);
979
- return;
980
- }
981
- callbacks.onError(
982
- {
983
- code: 0,
984
- text: aborted ? "enc.key fetch aborted/timeout" : error instanceof Error ? error.message : "enc.key fetch failed"
985
- },
986
- context,
987
- null,
988
- stats
989
- );
990
- }).finally(() => {
991
- clearTimeout(timer);
992
- });
993
- return;
1055
+ const signedKeyUrl = rewriteHlsRequestUrl(originalUrl, store, {
1056
+ ...options,
1057
+ keyProxyUrlBuilder: void 0
1058
+ });
1059
+ const proxyConfigured = Boolean(
1060
+ options.keyProxyUrlBuilder && options.videoId
1061
+ );
1062
+ const proxyUrl = proxyConfigured && !keyProxyState.circuitOpen ? rewriteHlsRequestUrl(originalUrl, store, options) : null;
1063
+ const accessToken = options.getKeyAccessToken?.() ?? null;
1064
+ this.keyAbort?.abort();
1065
+ this.keyAbort = new AbortController();
1066
+ const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
1067
+ const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
1068
+ void loadEncryptionKeyEnterprise(
1069
+ context,
1070
+ callbacks,
1071
+ {
1072
+ proxyUrl,
1073
+ signedKeyUrl,
1074
+ accessToken,
1075
+ maxProxyFailures
1076
+ },
1077
+ this.keyAbort.signal,
1078
+ keyProxyState
1079
+ ).then((result) => {
1080
+ if (result === "try_xhr") {
1081
+ context.url = signedKeyUrl;
1082
+ this.inner.load(context, config, callbacks);
994
1083
  }
995
- }
1084
+ }).catch((error) => {
1085
+ callbacks.onError(
1086
+ {
1087
+ code: 0,
1088
+ text: error instanceof Error ? error.message : "enc.key load failed"
1089
+ },
1090
+ context,
1091
+ null,
1092
+ createLoaderStats()
1093
+ );
1094
+ }).finally(() => {
1095
+ clearTimeout(timer);
1096
+ });
1097
+ return;
996
1098
  } catch {
997
1099
  }
998
1100
  this.inner.load(context, config, callbacks);
@@ -1062,7 +1164,8 @@ var HlsEngine = class {
1062
1164
  const loader = createAuthParamsLoader(Hls, authParams, {
1063
1165
  videoId,
1064
1166
  keyProxyUrlBuilder,
1065
- getKeyAccessToken
1167
+ getKeyAccessToken,
1168
+ keyProxyMaxFailures: this.options.keyProxyMaxFailures
1066
1169
  });
1067
1170
  this.hls = new Hls({
1068
1171
  enableWorker: hlsConfig.enableWorker,
@@ -1487,10 +1590,11 @@ function createVideoPlayer(options) {
1487
1590
  videoId: options.videoId,
1488
1591
  keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1489
1592
  // Enterprise AES key auth:
1490
- // 1) keyProxyUrlBuilder (same-origin BFF) when configured
1593
+ // 1) keyProxyUrlBuilder (same-origin BFF) when configured — max 2 failures
1491
1594
  // 2) else getKeyAccessToken / AuthManager Bearer via browser fetch
1492
1595
  // 3) else signed query + XHR (CDN hosts)
1493
1596
  getKeyAccessToken: config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1597
+ keyProxyMaxFailures: config.streaming.keyProxyMaxFailures,
1494
1598
  hlsConfig: config.streaming.hls,
1495
1599
  logger,
1496
1600
  callbacks: {
@@ -1808,7 +1912,8 @@ function createVideoPlayer(options) {
1808
1912
  engine: "hls",
1809
1913
  hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
1810
1914
  keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder,
1811
- getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken())
1915
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1916
+ keyProxyMaxFailures: partial?.streaming?.keyProxyMaxFailures ?? config.streaming.keyProxyMaxFailures
1812
1917
  },
1813
1918
  logging: { ...config.logging, ...partial?.logging },
1814
1919
  debug: { ...config.debug, ...partial?.debug },
@@ -1839,6 +1944,7 @@ export {
1839
1944
  DEFAULT_THEME,
1840
1945
  DEFAULT_WATERMARK,
1841
1946
  EventEmitter,
1947
+ KEY_PROXY_MAX_FAILURES,
1842
1948
  PACKAGE_VERSION,
1843
1949
  PLAYBACK_RATES,
1844
1950
  PlaybackSourceManager,