@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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.1
4
+
5
+ - Permanent AES key path: dedicated key loader with **session circuit breaker**
6
+ (`keyProxyMaxFailures` default **2**). A Cloudflare-blocked BFF is not hammered
7
+ by hls.js retries (previously 9–10× `/hls-key` 502s).
8
+ - After the circuit opens, the session uses AuthManager Bearer (or signed-query XHR) only.
9
+ - Host apps with `AUTH_MODE=browser` should omit `keyProxyUrlBuilder` entirely.
10
+
3
11
  ## 3.0.0
4
12
 
5
13
  ### Major Changes
package/dist/index.cjs CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  DEFAULT_THEME: () => DEFAULT_THEME,
40
40
  DEFAULT_WATERMARK: () => DEFAULT_WATERMARK,
41
41
  EventEmitter: () => EventEmitter,
42
+ KEY_PROXY_MAX_FAILURES: () => KEY_PROXY_MAX_FAILURES,
42
43
  PACKAGE_VERSION: () => PACKAGE_VERSION,
43
44
  PLAYBACK_RATES: () => PLAYBACK_RATES,
44
45
  PlaybackSourceManager: () => PlaybackSourceManager,
@@ -212,7 +213,7 @@ var AuthManager = class {
212
213
 
213
214
  // src/config/defaults.ts
214
215
  var PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
215
- var PACKAGE_VERSION = "2.0.1";
216
+ var PACKAGE_VERSION = "3.0.1";
216
217
  var DEFAULT_PLAYER_BEHAVIOR = {
217
218
  autoplay: true,
218
219
  startMuted: true,
@@ -391,7 +392,8 @@ function mergePlayerConfig(partial) {
391
392
  ...partial?.streaming?.hls
392
393
  },
393
394
  keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder,
394
- getKeyAccessToken: partial?.streaming?.getKeyAccessToken
395
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken,
396
+ keyProxyMaxFailures: partial?.streaming?.keyProxyMaxFailures
395
397
  },
396
398
  logging: { ...DEFAULT_PLAYER_CONFIG.logging, ...partial?.logging },
397
399
  debug: { ...DEFAULT_PLAYER_CONFIG.debug, ...partial?.debug },
@@ -834,6 +836,7 @@ function seekToLiveEdge(media, options) {
834
836
  var import_hls = require("hls.js");
835
837
  var ENC_KEY_PATH_RE = /enc\.key$/i;
836
838
  var BUILTIN_LOADER_NAMES = /* @__PURE__ */ new Set(["XhrLoader", "FetchLoader"]);
839
+ var KEY_PROXY_MAX_FAILURES = 2;
837
840
  function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
838
841
  try {
839
842
  return ENC_KEY_PATH_RE.test(new URL(rawUrl, baseOrigin).pathname);
@@ -894,6 +897,20 @@ function createLoaderStats() {
894
897
  buffering: { start: 0, first: 0, end: 0 }
895
898
  };
896
899
  }
900
+ function isProxyUpstreamBlocked(status, contentType, bodyHead, errorText) {
901
+ if (status === 0 || status === 502 || status === 503 || status === 504) {
902
+ return true;
903
+ }
904
+ if (status === 403 || status === 401) return true;
905
+ if (contentType?.includes("text/html")) return true;
906
+ if (/<!DOCTYPE|<html|Just a moment|Cloudflare|blocked/i.test(bodyHead)) {
907
+ return true;
908
+ }
909
+ if (errorText && /Cloudflare|HTML|DOCTYPE|blocked|502/i.test(errorText)) {
910
+ return true;
911
+ }
912
+ return false;
913
+ }
897
914
  async function loadEncryptionKeyWithFetch(context, callbacks, accessToken, signal) {
898
915
  const stats = createLoaderStats();
899
916
  stats.loading.first = performance.now();
@@ -952,10 +969,122 @@ async function loadEncryptionKeyWithFetch(context, callbacks, accessToken, signa
952
969
  response
953
970
  );
954
971
  }
972
+ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state) {
973
+ const stats = createLoaderStats();
974
+ stats.loading.first = performance.now();
975
+ const canTryProxy = Boolean(plan.proxyUrl) && !state.circuitOpen && state.proxyFailures < plan.maxProxyFailures;
976
+ if (canTryProxy && plan.proxyUrl) {
977
+ while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures && !signal.aborted) {
978
+ state.proxyFailures += 1;
979
+ try {
980
+ const response = await fetch(plan.proxyUrl, {
981
+ method: "GET",
982
+ headers: { Accept: "*/*" },
983
+ credentials: "same-origin",
984
+ cache: "no-store",
985
+ signal
986
+ });
987
+ const contentType = response.headers.get("content-type");
988
+ const data = await response.arrayBuffer();
989
+ const head = new TextDecoder().decode(data.slice(0, 64));
990
+ if (response.ok && !isProxyUpstreamBlocked(response.status, contentType, head)) {
991
+ const end = performance.now();
992
+ stats.loading.end = end;
993
+ stats.loaded = data.byteLength;
994
+ stats.total = data.byteLength;
995
+ callbacks.onSuccess(
996
+ {
997
+ url: response.url || plan.proxyUrl,
998
+ data,
999
+ code: response.status
1000
+ },
1001
+ stats,
1002
+ { ...context, url: plan.proxyUrl },
1003
+ response
1004
+ );
1005
+ return "ok";
1006
+ }
1007
+ if (isProxyUpstreamBlocked(
1008
+ response.status,
1009
+ contentType,
1010
+ head,
1011
+ response.statusText
1012
+ )) {
1013
+ if (response.status === 502 || contentType?.includes("text/html") || /Cloudflare|DOCTYPE|blocked/i.test(head)) {
1014
+ state.circuitOpen = true;
1015
+ break;
1016
+ }
1017
+ continue;
1018
+ }
1019
+ callbacks.onError(
1020
+ {
1021
+ code: response.status,
1022
+ text: `enc.key proxy HTTP ${response.status}`
1023
+ },
1024
+ { ...context, url: plan.proxyUrl },
1025
+ response,
1026
+ stats
1027
+ );
1028
+ return "ok";
1029
+ } catch (error) {
1030
+ if (error instanceof DOMException && error.name === "AbortError") {
1031
+ callbacks.onError(
1032
+ { code: 0, text: "enc.key proxy aborted/timeout" },
1033
+ context,
1034
+ null,
1035
+ stats
1036
+ );
1037
+ return "ok";
1038
+ }
1039
+ state.circuitOpen = true;
1040
+ break;
1041
+ }
1042
+ }
1043
+ state.circuitOpen = true;
1044
+ }
1045
+ if (plan.accessToken) {
1046
+ try {
1047
+ await loadEncryptionKeyWithFetch(
1048
+ { ...context, url: plan.signedKeyUrl },
1049
+ callbacks,
1050
+ plan.accessToken,
1051
+ signal
1052
+ );
1053
+ return "ok";
1054
+ } catch (error) {
1055
+ if (error instanceof DOMException && error.name === "AbortError") {
1056
+ callbacks.onError(
1057
+ { code: 0, text: "enc.key fetch aborted/timeout" },
1058
+ { ...context, url: plan.signedKeyUrl },
1059
+ null,
1060
+ stats
1061
+ );
1062
+ return "ok";
1063
+ }
1064
+ if (error instanceof TypeError || error instanceof Error && /Failed to fetch|NetworkError|CORS/i.test(error.message)) {
1065
+ return "try_xhr";
1066
+ }
1067
+ callbacks.onError(
1068
+ {
1069
+ code: 0,
1070
+ text: error instanceof Error ? error.message : "enc.key Bearer fetch failed"
1071
+ },
1072
+ { ...context, url: plan.signedKeyUrl },
1073
+ null,
1074
+ stats
1075
+ );
1076
+ return "ok";
1077
+ }
1078
+ }
1079
+ return "try_xhr";
1080
+ }
955
1081
  function createAuthParamsLoader(HlsClass, store, options = {}) {
956
1082
  const BaseLoader = resolveBuiltinHlsLoader(HlsClass);
1083
+ const maxProxyFailures = typeof options.keyProxyMaxFailures === "number" && options.keyProxyMaxFailures >= 0 ? options.keyProxyMaxFailures : KEY_PROXY_MAX_FAILURES;
1084
+ const keyProxyState = { proxyFailures: 0, circuitOpen: false };
957
1085
  return class AuthParamsLoader {
958
1086
  constructor(config) {
1087
+ this.context = null;
959
1088
  this.keyAbort = null;
960
1089
  this.inner = new BaseLoader(config);
961
1090
  this.stats = this.inner.stats;
@@ -963,14 +1092,16 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
963
1092
  abort() {
964
1093
  this.keyAbort?.abort();
965
1094
  this.keyAbort = null;
966
- this.inner.abort?.();
1095
+ this.inner.abort();
967
1096
  }
968
1097
  destroy() {
969
1098
  this.keyAbort?.abort();
970
1099
  this.keyAbort = null;
1100
+ this.context = null;
971
1101
  this.inner.destroy();
972
1102
  }
973
1103
  load(context, config, callbacks) {
1104
+ this.context = context;
974
1105
  if (!context.url) {
975
1106
  this.inner.load(context, config, callbacks);
976
1107
  return;
@@ -978,87 +1109,59 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
978
1109
  try {
979
1110
  const originalUrl = context.url;
980
1111
  const isKey = isEncryptionKeyUrl(originalUrl);
981
- const usingKeyProxy = Boolean(
982
- isKey && options.keyProxyUrlBuilder && options.videoId
983
- );
984
- const signedKeyUrl = isKey ? rewriteHlsRequestUrl(originalUrl, store, {
985
- ...options,
986
- keyProxyUrlBuilder: void 0
987
- }) : context.url;
988
- context.url = rewriteHlsRequestUrl(originalUrl, store, options);
989
- if (usingKeyProxy) {
990
- const accessToken = options.getKeyAccessToken?.() ?? null;
991
- if (!accessToken) {
992
- this.inner.load(context, config, callbacks);
993
- return;
1112
+ if (!isKey) {
1113
+ if (store.hasParams()) {
1114
+ context.url = rewriteHlsRequestUrl(originalUrl, store, {
1115
+ ...options,
1116
+ keyProxyUrlBuilder: void 0
1117
+ });
994
1118
  }
995
- const proxyCallbacks = {
996
- ...callbacks,
997
- onError: (error, errContext, networkDetails, stats) => {
998
- const code = error?.code;
999
- const retryable = code === 0 || code === 502 || code === 403 || code === 401 || /Cloudflare|HTML|DOCTYPE|blocked/i.test(error?.text ?? "");
1000
- if (!retryable) {
1001
- callbacks.onError(error, errContext, networkDetails, stats);
1002
- return;
1003
- }
1004
- this.keyAbort?.abort();
1005
- this.keyAbort = new AbortController();
1006
- const bearerContext = { ...context, url: signedKeyUrl };
1007
- void loadEncryptionKeyWithFetch(
1008
- bearerContext,
1009
- callbacks,
1010
- accessToken,
1011
- this.keyAbort.signal
1012
- ).catch((fetchError) => {
1013
- callbacks.onError(
1014
- {
1015
- code: 0,
1016
- text: fetchError instanceof Error ? fetchError.message : error.text || "enc.key proxy and Bearer fetch failed"
1017
- },
1018
- bearerContext,
1019
- null,
1020
- stats
1021
- );
1022
- });
1023
- }
1024
- };
1025
- this.inner.load(context, config, proxyCallbacks);
1119
+ this.inner.load(context, config, callbacks);
1026
1120
  return;
1027
1121
  }
1028
- if (isKey && typeof options.getKeyAccessToken === "function") {
1029
- const accessToken = options.getKeyAccessToken() ?? null;
1030
- if (accessToken) {
1031
- this.keyAbort?.abort();
1032
- this.keyAbort = new AbortController();
1033
- const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
1034
- const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
1035
- void loadEncryptionKeyWithFetch(
1036
- context,
1037
- callbacks,
1038
- accessToken,
1039
- this.keyAbort.signal
1040
- ).catch((error) => {
1041
- const stats = createLoaderStats();
1042
- const aborted = error instanceof DOMException && error.name === "AbortError";
1043
- if (!aborted && (error instanceof TypeError || error instanceof Error && /Failed to fetch|NetworkError|CORS/i.test(error.message))) {
1044
- this.inner.load(context, config, callbacks);
1045
- return;
1046
- }
1047
- callbacks.onError(
1048
- {
1049
- code: 0,
1050
- text: aborted ? "enc.key fetch aborted/timeout" : error instanceof Error ? error.message : "enc.key fetch failed"
1051
- },
1052
- context,
1053
- null,
1054
- stats
1055
- );
1056
- }).finally(() => {
1057
- clearTimeout(timer);
1058
- });
1059
- return;
1122
+ const signedKeyUrl = rewriteHlsRequestUrl(originalUrl, store, {
1123
+ ...options,
1124
+ keyProxyUrlBuilder: void 0
1125
+ });
1126
+ const proxyConfigured = Boolean(
1127
+ options.keyProxyUrlBuilder && options.videoId
1128
+ );
1129
+ const proxyUrl = proxyConfigured && !keyProxyState.circuitOpen ? rewriteHlsRequestUrl(originalUrl, store, options) : null;
1130
+ const accessToken = options.getKeyAccessToken?.() ?? null;
1131
+ this.keyAbort?.abort();
1132
+ this.keyAbort = new AbortController();
1133
+ const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
1134
+ const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
1135
+ void loadEncryptionKeyEnterprise(
1136
+ context,
1137
+ callbacks,
1138
+ {
1139
+ proxyUrl,
1140
+ signedKeyUrl,
1141
+ accessToken,
1142
+ maxProxyFailures
1143
+ },
1144
+ this.keyAbort.signal,
1145
+ keyProxyState
1146
+ ).then((result) => {
1147
+ if (result === "try_xhr") {
1148
+ context.url = signedKeyUrl;
1149
+ this.inner.load(context, config, callbacks);
1060
1150
  }
1061
- }
1151
+ }).catch((error) => {
1152
+ callbacks.onError(
1153
+ {
1154
+ code: 0,
1155
+ text: error instanceof Error ? error.message : "enc.key load failed"
1156
+ },
1157
+ context,
1158
+ null,
1159
+ createLoaderStats()
1160
+ );
1161
+ }).finally(() => {
1162
+ clearTimeout(timer);
1163
+ });
1164
+ return;
1062
1165
  } catch {
1063
1166
  }
1064
1167
  this.inner.load(context, config, callbacks);
@@ -1128,7 +1231,8 @@ var HlsEngine = class {
1128
1231
  const loader = createAuthParamsLoader(Hls, authParams, {
1129
1232
  videoId,
1130
1233
  keyProxyUrlBuilder,
1131
- getKeyAccessToken
1234
+ getKeyAccessToken,
1235
+ keyProxyMaxFailures: this.options.keyProxyMaxFailures
1132
1236
  });
1133
1237
  this.hls = new Hls({
1134
1238
  enableWorker: hlsConfig.enableWorker,
@@ -1553,10 +1657,11 @@ function createVideoPlayer(options) {
1553
1657
  videoId: options.videoId,
1554
1658
  keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1555
1659
  // Enterprise AES key auth:
1556
- // 1) keyProxyUrlBuilder (same-origin BFF) when configured
1660
+ // 1) keyProxyUrlBuilder (same-origin BFF) when configured — max 2 failures
1557
1661
  // 2) else getKeyAccessToken / AuthManager Bearer via browser fetch
1558
1662
  // 3) else signed query + XHR (CDN hosts)
1559
1663
  getKeyAccessToken: config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1664
+ keyProxyMaxFailures: config.streaming.keyProxyMaxFailures,
1560
1665
  hlsConfig: config.streaming.hls,
1561
1666
  logger,
1562
1667
  callbacks: {
@@ -1874,7 +1979,8 @@ function createVideoPlayer(options) {
1874
1979
  engine: "hls",
1875
1980
  hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
1876
1981
  keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder,
1877
- getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken())
1982
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1983
+ keyProxyMaxFailures: partial?.streaming?.keyProxyMaxFailures ?? config.streaming.keyProxyMaxFailures
1878
1984
  },
1879
1985
  logging: { ...config.logging, ...partial?.logging },
1880
1986
  debug: { ...config.debug, ...partial?.debug },
@@ -1906,6 +2012,7 @@ function createVideoPlayer(options) {
1906
2012
  DEFAULT_THEME,
1907
2013
  DEFAULT_WATERMARK,
1908
2014
  EventEmitter,
2015
+ KEY_PROXY_MAX_FAILURES,
1909
2016
  PACKAGE_VERSION,
1910
2017
  PLAYBACK_RATES,
1911
2018
  PlaybackSourceManager,