@codenkay/video-nsgplayer-core 0.1.5 → 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 +37 -11
- package/dist/index.cjs +207 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -14
- package/dist/index.d.ts +37 -14
- package/dist/index.js +205 -43
- package/dist/index.js.map +1 -1
- package/package.json +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,27 +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
|
+
|
|
3
36
|
## 0.1.5
|
|
4
37
|
|
|
5
|
-
- Add `streaming.getKeyAccessToken` for browser-direct AES `enc.key` loads with `Authorization: Bearer
|
|
6
|
-
- Clearer key-load guidance when neither key proxy nor browser Bearer is configured.
|
|
38
|
+
- Add `streaming.getKeyAccessToken` for browser-direct AES `enc.key` loads with `Authorization: Bearer`.
|
|
7
39
|
|
|
8
40
|
## 0.1.4
|
|
9
41
|
|
|
10
|
-
- Fix stall after segments return 200
|
|
11
|
-
- Only trigger signed-URL auth recovery on HTTP 401/403 (not every network blip).
|
|
12
|
-
- Require/surface `streaming.keyProxyUrlBuilder` when AES `enc.key` load fails (Appextech VOD needs same-origin BFF + API Bearer).
|
|
13
|
-
- Clear sticky `renewing` state after successful credential refresh.
|
|
42
|
+
- Fix stall after segments return 200; renew only on 401/403; clear sticky renewing state.
|
|
14
43
|
|
|
15
44
|
## 0.1.3
|
|
16
45
|
|
|
17
|
-
-
|
|
18
|
-
- Preserve optional `streaming.keyProxyUrlBuilder` through config merge.
|
|
46
|
+
- Re-apply signed query params on every HLS child request (fix CDN 403).
|
|
19
47
|
|
|
20
48
|
## 0.1.2
|
|
21
49
|
|
|
22
|
-
- Fix `
|
|
23
|
-
- Secret scanning is now plain-object only, iterative, depth-capped, and does not deep-walk `mount` / `auth` / `playback`.
|
|
24
|
-
- Harden `redactObject` against circular graphs.
|
|
50
|
+
- Fix `too much recursion` in secret config scanning.
|
|
25
51
|
|
|
26
52
|
## 0.1.1
|
|
27
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,
|
|
@@ -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
|
}
|
|
@@ -957,19 +1109,24 @@ var HlsEngine = class {
|
|
|
957
1109
|
}
|
|
958
1110
|
handleHlsError(Hls, data) {
|
|
959
1111
|
if (this.destroyed) return;
|
|
960
|
-
const { callbacks, hlsConfig
|
|
1112
|
+
const { callbacks, hlsConfig } = this.options;
|
|
961
1113
|
const status = data.response?.code;
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
+
);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
967
1125
|
}
|
|
968
|
-
const authFailure = isAuthHttpStatus(status) || data.details === Hls.ErrorDetails.KEY_LOAD_ERROR && (status === void 0 || status === 0 || isAuthHttpStatus(status));
|
|
969
1126
|
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
|
|
1127
|
+
if (authFailure && data.type === Hls.ErrorTypes.NETWORK_ERROR && (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR || data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR)) {
|
|
971
1128
|
callbacks.onAuthRecoveryNeeded?.(
|
|
972
|
-
data.details === Hls.ErrorDetails.
|
|
1129
|
+
data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
|
|
973
1130
|
);
|
|
974
1131
|
}
|
|
975
1132
|
return;
|
|
@@ -1198,6 +1355,11 @@ function createVideoPlayer(options) {
|
|
|
1198
1355
|
volume: config.player.volume,
|
|
1199
1356
|
videoId: options.videoId,
|
|
1200
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.
|
|
1201
1363
|
getKeyAccessToken: config.streaming.getKeyAccessToken,
|
|
1202
1364
|
hlsConfig: config.streaming.hls,
|
|
1203
1365
|
logger,
|
|
@@ -1513,11 +1675,13 @@ function createVideoPlayer(options) {
|
|
|
1513
1675
|
createVideoPlayer,
|
|
1514
1676
|
getLiveEdgeTime,
|
|
1515
1677
|
isAtLiveEdge,
|
|
1678
|
+
isEncryptionKeyUrl,
|
|
1516
1679
|
isPlainObject,
|
|
1517
1680
|
mergePlayerConfig,
|
|
1518
1681
|
redactObject,
|
|
1519
1682
|
redactSensitive,
|
|
1520
1683
|
redactValue,
|
|
1684
|
+
resolveBuiltinHlsLoader,
|
|
1521
1685
|
rewriteHlsRequestUrl,
|
|
1522
1686
|
seekToLiveEdge,
|
|
1523
1687
|
themeToCssVars
|