@codenkay/video-nsgplayer-core 0.1.3 → 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/dist/index.d.ts CHANGED
@@ -117,12 +117,24 @@ type StreamingConfig = {
117
117
  backBufferLength?: number;
118
118
  fatalRecoveryLimit?: number;
119
119
  };
120
+ /**
121
+ * Same-origin BFF proxy for `enc.key` (preferred in production when the
122
+ * server IP is allowlisted by the CDN / Cloudflare).
123
+ */
120
124
  keyProxyUrlBuilder?: (ctx: {
121
125
  videoId: string;
122
126
  keyUrl: string;
123
127
  token?: string;
124
128
  expires?: string;
129
+ username?: string;
130
+ mobile?: string;
125
131
  }) => string;
132
+ /**
133
+ * Browser-direct AES key auth: returns the Video API access token so hls.js
134
+ * can send `Authorization: Bearer …` on `enc.key` requests.
135
+ * Use when the BFF cannot reach Appextech (Cloudflare challenge on server IP).
136
+ */
137
+ getKeyAccessToken?: () => string | null | undefined;
126
138
  };
127
139
  type LoggingConfig = {
128
140
  enabled?: boolean;
@@ -169,6 +181,7 @@ type PlayerConfig = Required<{
169
181
  engine: "hls";
170
182
  hls: Required<NonNullable<StreamingConfig["hls"]>>;
171
183
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
184
+ getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
172
185
  };
173
186
  logging: Required<LoggingConfig>;
174
187
  debug: Required<DebugConfig>;
@@ -242,7 +255,7 @@ type PlayerEventMap = {
242
255
  };
243
256
 
244
257
  declare const PLAYBACK_RATES: readonly [0.5, 0.75, 1, 1.25, 1.5, 2];
245
- declare const PACKAGE_VERSION = "0.1.3";
258
+ declare const PACKAGE_VERSION = "0.1.5";
246
259
  declare const DEFAULT_PLAYER_BEHAVIOR: Required<PlayerBehaviorConfig>;
247
260
  declare const DEFAULT_CONTROLS: Required<ControlsConfig>;
248
261
  declare const DEFAULT_THEME: Required<ThemeConfig>;
@@ -274,6 +287,7 @@ type PlayerConfigPartial = Partial<{
274
287
  engine?: "hls";
275
288
  hls?: Partial<PlayerConfig["streaming"]["hls"]>;
276
289
  keyProxyUrlBuilder?: PlayerConfig["streaming"]["keyProxyUrlBuilder"];
290
+ getKeyAccessToken?: PlayerConfig["streaming"]["getKeyAccessToken"];
277
291
  };
278
292
  logging: Partial<PlayerConfig["logging"]>;
279
293
  debug: Partial<PlayerConfig["debug"]>;
@@ -388,6 +402,7 @@ type HlsEngineOptions = {
388
402
  volume?: number;
389
403
  videoId?: string;
390
404
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
405
+ getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
391
406
  hlsConfig: {
392
407
  enableWorker: boolean;
393
408
  lowLatencyMode: boolean;
@@ -400,10 +415,9 @@ type HlsEngineOptions = {
400
415
  /**
401
416
  * Dual-path HLS engine: hls.js when supported, otherwise native Safari HLS.
402
417
  *
403
- * Signed CDN playback requires auth query params on *every* HLS request
404
- * (master, media playlist, segments, keys). hls.js only receives the signed
405
- * master URL via loadSource; child URIs are relative and unsigned unless a
406
- * custom loader re-applies AuthParamsStore on each request.
418
+ * Signed CDN playback requires auth query params on every HLS request.
419
+ * Encrypted VOD also requires a same-origin `enc.key` proxy (Bearer + query)
420
+ * via `streaming.keyProxyUrlBuilder` — browser CORS cannot fetch Video API keys.
407
421
  */
408
422
  declare class HlsEngine {
409
423
  private readonly options;
@@ -417,6 +431,8 @@ declare class HlsEngine {
417
431
  constructor(options: HlsEngineOptions);
418
432
  start(): Promise<void>;
419
433
  private startWithHlsJs;
434
+ private handleHlsError;
435
+ private applyStartPositionAndAutoplay;
420
436
  private bindMediaEvents;
421
437
  updateAuthParams(): void;
422
438
  resumeAfterAuthRenew(): void;
@@ -582,11 +598,16 @@ type HlsLoader = new (config: object) => {
582
598
  type AuthParamsLoaderOptions = {
583
599
  videoId?: string;
584
600
  keyProxyUrlBuilder?: StreamingConfig["keyProxyUrlBuilder"];
601
+ getKeyAccessToken?: StreamingConfig["getKeyAccessToken"];
585
602
  };
586
603
  /**
587
604
  * Rewrites an HLS request URL so signed auth query params are present.
588
605
  * Master playlists are signed on loadSource; child playlists / segments / keys
589
606
  * are relative and would otherwise be fetched unsigned → CDN 403.
607
+ *
608
+ * AES `enc.key` handling (pick one):
609
+ * 1. `keyProxyUrlBuilder` — same-origin BFF (needs Cloudflare-allowlisted server IP)
610
+ * 2. `getKeyAccessToken` — browser-direct CDN fetch with API Bearer header
590
611
  */
591
612
  declare function rewriteHlsRequestUrl(rawUrl: string, store: AuthParamsStore, options?: AuthParamsLoaderOptions, baseOrigin?: string): string;
592
613
  /**
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 = "0.1.3";
149
+ var PACKAGE_VERSION = "0.1.5";
150
150
  var DEFAULT_PLAYER_BEHAVIOR = {
151
151
  autoplay: true,
152
152
  startMuted: true,
@@ -239,7 +239,8 @@ var SKIP_DEEP_SCAN_KEYS = /* @__PURE__ */ new Set([
239
239
  "gettoken",
240
240
  "getsource",
241
241
  "refreshsource",
242
- "keyproxyurlbuilder"
242
+ "keyproxyurlbuilder",
243
+ "getkeyaccesstoken"
243
244
  ]);
244
245
  var SECRET_SCAN_MAX_DEPTH = 32;
245
246
  function isPlainObject(value) {
@@ -323,7 +324,8 @@ function mergePlayerConfig(partial) {
323
324
  ...DEFAULT_PLAYER_CONFIG.streaming.hls,
324
325
  ...partial?.streaming?.hls
325
326
  },
326
- keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder
327
+ keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder,
328
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken
327
329
  },
328
330
  logging: { ...DEFAULT_PLAYER_CONFIG.logging, ...partial?.logging },
329
331
  debug: { ...DEFAULT_PLAYER_CONFIG.debug, ...partial?.debug },
@@ -753,7 +755,14 @@ function seekToLiveEdge(media, options) {
753
755
  }
754
756
 
755
757
  // src/streaming/authParamsLoader.ts
756
- var ENC_KEY_PATH_RE = /\/enc\.key$/i;
758
+ var ENC_KEY_PATH_RE = /enc\.key$/i;
759
+ function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
760
+ try {
761
+ return ENC_KEY_PATH_RE.test(new URL(rawUrl, baseOrigin).pathname);
762
+ } catch {
763
+ return /enc\.key/i.test(rawUrl);
764
+ }
765
+ }
757
766
  function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
758
767
  if (!rawUrl) return rawUrl;
759
768
  const parsed = new URL(rawUrl, baseOrigin);
@@ -764,7 +773,9 @@ function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof l
764
773
  videoId: options.videoId,
765
774
  keyUrl: parsed.toString(),
766
775
  token: snapshot.token,
767
- expires: snapshot.expires
776
+ expires: snapshot.expires,
777
+ username: snapshot.username,
778
+ mobile: snapshot.mobile
768
779
  });
769
780
  }
770
781
  if (!store.hasParams()) return parsed.toString();
@@ -776,7 +787,18 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
776
787
  load(context, config, callbacks) {
777
788
  if (context.url) {
778
789
  try {
779
- context.url = rewriteHlsRequestUrl(context.url, store, options);
790
+ const originalUrl = context.url;
791
+ const isKey = isEncryptionKeyUrl(originalUrl);
792
+ context.url = rewriteHlsRequestUrl(originalUrl, store, options);
793
+ if (isKey && !options.keyProxyUrlBuilder && options.getKeyAccessToken) {
794
+ const accessToken = options.getKeyAccessToken();
795
+ if (accessToken) {
796
+ context.headers = {
797
+ ...context.headers ?? {},
798
+ Authorization: `Bearer ${accessToken}`
799
+ };
800
+ }
801
+ }
780
802
  } catch {
781
803
  }
782
804
  }
@@ -786,6 +808,9 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
786
808
  }
787
809
 
788
810
  // src/streaming/HlsEngine.ts
811
+ function isAuthHttpStatus(code) {
812
+ return code === 401 || code === 403;
813
+ }
789
814
  var HlsEngine = class {
790
815
  constructor(options) {
791
816
  this.options = options;
@@ -798,49 +823,47 @@ var HlsEngine = class {
798
823
  this.qualitySelection = "auto";
799
824
  }
800
825
  async start() {
801
- const { videoEl, playbackUrl, autoplay, startMuted, volume, startPosition } = this.options;
826
+ const { videoEl, playbackUrl, startMuted, volume } = this.options;
802
827
  videoEl.muted = startMuted;
803
- if (typeof volume === "number") videoEl.volume = Math.min(1, Math.max(0, volume));
828
+ if (typeof volume === "number") {
829
+ videoEl.volume = Math.min(1, Math.max(0, volume));
830
+ }
804
831
  const Hls = (await import("hls.js")).default;
805
832
  if (Hls.isSupported()) {
806
833
  this.supportMode = "hls.js";
807
834
  this.options.callbacks.onSupportMode?.("hls.js");
808
835
  await this.startWithHlsJs(Hls, playbackUrl);
809
- } else if (videoEl.canPlayType("application/vnd.apple.mpegurl")) {
836
+ return;
837
+ }
838
+ if (videoEl.canPlayType("application/vnd.apple.mpegurl")) {
810
839
  this.supportMode = "native";
811
840
  this.options.callbacks.onSupportMode?.("native");
812
841
  videoEl.src = this.options.authParams.applyToUrl(playbackUrl);
813
842
  this.bindMediaEvents();
814
843
  this.options.callbacks.onStateChange?.("ready");
815
- } else {
816
- this.supportMode = "unsupported";
817
- this.options.callbacks.onSupportMode?.("unsupported");
818
- this.options.callbacks.onFatalError?.("HLS is not supported in this browser");
844
+ await this.applyStartPositionAndAutoplay();
819
845
  return;
820
846
  }
821
- if (typeof startPosition === "number" && startPosition > 0) {
822
- videoEl.currentTime = startPosition;
823
- }
824
- if (autoplay) {
825
- try {
826
- await videoEl.play();
827
- } catch {
828
- if (!videoEl.muted) {
829
- videoEl.muted = true;
830
- try {
831
- await videoEl.play();
832
- } catch {
833
- this.options.logger.warn("Autoplay blocked");
834
- }
835
- }
836
- }
837
- }
847
+ this.supportMode = "unsupported";
848
+ this.options.callbacks.onSupportMode?.("unsupported");
849
+ this.options.callbacks.onFatalError?.(
850
+ "HLS is not supported in this browser"
851
+ );
838
852
  }
839
853
  async startWithHlsJs(Hls, playbackUrl) {
840
- const { videoEl, authParams, hlsConfig, callbacks, videoId, keyProxyUrlBuilder } = this.options;
854
+ const {
855
+ videoEl,
856
+ authParams,
857
+ hlsConfig,
858
+ callbacks,
859
+ videoId,
860
+ keyProxyUrlBuilder,
861
+ getKeyAccessToken
862
+ } = this.options;
841
863
  const loader = createAuthParamsLoader(Hls, authParams, {
842
864
  videoId,
843
- keyProxyUrlBuilder
865
+ keyProxyUrlBuilder,
866
+ getKeyAccessToken
844
867
  });
845
868
  this.hls = new Hls({
846
869
  enableWorker: hlsConfig.enableWorker,
@@ -851,6 +874,7 @@ var HlsEngine = class {
851
874
  this.hls.attachMedia(videoEl);
852
875
  this.hls.loadSource(authParams.applyToUrl(playbackUrl));
853
876
  this.hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
877
+ if (this.destroyed) return;
854
878
  this.qualities = (data.levels ?? []).map((level, index) => ({
855
879
  index,
856
880
  height: level.height ?? 0,
@@ -860,42 +884,95 @@ var HlsEngine = class {
860
884
  }));
861
885
  callbacks.onQualitiesChange?.(this.qualities);
862
886
  callbacks.onStateChange?.("ready");
887
+ void this.applyStartPositionAndAutoplay();
863
888
  });
864
889
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
865
- if (!data.fatal) {
866
- 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)) {
867
- callbacks.onAuthRecoveryNeeded?.(
868
- data.details === Hls.ErrorDetails.KEY_LOAD_ERROR ? "key_error" : data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
869
- );
890
+ this.handleHlsError(Hls, data);
891
+ });
892
+ this.bindMediaEvents();
893
+ }
894
+ handleHlsError(Hls, data) {
895
+ if (this.destroyed) return;
896
+ const { callbacks, hlsConfig, keyProxyUrlBuilder, getKeyAccessToken } = this.options;
897
+ const status = data.response?.code;
898
+ if (data.details === Hls.ErrorDetails.KEY_LOAD_ERROR && !keyProxyUrlBuilder && !getKeyAccessToken) {
899
+ callbacks.onFatalError?.(
900
+ "Encryption key load failed. Configure streaming.getKeyAccessToken (browser Bearer) or streaming.keyProxyUrlBuilder (Cloudflare-allowlisted BFF) for Appextech AES-encrypted HLS."
901
+ );
902
+ return;
903
+ }
904
+ const authFailure = isAuthHttpStatus(status) || data.details === Hls.ErrorDetails.KEY_LOAD_ERROR && (status === void 0 || status === 0 || isAuthHttpStatus(status));
905
+ if (!data.fatal) {
906
+ 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)) {
907
+ callbacks.onAuthRecoveryNeeded?.(
908
+ data.details === Hls.ErrorDetails.KEY_LOAD_ERROR ? "key_error" : data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
909
+ );
910
+ }
911
+ return;
912
+ }
913
+ if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
914
+ this.networkRecoveryCount += 1;
915
+ if (this.networkRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
916
+ if (authFailure) {
917
+ callbacks.onAuthRecoveryNeeded?.("fatal_network");
870
918
  }
919
+ this.hls?.startLoad();
871
920
  return;
872
921
  }
873
- if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
874
- this.networkRecoveryCount += 1;
875
- if (this.networkRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
876
- callbacks.onAuthRecoveryNeeded?.("fatal_network");
877
- this.hls?.startLoad();
878
- return;
922
+ }
923
+ if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
924
+ this.mediaRecoveryCount += 1;
925
+ if (this.mediaRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
926
+ this.hls?.recoverMediaError();
927
+ return;
928
+ }
929
+ }
930
+ callbacks.onFatalError?.(data.details || "Fatal HLS error");
931
+ }
932
+ async applyStartPositionAndAutoplay() {
933
+ if (this.destroyed) return;
934
+ const { videoEl, autoplay, startPosition } = this.options;
935
+ if (typeof startPosition === "number" && startPosition > 0) {
936
+ const seek = () => {
937
+ if (this.destroyed) return;
938
+ try {
939
+ videoEl.currentTime = startPosition;
940
+ } catch {
879
941
  }
942
+ };
943
+ if (videoEl.seekable.length > 0) {
944
+ seek();
945
+ } else {
946
+ videoEl.addEventListener("loadedmetadata", seek, { once: true });
880
947
  }
881
- if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
882
- this.mediaRecoveryCount += 1;
883
- if (this.mediaRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
884
- this.hls?.recoverMediaError();
885
- return;
948
+ }
949
+ if (!autoplay) return;
950
+ try {
951
+ await videoEl.play();
952
+ } catch {
953
+ if (!videoEl.muted) {
954
+ videoEl.muted = true;
955
+ try {
956
+ await videoEl.play();
957
+ } catch {
958
+ this.options.logger.warn("Autoplay blocked");
886
959
  }
887
960
  }
888
- callbacks.onFatalError?.(data.details || "Fatal HLS error");
889
- });
890
- this.bindMediaEvents();
961
+ }
891
962
  }
892
963
  bindMediaEvents() {
893
964
  const { videoEl, callbacks } = this.options;
894
- videoEl.addEventListener("playing", () => callbacks.onStateChange?.("playing"));
965
+ videoEl.addEventListener(
966
+ "playing",
967
+ () => callbacks.onStateChange?.("playing")
968
+ );
895
969
  videoEl.addEventListener("pause", () => {
896
970
  if (!videoEl.ended) callbacks.onStateChange?.("paused");
897
971
  });
898
- videoEl.addEventListener("waiting", () => callbacks.onStateChange?.("buffering"));
972
+ videoEl.addEventListener(
973
+ "waiting",
974
+ () => callbacks.onStateChange?.("buffering")
975
+ );
899
976
  videoEl.addEventListener("ended", () => {
900
977
  callbacks.onStateChange?.("ended");
901
978
  callbacks.onEnded?.();
@@ -913,6 +990,7 @@ var HlsEngine = class {
913
990
  }
914
991
  }
915
992
  resumeAfterAuthRenew() {
993
+ this.networkRecoveryCount = 0;
916
994
  this.hls?.startLoad();
917
995
  this.updateAuthParams();
918
996
  }
@@ -1055,7 +1133,8 @@ function createVideoPlayer(options) {
1055
1133
  startPosition: config.player.startPosition,
1056
1134
  volume: config.player.volume,
1057
1135
  videoId: options.videoId,
1058
- keyProxyUrlBuilder: options.streaming?.keyProxyUrlBuilder,
1136
+ keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1137
+ getKeyAccessToken: config.streaming.getKeyAccessToken,
1059
1138
  hlsConfig: config.streaming.hls,
1060
1139
  logger,
1061
1140
  callbacks: {
@@ -1116,6 +1195,14 @@ function createVideoPlayer(options) {
1116
1195
  onRenewing: () => setState("renewing"),
1117
1196
  onRenewed: (nextExpiresAt) => {
1118
1197
  engine?.resumeAfterAuthRenew();
1198
+ if (!destroyed) {
1199
+ const el2 = videoEl;
1200
+ if (el2 && !el2.paused && !el2.ended) {
1201
+ setState("playing");
1202
+ } else {
1203
+ setState("ready");
1204
+ }
1205
+ }
1119
1206
  events.emit("tokenrefresh", { expiresAt: nextExpiresAt });
1120
1207
  events.emit("sourcechange", { expiresAt: nextExpiresAt });
1121
1208
  },
@@ -1317,7 +1404,8 @@ function createVideoPlayer(options) {
1317
1404
  streaming: {
1318
1405
  engine: "hls",
1319
1406
  hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
1320
- keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder
1407
+ keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder,
1408
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken
1321
1409
  },
1322
1410
  logging: { ...config.logging, ...partial?.logging },
1323
1411
  debug: { ...config.debug, ...partial?.debug },