@codenkay/video-nsgplayer-core 0.1.2 → 0.1.5

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
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.
7
+
8
+ ## 0.1.4
9
+
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.
14
+
15
+ ## 0.1.3
16
+
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.
19
+
3
20
  ## 0.1.2
4
21
 
5
22
  - Fix `InternalError: too much recursion` / stack overflow in `assertNoSecretsInConfig` when `createVideoPlayer` options include an `HTMLVideoElement` mount or credential callbacks.
package/dist/index.cjs CHANGED
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  RefreshManager: () => RefreshManager,
47
47
  SECRET_SCAN_MAX_DEPTH: () => SECRET_SCAN_MAX_DEPTH,
48
48
  assertNoSecretsInConfig: () => assertNoSecretsInConfig,
49
+ createAuthParamsLoader: () => createAuthParamsLoader,
49
50
  createPlayerError: () => createPlayerError,
50
51
  createVideoPlayer: () => createVideoPlayer,
51
52
  getLiveEdgeTime: () => getLiveEdgeTime,
@@ -55,6 +56,7 @@ __export(index_exports, {
55
56
  redactObject: () => redactObject,
56
57
  redactSensitive: () => redactSensitive,
57
58
  redactValue: () => redactValue,
59
+ rewriteHlsRequestUrl: () => rewriteHlsRequestUrl,
58
60
  seekToLiveEdge: () => seekToLiveEdge,
59
61
  themeToCssVars: () => themeToCssVars
60
62
  });
@@ -208,7 +210,7 @@ var AuthManager = class {
208
210
 
209
211
  // src/config/defaults.ts
210
212
  var PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
211
- var PACKAGE_VERSION = "0.1.2";
213
+ var PACKAGE_VERSION = "0.1.5";
212
214
  var DEFAULT_PLAYER_BEHAVIOR = {
213
215
  autoplay: true,
214
216
  startMuted: true,
@@ -301,7 +303,8 @@ var SKIP_DEEP_SCAN_KEYS = /* @__PURE__ */ new Set([
301
303
  "gettoken",
302
304
  "getsource",
303
305
  "refreshsource",
304
- "keyproxyurlbuilder"
306
+ "keyproxyurlbuilder",
307
+ "getkeyaccesstoken"
305
308
  ]);
306
309
  var SECRET_SCAN_MAX_DEPTH = 32;
307
310
  function isPlainObject(value) {
@@ -384,7 +387,9 @@ function mergePlayerConfig(partial) {
384
387
  hls: {
385
388
  ...DEFAULT_PLAYER_CONFIG.streaming.hls,
386
389
  ...partial?.streaming?.hls
387
- }
390
+ },
391
+ keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder,
392
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken
388
393
  },
389
394
  logging: { ...DEFAULT_PLAYER_CONFIG.logging, ...partial?.logging },
390
395
  debug: { ...DEFAULT_PLAYER_CONFIG.debug, ...partial?.debug },
@@ -560,11 +565,25 @@ var AuthParamsStore = class {
560
565
  });
561
566
  return out;
562
567
  }
568
+ /** True when at least one auth query param is stored. */
569
+ hasParams() {
570
+ return [...this.params.keys()].length > 0;
571
+ }
572
+ toString() {
573
+ return this.params.toString();
574
+ }
563
575
  update(next) {
564
576
  this.params = next instanceof URLSearchParams ? new URLSearchParams(next) : new URLSearchParams(next);
565
577
  }
