@codenkay/video-nsgplayer-core 3.0.0 → 3.0.2

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.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.2";
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 },
@@ -765,9 +766,13 @@ function seekToLiveEdge(media, options) {
765
766
  }
766
767
 
767
768
  // src/streaming/authParamsLoader.ts
768
- import { XhrLoader as BuiltinXhrLoader } from "hls.js";
769
769
  var ENC_KEY_PATH_RE = /enc\.key$/i;
770
770
  var BUILTIN_LOADER_NAMES = /* @__PURE__ */ new Set(["XhrLoader", "FetchLoader"]);
771
+ var BUILTIN_LOADER_REF = /* @__PURE__ */ Symbol.for(
772
+ "@codenkay/video-nsgplayer-core/builtin-hls-loader"
773
+ );
774
+ var builtinLoaderByHls = /* @__PURE__ */ new WeakMap();
775
+ var KEY_PROXY_MAX_FAILURES = 2;
771
776
  function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
772
777
  try {
773
778
  return ENC_KEY_PATH_RE.test(new URL(rawUrl, baseOrigin).pathname);
@@ -775,18 +780,34 @@ function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined"
775
780
  return /enc\.key/i.test(rawUrl);
776
781
  }
777
782
  }
778
- function resolveBuiltinHlsLoader(HlsClass) {
779
- if (typeof BuiltinXhrLoader === "function") {
780
- return BuiltinXhrLoader;
783
+ function isBuiltinLoaderName(name) {
784
+ return BUILTIN_LOADER_NAMES.has(name);
785
+ }
786
+ function rememberBuiltin(HlsClass, loader) {
787
+ if (isBuiltinLoaderName(typeof loader.name === "string" ? loader.name : "")) {
788
+ builtinLoaderByHls.set(HlsClass, loader);
781
789
  }
790
+ return loader;
791
+ }
792
+ function getStashedBuiltinLoader(loader) {
793
+ return loader[BUILTIN_LOADER_REF];
794
+ }
795
+ function resolveBuiltinHlsLoader(HlsClass) {
796
+ const cached = builtinLoaderByHls.get(HlsClass);
797
+ if (cached) return cached;
782
798
  const start = HlsClass.DefaultConfig.loader;
783
799
  let current = start;
784
800
  const seen = /* @__PURE__ */ new Set();
785
801
  while (current && !seen.has(current)) {
786
802
  seen.add(current);
787
803
  const name = typeof current.name === "string" ? current.name : "";
788
- if (BUILTIN_LOADER_NAMES.has(name)) {
789
- return current;
804
+ if (isBuiltinLoaderName(name)) {
805
+ return rememberBuiltin(HlsClass, current);
806
+ }
807
+ const stashed = getStashedBuiltinLoader(current);
808
+ if (stashed) {
809
+ current = stashed;
810
+ continue;
790
811
  }
791
812
  const parentCtor = Object.getPrototypeOf(current.prototype)?.constructor;
792
813
  if (!parentCtor || parentCtor === Object || parentCtor === Function) {
@@ -794,7 +815,8 @@ function resolveBuiltinHlsLoader(HlsClass) {
794
815
  }
795
816
  current = parentCtor;
796
817
  }
797
- return start;
818
+ const fallback = builtinLoaderByHls.get(HlsClass);
819
+ return fallback ?? start;
798
820
  }
799
821
  function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
800
822
  if (!rawUrl) return rawUrl;
@@ -828,68 +850,230 @@ function createLoaderStats() {
828
850
  buffering: { start: 0, first: 0, end: 0 }
829
851
  };
830
852
  }
831
- async function loadEncryptionKeyWithFetch(context, callbacks, accessToken, signal) {
832
- const stats = createLoaderStats();
833
- stats.loading.first = performance.now();
834
- const headers = {
835
- Accept: "*/*",
836
- ...context.headers
837
- };
838
- if (accessToken) {
839
- headers.Authorization = `Bearer ${accessToken}`;
840
- }
841
- const response = await fetch(context.url, {
842
- method: "GET",
843
- headers,
844
- credentials: "omit",
845
- cache: "no-store",
846
- signal
847
- });
848
- if (!response.ok) {
849
- callbacks.onError(
850
- {
851
- code: response.status,
852
- text: response.statusText || `enc.key HTTP ${response.status}`
853
- },
854
- context,
853
+ function isProxyUpstreamBlocked(status, contentType, bodyHead, errorText) {
854
+ if (status === 0 || status === 502 || status === 503 || status === 504) {
855
+ return true;
856
+ }
857
+ if (status === 403 || status === 401) return true;
858
+ if (contentType?.includes("text/html")) return true;
859
+ if (/<!DOCTYPE|<html|Just a moment|Cloudflare|blocked/i.test(bodyHead)) {
860
+ return true;
861
+ }
862
+ if (errorText && /Cloudflare|HTML|DOCTYPE|blocked|502/i.test(errorText)) {
863
+ return true;
864
+ }
865
+ return false;
866
+ }
867
+ async function fetchEncryptionKeyBytes(url, accessToken, signal) {
868
+ const headers = accessToken ? { Authorization: `Bearer ${accessToken}`, Accept: "*/*" } : void 0;
869
+ try {
870
+ const response = await fetch(url, {
871
+ method: "GET",
872
+ ...headers ? { headers } : {},
873
+ credentials: "omit",
874
+ cache: "no-store",
875
+ signal
876
+ });
877
+ if (!response.ok) {
878
+ return {
879
+ ok: false,
880
+ status: response.status,
881
+ text: response.statusText || `enc.key HTTP ${response.status}`,
882
+ cors: false,
883
+ response
884
+ };
885
+ }
886
+ const data = await response.arrayBuffer();
887
+ const head = new TextDecoder().decode(data.slice(0, 32));
888
+ if (/<!DOCTYPE|<html/i.test(head)) {
889
+ return {
890
+ ok: false,
891
+ status: 403,
892
+ text: "enc.key returned HTML (CDN challenge/block), not an AES key",
893
+ cors: false,
894
+ response
895
+ };
896
+ }
897
+ return {
898
+ ok: true,
899
+ data,
855
900
  response,
856
- stats
857
- );
858
- return;
901
+ url: response.url || url
902
+ };
903
+ } catch (error) {
904
+ if (error instanceof DOMException && error.name === "AbortError") {
905
+ return {
906
+ ok: false,
907
+ status: 0,
908
+ text: "enc.key fetch aborted/timeout",
909
+ cors: false,
910
+ response: null
911
+ };
912
+ }
913
+ const cors = error instanceof TypeError || error instanceof Error && /Failed to fetch|NetworkError|CORS/i.test(error.message);
914
+ return {
915
+ ok: false,
916
+ status: 0,
917
+ text: error instanceof Error ? error.message : "enc.key fetch failed",
918
+ cors,
919
+ response: null
920
+ };
859
921
  }
860
- const data = await response.arrayBuffer();
922
+ }
923
+ function reportKeySuccess(context, callbacks, result) {
924
+ const stats = createLoaderStats();
861
925
  const end = performance.now();
926
+ stats.loading.first = stats.loading.start;
862
927
  stats.loading.end = end;
863
- stats.loaded = data.byteLength;
864
- stats.total = data.byteLength;
865
- const head = new TextDecoder().decode(data.slice(0, 32));
866
- if (/<!DOCTYPE|<html/i.test(head)) {
928
+ stats.loaded = result.data.byteLength;
929
+ stats.total = result.data.byteLength;
930
+ callbacks.onSuccess(
931
+ {
932
+ url: result.url,
933
+ data: result.data,
934
+ code: result.response.status
935
+ },
936
+ stats,
937
+ context,
938
+ result.response
939
+ );
940
+ }
941
+ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state) {
942
+ const stats = createLoaderStats();
943
+ const canTryProxy = Boolean(plan.proxyUrl) && !state.circuitOpen && state.proxyFailures < plan.maxProxyFailures;
944
+ if (canTryProxy && plan.proxyUrl) {
945
+ while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures && !signal.aborted) {
946
+ state.proxyFailures += 1;
947
+ try {
948
+ const response = await fetch(plan.proxyUrl, {
949
+ method: "GET",
950
+ headers: { Accept: "*/*" },
951
+ credentials: "same-origin",
952
+ cache: "no-store",
953
+ signal
954
+ });
955
+ const contentType = response.headers.get("content-type");
956
+ const data = await response.arrayBuffer();
957
+ const head = new TextDecoder().decode(data.slice(0, 64));
958
+ if (response.ok && !isProxyUpstreamBlocked(response.status, contentType, head)) {
959
+ const end = performance.now();
960
+ stats.loading.end = end;
961
+ stats.loaded = data.byteLength;
962
+ stats.total = data.byteLength;
963
+ callbacks.onSuccess(
964
+ {
965
+ url: response.url || plan.proxyUrl,
966
+ data,
967
+ code: response.status
968
+ },
969
+ stats,
970
+ { ...context, url: plan.proxyUrl },
971
+ response
972
+ );
973
+ return "ok";
974
+ }
975
+ if (isProxyUpstreamBlocked(
976
+ response.status,
977
+ contentType,
978
+ head,
979
+ response.statusText
980
+ )) {
981
+ if (response.status === 502 || contentType?.includes("text/html") || /Cloudflare|DOCTYPE|blocked/i.test(head)) {
982
+ state.circuitOpen = true;
983
+ break;
984
+ }
985
+ continue;
986
+ }
987
+ callbacks.onError(
988
+ {
989
+ code: response.status,
990
+ text: `enc.key proxy HTTP ${response.status}`
991
+ },
992
+ { ...context, url: plan.proxyUrl },
993
+ response,
994
+ stats
995
+ );
996
+ return "ok";
997
+ } catch (error) {
998
+ if (error instanceof DOMException && error.name === "AbortError") {
999
+ callbacks.onError(
1000
+ { code: 0, text: "enc.key proxy aborted/timeout" },
1001
+ context,
1002
+ null,
1003
+ stats
1004
+ );
1005
+ return "ok";
1006
+ }
1007
+ state.circuitOpen = true;
1008
+ break;
1009
+ }
1010
+ }
1011
+ state.circuitOpen = true;
1012
+ }
1013
+ const queryResult = await fetchEncryptionKeyBytes(
1014
+ plan.signedKeyUrl,
1015
+ null,
1016
+ signal
1017
+ );
1018
+ if (queryResult.ok) {
1019
+ reportKeySuccess(
1020
+ { ...context, url: plan.signedKeyUrl },
1021
+ callbacks,
1022
+ queryResult
1023
+ );
1024
+ return "ok";
1025
+ }
1026
+ const needsBearer = Boolean(plan.accessToken) && (queryResult.status === 401 || queryResult.status === 403);
1027
+ if (needsBearer && plan.accessToken) {
1028
+ const bearerResult = await fetchEncryptionKeyBytes(
1029
+ plan.signedKeyUrl,
1030
+ plan.accessToken,
1031
+ signal
1032
+ );
1033
+ if (bearerResult.ok) {
1034
+ reportKeySuccess(
1035
+ { ...context, url: plan.signedKeyUrl },
1036
+ callbacks,
1037
+ bearerResult
1038
+ );
1039
+ return "ok";
1040
+ }
1041
+ if (bearerResult.cors) {
1042
+ return "try_xhr";
1043
+ }
867
1044
  callbacks.onError(
868
1045
  {
869
- code: 403,
870
- text: "enc.key returned HTML (CDN challenge/block), not an AES key"
1046
+ code: bearerResult.status,
1047
+ text: bearerResult.text
871
1048
  },
872
- context,
873
- response,
1049
+ { ...context, url: plan.signedKeyUrl },
1050
+ bearerResult.response,
874
1051
  stats
875
1052
  );
876
- return;
1053
+ return "ok";
877
1054
  }
878
- callbacks.onSuccess(
1055
+ if (queryResult.cors || queryResult.status === 0) {
1056
+ return "try_xhr";
1057
+ }
1058
+ callbacks.onError(
879
1059
  {
880
- url: response.url || context.url,
881
- data,
882
- code: response.status
1060
+ code: queryResult.status,
1061
+ text: queryResult.text
883
1062
  },
884
- stats,
885
- context,
886
- response
1063
+ { ...context, url: plan.signedKeyUrl },
1064
+ queryResult.response,
1065
+ stats
887
1066
  );
1067
+ return "ok";
888
1068
  }
889
1069
  function createAuthParamsLoader(HlsClass, store, options = {}) {
890
1070
  const BaseLoader = resolveBuiltinHlsLoader(HlsClass);
891
- return class AuthParamsLoader {
1071
+ rememberBuiltin(HlsClass, BaseLoader);
1072
+ const maxProxyFailures = typeof options.keyProxyMaxFailures === "number" && options.keyProxyMaxFailures >= 0 ? options.keyProxyMaxFailures : KEY_PROXY_MAX_FAILURES;
1073
+ const keyProxyState = { proxyFailures: 0, circuitOpen: false };
1074
+ class AuthParamsLoader {
892
1075
  constructor(config) {
1076
+ this.context = null;
893
1077
  this.keyAbort = null;
894
1078
  this.inner = new BaseLoader(config);
895
1079
  this.stats = this.inner.stats;
@@ -897,14 +1081,16 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
897
1081
  abort() {
898
1082
  this.keyAbort?.abort();
899
1083
  this.keyAbort = null;
900
- this.inner.abort?.();
1084
+ this.inner.abort();
901
1085
  }
902
1086
  destroy() {
903
1087
  this.keyAbort?.abort();
904
1088
  this.keyAbort = null;
1089
+ this.context = null;
905
1090
  this.inner.destroy();
906
1091
  }
907
1092
  load(context, config, callbacks) {
1093
+ this.context = context;
908
1094
  if (!context.url) {
909
1095
  this.inner.load(context, config, callbacks);
910
1096
  return;
@@ -912,92 +1098,70 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
912
1098
  try {
913
1099
  const originalUrl = context.url;
914
1100
  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;
1101
+ if (!isKey) {
1102
+ if (store.hasParams()) {
1103
+ context.url = rewriteHlsRequestUrl(originalUrl, store, {
1104
+ ...options,
1105
+ keyProxyUrlBuilder: void 0
1106
+ });
928
1107
  }
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);
1108
+ this.inner.load(context, config, callbacks);
960
1109
  return;
961
1110
  }
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;
1111
+ const signedKeyUrl = rewriteHlsRequestUrl(originalUrl, store, {
1112
+ ...options,
1113
+ keyProxyUrlBuilder: void 0
1114
+ });
1115
+ const proxyConfigured = Boolean(
1116
+ options.keyProxyUrlBuilder && options.videoId
1117
+ );
1118
+ const proxyUrl = proxyConfigured && !keyProxyState.circuitOpen ? rewriteHlsRequestUrl(originalUrl, store, options) : null;
1119
+ const accessToken = options.getKeyAccessToken?.() ?? null;
1120
+ this.keyAbort?.abort();
1121
+ this.keyAbort = new AbortController();
1122
+ const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
1123
+ const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
1124
+ void loadEncryptionKeyEnterprise(
1125
+ context,
1126
+ callbacks,
1127
+ {
1128
+ proxyUrl,
1129
+ signedKeyUrl,
1130
+ accessToken,
1131
+ maxProxyFailures
1132
+ },
1133
+ this.keyAbort.signal,
1134
+ keyProxyState
1135
+ ).then((result) => {
1136
+ if (result === "try_xhr") {
1137
+ context.url = signedKeyUrl;
1138
+ this.inner.load(context, config, callbacks);
994
1139
  }
995
- }
1140
+ }).catch((error) => {
1141
+ callbacks.onError(
1142
+ {
1143
+ code: 0,
1144
+ text: error instanceof Error ? error.message : "enc.key load failed"
1145
+ },
1146
+ context,
1147
+ null,
1148
+ createLoaderStats()
1149
+ );
1150
+ }).finally(() => {
1151
+ clearTimeout(timer);
1152
+ });
1153
+ return;
996
1154
  } catch {
997
1155
  }
998
1156
  this.inner.load(context, config, callbacks);
999
1157
  }
1000
- };
1158
+ }
1159
+ Object.defineProperty(AuthParamsLoader, BUILTIN_LOADER_REF, {
1160
+ value: BaseLoader,
1161
+ enumerable: false,
1162
+ configurable: false
1163
+ });
1164
+ return AuthParamsLoader;
1001
1165
  }
1002
1166
 
1003
1167
  // src/streaming/HlsEngine.ts
@@ -1062,7 +1226,8 @@ var HlsEngine = class {
1062
1226
  const loader = createAuthParamsLoader(Hls, authParams, {
1063
1227
  videoId,
1064
1228
  keyProxyUrlBuilder,
1065
- getKeyAccessToken
1229
+ getKeyAccessToken,
1230
+ keyProxyMaxFailures: this.options.keyProxyMaxFailures
1066
1231
  });
1067
1232
  this.hls = new Hls({
1068
1233
  enableWorker: hlsConfig.enableWorker,
@@ -1486,11 +1651,13 @@ function createVideoPlayer(options) {
1486
1651
  volume: config.player.volume,
1487
1652
  videoId: options.videoId,
1488
1653
  keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1489
- // Enterprise AES key auth:
1490
- // 1) keyProxyUrlBuilder (same-origin BFF) when configured
1491
- // 2) else getKeyAccessToken / AuthManager Bearer via browser fetch
1492
- // 3) else signed query + XHR (CDN hosts)
1654
+ // Enterprise AES key auth (see authParamsLoader):
1655
+ // 1) keyProxyUrlBuilder BFF — max 2 failures then circuit
1656
+ // 2) signed-query fetch (no Bearer / no CORS preflight)
1657
+ // 3) Bearer only on HTTP 401/403 when getKeyAccessToken is set
1658
+ // 4) signed-query XHR fallback if fetch is CORS-blocked
1493
1659
  getKeyAccessToken: config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1660
+ keyProxyMaxFailures: config.streaming.keyProxyMaxFailures,
1494
1661
  hlsConfig: config.streaming.hls,
1495
1662
  logger,
1496
1663
  callbacks: {
@@ -1808,7 +1975,8 @@ function createVideoPlayer(options) {
1808
1975
  engine: "hls",
1809
1976
  hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
1810
1977
  keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder,
1811
- getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken())
1978
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1979
+ keyProxyMaxFailures: partial?.streaming?.keyProxyMaxFailures ?? config.streaming.keyProxyMaxFailures
1812
1980
  },
1813
1981
  logging: { ...config.logging, ...partial?.logging },
1814
1982
  debug: { ...config.debug, ...partial?.debug },
@@ -1839,6 +2007,7 @@ export {
1839
2007
  DEFAULT_THEME,
1840
2008
  DEFAULT_WATERMARK,
1841
2009
  EventEmitter,
2010
+ KEY_PROXY_MAX_FAILURES,
1842
2011
  PACKAGE_VERSION,
1843
2012
  PLAYBACK_RATES,
1844
2013
  PlaybackSourceManager,