@codenkay/video-nsgplayer-core 0.1.5 → 2.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/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
  }
@@ -885,6 +1037,11 @@ var HlsEngine = class {
885
1037
  this.mediaRecoveryCount = 0;
886
1038
  this.qualities = [];
887
1039
  this.qualitySelection = "auto";
1040
+ this.textTracks = [];
1041
+ this.textTrackSelection = "off";
1042
+ this.audioTracks = [];
1043
+ this.audioTrackSelection = 0;
1044
+ this.nativeTextTrackCleanup = null;
888
1045
  }
889
1046
  async start() {
890
1047
  const { videoEl, playbackUrl, startMuted, volume } = this.options;
@@ -904,6 +1061,7 @@ var HlsEngine = class {
904
1061
  this.options.callbacks.onSupportMode?.("native");
905
1062
  videoEl.src = this.options.authParams.applyToUrl(playbackUrl);
906
1063
  this.bindMediaEvents();
1064
+ this.bindNativeTextTracks();
907
1065
  this.options.callbacks.onStateChange?.("ready");
908
1066
  await this.applyStartPositionAndAutoplay();
909
1067
  return;
@@ -947,9 +1105,19 @@ var HlsEngine = class {
947
1105
  label: level.height != null && level.height > 0 ? `${level.height}p` : `Level ${index}`
948
1106
  }));
949
1107
  callbacks.onQualitiesChange?.(this.qualities);
1108
+ this.syncHlsSubtitleTracks();
1109
+ this.syncHlsAudioTracks();
950
1110
  callbacks.onStateChange?.("ready");
951
1111
  void this.applyStartPositionAndAutoplay();
952
1112
  });
1113
+ this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
1114
+ if (this.destroyed) return;
1115
+ this.syncHlsSubtitleTracks();
1116
+ });
1117
+ this.hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => {
1118
+ if (this.destroyed) return;
1119
+ this.syncHlsAudioTracks();
1120
+ });
953
1121
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
954
1122
  this.handleHlsError(Hls, data);
955
1123
  });
@@ -957,19 +1125,24 @@ var HlsEngine = class {
957
1125
  }
