@codenkay/video-nsgplayer-core 0.1.5 → 1.0.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/CHANGELOG.md CHANGED
@@ -1,27 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Fix minor errors
8
+
9
+ ### Minor Changes
10
+
11
+ - 1a4dc9b: Initial public SDK packages: core createVideoPlayer API with dual auth/playback lifecycles, React and Angular adapters, full PlayerConfig parity with demo settings.
12
+
13
+ ## 0.1.8
14
+
15
+ - Fix Firefox `InternalError: too much recursion` from nested hls.js loaders:
16
+ `createAuthParamsLoader` now **composes** a builtin `XhrLoader`/`FetchLoader`
17
+ instead of `extends DefaultConfig.loader` (which could nest `super.load()` after
18
+ HMR / repeated mounts if DefaultConfig was polluted).
19
+ - `resolveBuiltinHlsLoader()` walks past any wrapper classes to the builtin loader.
20
+
21
+ ## 0.1.7
22
+
23
+ - Fix false AES key fatal: remove the post-publish gate that required
24
+ `getKeyAccessToken` or `keyProxyUrlBuilder` even when CDN `enc.key` only needs
25
+ signed query params (same auth as segments — pre-0.1.5 behavior).
26
+ - Load `enc.key` via signed query + XHR by default; use browser `fetch` + Bearer
27
+ only when `streaming.getKeyAccessToken` returns a token; use BFF when
28
+ `keyProxyUrlBuilder` is set.
29
+ - Clearer key-load errors that report HTTP/network cause instead of a configure prompt.
30
+
31
+ ## 0.1.6
32
+
33
+ - Fix false "Playback refresh failed": `PlayerError` is not an `Error`, so refresh failures lost their real message.
34
+ - Do not treat HLS key CORS/`status 0` as signed-URL expiry (that caused renew loops). Only 401/403 trigger auth recovery.
35
+
3
36
  ## 0.1.5
4
37
 
5
- - Add `streaming.getKeyAccessToken` for browser-direct AES `enc.key` loads with `Authorization: Bearer` (needed when Cloudflare blocks server/BFF fetches to Appextech).
6
- - Clearer key-load guidance when neither key proxy nor browser Bearer is configured.
38
+ - Add `streaming.getKeyAccessToken` for browser-direct AES `enc.key` loads with `Authorization: Bearer`.
7
39
 
8
40
  ## 0.1.4
9
41
 
10
- - Fix stall after segments return 200: wait for `MANIFEST_PARSED` before autoplay/seek.
11
- - Only trigger signed-URL auth recovery on HTTP 401/403 (not every network blip).
12
- - Require/surface `streaming.keyProxyUrlBuilder` when AES `enc.key` load fails (Appextech VOD needs same-origin BFF + API Bearer).
13
- - Clear sticky `renewing` state after successful credential refresh.
42
+ - Fix stall after segments return 200; renew only on 401/403; clear sticky renewing state.
14
43
 
15
44
  ## 0.1.3
16
45
 
17
- - Fix CDN **403 on child HLS playlists/segments** (`stream_N.m3u8`, `.ts`, keys): hls.js now uses an auth-params loader that re-applies signed `token`/`expires` query params on every request, not only the master playlist.
18
- - Preserve optional `streaming.keyProxyUrlBuilder` through config merge.
46
+ - Re-apply signed query params on every HLS child request (fix CDN 403).
19
47
 
20
48
  ## 0.1.2
21
49
 
22
- - Fix `InternalError: too much recursion` / stack overflow in `assertNoSecretsInConfig` when `createVideoPlayer` options include an `HTMLVideoElement` mount or credential callbacks.
23
- - Secret scanning is now plain-object only, iterative, depth-capped, and does not deep-walk `mount` / `auth` / `playback`.
24
- - Harden `redactObject` against circular graphs.
50
+ - Fix `too much recursion` in secret config scanning.
25
51
 
26
52
  ## 0.1.1
27
53
 
