@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.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.5";
149
+ var PACKAGE_VERSION = "0.1.8";
150
150
  var DEFAULT_PLAYER_BEHAVIOR = {
151
151
  autoplay: true,
152
152
  startMuted: true,
@@ -406,23 +406,25 @@ function isPlainObject2(value) {
406
406
  const proto = Object.getPrototypeOf(value);
407
407
  return proto === Object.prototype || proto === null;
408
408
  }
409
- function redactValue(key, value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
410
- if (SENSITIVE_KEYS.test(key)) return "[REDACTED]";
411
- if (typeof value === "string") {
412
- if (/^https?:\/\//i.test(value) && /[?&](token|expires)=/i.test(value)) {
413
- try {
414
- const url = new URL(value);
415
- for (const param of ["token", "expires", "signature", "sig"]) {
416
- if (url.searchParams.has(param)) {
417
- url.searchParams.set(param, "[REDACTED]");
418
- }
419
- }
420
- return url.toString();
421
- } catch {
422
- return "[REDACTED_URL]";
409
+ function redactStringUrl(value) {
410
+ if (!/^https?:\/\//i.test(value) || !/[?&](token|expires)=/i.test(value)) {
411
+ return value;
412
+ }
413
+ try {
414
+ const url = new URL(value);
415
+ for (const param of ["token", "expires", "signature", "sig"]) {
416
+ if (url.searchParams.has(param)) {
417
+ url.searchParams.set(param, "[REDACTED]");
423
418
  }
424
419
  }
420
+ return url.toString();
421
+ } catch {
422
+ return "[REDACTED_URL]";
425
423
  }
424
+ }
425
+ function redactValue(key, value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
426
+ if (SENSITIVE_KEYS.test(key)) return "[REDACTED]";
427
+ if (typeof value === "string") return redactStringUrl(value);
426
428
  if (Array.isArray(value)) {
427
429
  if (depth >= REDACT_MAX_DEPTH || seen.has(value)) return "[REDACTED_CYCLE]";
428
430
  seen.add(value);
@@ -689,10 +691,11 @@ var RefreshManager = class {
689
691
  this.options.logger.debug("Playback source renewed", { reason });
690
692
  } catch (error) {
691
693
  this.consecutiveFailures += 1;
692
- const message = error instanceof Error ? error.message : "Playback refresh failed";
694
+ const message = refreshErrorMessage(error);
693
695
  this.options.logger.warn("Playback refresh failed", {
694
696
  reason,
695
- failure: this.consecutiveFailures
697
+ failure: this.consecutiveFailures,
698
+ message
696
699
  });
697
700
  if (this.consecutiveFailures >= this.options.renewMaxRetries) {
698
701
  this.options.onFatal?.(message);
@@ -712,6 +715,13 @@ var RefreshManager = class {
712
715
  }
713
716
  }
714
717
  };
718
+ function refreshErrorMessage(error) {
719
+ if (error instanceof Error && error.message) return error.message;
720
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string" && error.message) {
721
+ return error.message;
722
+ }
723
+ return "Playback refresh failed";
724
+ }
715
725
 
716
726
  // src/playback/liveEdge.ts
717
727
  var DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS = 3;
@@ -755,7 +765,9 @@ function seekToLiveEdge(media, options) {
755
765
  }
756
766
 
757
767
  // src/streaming/authParamsLoader.ts
768
+ import { XhrLoader as BuiltinXhrLoader } from "hls.js";
758
769
  var ENC_KEY_PATH_RE = /enc\.key$/i;
770
+ var BUILTIN_LOADER_NAMES = /* @__PURE__ */ new Set(["XhrLoader", "FetchLoader"]);
759
771
  function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
760
772
  try {
761
773
  return ENC_KEY_PATH_RE.test(new URL(rawUrl, baseOrigin).pathname);
@@ -763,6 +775,27 @@ function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined"
763
775
  return /enc\.key/i.test(rawUrl);
764
776
  }
765
777
  }
778
+ function resolveBuiltinHlsLoader(HlsClass) {
779
+ if (typeof BuiltinXhrLoader === "function") {
780
+ return BuiltinXhrLoader;
781
+ }
782
+ const start = HlsClass.DefaultConfig.loader;
783
+ let current = start;
784
+ const seen = /* @__PURE__ */ new Set();
785
+ while (current && !seen.has(current)) {
786
+ seen.add(current);
787
+ const name = typeof current.name === "string" ? current.name : "";
788
+ if (BUILTIN_LOADER_NAMES.has(name)) {
789
+ return current;
790
+ }
791
+ const parentCtor = Object.getPrototypeOf(current.prototype)?.constructor;
792
+ if (!parentCtor || parentCtor === Object || parentCtor === Function) {
793
+ break;
794
+ }
795
+ current = parentCtor;
796
+ }
797
+ return start;
798
+ }
766
799
  function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
767
800
  if (!rawUrl) return rawUrl;
768
801
  const parsed = new URL(rawUrl, baseOrigin);
@@ -781,28 +814,145 @@ function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof l
781
814
  if (!store.hasParams()) return parsed.toString();
782
815
  return store.applyToUrl(rawUrl, baseOrigin);
783
816
  }
817
+ function createLoaderStats() {
818
+ const now = performance.now();
819
+ return {
820
+ aborted: false,
821
+ loaded: 0,
822
+ retry: 0,
823
+ total: 0,
824
+ chunkCount: 0,
825
+ bwEstimate: 0,
826
+ loading: { start: now, first: 0, end: 0 },
827
+ parsing: { start: 0, end: 0 },
828
+ buffering: { start: 0, first: 0, end: 0 }
829
+ };
830
+ }
831
+ async function loadEncryptionKeyWithFetch(context, callbacks, accessToken, signal) {
832
+ const stats = createLoaderStats();
833
+ stats.loading.first = performance.now();
834
+ const headers = {
835
+ Accept: "*/*",
836
+ ...context.headers
837
+ };
838
+ if (accessToken) {
839
+ headers.Authorization = `Bearer ${accessToken}`;
840
+ }
841
+ const response = await fetch(context.url, {
842
+ method: "GET",
843
+ headers,
844
+ credentials: "omit",
845
+ cache: "no-store",
846
+ signal
847
+ });
848
+ if (!response.ok) {
849
+ callbacks.onError(
850
+ {
851
+ code: response.status,
852
+ text: response.statusText || `enc.key HTTP ${response.status}`
853
+ },
854
+ context,
855
+ response,
856
+ stats
857
+ );
858
+ return;
859
+ }
860
+ const data = await response.arrayBuffer();
861
+ const end = performance.now();
862
+ stats.loading.end = end;
863
+ stats.loaded = data.byteLength;
864
+ stats.total = data.byteLength;
865
+ const head = new TextDecoder().decode(data.slice(0, 32));
866
+ if (/<!DOCTYPE|<html/i.test(head)) {
867
+ callbacks.onError(
868
+ {
869
+ code: 403,
870
+ text: "enc.key returned HTML (CDN challenge/block), not an AES key"
871
+ },
872
+ context,
873
+ response,
874
+ stats
875
+ );
876
+ return;
877
+ }
878
+ callbacks.onSuccess(
879
+ {
880
+ url: response.url || context.url,
881
+ data,
882
+ code: response.status
883
+ },
884
+ stats,
885
+ context,
886
+ response
887
+ );
888
+ }
784
889
  function createAuthParamsLoader(HlsClass, store, options = {}) {
785
- const BaseLoader = HlsClass.DefaultConfig.loader;
786
- return class AuthParamsLoader extends BaseLoader {
890
+ const BaseLoader = resolveBuiltinHlsLoader(HlsClass);
891
+ return class AuthParamsLoader {
892
+ constructor(config) {
893
+ this.keyAbort = null;
894
+ this.inner = new BaseLoader(config);
895
+ this.stats = this.inner.stats;
896
+ }
897
+ abort() {
898
+ this.keyAbort?.abort();
899
+ this.keyAbort = null;
900
+ this.inner.abort?.();
901
+ }
902
+ destroy() {
903
+ this.keyAbort?.abort();
904
+ this.keyAbort = null;
905
+ this.inner.destroy();
906
+ }
787
907
  load(context, config, callbacks) {
788
- if (context.url) {
789
- try {
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
- }
908
+ if (!context.url) {
909
+ this.inner.load(context, config, callbacks);
910
+ return;
911
+ }
912
+ try {
913
+ const originalUrl = context.url;
914
+ const isKey = isEncryptionKeyUrl(originalUrl);
915
+ const usingKeyProxy = Boolean(
916
+ isKey && options.keyProxyUrlBuilder && options.videoId
917
+ );
918
+ context.url = rewriteHlsRequestUrl(originalUrl, store, options);
919
+ if (usingKeyProxy) {
920
+ this.inner.load(context, config, callbacks);
921
+ return;
922
+ }
923
+ if (isKey && typeof options.getKeyAccessToken === "function") {
924
+ const accessToken = options.getKeyAccessToken() ?? null;
925
+ if (accessToken) {
926
+ this.keyAbort?.abort();
927
+ this.keyAbort = new AbortController();
928
+ const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
929
+ const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
930
+ void loadEncryptionKeyWithFetch(
931
+ context,
932
+ callbacks,
933
+ accessToken,
934
+ this.keyAbort.signal
935
+ ).catch((error) => {
936
+ const stats = createLoaderStats();
937
+ const aborted = error instanceof DOMException && error.name === "AbortError";
938
+ callbacks.onError(
939
+ {
940
+ code: 0,
941
+ text: aborted ? "enc.key fetch aborted/timeout" : error instanceof Error ? error.message : "enc.key fetch failed"
942
+ },
943
+ context,
944
+ null,
945
+ stats
946
+ );
947
+ }).finally(() => {
948
+ clearTimeout(timer);
949
+ });
950
+ return;
801
951
  }
802
- } catch {
803
952
  }
953
+ } catch {
804
954
  }
805
- super.load(context, config, callbacks);
955
+ this.inner.load(context, config, callbacks);
806
956
  }
807
957
  };
808
958
  }
@@ -821,6 +971,11 @@ var HlsEngine = class {
821
971
  this.mediaRecoveryCount = 0;
822
972
  this.qualities = [];
823
973
  this.qualitySelection = "auto";
974
+ this.textTracks = [];
975
+ this.textTrackSelection = "off";
976
+ this.audioTracks = [];
977
+ this.audioTrackSelection = 0;
978
+ this.nativeTextTrackCleanup = null;
824
979
  }
825
980
  async start() {
826
981
  const { videoEl, playbackUrl, startMuted, volume } = this.options;
@@ -840,6 +995,7 @@ var HlsEngine = class {
840
995
  this.options.callbacks.onSupportMode?.("native");
841
996
  videoEl.src = this.options.authParams.applyToUrl(playbackUrl);
842
997
  this.bindMediaEvents();
998
+ this.bindNativeTextTracks();
843
999
  this.options.callbacks.onStateChange?.("ready");
844
1000
  await this.applyStartPositionAndAutoplay();
845
1001
  return;
@@ -883,9 +1039,19 @@ var HlsEngine = class {
883
1039
  label: level.height != null && level.height > 0 ? `${level.height}p` : `Level ${index}`
884
1040
  }));
885
1041
  callbacks.onQualitiesChange?.(this.qualities);
1042
+ this.syncHlsSubtitleTracks();
1043
+ this.syncHlsAudioTracks();
886
1044
  callbacks.onStateChange?.("ready");
887
1045
  void this.applyStartPositionAndAutoplay();
888
1046
  });
1047
+ this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
1048
+ if (this.destroyed) return;
1049
+ this.syncHlsSubtitleTracks();
1050
+ });
1051
+ this.hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => {
1052
+ if (this.destroyed) return;
1053
+ this.syncHlsAudioTracks();
1054
+ });
889
1055
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
890
1056
  this.handleHlsError(Hls, data);
891
1057
  });