958
1126
  handleHlsError(Hls, data) {
959
1127
  if (this.destroyed) return;
960
- const { callbacks, hlsConfig, keyProxyUrlBuilder, getKeyAccessToken } = this.options;
1128
+ const { callbacks, hlsConfig } = this.options;
961
1129
  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;
1130
+ const authFailure = isAuthHttpStatus(status);
1131
+ if (data.details === Hls.ErrorDetails.KEY_LOAD_ERROR) {
1132
+ if (authFailure) {
1133
+ callbacks.onAuthRecoveryNeeded?.("key_error");
1134
+ if (!data.fatal) return;
1135
+ } else if (!data.fatal) {
1136
+ callbacks.onFatalError?.(
1137
+ 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."
1138
+ );
1139
+ return;
1140
+ }
967
1141
  }
968
- const authFailure = isAuthHttpStatus(status) || data.details === Hls.ErrorDetails.KEY_LOAD_ERROR && (status === void 0 || status === 0 || isAuthHttpStatus(status));
969
1142
  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)) {
1143
+ if (authFailure && data.type === Hls.ErrorTypes.NETWORK_ERROR && (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR || data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR)) {
971
1144
  callbacks.onAuthRecoveryNeeded?.(
972
- data.details === Hls.ErrorDetails.KEY_LOAD_ERROR ? "key_error" : data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
1145
+ data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
973
1146
  );
974
1147
  }
975
1148
  return;
@@ -1070,12 +1243,120 @@ var HlsEngine = class {
1070
1243
  getQualities() {
1071
1244
  return this.qualities;
1072
1245
  }
1246
+ getTextTracks() {
1247
+ return this.textTracks;
1248
+ }
1249
+ getTextTrackSelection() {
1250
+ return this.textTrackSelection;
1251
+ }
1252
+ setTextTrack(selection) {
1253
+ if (this.destroyed) return;
1254
+ this.textTrackSelection = selection;
1255
+ this.options.callbacks.onTextTrackSelectionChange?.(selection);
1256
+ if (this.supportMode === "hls.js" && this.hls) {
1257
+ this.hls.subtitleDisplay = selection !== "off";
1258
+ this.hls.subtitleTrack = selection === "off" ? -1 : selection;
1259
+ return;
1260
+ }
1261
+ if (this.supportMode === "native" && this.options.videoEl) {
1262
+ const tracks = this.options.videoEl.textTracks;
1263
+ for (let i = 0; i < tracks.length; i += 1) {
1264
+ const track = tracks[i];
1265
+ track.mode = selection !== "off" && i === selection ? "showing" : "disabled";
1266
+ }
1267
+ }
1268
+ }
1269
+ getAudioTracks() {
1270
+ return this.audioTracks;
1271
+ }
1272
+ getAudioTrackSelection() {
1273
+ return this.audioTrackSelection;
1274
+ }
1275
+ setAudioTrack(index) {
1276
+ if (this.destroyed) return;
1277
+ if (!Number.isInteger(index) || index < 0 || this.audioTracks.length > 0 && index >= this.audioTracks.length) {
1278
+ return;
1279
+ }
1280
+ this.audioTrackSelection = index;
1281
+ this.options.callbacks.onAudioTrackSelectionChange?.(index);
1282
+ if (this.supportMode === "hls.js" && this.hls) {
1283
+ this.hls.audioTrack = index;
1284
+ return;
1285
+ }
1286
+ if (this.supportMode === "native" && this.options.videoEl) {
1287
+ const audioTracks = this.options.videoEl.audioTracks;
1288
+ if (audioTracks && audioTracks.length > 0) {
1289
+ for (let i = 0; i < audioTracks.length; i += 1) {
1290
+ audioTracks[i].enabled = i === index;
1291
+ }
1292
+ }
1293
+ }
1294
+ }
1073
1295
  getSupportMode() {
1074
1296
  return this.supportMode;
1075
1297
  }
1298
+ syncHlsSubtitleTracks() {
1299
+ if (!this.hls) return;
1300
+ const tracks = (this.hls.subtitleTracks ?? []).map(
1301
+ (track, index) => ({
1302
+ index,
1303
+ id: String(track.id ?? index),
1304
+ label: track.name || track.lang || `Subtitles ${index + 1}`,
1305
+ language: track.lang
1306
+ })
1307
+ );
1308
+ this.textTracks = tracks;
1309
+ this.options.callbacks.onTextTracksChange?.(tracks);
1310
+ }
1311
+ syncHlsAudioTracks() {
1312
+ if (!this.hls) return;
1313
+ const tracks = (this.hls.audioTracks ?? []).map(
1314
+ (track, index) => ({
1315
+ index,
1316
+ id: String(track.id ?? index),
1317
+ label: track.name || track.lang || `Audio ${index + 1}`,
1318
+ language: track.lang
1319
+ })
1320
+ );
1321
+ this.audioTracks = tracks;
1322
+ this.options.callbacks.onAudioTracksChange?.(tracks);
1323
+ if (tracks.length > 0) {
1324
+ this.audioTrackSelection = 0;
1325
+ this.options.callbacks.onAudioTrackSelectionChange?.(0);
1326
+ }
1327
+ }
1328
+ bindNativeTextTracks() {
1329
+ const { videoEl } = this.options;
1330
+ const syncFromElement = () => {
1331
+ if (this.destroyed || this.textTracks.length > 0) return;
1332
+ const list = videoEl.textTracks;
1333
+ const tracks = [];
1334
+ for (let i = 0; i < list.length; i += 1) {
1335
+ const track = list[i];
1336
+ if (track.kind !== "subtitles" && track.kind !== "captions") continue;
1337
+ tracks.push({
1338
+ index: tracks.length,
1339
+ id: track.id || `native-${tracks.length}`,
1340
+ label: track.label || track.language || `Track ${tracks.length + 1}`,
1341
+ language: track.language || void 0
1342
+ });
1343
+ }
1344
+ if (tracks.length === 0) return;
1345
+ this.textTracks = tracks;
1346
+ this.options.callbacks.onTextTracksChange?.(tracks);
1347
+ };
1348
+ const textTracks = videoEl.textTracks;
1349
+ textTracks.addEventListener?.("addtrack", syncFromElement);
1350
+ syncFromElement();
1351
+ this.nativeTextTrackCleanup = () => {
1352
+ textTracks.removeEventListener?.("addtrack", syncFromElement);
1353
+ };
1354
+ }
1076
1355
  destroy() {
1077
1356
  if (this.destroyed) return;
1078
1357
  this.destroyed = true;
1358
+ this.nativeTextTrackCleanup?.();
1359
+ this.nativeTextTrackCleanup = null;
1079
1360
  this.hls?.destroy();
1080
1361
  this.hls = null;
1081
1362
  const { videoEl } = this.options;
@@ -1132,11 +1413,40 @@ function createVideoPlayer(options) {
1132
1413
  let refreshManager = null;
1133
1414
  let fullscreenEl = null;
1134
1415
  let isLiveStream = false;
1416
+ let mediaCleanup = null;
1135
1417
  const setState = (next) => {
1136
1418
  if (destroyed && next !== "destroyed") return;
1137
1419
  state = next;
1138
1420
  events.emit("statechange", { state: next });
1139
1421
  };
1422
+ const bindMediaProgressEvents = (el) => {
1423
+ mediaCleanup?.();
1424
+ const onTimeUpdate = () => {
1425
+ if (destroyed) return;
1426
+ events.emit("timeupdate", {
1427
+ currentTime: el.currentTime,
1428
+ duration: Number.isFinite(el.duration) ? el.duration : 0
1429
+ });
1430
+ };
1431
+ const onProgress = () => {
1432
+ if (destroyed) return;
1433
+ let bufferedEnd = 0;
1434
+ try {
1435
+ if (el.buffered.length > 0) {
1436
+ bufferedEnd = el.buffered.end(el.buffered.length - 1);
1437
+ }
1438
+ } catch {
1439
+ bufferedEnd = 0;
1440
+ }
1441
+ events.emit("progress", { bufferedEnd });
1442
+ };
1443
+ el.addEventListener("timeupdate", onTimeUpdate);
1444
+ el.addEventListener("progress", onProgress);
1445
+ mediaCleanup = () => {
1446
+ el.removeEventListener("timeupdate", onTimeUpdate);
1447
+ el.removeEventListener("progress", onProgress);
1448
+ };
1449
+ };
1140
1450
  const authManager = new AuthManager(
1141
1451
  options.auth,
1142
1452
  options.auth?.refreshBeforeExpiryMs ?? 6e4,
@@ -1187,6 +1497,7 @@ function createVideoPlayer(options) {
1187
1497
  isLiveStream = Boolean(source.live);
1188
1498
  }
1189
1499
  const el = ensureVideoElement();
1500
+ bindMediaProgressEvents(el);
1190
1501
  engine?.destroy();
1191
1502
  engine = new HlsEngine({
1192
1503
  videoEl: el,
@@ -1198,6 +1509,11 @@ function createVideoPlayer(options) {
1198
1509
  volume: config.player.volume,
1199
1510
  videoId: options.videoId,
1200
1511
  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.
1201
1517
  getKeyAccessToken: config.streaming.getKeyAccessToken,
1202
1518
  hlsConfig: config.streaming.hls,
1203
1519
  logger,
@@ -1240,6 +1556,30 @@ function createVideoPlayer(options) {
1240
1556
  label: typeof selection === "number" ? level?.label : "auto"
1241
1557
  });
1242
1558
  },
1559
+ onTextTracksChange: (tracks) => {
1560
+ events.emit("texttrackchange", {
1561
+ tracks,
1562
+ selection: engine?.getTextTrackSelection() ?? "off"
1563
+ });
1564
+ },
1565
+ onTextTrackSelectionChange: (selection) => {
1566
+ events.emit("texttrackchange", {
1567
+ tracks: engine?.getTextTracks() ?? [],
1568
+ selection
1569
+ });
1570
+ },
1571
+ onAudioTracksChange: (tracks) => {
1572
+ events.emit("audiotrackchange", {
1573
+ tracks,
1574
+ selection: engine?.getAudioTrackSelection() ?? 0
1575
+ });
1576
+ },
1577
+ onAudioTrackSelectionChange: (selection) => {
1578
+ events.emit("audiotrackchange", {
1579
+ tracks: engine?.getAudioTracks() ?? [],
1580
+ selection
1581
+ });
1582
+ },
1243
1583
  onEnded: () => events.emit("ended")
1244
1584
  }
1245
1585
  });
@@ -1398,6 +1738,24 @@ function createVideoPlayer(options) {
1398
1738
  getAvailableQualities() {
1399
1739
  return engine?.getQualities() ?? [];
1400
1740
  },
1741
+ getTextTracks() {
1742
+ return engine?.getTextTracks() ?? [];
1743
+ },
1744
+ getSelectedTextTrack() {
1745
+ return engine?.getTextTrackSelection() ?? "off";
1746
+ },
1747
+ setTextTrack(selection) {
1748
+ engine?.setTextTrack(selection);
1749
+ },
1750
+ getAudioTracks() {
1751
+ return engine?.getAudioTracks() ?? [];
1752
+ },
1753
+ getSelectedAudioTrack() {
1754
+ return engine?.getAudioTrackSelection() ?? 0;
1755
+ },
1756
+ setAudioTrack(index) {
1757
+ engine?.setAudioTrack(index);
1758
+ },
1401
1759
  async enterFullscreen() {
1402
1760
  const target = fullscreenEl ?? videoEl?.parentElement ?? videoEl;
1403
1761
  if (target && "requestFullscreen" in target) {
@@ -1414,6 +1772,9 @@ function createVideoPlayer(options) {
1414
1772
  isFullscreen() {
1415
1773
  return Boolean(document.fullscreenElement);
1416
1774
  },
1775
+ setFullscreenElement(el) {
1776
+ fullscreenEl = el;
1777
+ },
1417
1778
  async reload() {
1418
1779
  refreshManager?.stop();
1419
1780
  engine?.destroy();
@@ -1422,6 +1783,8 @@ function createVideoPlayer(options) {
1422
1783
  destroy() {
1423
1784
  if (destroyed) return;
1424
1785
  destroyed = true;
1786
+ mediaCleanup?.();
1787
+ mediaCleanup = null;
1425
1788
  refreshManager?.stop();
1426
1789
  refreshManager = null;
1427
1790
  engine?.destroy();
@@ -1513,11 +1876,13 @@ function createVideoPlayer(options) {
1513
1876
  createVideoPlayer,
1514
1877
  getLiveEdgeTime,
1515
1878
  isAtLiveEdge,
1879
+ isEncryptionKeyUrl,
1516
1880
  isPlainObject,
1517
1881
  mergePlayerConfig,
1518
1882
  redactObject,
1519
1883
  redactSensitive,
1520
1884
  redactValue,
1885
+ resolveBuiltinHlsLoader,
1521
1886
  rewriteHlsRequestUrl,
1522
1887
  seekToLiveEdge,
1523
1888
  themeToCssVars