@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/CHANGELOG.md +51 -11
- package/dist/index.cjs +408 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -14
- package/dist/index.d.ts +87 -14
- package/dist/index.js +406 -43
- package/dist/index.js.map +1 -1
- package/package.json +14 -3
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.
|
|
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
|
|
474
|
-
if (
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
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
|
|
850
|
-
return class AuthParamsLoader
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
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
|
|
1128
|
+
const { callbacks, hlsConfig } = this.options;
|
|
961
1129
|
const status = data.response?.code;
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
|
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.
|
|
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
|