@codenkay/video-nsgplayer-core 2.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,25 @@
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
+
11
+ ## 3.0.0
12
+
13
+ ### Major Changes
14
+
15
+ - Fix minor issues
16
+
17
+ ## 2.0.1
18
+
19
+ - Auto-wire `AuthManager.getAccessToken()` as default `streaming.getKeyAccessToken` so AES `enc.key` can load with Bearer when no BFF key proxy is configured.
20
+ - When `keyProxyUrlBuilder` returns 401/403/502/CORS (e.g. Cloudflare-blocked BFF), automatically fall back to browser Bearer `enc.key` fetch.
21
+ - On Bearer `enc.key` fetch network/CORS failure, fall back to signed-query XHR (CDN hosts).
22
+
3
23
  ## 2.0.0
4
24
 
5
25
  ### Minor 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 = "0.1.8";
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,44 +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
- context.url = rewriteHlsRequestUrl(originalUrl, store, options);
985
- if (usingKeyProxy) {
1112
+ if (!isKey) {
1113
+ if (store.hasParams()) {
1114
+ context.url = rewriteHlsRequestUrl(originalUrl, store, {
1115
+ ...options,
1116
+ keyProxyUrlBuilder: void 0
1117
+ });
1118
+ }
986
1119
  this.inner.load(context, config, callbacks);
987
1120
  return;
988
1121
  }
989
- if (isKey && typeof options.getKeyAccessToken === "function") {
990
- const accessToken = options.getKeyAccessToken() ?? null;
991
- if (accessToken) {
992
- this.keyAbort?.abort();
993
- this.keyAbort = new AbortController();
994
- const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
995
- const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
996
- void loadEncryptionKeyWithFetch(
997
- context,
998
- callbacks,
999
- accessToken,
1000
- this.keyAbort.signal
1001
- ).catch((error) => {
1002
- const stats = createLoaderStats();
1003
- const aborted = error instanceof DOMException && error.name === "AbortError";
1004
- callbacks.onError(
1005
- {
1006
- code: 0,
1007
- text: aborted ? "enc.key fetch aborted/timeout" : error instanceof Error ? error.message : "enc.key fetch failed"
1008
- },
1009
- context,
1010
- null,
1011
- stats
1012
- );
1013
- }).finally(() => {
1014
- clearTimeout(timer);
1015
- });
1016
- 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);
1017
1150
  }
1018
- }
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;
1019
1165
  } catch {
1020
1166
  }
1021
1167
  this.inner.load(context, config, callbacks);
@@ -1085,7 +1231,8 @@ var HlsEngine = class {
1085
1231
  const loader = createAuthParamsLoader(Hls, authParams, {
1086
1232
  videoId,
1087
1233
  keyProxyUrlBuilder,
1088
- getKeyAccessToken
1234
+ getKeyAccessToken,
1235
+ keyProxyMaxFailures: this.options.keyProxyMaxFailures
1089
1236
  });
1090
1237
  this.hls = new Hls({
1091
1238
  enableWorker: hlsConfig.enableWorker,
@@ -1509,12 +1656,12 @@ function createVideoPlayer(options) {
1509
1656
  volume: config.player.volume,
1510
1657
  videoId: options.videoId,
1511
1658
  keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1512
- // Optional: browser Bearer for Video API–origin enc.key (CORS-dependent).
1513
- // CDN keys use signed query via the auth loader (no Bearer) — same as
1514
- // segments. Apps can pass getKeyAccessToken or rely on keyProxyUrlBuilder.
1515
- // When unset, defaults to AuthManager token only if explicitly requested
1516
- // via streaming.getKeyAccessToken; otherwise query-signed XHR.
1517
- getKeyAccessToken: config.streaming.getKeyAccessToken,
1659
+ // Enterprise AES key auth:
1660
+ // 1) keyProxyUrlBuilder (same-origin BFF) when configured — max 2 failures
1661
+ // 2) else getKeyAccessToken / AuthManager Bearer via browser fetch
1662
+ // 3) else signed query + XHR (CDN hosts)
1663
+ getKeyAccessToken: config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1664
+ keyProxyMaxFailures: config.streaming.keyProxyMaxFailures,
1518
1665
  hlsConfig: config.streaming.hls,
1519
1666
  logger,
1520
1667
  callbacks: {
@@ -1832,7 +1979,8 @@ function createVideoPlayer(options) {
1832
1979
  engine: "hls",
1833
1980
  hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
1834
1981
  keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder,
1835
- getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken
1982
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1983
+ keyProxyMaxFailures: partial?.streaming?.keyProxyMaxFailures ?? config.streaming.keyProxyMaxFailures
1836
1984
  },
1837
1985
  logging: { ...config.logging, ...partial?.logging },
1838
1986
  debug: { ...config.debug, ...partial?.debug },
@@ -1864,6 +2012,7 @@ function createVideoPlayer(options) {
1864
2012
  DEFAULT_THEME,
1865
2013
  DEFAULT_WATERMARK,
1866
2014
  EventEmitter,
2015
+ KEY_PROXY_MAX_FAILURES,
1867
2016
  PACKAGE_VERSION,
1868
2017
  PLAYBACK_RATES,
1869
2018
  PlaybackSourceManager,