578
+ /**
579
+ * Merges current auth params onto a target URL, preserving unrelated query keys.
580
+ * Critical for child HLS playlists/segments that omit signed query params.
581
+ */
566
582
  applyToUrl(rawUrl, baseOrigin) {
567
- const target = new URL(rawUrl, baseOrigin ?? (typeof location !== "undefined" ? location.origin : "http://localhost"));
583
+ const target = new URL(
584
+ rawUrl,
585
+ baseOrigin ?? (typeof location !== "undefined" ? location.origin : "http://localhost")
586
+ );
568
587
  this.params.forEach((value, key) => {
569
588
  target.searchParams.set(key, value);
570
589
  });
@@ -799,7 +818,63 @@ function seekToLiveEdge(media, options) {
799
818
  return { moved: true, edge };
800
819
  }
801
820
 
821
+ // src/streaming/authParamsLoader.ts
822
+ var ENC_KEY_PATH_RE = /enc\.key$/i;
823
+ function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
824
+ try {
825
+ return ENC_KEY_PATH_RE.test(new URL(rawUrl, baseOrigin).pathname);
826
+ } catch {
827
+ return /enc\.key/i.test(rawUrl);
828
+ }
829
+ }
830
+ function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
831
+ if (!rawUrl) return rawUrl;
832
+ const parsed = new URL(rawUrl, baseOrigin);
833
+ const isKey = ENC_KEY_PATH_RE.test(parsed.pathname);
834
+ if (isKey && options.keyProxyUrlBuilder && options.videoId) {
835
+ const snapshot = store.toRecord();
836
+ return options.keyProxyUrlBuilder({
837
+ videoId: options.videoId,
838
+ keyUrl: parsed.toString(),
839
+ token: snapshot.token,
840
+ expires: snapshot.expires,
841
+ username: snapshot.username,
842
+ mobile: snapshot.mobile
843
+ });
844
+ }
845
+ if (!store.hasParams()) return parsed.toString();
846
+ return store.applyToUrl(rawUrl, baseOrigin);
847
+ }
848
+ function createAuthParamsLoader(HlsClass, store, options = {}) {
849
+ const BaseLoader = HlsClass.DefaultConfig.loader;
850
+ return class AuthParamsLoader extends BaseLoader {
851
+ 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
+ }
865
+ }
866
+ } catch {
867
+ }
868
+ }
869
+ super.load(context, config, callbacks);
870
+ }
871
+ };
872
+ }
873
+
802
874
  // src/streaming/HlsEngine.ts