package/dist/index.cjs CHANGED
@@ -51,11 +51,13 @@ __export(index_exports, {
51
51
  createVideoPlayer: () => createVideoPlayer,
52
52
  getLiveEdgeTime: () => getLiveEdgeTime,
53
53
  isAtLiveEdge: () => isAtLiveEdge,
54
+ isEncryptionKeyUrl: () => isEncryptionKeyUrl,
54
55
  isPlainObject: () => isPlainObject,
55
56
  mergePlayerConfig: () => mergePlayerConfig,
56
57
  redactObject: () => redactObject,
57
58
  redactSensitive: () => redactSensitive,
58
59
  redactValue: () => redactValue,
60
+ resolveBuiltinHlsLoader: () => resolveBuiltinHlsLoader,
59
61
  rewriteHlsRequestUrl: () => rewriteHlsRequestUrl,
60
62
  seekToLiveEdge: () => seekToLiveEdge,
61
63
  themeToCssVars: () => themeToCssVars
@@ -210,7 +212,7 @@ var AuthManager = class {
210
212
 
211
213
  // src/config/defaults.ts
212
214
  var PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
213
- var PACKAGE_VERSION = "0.1.5";
215
+ var PACKAGE_VERSION = "0.1.8";
214
216
  var DEFAULT_PLAYER_BEHAVIOR = {
215
217
  autoplay: true,
216
218
  startMuted: true,
@@ -470,23 +472,25 @@ function isPlainObject2(value) {
470
472
  const proto = Object.getPrototypeOf(value);
471
473
  return proto === Object.prototype || proto === null;
472
474
  }
473
- function redactValue(key, value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
474
- if (SENSITIVE_KEYS.test(key)) return "[REDACTED]";
475
- if (typeof value === "string") {
476
- if (/^https?:\/\//i.test(value) && /[?&](token|expires)=/i.test(value)) {
477
- try {
478
- const url = new URL(value);
479
- for (const param of ["token", "expires", "signature", "sig"]) {
480
- if (url.searchParams.has(param)) {
481
- url.searchParams.set(param, "[REDACTED]");
482
- }
483
- }
484
- return url.toString();
485
- } catch {
486
- return "[REDACTED_URL]";
475
+ function redactStringUrl(value) {
476
+ if (!/^https?:\/\//i.test(value) || !/[?&](token|expires)=/i.test(value)) {
477
+ return value;
478
+ }
479
+ try {
480
+ const url = new URL(value);
481
+ for (const param of ["token", "expires", "signature", "sig"]) {
482
+ if (url.searchParams.has(param)) {
483
+ url.searchParams.set(param, "[REDACTED]");
487
484
  }
488
485
  }
486
+ return url.toString();
487
+ } catch {
488
+ return "[REDACTED_URL]";
489
489
  }
490
+ }
491
+ function redactValue(key, value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
492
+ if (SENSITIVE_KEYS.test(key)) return "[REDACTED]";
493
+ if (typeof value === "string") return redactStringUrl(value);
490
494
  if (Array.isArray(value)) {
491
495
  if (depth >= REDACT_MAX_DEPTH || seen.has(value)) return "[REDACTED_CYCLE]";
492
496
  seen.add(value);
@@ -753,10 +757,11 @@ var RefreshManager = class {
753
757
  this.options.logger.debug("Playback source renewed", { reason });
754
758
  } catch (error) {
755
759
  this.consecutiveFailures += 1;
756
- const message = error instanceof Error ? error.message : "Playback refresh failed";
760
+ const message = refreshErrorMessage(error);
757
761
  this.options.logger.warn("Playback refresh failed", {
758
762
  reason,
759
- failure: this.consecutiveFailures
763
+ failure: this.consecutiveFailures,
764
+ message
760
765
  });
761
766
  if (this.consecutiveFailures >= this.options.renewMaxRetries) {
762
767
  this.options.onFatal?.(message);
@@ -776,6 +781,13 @@ var RefreshManager = class {
776
781
  }
777
782
  }
778
783
  };
784
+ function refreshErrorMessage(error) {
785
+ if (error instanceof Error && error.message) return error.message;
786
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string" && error.message) {
787
+ return error.message;
788
+ }
789
+ return "Playback refresh failed";
790
+ }
779
791
 
780
792
  // src/playback/liveEdge.ts
781
793
  var DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS = 3;
@@ -819,7 +831,9 @@ function seekToLiveEdge(media, options) {
819
831
  }
820
832
 
821
833
  // src/streaming/authParamsLoader.ts
834
+ var import_hls = require("hls.js");
822
835
  var ENC_KEY_PATH_RE = /enc\.key$/i;
836
+ var BUILTIN_LOADER_NAMES = /* @__PURE__ */ new Set(["XhrLoader", "FetchLoader"]);
823
837
  function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
824
838
  try {
825
839
  return ENC_KEY_PATH_RE.test(new URL(rawUrl, baseOrigin).pathname);
@@ -827,6 +841,27 @@ function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined"
827
841
  return /enc\.key/i.test(rawUrl);
828
842
  }
829
843
  }
844
+ function resolveBuiltinHlsLoader(HlsClass) {
845
+ if (typeof import_hls.XhrLoader === "function") {
846
+ return import_hls.XhrLoader;
847
+ }
848
+ const start = HlsClass.DefaultConfig.loader;
849
+ let current = start;
850
+ const seen = /* @__PURE__ */ new Set();
851
+ while (current && !seen.has(current)) {
852
+ seen.add(current);
853
+ const name = typeof current.name === "string" ? current.name : "";
854
+ if (BUILTIN_LOADER_NAMES.has(name)) {
855
+ return current;
856
+ }
857
+ const parentCtor = Object.getPrototypeOf(current.prototype)?.constructor;
858
+ if (!parentCtor || parentCtor === Object || parentCtor === Function) {
859
+ break;
860
+ }
861
+ current = parentCtor;
862
+ }
863
+ return start;
864
+ }
830
865
  function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
831
866
  if (!rawUrl) return rawUrl;
832
867
  const parsed = new URL(rawUrl, baseOrigin);
@@ -845,28 +880,145 @@ function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof l
845
880
  if (!store.hasParams()) return parsed.toString();
846
881
  return store.applyToUrl(rawUrl, baseOrigin);
847
882
  }
883
+ function createLoaderStats() {
884
+ const now = performance.now();
885
+ return {
886
+ aborted: false,
887
+ loaded: 0,
888
+ retry: 0,
889
+ total: 0,
890
+ chunkCount: 0,
891
+ bwEstimate: 0,
892
+ loading: { start: now, first: 0, end: 0 },
893
+ parsing: { start: 0, end: 0 },
894
+ buffering: { start: 0, first: 0, end: 0 }
895
+ };
896
+ }
897
+ async function loadEncryptionKeyWithFetch(context, callbacks, accessToken, signal) {
898
+ const stats = createLoaderStats();
899
+ stats.loading.first = performance.now();
900
+ const headers = {
901
+ Accept: "*/*",
902
+ ...context.headers
903
+ };
904
+ if (accessToken) {
905
+ headers.Authorization = `Bearer ${accessToken}`;
906
+ }
907
+ const response = await fetch(context.url, {
908
+ method: "GET",
909
+ headers,
910
+ credentials: "omit",
911
+ cache: "no-store",
912
+ signal
913
+ });
914
+ if (!response.ok) {
915
+ callbacks.onError(
916
+ {
917
+ code: response.status,
918
+ text: response.statusText || `enc.key HTTP ${response.status}`
919
+ },
920
+ context,
921
+ response,
922
+ stats
923
+ );
924
+ return;
925
+ }
926
+ const data = await response.arrayBuffer();
927
+ const end = performance.now();
928
+ stats.loading.end = end;
929
+ stats.loaded = data.byteLength;
930
+ stats.total = data.byteLength;
931
+ const head = new TextDecoder().decode(data.slice(0, 32));
932
+ if (/<!DOCTYPE|<html/i.test(head)) {
933
+ callbacks.onError(
934
+ {
935
+ code: 403,
936
+ text: "enc.key returned HTML (CDN challenge/block), not an AES key"
937
+ },
938
+ context,
939
+ response,
940
+ stats
941
+ );
942
+ return;
943
+ }
944
+ callbacks.onSuccess(
945
+ {
946
+ url: response.url || context.url,
947
+ data,
948
+ code: response.status
949
+ },
950
+ stats,
951
+ context,
952
+ response
953
+ );
954
+ }
848
955
  function createAuthParamsLoader(HlsClass, store, options = {}) {
849
- const BaseLoader = HlsClass.DefaultConfig.loader;
850
- return class AuthParamsLoader extends BaseLoader {
956
+ const BaseLoader = resolveBuiltinHlsLoader(HlsClass);
957
+ return class AuthParamsLoader {
958
+ constructor(config) {
959
+ this.keyAbort = null;
960
+ this.inner = new BaseLoader(config);
961
+ this.stats = this.inner.stats;
962
+ }
963
+ abort() {
964
+ this.keyAbort?.abort();
965
+ this.keyAbort = null;
966
+ this.inner.abort?.();
967
+ }
968
+ destroy() {
969
+ this.keyAbort?.abort();
970
+ this.keyAbort = null;
971
+ this.inner.destroy();
972
+ }
851
973
  load(context, config, callbacks) {
852
- if (context.url) {
853
- try {
854
- const originalUrl = context.url;
855
- const isKey = isEncryptionKeyUrl(originalUrl);
856
- context.url = rewriteHlsRequestUrl(originalUrl, store, options);
857
- if (isKey && !options.keyProxyUrlBuilder && options.getKeyAccessToken) {
858
- const accessToken = options.getKeyAccessToken();
859
- if (accessToken) {
860
- context.headers = {
861
- ...context.headers ?? {},
862
- Authorization: `Bearer ${accessToken}`
863
- };
864
- }
974
+ if (!context.url) {
975
+ this.inner.load(context, config, callbacks);
976
+ return;
977
+ }
978
+ try {
979
+ const originalUrl = context.url;
980
+ 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) {
986
+ this.inner.load(context, config, callbacks);
987
+ return;
988
+ }
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;
865
1017
  }
866
- } catch {
867
1018
  }
1019
+ } catch {
868
1020
  }
869
- super.load(context, config, callbacks);
1021
+ this.inner.load(context, config, callbacks);
870
1022
  }
871
1023
  };
872
1024
  }
@@ -957,19 +1109,24 @@ var HlsEngine = class {
957
1109
  }
958
1110
  handleHlsError(Hls, data) {
959
1111
  if (this.destroyed) return;
960
- const { callbacks, hlsConfig, keyProxyUrlBuilder, getKeyAccessToken } = this.options;
1112
+ const { callbacks, hlsConfig } = this.options;
961
1113
  const status = data.response?.code;
962
- if (data.details === Hls.ErrorDetails.KEY_LOAD_ERROR && !keyProxyUrlBuilder && !getKeyAccessToken) {
963
- callbacks.onFatalError?.(
964
- "Encryption key load failed. Configure streaming.getKeyAccessToken (browser Bearer) or streaming.keyProxyUrlBuilder (Cloudflare-allowlisted BFF) for Appextech AES-encrypted HLS."
965
- );
966
- return;
1114
+ const authFailure = isAuthHttpStatus(status);
1115
+ if (data.details === Hls.ErrorDetails.KEY_LOAD_ERROR) {
1116
+ if (authFailure) {
1117
+ callbacks.onAuthRecoveryNeeded?.("key_error");
1118
+ if (!data.fatal) return;
1119
+ } else if (!data.fatal) {
1120
+ callbacks.onFatalError?.(
1121
+ status ? `Encryption key load failed (HTTP ${status}).` : "Encryption key load failed (network/CORS). Ensure signed query params are applied, or use keyProxyUrlBuilder / getKeyAccessToken when the key host requires API Bearer."
1122
+ );
1123
+ return;
1124
+ }
967
1125
  }
968
- const authFailure = isAuthHttpStatus(status) || data.details === Hls.ErrorDetails.KEY_LOAD_ERROR && (status === void 0 || status === 0 || isAuthHttpStatus(status));
969
1126
  if (!data.fatal) {
970
- if (authFailure && data.type === Hls.ErrorTypes.NETWORK_ERROR && (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR || data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR || data.details === Hls.ErrorDetails.KEY_LOAD_ERROR)) {
1127
+ if (authFailure && data.type === Hls.ErrorTypes.NETWORK_ERROR && (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR || data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR)) {
971
1128
  callbacks.onAuthRecoveryNeeded?.(
972
- data.details === Hls.ErrorDetails.KEY_LOAD_ERROR ? "key_error" : data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
1129
+ data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
973
1130
  );
974
1131
  }
975
1132
  return;
@@ -1198,6 +1355,11 @@ function createVideoPlayer(options) {
1198
1355
  volume: config.player.volume,
1199
1356
  videoId: options.videoId,
1200
1357
  keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1358
+ // Optional: browser Bearer for Video API–origin enc.key (CORS-dependent).
1359
+ // CDN keys use signed query via the auth loader (no Bearer) — same as
1360
+ // segments. Apps can pass getKeyAccessToken or rely on keyProxyUrlBuilder.
1361
+ // When unset, defaults to AuthManager token only if explicitly requested
1362
+ // via streaming.getKeyAccessToken; otherwise query-signed XHR.
1201
1363
  getKeyAccessToken: config.streaming.getKeyAccessToken,
1202
1364
  hlsConfig: config.streaming.hls,
1203
1365
  logger,
@@ -1513,11 +1675,13 @@ function createVideoPlayer(options) {
1513
1675
  createVideoPlayer,
1514
1676
  getLiveEdgeTime,
1515
1677
  isAtLiveEdge,
1678
+ isEncryptionKeyUrl,
1516
1679
  isPlainObject,
1517
1680
  mergePlayerConfig,
1518
1681
  redactObject,
1519
1682
  redactSensitive,
1520
1683
  redactValue,
1684
+ resolveBuiltinHlsLoader,
1521
1685
  rewriteHlsRequestUrl,
1522
1686
  seekToLiveEdge,
1523
1687
  themeToCssVars