@codenkay/video-nsgplayer-core 0.1.3 → 1.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 +43 -5
- package/dist/index.cjs +329 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -12
- package/dist/index.d.ts +56 -12
- package/dist/index.js +327 -77
- package/dist/index.js.map +1 -1
- package/package.json +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- Fix minor errors
|
|
8
|
+
|
|
9
|
+
### Minor Changes
|
|
10
|
+
|
|
11
|
+
- 1a4dc9b: Initial public SDK packages: core createVideoPlayer API with dual auth/playback lifecycles, React and Angular adapters, full PlayerConfig parity with demo settings.
|
|
12
|
+
|
|
13
|
+
## 0.1.8
|
|
14
|
+
|
|
15
|
+
- Fix Firefox `InternalError: too much recursion` from nested hls.js loaders:
|
|
16
|
+
`createAuthParamsLoader` now **composes** a builtin `XhrLoader`/`FetchLoader`
|
|
17
|
+
instead of `extends DefaultConfig.loader` (which could nest `super.load()` after
|
|
18
|
+
HMR / repeated mounts if DefaultConfig was polluted).
|
|
19
|
+
- `resolveBuiltinHlsLoader()` walks past any wrapper classes to the builtin loader.
|
|
20
|
+
|
|
21
|
+
## 0.1.7
|
|
22
|
+
|
|
23
|
+
- Fix false AES key fatal: remove the post-publish gate that required
|
|
24
|
+
`getKeyAccessToken` or `keyProxyUrlBuilder` even when CDN `enc.key` only needs
|
|
25
|
+
signed query params (same auth as segments — pre-0.1.5 behavior).
|
|
26
|
+
- Load `enc.key` via signed query + XHR by default; use browser `fetch` + Bearer
|
|
27
|
+
only when `streaming.getKeyAccessToken` returns a token; use BFF when
|
|
28
|
+
`keyProxyUrlBuilder` is set.
|
|
29
|
+
- Clearer key-load errors that report HTTP/network cause instead of a configure prompt.
|
|
30
|
+
|
|
31
|
+
## 0.1.6
|
|
32
|
+
|
|
33
|
+
- Fix false "Playback refresh failed": `PlayerError` is not an `Error`, so refresh failures lost their real message.
|
|
34
|
+
- Do not treat HLS key CORS/`status 0` as signed-URL expiry (that caused renew loops). Only 401/403 trigger auth recovery.
|
|
35
|
+
|
|
36
|
+
## 0.1.5
|
|
37
|
+
|
|
38
|
+
- Add `streaming.getKeyAccessToken` for browser-direct AES `enc.key` loads with `Authorization: Bearer`.
|
|
39
|
+
|
|
40
|
+
## 0.1.4
|
|
41
|
+
|
|
42
|
+
- Fix stall after segments return 200; renew only on 401/403; clear sticky renewing state.
|
|
43
|
+
|
|
3
44
|
## 0.1.3
|
|
4
45
|
|
|
5
|
-
-
|
|
6
|
-
- Preserve optional `streaming.keyProxyUrlBuilder` through config merge.
|
|
46
|
+
- Re-apply signed query params on every HLS child request (fix CDN 403).
|
|
7
47
|
|
|
8
48
|
## 0.1.2
|
|
9
49
|
|
|
10
|
-
- Fix `
|
|
11
|
-
- Secret scanning is now plain-object only, iterative, depth-capped, and does not deep-walk `mount` / `auth` / `playback`.
|
|
12
|
-
- Harden `redactObject` against circular graphs.
|
|
50
|
+
- Fix `too much recursion` in secret config scanning.
|
|
13
51
|
|
|
14
52
|
## 0.1.1
|
|
15
53
|
|
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,
|
|
@@ -303,7 +305,8 @@ var SKIP_DEEP_SCAN_KEYS = /* @__PURE__ */ new Set([
|
|
|
303
305
|
"gettoken",
|
|
304
306
|
"getsource",
|
|
305
307
|
"refreshsource",
|
|
306
|
-
"keyproxyurlbuilder"
|
|
308
|
+
"keyproxyurlbuilder",
|
|
309
|
+
"getkeyaccesstoken"
|
|
307
310
|
]);
|
|
308
311
|
var SECRET_SCAN_MAX_DEPTH = 32;
|
|
309
312
|
function isPlainObject(value) {
|
|
@@ -387,7 +390,8 @@ function mergePlayerConfig(partial) {
|
|
|
387
390
|
...DEFAULT_PLAYER_CONFIG.streaming.hls,
|
|
388
391
|
...partial?.streaming?.hls
|
|
389
392
|
},
|
|
390
|
-
keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder
|
|
393
|
+
keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder,
|
|
394
|
+
getKeyAccessToken: partial?.streaming?.getKeyAccessToken
|
|
391
395
|
},
|
|
392
396
|
logging: { ...DEFAULT_PLAYER_CONFIG.logging, ...partial?.logging },
|
|
393
397
|
debug: { ...DEFAULT_PLAYER_CONFIG.debug, ...partial?.debug },
|
|
@@ -468,23 +472,25 @@ function isPlainObject2(value) {
|
|
|
468
472
|
const proto = Object.getPrototypeOf(value);
|
|
469
473
|
return proto === Object.prototype || proto === null;
|
|
470
474
|
}
|
|
471
|
-
function
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return url.toString();
|
|
483
|
-
} catch {
|
|
484
|
-
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]");
|
|
485
484
|
}
|
|
486
485
|
}
|
|
486
|
+
return url.toString();
|
|
487
|
+
} catch {
|
|
488
|
+
return "[REDACTED_URL]";
|
|
487
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);
|
|
488
494
|
if (Array.isArray(value)) {
|
|
489
495
|
if (depth >= REDACT_MAX_DEPTH || seen.has(value)) return "[REDACTED_CYCLE]";
|
|
490
496
|
seen.add(value);
|
|
@@ -751,10 +757,11 @@ var RefreshManager = class {
|
|
|
751
757
|
this.options.logger.debug("Playback source renewed", { reason });
|
|
752
758
|
} catch (error) {
|
|
753
759
|
this.consecutiveFailures += 1;
|
|
754
|
-
const message = error
|
|
760
|
+
const message = refreshErrorMessage(error);
|
|
755
761
|
this.options.logger.warn("Playback refresh failed", {
|
|
756
762
|
reason,
|
|
757
|
-
failure: this.consecutiveFailures
|
|
763
|
+
failure: this.consecutiveFailures,
|
|
764
|
+
message
|
|
758
765
|
});
|
|
759
766
|
if (this.consecutiveFailures >= this.options.renewMaxRetries) {
|
|
760
767
|
this.options.onFatal?.(message);
|
|
@@ -774,6 +781,13 @@ var RefreshManager = class {
|
|
|
774
781
|
}
|
|
775
782
|
}
|
|
776
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
|
+
}
|
|
777
791
|
|
|
778
792
|
// src/playback/liveEdge.ts
|
|
779
793
|
var DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS = 3;
|
|
@@ -817,7 +831,37 @@ function seekToLiveEdge(media, options) {
|
|
|
817
831
|
}
|
|
818
832
|
|
|
819
833
|
// src/streaming/authParamsLoader.ts
|
|
820
|
-
var
|
|
834
|
+
var import_hls = require("hls.js");
|
|
835
|
+
var ENC_KEY_PATH_RE = /enc\.key$/i;
|
|
836
|
+
var BUILTIN_LOADER_NAMES = /* @__PURE__ */ new Set(["XhrLoader", "FetchLoader"]);
|
|
837
|
+
function isEncryptionKeyUrl(rawUrl, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
|
|
838
|
+
try {
|
|
839
|
+
return ENC_KEY_PATH_RE.test(new URL(rawUrl, baseOrigin).pathname);
|
|
840
|
+
} catch {
|
|
841
|
+
return /enc\.key/i.test(rawUrl);
|
|
842
|
+
}
|
|
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
|
+
}
|
|
821
865
|
function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
|
|
822
866
|
if (!rawUrl) return rawUrl;
|
|
823
867
|
const parsed = new URL(rawUrl, baseOrigin);
|
|
@@ -828,28 +872,161 @@ function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof l
|
|
|
828
872
|
videoId: options.videoId,
|
|
829
873
|
keyUrl: parsed.toString(),
|
|
830
874
|
token: snapshot.token,
|
|
831
|
-
expires: snapshot.expires
|
|
875
|
+
expires: snapshot.expires,
|
|
876
|
+
username: snapshot.username,
|
|
877
|
+
mobile: snapshot.mobile
|
|
832
878
|
});
|
|
833
879
|
}
|
|
834
880
|
if (!store.hasParams()) return parsed.toString();
|
|
835
881
|
return store.applyToUrl(rawUrl, baseOrigin);
|
|
836
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
|
+
}
|
|
837
955
|
function createAuthParamsLoader(HlsClass, store, options = {}) {
|
|
838
|
-
const BaseLoader = HlsClass
|
|
839
|
-
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
|
+
}
|
|
840
973
|
load(context, config, callbacks) {
|
|
841
|
-
if (context.url) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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;
|
|
845
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;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
} catch {
|
|
846
1020
|
}
|
|
847
|
-
|
|
1021
|
+
this.inner.load(context, config, callbacks);
|
|
848
1022
|
}
|
|
849
1023
|
};
|
|
850
1024
|
}
|
|
851
1025
|
|
|
852
1026
|
// src/streaming/HlsEngine.ts
|
|
1027
|
+
function isAuthHttpStatus(code) {
|
|
1028
|
+
return code === 401 || code === 403;
|
|
1029
|
+
}
|
|
853
1030
|
var HlsEngine = class {
|
|
854
1031
|
constructor(options) {
|
|
855
1032
|
this.options = options;
|
|
@@ -862,49 +1039,47 @@ var HlsEngine = class {
|
|
|
862
1039
|
this.qualitySelection = "auto";
|
|
863
1040
|
}
|
|
864
1041
|
async start() {
|
|
865
|
-
const { videoEl, playbackUrl,
|
|
1042
|
+
const { videoEl, playbackUrl, startMuted, volume } = this.options;
|
|
866
1043
|
videoEl.muted = startMuted;
|
|
867
|
-
if (typeof volume === "number")
|
|
1044
|
+
if (typeof volume === "number") {
|
|
1045
|
+
videoEl.volume = Math.min(1, Math.max(0, volume));
|
|
1046
|
+
}
|
|
868
1047
|
const Hls = (await import("hls.js")).default;
|
|
869
1048
|
if (Hls.isSupported()) {
|
|
870
1049
|
this.supportMode = "hls.js";
|
|
871
1050
|
this.options.callbacks.onSupportMode?.("hls.js");
|
|
872
1051
|
await this.startWithHlsJs(Hls, playbackUrl);
|
|
873
|
-
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (videoEl.canPlayType("application/vnd.apple.mpegurl")) {
|
|
874
1055
|
this.supportMode = "native";
|
|
875
1056
|
this.options.callbacks.onSupportMode?.("native");
|
|
876
1057
|
videoEl.src = this.options.authParams.applyToUrl(playbackUrl);
|
|
877
1058
|
this.bindMediaEvents();
|
|
878
1059
|
this.options.callbacks.onStateChange?.("ready");
|
|
879
|
-
|
|
880
|
-
this.supportMode = "unsupported";
|
|
881
|
-
this.options.callbacks.onSupportMode?.("unsupported");
|
|
882
|
-
this.options.callbacks.onFatalError?.("HLS is not supported in this browser");
|
|
1060
|
+
await this.applyStartPositionAndAutoplay();
|
|
883
1061
|
return;
|
|
884
1062
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
await videoEl.play();
|
|
891
|
-
} catch {
|
|
892
|
-
if (!videoEl.muted) {
|
|
893
|
-
videoEl.muted = true;
|
|
894
|
-
try {
|
|
895
|
-
await videoEl.play();
|
|
896
|
-
} catch {
|
|
897
|
-
this.options.logger.warn("Autoplay blocked");
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
}
|
|
1063
|
+
this.supportMode = "unsupported";
|
|
1064
|
+
this.options.callbacks.onSupportMode?.("unsupported");
|
|
1065
|
+
this.options.callbacks.onFatalError?.(
|
|
1066
|
+
"HLS is not supported in this browser"
|
|
1067
|
+
);
|
|
902
1068
|
}
|
|
903
1069
|
async startWithHlsJs(Hls, playbackUrl) {
|
|
904
|
-
const {
|
|
1070
|
+
const {
|
|
1071
|
+
videoEl,
|
|
1072
|
+
authParams,
|
|
1073
|
+
hlsConfig,
|
|
1074
|
+
callbacks,
|
|
1075
|
+
videoId,
|
|
1076
|
+
keyProxyUrlBuilder,
|
|
1077
|
+
getKeyAccessToken
|
|
1078
|
+
} = this.options;
|
|
905
1079
|
const loader = createAuthParamsLoader(Hls, authParams, {
|
|
906
1080
|
videoId,
|
|
907
|
-
keyProxyUrlBuilder
|
|
1081
|
+
keyProxyUrlBuilder,
|
|
1082
|
+
getKeyAccessToken
|
|
908
1083
|
});
|
|
909
1084
|
this.hls = new Hls({
|
|
910
1085
|
enableWorker: hlsConfig.enableWorker,
|
|
@@ -915,6 +1090,7 @@ var HlsEngine = class {
|
|
|
915
1090
|
this.hls.attachMedia(videoEl);
|
|
916
1091
|
this.hls.loadSource(authParams.applyToUrl(playbackUrl));
|
|
917
1092
|
this.hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
|
|
1093
|
+
if (this.destroyed) return;
|
|
918
1094
|
this.qualities = (data.levels ?? []).map((level, index) => ({
|
|
919
1095
|
index,
|
|
920
1096
|
height: level.height ?? 0,
|
|
@@ -924,42 +1100,100 @@ var HlsEngine = class {
|
|
|
924
1100
|
}));
|
|
925
1101
|
callbacks.onQualitiesChange?.(this.qualities);
|
|
926
1102
|
callbacks.onStateChange?.("ready");
|
|
1103
|
+
void this.applyStartPositionAndAutoplay();
|
|
927
1104
|
});
|
|
928
1105
|
this.hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1106
|
+
this.handleHlsError(Hls, data);
|
|
1107
|
+
});
|
|
1108
|
+
this.bindMediaEvents();
|
|
1109
|
+
}
|
|
1110
|
+
handleHlsError(Hls, data) {
|
|
1111
|
+
if (this.destroyed) return;
|
|
1112
|
+
const { callbacks, hlsConfig } = this.options;
|
|
1113
|
+
const status = data.response?.code;
|
|
1114
|
+
const authFailure = isAuthHttpStatus(status);
|
|
1115
|
+
if (data.details === Hls.ErrorDetails.KEY_LOAD_ERROR) {
|
|
1116
|
+
if (authFailure) {
|
|
1117
|
+
callbacks.onAuthRecoveryNeeded?.("key_error");
|
|
1118
|
+
if (!data.fatal) return;
|
|
1119
|
+
} else if (!data.fatal) {
|
|
1120
|
+
callbacks.onFatalError?.(
|
|
1121
|
+
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."
|
|
1122
|
+
);
|
|
935
1123
|
return;
|
|
936
1124
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1125
|
+
}
|
|
1126
|
+
if (!data.fatal) {
|
|
1127
|
+
if (authFailure && data.type === Hls.ErrorTypes.NETWORK_ERROR && (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR || data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR)) {
|
|
1128
|
+
callbacks.onAuthRecoveryNeeded?.(
|
|
1129
|
+
data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
|
1135
|
+
this.networkRecoveryCount += 1;
|
|
1136
|
+
if (this.networkRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
|
|
1137
|
+
if (authFailure) {
|
|
940
1138
|
callbacks.onAuthRecoveryNeeded?.("fatal_network");
|
|
941
|
-
this.hls?.startLoad();
|
|
942
|
-
return;
|
|
943
1139
|
}
|
|
1140
|
+
this.hls?.startLoad();
|
|
1141
|
+
return;
|
|
944
1142
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1143
|
+
}
|
|
1144
|
+
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
1145
|
+
this.mediaRecoveryCount += 1;
|
|
1146
|
+
if (this.mediaRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
|
|
1147
|
+
this.hls?.recoverMediaError();
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
callbacks.onFatalError?.(data.details || "Fatal HLS error");
|
|
1152
|
+
}
|
|
1153
|
+
async applyStartPositionAndAutoplay() {
|
|
1154
|
+
if (this.destroyed) return;
|
|
1155
|
+
const { videoEl, autoplay, startPosition } = this.options;
|
|
1156
|
+
if (typeof startPosition === "number" && startPosition > 0) {
|
|
1157
|
+
const seek = () => {
|
|
1158
|
+
if (this.destroyed) return;
|
|
1159
|
+
try {
|
|
1160
|
+
videoEl.currentTime = startPosition;
|
|
1161
|
+
} catch {
|
|
950
1162
|
}
|
|
1163
|
+
};
|
|
1164
|
+
if (videoEl.seekable.length > 0) {
|
|
1165
|
+
seek();
|
|
1166
|
+
} else {
|
|
1167
|
+
videoEl.addEventListener("loadedmetadata", seek, { once: true });
|
|
951
1168
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1169
|
+
}
|
|
1170
|
+
if (!autoplay) return;
|
|
1171
|
+
try {
|
|
1172
|
+
await videoEl.play();
|
|
1173
|
+
} catch {
|
|
1174
|
+
if (!videoEl.muted) {
|
|
1175
|
+
videoEl.muted = true;
|
|
1176
|
+
try {
|
|
1177
|
+
await videoEl.play();
|
|
1178
|
+
} catch {
|
|
1179
|
+
this.options.logger.warn("Autoplay blocked");
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
955
1183
|
}
|
|
956
1184
|
bindMediaEvents() {
|
|
957
1185
|
const { videoEl, callbacks } = this.options;
|
|
958
|
-
videoEl.addEventListener(
|
|
1186
|
+
videoEl.addEventListener(
|
|
1187
|
+
"playing",
|
|
1188
|
+
() => callbacks.onStateChange?.("playing")
|
|
1189
|
+
);
|
|
959
1190
|
videoEl.addEventListener("pause", () => {
|
|
960
1191
|
if (!videoEl.ended) callbacks.onStateChange?.("paused");
|
|
961
1192
|
});
|
|
962
|
-
videoEl.addEventListener(
|
|
1193
|
+
videoEl.addEventListener(
|
|
1194
|
+
"waiting",
|
|
1195
|
+
() => callbacks.onStateChange?.("buffering")
|
|
1196
|
+
);
|
|
963
1197
|
videoEl.addEventListener("ended", () => {
|
|
964
1198
|
callbacks.onStateChange?.("ended");
|
|
965
1199
|
callbacks.onEnded?.();
|
|
@@ -977,6 +1211,7 @@ var HlsEngine = class {
|
|
|
977
1211
|
}
|
|
978
1212
|
}
|
|
979
1213
|
resumeAfterAuthRenew() {
|
|
1214
|
+
this.networkRecoveryCount = 0;
|
|
980
1215
|
this.hls?.startLoad();
|
|
981
1216
|
this.updateAuthParams();
|
|
982
1217
|
}
|
|
@@ -1119,7 +1354,13 @@ function createVideoPlayer(options) {
|
|
|
1119
1354
|
startPosition: config.player.startPosition,
|
|
1120
1355
|
volume: config.player.volume,
|
|
1121
1356
|
videoId: options.videoId,
|
|
1122
|
-
keyProxyUrlBuilder:
|
|
1357
|
+
keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
|
|
1358
|
+
// Optional: browser Bearer for Video API–origin enc.key (CORS-dependent).
|
|
1359
|
+
// CDN keys use signed query via the auth loader (no Bearer) — same as
|
|
1360
|
+
// segments. Apps can pass getKeyAccessToken or rely on keyProxyUrlBuilder.
|
|
1361
|
+
// When unset, defaults to AuthManager token only if explicitly requested
|
|
1362
|
+
// via streaming.getKeyAccessToken; otherwise query-signed XHR.
|
|
1363
|
+
getKeyAccessToken: config.streaming.getKeyAccessToken,
|
|
1123
1364
|
hlsConfig: config.streaming.hls,
|
|
1124
1365
|
logger,
|
|
1125
1366
|
callbacks: {
|
|
@@ -1180,6 +1421,14 @@ function createVideoPlayer(options) {
|
|
|
1180
1421
|
onRenewing: () => setState("renewing"),
|
|
1181
1422
|
onRenewed: (nextExpiresAt) => {
|
|
1182
1423
|
engine?.resumeAfterAuthRenew();
|
|
1424
|
+
if (!destroyed) {
|
|
1425
|
+
const el2 = videoEl;
|
|
1426
|
+
if (el2 && !el2.paused && !el2.ended) {
|
|
1427
|
+
setState("playing");
|
|
1428
|
+
} else {
|
|
1429
|
+
setState("ready");
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1183
1432
|
events.emit("tokenrefresh", { expiresAt: nextExpiresAt });
|
|
1184
1433
|
events.emit("sourcechange", { expiresAt: nextExpiresAt });
|
|
1185
1434
|
},
|
|
@@ -1381,7 +1630,8 @@ function createVideoPlayer(options) {
|
|
|
1381
1630
|
streaming: {
|
|
1382
1631
|
engine: "hls",
|
|
1383
1632
|
hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
|
|
1384
|
-
keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder
|
|
1633
|
+
keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder,
|
|
1634
|
+
getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken
|
|
1385
1635
|
},
|
|
1386
1636
|
logging: { ...config.logging, ...partial?.logging },
|
|
1387
1637
|
debug: { ...config.debug, ...partial?.debug },
|
|
@@ -1425,11 +1675,13 @@ function createVideoPlayer(options) {
|
|
|
1425
1675
|
createVideoPlayer,
|
|
1426
1676
|
getLiveEdgeTime,
|
|
1427
1677
|
isAtLiveEdge,
|
|
1678
|
+
isEncryptionKeyUrl,
|
|
1428
1679
|
isPlainObject,
|
|
1429
1680
|
mergePlayerConfig,
|
|
1430
1681
|
redactObject,
|
|
1431
1682
|
redactSensitive,
|
|
1432
1683
|
redactValue,
|
|
1684
|
+
resolveBuiltinHlsLoader,
|
|
1433
1685
|
rewriteHlsRequestUrl,
|
|
1434
1686
|
seekToLiveEdge,
|
|
1435
1687
|
themeToCssVars
|