875
+ function isAuthHttpStatus(code) {
876
+ return code === 401 || code === 403;
877
+ }
803
878
  var HlsEngine = class {
804
879
  constructor(options) {
805
880
  this.options = options;
@@ -812,58 +887,58 @@ var HlsEngine = class {
812
887
  this.qualitySelection = "auto";
813
888
  }
814
889
  async start() {
815
- const { videoEl, playbackUrl, autoplay, startMuted, volume, startPosition } = this.options;
890
+ const { videoEl, playbackUrl, startMuted, volume } = this.options;
816
891
  videoEl.muted = startMuted;
817
- if (typeof volume === "number") videoEl.volume = Math.min(1, Math.max(0, volume));
892
+ if (typeof volume === "number") {
893
+ videoEl.volume = Math.min(1, Math.max(0, volume));
894
+ }
818
895
  const Hls = (await import("hls.js")).default;
819
896
  if (Hls.isSupported()) {
820
897
  this.supportMode = "hls.js";
821
898
  this.options.callbacks.onSupportMode?.("hls.js");
822
899
  await this.startWithHlsJs(Hls, playbackUrl);
823
- } else if (videoEl.canPlayType("application/vnd.apple.mpegurl")) {
900
+ return;
901
+ }
902
+ if (videoEl.canPlayType("application/vnd.apple.mpegurl")) {
824
903
  this.supportMode = "native";
825
904
  this.options.callbacks.onSupportMode?.("native");
826
905
  videoEl.src = this.options.authParams.applyToUrl(playbackUrl);
827
906
  this.bindMediaEvents();
828
907
  this.options.callbacks.onStateChange?.("ready");
829
- } else {
830
- this.supportMode = "unsupported";
831
- this.options.callbacks.onSupportMode?.("unsupported");
832
- this.options.callbacks.onFatalError?.("HLS is not supported in this browser");
908
+ await this.applyStartPositionAndAutoplay();
833
909
  return;
834
910
  }
835
- if (typeof startPosition === "number" && startPosition > 0) {
836
- videoEl.currentTime = startPosition;
837
- }
838
- if (autoplay) {
839
- try {
840
- await videoEl.play();
841
- } catch {
842
- if (!videoEl.muted) {
843
- videoEl.muted = true;
844
- try {
845
- await videoEl.play();
846
- } catch {
847
- this.options.logger.warn("Autoplay blocked");
848
- }
849
- }
850
- }
851
- }
911
+ this.supportMode = "unsupported";
912
+ this.options.callbacks.onSupportMode?.("unsupported");
913
+ this.options.callbacks.onFatalError?.(
914
+ "HLS is not supported in this browser"
915
+ );
852
916
  }
853
917
  async startWithHlsJs(Hls, playbackUrl) {
854
- const { videoEl, authParams, hlsConfig, callbacks } = this.options;
918
+ const {
919
+ videoEl,
920
+ authParams,
921
+ hlsConfig,
922
+ callbacks,
923
+ videoId,
924
+ keyProxyUrlBuilder,
925
+ getKeyAccessToken
926
+ } = this.options;
927
+ const loader = createAuthParamsLoader(Hls, authParams, {
928
+ videoId,
929
+ keyProxyUrlBuilder,
930
+ getKeyAccessToken
931
+ });
855
932
  this.hls = new Hls({
856
933
  enableWorker: hlsConfig.enableWorker,
857
934
  lowLatencyMode: hlsConfig.lowLatencyMode,
858
935
  backBufferLength: hlsConfig.backBufferLength,
859
- xhrSetup: (xhr, url) => {
860
- void xhr;
861
- void url;
862
- }
936
+ loader
863
937
  });
864
938
  this.hls.attachMedia(videoEl);
865
939
  this.hls.loadSource(authParams.applyToUrl(playbackUrl));
866
940
  this.hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
941
+ if (this.destroyed) return;
867
942
  this.qualities = (data.levels ?? []).map((level, index) => ({
868
943
  index,
869
944
  height: level.height ?? 0,
@@ -873,42 +948,95 @@ var HlsEngine = class {
873
948
  }));
874
949
  callbacks.onQualitiesChange?.(this.qualities);
875
950
  callbacks.onStateChange?.("ready");
951
+ void this.applyStartPositionAndAutoplay();
876
952
  });
877
953
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
878
- if (!data.fatal) {
879
- if (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)) {
880
- callbacks.onAuthRecoveryNeeded?.(
881
- data.details === Hls.ErrorDetails.KEY_LOAD_ERROR ? "key_error" : data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
882
- );
954
+ this.handleHlsError(Hls, data);
955
+ });
956
+ this.bindMediaEvents();
957
+ }
958
+ handleHlsError(Hls, data) {
959
+ if (this.destroyed) return;
960
+ const { callbacks, hlsConfig, keyProxyUrlBuilder, getKeyAccessToken } = this.options;
961
+ 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;
967
+ }
968
+ const authFailure = isAuthHttpStatus(status) || data.details === Hls.ErrorDetails.KEY_LOAD_ERROR && (status === void 0 || status === 0 || isAuthHttpStatus(status));
969
+ 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)) {
971
+ callbacks.onAuthRecoveryNeeded?.(
972
+ data.details === Hls.ErrorDetails.KEY_LOAD_ERROR ? "key_error" : data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
973
+ );
974
+ }
975
+ return;
976
+ }
977
+ if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
978
+ this.networkRecoveryCount += 1;
979
+ if (this.networkRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
980
+ if (authFailure) {
981
+ callbacks.onAuthRecoveryNeeded?.("fatal_network");
883
982
  }
983
+ this.hls?.startLoad();
884
984
  return;
885
985
  }
886
- if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
887
- this.networkRecoveryCount += 1;
888
- if (this.networkRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
889
- callbacks.onAuthRecoveryNeeded?.("fatal_network");
890
- this.hls?.startLoad();
891
- return;
986
+ }
987
+ if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
988
+ this.mediaRecoveryCount += 1;
989
+ if (this.mediaRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
990
+ this.hls?.recoverMediaError();
991
+ return;
992
+ }
993
+ }
994
+ callbacks.onFatalError?.(data.details || "Fatal HLS error");
995
+ }
996
+ async applyStartPositionAndAutoplay() {
997
+ if (this.destroyed) return;
998
+ const { videoEl, autoplay, startPosition } = this.options;
999
+ if (typeof startPosition === "number" && startPosition > 0) {
1000
+ const seek = () => {
1001
+ if (this.destroyed) return;
1002
+ try {
1003
+ videoEl.currentTime = startPosition;
1004
+ } catch {
892
1005
  }
1006
+ };
1007
+ if (videoEl.seekable.length > 0) {
1008
+ seek();
1009
+ } else {
1010
+ videoEl.addEventListener("loadedmetadata", seek, { once: true });
893
1011
  }
894
- if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
895
- this.mediaRecoveryCount += 1;
896
- if (this.mediaRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
897
- this.hls?.recoverMediaError();
898
- return;
1012
+ }
1013
+ if (!autoplay) return;
1014
+ try {
1015
+ await videoEl.play();
1016
+ } catch {
1017
+ if (!videoEl.muted) {
1018
+ videoEl.muted = true;
1019
+ try {
1020
+ await videoEl.play();
1021
+ } catch {
1022
+ this.options.logger.warn("Autoplay blocked");
899
1023
  }
900
1024
  }
901
- callbacks.onFatalError?.(data.details || "Fatal HLS error");
902
- });
903
- this.bindMediaEvents();
1025
+ }
904
1026
  }
