@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.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.
|
|
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
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
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
|
|
786
|
-
return class AuthParamsLoader
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
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
|
|
1062
|
+
const { callbacks, hlsConfig } = this.options;
|
|
897
1063
|
const status = data.response?.code;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
|
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.
|
|
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
|