@@ -893,19 +1059,24 @@ var HlsEngine = class {
893
1059
  }
894
1060
  handleHlsError(Hls, data) {
895
1061
  if (this.destroyed) return;
896
- const { callbacks, hlsConfig, keyProxyUrlBuilder, getKeyAccessToken } = this.options;
1062
+ const { callbacks, hlsConfig } = this.options;
897
1063
  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;
1064
+ const authFailure = isAuthHttpStatus(status);
1065
+ if (data.details === Hls.ErrorDetails.KEY_LOAD_ERROR) {
1066
+ if (authFailure) {
1067
+ callbacks.onAuthRecoveryNeeded?.("key_error");
1068
+ if (!data.fatal) return;
1069
+ } else if (!data.fatal) {
1070
+ callbacks.onFatalError?.(
1071
+ 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."
1072
+ );
1073
+ return;
1074
+ }
903
1075
  }
904
- const authFailure = isAuthHttpStatus(status) || data.details === Hls.ErrorDetails.KEY_LOAD_ERROR && (status === void 0 || status === 0 || isAuthHttpStatus(status));
905
1076
  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)) {
1077
+ if (authFailure && data.type === Hls.ErrorTypes.NETWORK_ERROR && (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR || data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR)) {
907
1078
  callbacks.onAuthRecoveryNeeded?.(
908
- data.details === Hls.ErrorDetails.KEY_LOAD_ERROR ? "key_error" : data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
1079
+ data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
909
1080
  );
910
1081
  }
911
1082
  return;
@@ -1006,12 +1177,120 @@ var HlsEngine = class {
1006
1177
  getQualities() {
1007
1178
  return this.qualities;
1008
1179
  }
1180
+ getTextTracks() {
1181
+ return this.textTracks;
1182
+ }
1183
+ getTextTrackSelection() {
1184
+ return this.textTrackSelection;
1185
+ }
1186
+ setTextTrack(selection) {
1187
+ if (this.destroyed) return;
1188
+ this.textTrackSelection = selection;
1189
+ this.options.callbacks.onTextTrackSelectionChange?.(selection);
1190
+ if (this.supportMode === "hls.js" && this.hls) {
1191
+ this.hls.subtitleDisplay = selection !== "off";
1192
+ this.hls.subtitleTrack = selection === "off" ? -1 : selection;
1193
+ return;
1194
+ }
1195
+ if (this.supportMode === "native" && this.options.videoEl) {
1196
+ const tracks = this.options.videoEl.textTracks;
1197
+ for (let i = 0; i < tracks.length; i += 1) {
1198
+ const track = tracks[i];
1199
+ track.mode = selection !== "off" && i === selection ? "showing" : "disabled";
1200
+ }
1201
+ }
1202
+ }
1203
+ getAudioTracks() {
1204
+ return this.audioTracks;
1205
+ }
1206
+ getAudioTrackSelection() {
1207
+ return this.audioTrackSelection;
1208
+ }
1209
+ setAudioTrack(index) {
1210
+ if (this.destroyed) return;
1211
+ if (!Number.isInteger(index) || index < 0 || this.audioTracks.length > 0 && index >= this.audioTracks.length) {
1212
+ return;
1213
+ }
1214
+ this.audioTrackSelection = index;
1215
+ this.options.callbacks.onAudioTrackSelectionChange?.(index);
1216
+ if (this.supportMode === "hls.js" && this.hls) {
1217
+ this.hls.audioTrack = index;
1218
+ return;
1219
+ }
1220
+ if (this.supportMode === "native" && this.options.videoEl) {
1221
+ const audioTracks = this.options.videoEl.audioTracks;
1222
+ if (audioTracks && audioTracks.length > 0) {
1223
+ for (let i = 0; i < audioTracks.length; i += 1) {
1224
+ audioTracks[i].enabled = i === index;
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1009
1229
  getSupportMode() {
1010
1230
  return this.supportMode;
1011
1231
  }
1232
+ syncHlsSubtitleTracks() {
1233
+ if (!this.hls) return;
1234
+ const tracks = (this.hls.subtitleTracks ?? []).map(
1235
+ (track, index) => ({
1236
+ index,
1237
+ id: String(track.id ?? index),
1238
+ label: track.name || track.lang || `Subtitles ${index + 1}`,
1239
+ language: track.lang
1240
+ })
1241
+ );
1242
+ this.textTracks = tracks;
1243
+ this.options.callbacks.onTextTracksChange?.(tracks);
1244
+ }
1245
+ syncHlsAudioTracks() {
1246
+ if (!this.hls) return;
1247
+ const tracks = (this.hls.audioTracks ?? []).map(
1248
+ (track, index) => ({
1249
+ index,
1250
+ id: String(track.id ?? index),
1251
+ label: track.name || track.lang || `Audio ${index + 1}`,
1252
+ language: track.lang
1253
+ })
1254
+ );
1255
+ this.audioTracks = tracks;
1256
+ this.options.callbacks.onAudioTracksChange?.(tracks);
1257
+ if (tracks.length > 0) {
1258
+ this.audioTrackSelection = 0;
1259
+ this.options.callbacks.onAudioTrackSelectionChange?.(0);
1260
+ }
1261
+ }
1262
+ bindNativeTextTracks() {
1263
+ const { videoEl } = this.options;
1264
+ const syncFromElement = () => {
1265
+ if (this.destroyed || this.textTracks.length > 0) return;
1266
+ const list = videoEl.textTracks;
1267
+ const tracks = [];
1268
+ for (let i = 0; i < list.length; i += 1) {
1269
+ const track = list[i];
1270
+ if (track.kind !== "subtitles" && track.kind !== "captions") continue;
1271
+ tracks.push({
1272
+ index: tracks.length,
1273
+ id: track.id || `native-${tracks.length}`,
1274
+ label: track.label || track.language || `Track ${tracks.length + 1}`,
1275
+ language: track.language || void 0
1276
+ });
1277
+ }
1278
+ if (tracks.length === 0) return;
1279
+ this.textTracks = tracks;
1280
+ this.options.callbacks.onTextTracksChange?.(tracks);
1281
+ };
1282
+ const textTracks = videoEl.textTracks;
1283
+ textTracks.addEventListener?.("addtrack", syncFromElement);
1284
+ syncFromElement();
1285
+ this.nativeTextTrackCleanup = () => {
1286
+ textTracks.removeEventListener?.("addtrack", syncFromElement);
1287
+ };
1288
+ }
1012
1289
  destroy() {
1013
1290
  if (this.destroyed) return;
1014
1291
  this.destroyed = true;
1292
+ this.nativeTextTrackCleanup?.();
1293
+ this.nativeTextTrackCleanup = null;
1015
1294
  this.hls?.destroy();
1016
1295
  this.hls = null;
1017
1296
  const { videoEl } = this.options;
@@ -1068,11 +1347,40 @@ function createVideoPlayer(options) {
1068
1347
  let refreshManager = null;
1069
1348
  let fullscreenEl = null;
1070
1349
  let isLiveStream = false;
1350
+ let mediaCleanup = null;
1071
1351
  const setState = (next) => {
1072
1352
  if (destroyed && next !== "destroyed") return;
1073
1353
  state = next;
1074
1354
  events.emit("statechange", { state: next });
1075
1355
  };
1356
+ const bindMediaProgressEvents = (el) => {
1357
+ mediaCleanup?.();
1358
+ const onTimeUpdate = () => {
1359
+ if (destroyed) return;
1360
+ events.emit("timeupdate", {
1361
+ currentTime: el.currentTime,
1362
+ duration: Number.isFinite(el.duration) ? el.duration : 0
1363
+ });
1364
+ };
1365
+ const onProgress = () => {
1366
+ if (destroyed) return;
1367
+ let bufferedEnd = 0;
1368
+ try {
1369
+ if (el.buffered.length > 0) {
1370
+ bufferedEnd = el.buffered.end(el.buffered.length - 1);
1371
+ }
1372
+ } catch {
1373
+ bufferedEnd = 0;
1374
+ }
1375
+ events.emit("progress", { bufferedEnd });
1376
+ };
1377
+ el.addEventListener("timeupdate", onTimeUpdate);
1378
+ el.addEventListener("progress", onProgress);
1379
+ mediaCleanup = () => {
1380
+ el.removeEventListener("timeupdate", onTimeUpdate);
1381
+ el.removeEventListener("progress", onProgress);
1382
+ };
1383
+ };
1076
1384
  const authManager = new AuthManager(
1077
1385
  options.auth,
1078
1386
  options.auth?.refreshBeforeExpiryMs ?? 6e4,
@@ -1123,6 +1431,7 @@ function createVideoPlayer(options) {
1123
1431
  isLiveStream = Boolean(source.live);
1124
1432
  }
1125
1433
  const el = ensureVideoElement();
1434
+ bindMediaProgressEvents(el);
1126
1435
  engine?.destroy();
1127
1436
  engine = new HlsEngine({
1128
1437
  videoEl: el,
@@ -1134,6 +1443,11 @@ function createVideoPlayer(options) {
1134
1443
  volume: config.player.volume,
1135
1444
  videoId: options.videoId,
1136
1445
  keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1446
+ // Optional: browser Bearer for Video API–origin enc.key (CORS-dependent).
1447
+ // CDN keys use signed query via the auth loader (no Bearer) — same as
1448
+ // segments. Apps can pass getKeyAccessToken or rely on keyProxyUrlBuilder.
1449
+ // When unset, defaults to AuthManager token only if explicitly requested
1450
+ // via streaming.getKeyAccessToken; otherwise query-signed XHR.
1137
1451
  getKeyAccessToken: config.streaming.getKeyAccessToken,
1138
1452
  hlsConfig: config.streaming.hls,
1139
1453
  logger,
@@ -1176,6 +1490,30 @@ function createVideoPlayer(options) {
1176
1490
  label: typeof selection === "number" ? level?.label : "auto"
1177
1491
  });
1178
1492
  },
1493
+ onTextTracksChange: (tracks) => {
1494
+ events.emit("texttrackchange", {
1495
+ tracks,
1496
+ selection: engine?.getTextTrackSelection() ?? "off"
1497
+ });
1498
+ },
1499
+ onTextTrackSelectionChange: (selection) => {
1500
+ events.emit("texttrackchange", {
1501
+ tracks: engine?.getTextTracks() ?? [],
1502
+ selection
1503
+ });
1504
+ },
1505
+ onAudioTracksChange: (tracks) => {
1506
+ events.emit("audiotrackchange", {
1507
+ tracks,
1508
+ selection: engine?.getAudioTrackSelection() ?? 0
1509
+ });
1510
+ },
1511
+ onAudioTrackSelectionChange: (selection) => {
1512
+ events.emit("audiotrackchange", {
1513
+ tracks: engine?.getAudioTracks() ?? [],
1514
+ selection
1515
+ });
1516
+ },
1179
1517
  onEnded: () => events.emit("ended")
1180
1518
  }
1181
1519
  });
@@ -1334,6 +1672,24 @@ function createVideoPlayer(options) {
1334
1672
  getAvailableQualities() {
1335
1673
  return engine?.getQualities() ?? [];
1336
1674
  },
1675
+ getTextTracks() {
1676
+ return engine?.getTextTracks() ?? [];
1677
+ },
1678
+ getSelectedTextTrack() {
1679
+ return engine?.getTextTrackSelection() ?? "off";
1680
+ },
1681
+ setTextTrack(selection) {
1682
+ engine?.setTextTrack(selection);
1683
+ },
1684
+ getAudioTracks() {
1685
+ return engine?.getAudioTracks() ?? [];
1686
+ },
1687
+ getSelectedAudioTrack() {
1688
+ return engine?.getAudioTrackSelection() ?? 0;
1689
+ },
1690
+ setAudioTrack(index) {
1691
+ engine?.setAudioTrack(index);
1692
+ },
1337
1693
  async enterFullscreen() {
1338
1694
  const target = fullscreenEl ?? videoEl?.parentElement ?? videoEl;
1339
1695
  if (target && "requestFullscreen" in target) {
@@ -1350,6 +1706,9 @@ function createVideoPlayer(options) {
1350
1706
  isFullscreen() {
1351
1707
  return Boolean(document.fullscreenElement);
1352
1708
  },
1709
+ setFullscreenElement(el) {
1710
+ fullscreenEl = el;
1711
+ },
1353
1712
  async reload() {
1354
1713
  refreshManager?.stop();
1355
1714
  engine?.destroy();
@@ -1358,6 +1717,8 @@ function createVideoPlayer(options) {
1358
1717
  destroy() {
1359
1718
  if (destroyed) return;
1360
1719
  destroyed = true;
1720
+ mediaCleanup?.();
1721
+ mediaCleanup = null;
1361
1722
  refreshManager?.stop();
1362
1723
  refreshManager = null;
1363
1724
  engine?.destroy();
@@ -1448,11 +1809,13 @@ export {
1448
1809
  createVideoPlayer,
1449
1810
  getLiveEdgeTime,
1450
1811
  isAtLiveEdge,
1812
+ isEncryptionKeyUrl,
1451
1813
  isPlainObject,
1452
1814
  mergePlayerConfig,
1453
1815
  redactObject,
1454
1816
  redactSensitive,
1455
1817
  redactValue,
1818
+ resolveBuiltinHlsLoader,
1456
1819
  rewriteHlsRequestUrl,
1457
1820
  seekToLiveEdge,
1458
1821
  themeToCssVars