905
1027
  bindMediaEvents() {
906
1028
  const { videoEl, callbacks } = this.options;
907
- videoEl.addEventListener("playing", () => callbacks.onStateChange?.("playing"));
1029
+ videoEl.addEventListener(
1030
+ "playing",
1031
+ () => callbacks.onStateChange?.("playing")
1032
+ );
908
1033
  videoEl.addEventListener("pause", () => {
909
1034
  if (!videoEl.ended) callbacks.onStateChange?.("paused");
910
1035
  });
911
- videoEl.addEventListener("waiting", () => callbacks.onStateChange?.("buffering"));
1036
+ videoEl.addEventListener(
1037
+ "waiting",
1038
+ () => callbacks.onStateChange?.("buffering")
1039
+ );
912
1040
  videoEl.addEventListener("ended", () => {
913
1041
  callbacks.onStateChange?.("ended");
914
1042
  callbacks.onEnded?.();
@@ -926,6 +1054,7 @@ var HlsEngine = class {
926
1054
  }
927
1055
  }
928
1056
  resumeAfterAuthRenew() {
1057
+ this.networkRecoveryCount = 0;
929
1058
  this.hls?.startLoad();
930
1059
  this.updateAuthParams();
931
1060
  }
@@ -1067,6 +1196,9 @@ function createVideoPlayer(options) {
1067
1196
  startMuted: config.player.startMuted,
1068
1197
  startPosition: config.player.startPosition,
1069
1198
  volume: config.player.volume,
1199
+ videoId: options.videoId,
1200
+ keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1201
+ getKeyAccessToken: config.streaming.getKeyAccessToken,
1070
1202
  hlsConfig: config.streaming.hls,
1071
1203
  logger,
1072
1204
  callbacks: {
@@ -1127,6 +1259,14 @@ function createVideoPlayer(options) {
1127
1259
  onRenewing: () => setState("renewing"),
1128
1260
  onRenewed: (nextExpiresAt) => {
1129
1261
  engine?.resumeAfterAuthRenew();
1262
+ if (!destroyed) {
1263
+ const el2 = videoEl;
1264
+ if (el2 && !el2.paused && !el2.ended) {
1265
+ setState("playing");
1266
+ } else {
1267
+ setState("ready");
1268
+ }
1269
+ }
1130
1270
  events.emit("tokenrefresh", { expiresAt: nextExpiresAt });
1131
1271
  events.emit("sourcechange", { expiresAt: nextExpiresAt });
1132
1272
  },
@@ -1327,7 +1467,9 @@ function createVideoPlayer(options) {
1327
1467
  watermark: { ...config.watermark, ...partial?.watermark },
1328
1468
  streaming: {
1329
1469
  engine: "hls",
1330
- hls: { ...config.streaming.hls, ...partial?.streaming?.hls }
1470
+ hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
1471
+ keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder,
1472
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken
1331
1473
  },
1332
1474
  logging: { ...config.logging, ...partial?.logging },
1333
1475
  debug: { ...config.debug, ...partial?.debug },
@@ -1366,6 +1508,7 @@ function createVideoPlayer(options) {
1366
1508
  RefreshManager,
1367
1509
  SECRET_SCAN_MAX_DEPTH,
1368
1510
  assertNoSecretsInConfig,
1511
+ createAuthParamsLoader,
1369
1512
  createPlayerError,
1370
1513
  createVideoPlayer,
1371
1514
  getLiveEdgeTime,
@@ -1375,6 +1518,7 @@ function createVideoPlayer(options) {
1375
1518
  redactObject,
1376
1519
  redactSensitive,
1377
1520
  redactValue,
1521
+ rewriteHlsRequestUrl,
1378
1522
  seekToLiveEdge,
1379
1523
  themeToCssVars
1380
1524
  });