@codenkay/video-nsgplayer-core 3.0.2 → 4.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 +19 -0
- package/README.md +12 -0
- package/dist/index.cjs +226 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -17
- package/dist/index.d.ts +66 -17
- package/dist/index.js +226 -48
- package/dist/index.js.map +1 -1
- package/package.json +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- Fix minor issues
|
|
8
|
+
|
|
9
|
+
## 3.0.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Preserve "use client" in React/UI builds, forward analytics, intermittent watermark, Angular Ivy packaging, and packaging metadata.
|
|
14
|
+
|
|
15
|
+
## 3.0.3
|
|
16
|
+
|
|
17
|
+
- Browser-direct AES keys: after signed-query CORS/network failure, retry once
|
|
18
|
+
with `getKeyAccessToken` Bearer before XHR (needed when AUTH_MODE=browser and
|
|
19
|
+
Appextech enables CORS + Authorization on `enc.key`).
|
|
20
|
+
- Clearer KEY_LOAD_ERROR message distinguishing BFF vs browser-direct CORS.
|
|
21
|
+
|
|
3
22
|
## 3.0.2
|
|
4
23
|
|
|
5
24
|
- Fix pack/build regression on AES `enc.key`: do **not** statically import
|
package/README.md
CHANGED
|
@@ -40,3 +40,15 @@ await player.play();
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
See repository `docs/` for configuration, authentication, and security.
|
|
43
|
+
|
|
44
|
+
## Samples
|
|
45
|
+
|
|
46
|
+
Framework adapters wrap this engine. Clone a ready playground:
|
|
47
|
+
|
|
48
|
+
| Lab | Repo |
|
|
49
|
+
|-----|------|
|
|
50
|
+
| React (Vite, no BFF) | [nsgplayer-sample-react](https://github.com/codenannu/nsgplayer-sample-react) |
|
|
51
|
+
| Next.js + thin BFF | [nsgplayer-sample-nextjs](https://github.com/codenannu/nsgplayer-sample-nextjs) |
|
|
52
|
+
| Angular (headless) | [nsgplayer-sample-angular](https://github.com/codenannu/nsgplayer-sample-angular) |
|
|
53
|
+
|
|
54
|
+
Full matrix: [docs/samples.md](../../docs/samples.md).
|
package/dist/index.cjs
CHANGED
|
@@ -213,7 +213,7 @@ var AuthManager = class {
|
|
|
213
213
|
|
|
214
214
|
// src/config/defaults.ts
|
|
215
215
|
var PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
216
|
-
var PACKAGE_VERSION = "3.0.
|
|
216
|
+
var PACKAGE_VERSION = "3.0.3";
|
|
217
217
|
var DEFAULT_PLAYER_BEHAVIOR = {
|
|
218
218
|
autoplay: true,
|
|
219
219
|
startMuted: true,
|
|
@@ -237,7 +237,8 @@ var DEFAULT_CONTROLS = {
|
|
|
237
237
|
showSkip: true,
|
|
238
238
|
showLive: true,
|
|
239
239
|
seekCueDurationMs: 720,
|
|
240
|
-
keyboardShortcuts: true
|
|
240
|
+
keyboardShortcuts: true,
|
|
241
|
+
persistQualityPreference: false
|
|
241
242
|
};
|
|
242
243
|
var DEFAULT_THEME = {
|
|
243
244
|
playIconPreset: "triangle",
|
|
@@ -282,7 +283,8 @@ var DEFAULT_PLAYER_CONFIG = {
|
|
|
282
283
|
engine: "hls",
|
|
283
284
|
hls: {
|
|
284
285
|
enableWorker: true,
|
|
285
|
-
|
|
286
|
+
// VOD + AES-128: LL mode causes ABR thrash → bufferAppendError on MSE.
|
|
287
|
+
lowLatencyMode: false,
|
|
286
288
|
backBufferLength: 90,
|
|
287
289
|
fatalRecoveryLimit: 3
|
|
288
290
|
}
|
|
@@ -859,7 +861,8 @@ function rememberBuiltin(HlsClass, loader) {
|
|
|
859
861
|
function getStashedBuiltinLoader(loader) {
|
|
860
862
|
return loader[BUILTIN_LOADER_REF];
|
|
861
863
|
}
|
|
862
|
-
function resolveBuiltinHlsLoader(
|
|
864
|
+
function resolveBuiltinHlsLoader(HlsModule) {
|
|
865
|
+
const HlsClass = HlsModule;
|
|
863
866
|
const cached = builtinLoaderByHls.get(HlsClass);
|
|
864
867
|
if (cached) return cached;
|
|
865
868
|
const start = HlsClass.DefaultConfig.loader;
|
|
@@ -987,29 +990,52 @@ async function fetchEncryptionKeyBytes(url, accessToken, signal) {
|
|
|
987
990
|
};
|
|
988
991
|
}
|
|
989
992
|
}
|
|
993
|
+
var AES_128_KEY_BYTES = 16;
|
|
994
|
+
function normalizeAes128Key(data) {
|
|
995
|
+
if (data.byteLength === AES_128_KEY_BYTES) {
|
|
996
|
+
return data.slice(0);
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
990
1000
|
function reportKeySuccess(context, callbacks, result) {
|
|
1001
|
+
const key = normalizeAes128Key(result.data);
|
|
1002
|
+
if (!key) {
|
|
1003
|
+
const stats2 = createLoaderStats();
|
|
1004
|
+
callbacks.onError(
|
|
1005
|
+
{
|
|
1006
|
+
code: 422,
|
|
1007
|
+
text: `enc.key must be ${AES_128_KEY_BYTES} bytes (got ${result.data.byteLength})`
|
|
1008
|
+
},
|
|
1009
|
+
context,
|
|
1010
|
+
result.response,
|
|
1011
|
+
stats2
|
|
1012
|
+
);
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
991
1015
|
const stats = createLoaderStats();
|
|
992
1016
|
const end = performance.now();
|
|
993
1017
|
stats.loading.first = stats.loading.start;
|
|
994
1018
|
stats.loading.end = end;
|
|
995
|
-
stats.loaded =
|
|
996
|
-
stats.total =
|
|
1019
|
+
stats.loaded = key.byteLength;
|
|
1020
|
+
stats.total = key.byteLength;
|
|
997
1021
|
callbacks.onSuccess(
|
|
998
1022
|
{
|
|
999
1023
|
url: result.url,
|
|
1000
|
-
data:
|
|
1024
|
+
data: key,
|
|
1001
1025
|
code: result.response.status
|
|
1002
1026
|
},
|
|
1003
1027
|
stats,
|
|
1004
1028
|
context,
|
|
1005
1029
|
result.response
|
|
1006
1030
|
);
|
|
1031
|
+
return true;
|
|
1007
1032
|
}
|
|
1008
|
-
async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state) {
|
|
1033
|
+
async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state, lifecycle) {
|
|
1009
1034
|
const stats = createLoaderStats();
|
|
1035
|
+
const stillCurrent = () => lifecycle.isCurrent() && !signal.aborted;
|
|
1010
1036
|
const canTryProxy = Boolean(plan.proxyUrl) && !state.circuitOpen && state.proxyFailures < plan.maxProxyFailures;
|
|
1011
1037
|
if (canTryProxy && plan.proxyUrl) {
|
|
1012
|
-
while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures &&
|
|
1038
|
+
while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures && stillCurrent()) {
|
|
1013
1039
|
state.proxyFailures += 1;
|
|
1014
1040
|
try {
|
|
1015
1041
|
const response = await fetch(plan.proxyUrl, {
|
|
@@ -1019,18 +1045,33 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1019
1045
|
cache: "no-store",
|
|
1020
1046
|
signal
|
|
1021
1047
|
});
|
|
1048
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1022
1049
|
const contentType = response.headers.get("content-type");
|
|
1023
1050
|
const data = await response.arrayBuffer();
|
|
1051
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1024
1052
|
const head = new TextDecoder().decode(data.slice(0, 64));
|
|
1025
1053
|
if (response.ok && !isProxyUpstreamBlocked(response.status, contentType, head)) {
|
|
1054
|
+
const key = normalizeAes128Key(data);
|
|
1055
|
+
if (!key) {
|
|
1056
|
+
callbacks.onError(
|
|
1057
|
+
{
|
|
1058
|
+
code: 422,
|
|
1059
|
+
text: `enc.key proxy returned ${data.byteLength} bytes (need ${AES_128_KEY_BYTES})`
|
|
1060
|
+
},
|
|
1061
|
+
{ ...context, url: plan.proxyUrl },
|
|
1062
|
+
response,
|
|
1063
|
+
stats
|
|
1064
|
+
);
|
|
1065
|
+
return "ok";
|
|
1066
|
+
}
|
|
1026
1067
|
const end = performance.now();
|
|
1027
1068
|
stats.loading.end = end;
|
|
1028
|
-
stats.loaded =
|
|
1029
|
-
stats.total =
|
|
1069
|
+
stats.loaded = key.byteLength;
|
|
1070
|
+
stats.total = key.byteLength;
|
|
1030
1071
|
callbacks.onSuccess(
|
|
1031
1072
|
{
|
|
1032
1073
|
url: response.url || plan.proxyUrl,
|
|
1033
|
-
data,
|
|
1074
|
+
data: key,
|
|
1034
1075
|
code: response.status
|
|
1035
1076
|
},
|
|
1036
1077
|
stats,
|
|
@@ -1063,25 +1104,22 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1063
1104
|
return "ok";
|
|
1064
1105
|
} catch (error) {
|
|
1065
1106
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
1066
|
-
|
|
1067
|
-
{ code: 0, text: "enc.key proxy aborted/timeout" },
|
|
1068
|
-
context,
|
|
1069
|
-
null,
|
|
1070
|
-
stats
|
|
1071
|
-
);
|
|
1072
|
-
return "ok";
|
|
1107
|
+
return "aborted";
|
|
1073
1108
|
}
|
|
1074
1109
|
state.circuitOpen = true;
|
|
1075
1110
|
break;
|
|
1076
1111
|
}
|
|
1077
1112
|
}
|
|
1113
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1078
1114
|
state.circuitOpen = true;
|
|
1079
1115
|
}
|
|
1116
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1080
1117
|
const queryResult = await fetchEncryptionKeyBytes(
|
|
1081
1118
|
plan.signedKeyUrl,
|
|
1082
1119
|
null,
|
|
1083
1120
|
signal
|
|
1084
1121
|
);
|
|
1122
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1085
1123
|
if (queryResult.ok) {
|
|
1086
1124
|
reportKeySuccess(
|
|
1087
1125
|
{ ...context, url: plan.signedKeyUrl },
|
|
@@ -1090,13 +1128,19 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1090
1128
|
);
|
|
1091
1129
|
return "ok";
|
|
1092
1130
|
}
|
|
1093
|
-
|
|
1131
|
+
if (queryResult.status === 0 && signal.aborted) {
|
|
1132
|
+
return "aborted";
|
|
1133
|
+
}
|
|
1134
|
+
const needsBearer = Boolean(plan.accessToken) && (queryResult.status === 401 || queryResult.status === 403 || // Browser-direct: opaque CORS/network on unsigned GET — try Bearer once
|
|
1135
|
+
// Appextech allows ACAO + Authorization (AUTH_MODE=browser, no BFF).
|
|
1136
|
+
queryResult.cors || queryResult.status === 0);
|
|
1094
1137
|
if (needsBearer && plan.accessToken) {
|
|
1095
1138
|
const bearerResult = await fetchEncryptionKeyBytes(
|
|
1096
1139
|
plan.signedKeyUrl,
|
|
1097
1140
|
plan.accessToken,
|
|
1098
1141
|
signal
|
|
1099
1142
|
);
|
|
1143
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1100
1144
|
if (bearerResult.ok) {
|
|
1101
1145
|
reportKeySuccess(
|
|
1102
1146
|
{ ...context, url: plan.signedKeyUrl },
|
|
@@ -1105,7 +1149,10 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1105
1149
|
);
|
|
1106
1150
|
return "ok";
|
|
1107
1151
|
}
|
|
1108
|
-
if (bearerResult.
|
|
1152
|
+
if (bearerResult.status === 0 && signal.aborted) {
|
|
1153
|
+
return "aborted";
|
|
1154
|
+
}
|
|
1155
|
+
if (bearerResult.cors || bearerResult.status === 0) {
|
|
1109
1156
|
return "try_xhr";
|
|
1110
1157
|
}
|
|
1111
1158
|
callbacks.onError(
|
|
@@ -1133,7 +1180,8 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1133
1180
|
);
|
|
1134
1181
|
return "ok";
|
|
1135
1182
|
}
|
|
1136
|
-
function createAuthParamsLoader(
|
|
1183
|
+
function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
1184
|
+
const HlsClass = HlsModule;
|
|
1137
1185
|
const BaseLoader = resolveBuiltinHlsLoader(HlsClass);
|
|
1138
1186
|
rememberBuiltin(HlsClass, BaseLoader);
|
|
1139
1187
|
const maxProxyFailures = typeof options.keyProxyMaxFailures === "number" && options.keyProxyMaxFailures >= 0 ? options.keyProxyMaxFailures : KEY_PROXY_MAX_FAILURES;
|
|
@@ -1142,17 +1190,34 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
|
|
|
1142
1190
|
constructor(config) {
|
|
1143
1191
|
this.context = null;
|
|
1144
1192
|
this.keyAbort = null;
|
|
1193
|
+
/** Bumps on each key load so superseded aborts never call onError. */
|
|
1194
|
+
this.keyLoadGeneration = 0;
|
|
1195
|
+
this.pendingKeyCallbacks = null;
|
|
1145
1196
|
this.inner = new BaseLoader(config);
|
|
1146
1197
|
this.stats = this.inner.stats;
|
|
1147
1198
|
}
|
|
1148
|
-
|
|
1199
|
+
finishKeyAbort() {
|
|
1200
|
+
const cbs = this.pendingKeyCallbacks;
|
|
1201
|
+
this.pendingKeyCallbacks = null;
|
|
1149
1202
|
this.keyAbort?.abort();
|
|
1150
1203
|
this.keyAbort = null;
|
|
1204
|
+
if (cbs?.onAbort && this.context) {
|
|
1205
|
+
const stats = createLoaderStats();
|
|
1206
|
+
stats.aborted = true;
|
|
1207
|
+
try {
|
|
1208
|
+
cbs.onAbort(stats, this.context, null);
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
abort() {
|
|
1214
|
+
this.keyLoadGeneration += 1;
|
|
1215
|
+
this.finishKeyAbort();
|
|
1151
1216
|
this.inner.abort();
|
|
1152
1217
|
}
|
|
1153
1218
|
destroy() {
|
|
1154
|
-
this.
|
|
1155
|
-
this.
|
|
1219
|
+
this.keyLoadGeneration += 1;
|
|
1220
|
+
this.finishKeyAbort();
|
|
1156
1221
|
this.context = null;
|
|
1157
1222
|
this.inner.destroy();
|
|
1158
1223
|
}
|
|
@@ -1184,10 +1249,19 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
|
|
|
1184
1249
|
);
|
|
1185
1250
|
const proxyUrl = proxyConfigured && !keyProxyState.circuitOpen ? rewriteHlsRequestUrl(originalUrl, store, options) : null;
|
|
1186
1251
|
const accessToken = options.getKeyAccessToken?.() ?? null;
|
|
1187
|
-
this.keyAbort
|
|
1252
|
+
if (this.keyAbort || this.pendingKeyCallbacks) {
|
|
1253
|
+
this.keyLoadGeneration += 1;
|
|
1254
|
+
this.finishKeyAbort();
|
|
1255
|
+
}
|
|
1256
|
+
const generation = ++this.keyLoadGeneration;
|
|
1188
1257
|
this.keyAbort = new AbortController();
|
|
1258
|
+
this.pendingKeyCallbacks = callbacks;
|
|
1259
|
+
context.responseType = "arraybuffer";
|
|
1189
1260
|
const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
|
|
1190
1261
|
const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
|
|
1262
|
+
const lifecycle = {
|
|
1263
|
+
isCurrent: () => generation === this.keyLoadGeneration
|
|
1264
|
+
};
|
|
1191
1265
|
void loadEncryptionKeyEnterprise(
|
|
1192
1266
|
context,
|
|
1193
1267
|
callbacks,
|
|
@@ -1198,13 +1272,22 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
|
|
|
1198
1272
|
maxProxyFailures
|
|
1199
1273
|
},
|
|
1200
1274
|
this.keyAbort.signal,
|
|
1201
|
-
keyProxyState
|
|
1275
|
+
keyProxyState,
|
|
1276
|
+
lifecycle
|
|
1202
1277
|
).then((result) => {
|
|
1278
|
+
if (!lifecycle.isCurrent()) return;
|
|
1279
|
+
if (result === "aborted") {
|
|
1280
|
+
this.finishKeyAbort();
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
this.pendingKeyCallbacks = null;
|
|
1203
1284
|
if (result === "try_xhr") {
|
|
1204
1285
|
context.url = signedKeyUrl;
|
|
1205
1286
|
this.inner.load(context, config, callbacks);
|
|
1206
1287
|
}
|
|
1207
1288
|
}).catch((error) => {
|
|
1289
|
+
if (!lifecycle.isCurrent()) return;
|
|
1290
|
+
this.pendingKeyCallbacks = null;
|
|
1208
1291
|
callbacks.onError(
|
|
1209
1292
|
{
|
|
1210
1293
|
code: 0,
|
|
@@ -1235,6 +1318,10 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
|
|
|
1235
1318
|
function isAuthHttpStatus(code) {
|
|
1236
1319
|
return code === 401 || code === 403;
|
|
1237
1320
|
}
|
|
1321
|
+
function isGeckoBrowser() {
|
|
1322
|
+
if (typeof navigator === "undefined") return false;
|
|
1323
|
+
return /firefox|gecko\//i.test(navigator.userAgent);
|
|
1324
|
+
}
|
|
1238
1325
|
var HlsEngine = class {
|
|
1239
1326
|
constructor(options) {
|
|
1240
1327
|
this.options = options;
|
|
@@ -1250,6 +1337,8 @@ var HlsEngine = class {
|
|
|
1250
1337
|
this.audioTracks = [];
|
|
1251
1338
|
this.audioTrackSelection = 0;
|
|
1252
1339
|
this.nativeTextTrackCleanup = null;
|
|
1340
|
+
this.startPositionApplied = false;
|
|
1341
|
+
this.autoplayApplied = false;
|
|
1253
1342
|
}
|
|
1254
1343
|
async start() {
|
|
1255
1344
|
const { videoEl, playbackUrl, startMuted, volume } = this.options;
|
|
@@ -1296,10 +1385,27 @@ var HlsEngine = class {
|
|
|
1296
1385
|
getKeyAccessToken,
|
|
1297
1386
|
keyProxyMaxFailures: this.options.keyProxyMaxFailures
|
|
1298
1387
|
});
|
|
1388
|
+
const enableWorker = hlsConfig.enableWorker && !isGeckoBrowser();
|
|
1389
|
+
if (hlsConfig.enableWorker && !enableWorker) {
|
|
1390
|
+
this.options.logger.warn(
|
|
1391
|
+
"Disabling hls.js worker on Firefox to avoid MSE bufferAppendError"
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1299
1394
|
this.hls = new Hls({
|
|
1300
|
-
enableWorker
|
|
1395
|
+
enableWorker,
|
|
1396
|
+
// LL mode on short encrypted VOD drives aggressive ABR thrash → MSE errors.
|
|
1301
1397
|
lowLatencyMode: hlsConfig.lowLatencyMode,
|
|
1302
1398
|
backBufferLength: hlsConfig.backBufferLength,
|
|
1399
|
+
// Stabilize ABR: avoid first-frag-only bandwidth test that immediately
|
|
1400
|
+
// hops levels on single-segment Appextech VODs (HAR showed truncated
|
|
1401
|
+
// 720p body vs Content-Length during level switches).
|
|
1402
|
+
testBandwidth: false,
|
|
1403
|
+
abrEwmaDefaultEstimate: 5e5,
|
|
1404
|
+
abrBandWidthFactor: 0.8,
|
|
1405
|
+
abrBandWidthUpFactor: 0.5,
|
|
1406
|
+
maxStarvationDelay: 4,
|
|
1407
|
+
capLevelToPlayerSize: true,
|
|
1408
|
+
startLevel: -1,
|
|
1303
1409
|
loader
|
|
1304
1410
|
});
|
|
1305
1411
|
this.hls.attachMedia(videoEl);
|
|
@@ -1317,7 +1423,11 @@ var HlsEngine = class {
|
|
|
1317
1423
|
this.syncHlsSubtitleTracks();
|
|
1318
1424
|
this.syncHlsAudioTracks();
|
|
1319
1425
|
callbacks.onStateChange?.("ready");
|
|
1320
|
-
void this.
|
|
1426
|
+
void this.applyAutoplayOnly();
|
|
1427
|
+
});
|
|
1428
|
+
this.hls.on(Hls.Events.FRAG_BUFFERED, () => {
|
|
1429
|
+
if (this.destroyed || this.startPositionApplied) return;
|
|
1430
|
+
void this.applyStartPositionOnly();
|
|
1321
1431
|
});
|
|
1322
1432
|
this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
|
|
1323
1433
|
if (this.destroyed) return;
|
|
@@ -1342,8 +1452,16 @@ var HlsEngine = class {
|
|
|
1342
1452
|
callbacks.onAuthRecoveryNeeded?.("key_error");
|
|
1343
1453
|
if (!data.fatal) return;
|
|
1344
1454
|
} else if (!data.fatal) {
|
|
1455
|
+
if (!status) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1345
1458
|
callbacks.onFatalError?.(
|
|
1346
|
-
|
|
1459
|
+
`Encryption key load failed (HTTP ${status}).`
|
|
1460
|
+
);
|
|
1461
|
+
return;
|
|
1462
|
+
} else if (!status) {
|
|
1463
|
+
callbacks.onFatalError?.(
|
|
1464
|
+
"Encryption key load aborted or failed. Retry playback; if it persists, check the key proxy / CDN path."
|
|
1347
1465
|
);
|
|
1348
1466
|
return;
|
|
1349
1467
|
}
|
|
@@ -1368,30 +1486,67 @@ var HlsEngine = class {
|
|
|
1368
1486
|
}
|
|
1369
1487
|
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
1370
1488
|
this.mediaRecoveryCount += 1;
|
|
1371
|
-
if (this.mediaRecoveryCount
|
|
1372
|
-
|
|
1489
|
+
if (this.mediaRecoveryCount > hlsConfig.fatalRecoveryLimit) {
|
|
1490
|
+
callbacks.onFatalError?.(data.details || "Fatal HLS error");
|
|
1373
1491
|
return;
|
|
1374
1492
|
}
|
|
1493
|
+
if (data.details === Hls.ErrorDetails.BUFFER_APPEND_ERROR || data.details === Hls.ErrorDetails.BUFFER_FULL_ERROR) {
|
|
1494
|
+
this.lockPlaybackToSafeLevel();
|
|
1495
|
+
}
|
|
1496
|
+
if (this.mediaRecoveryCount === 2) {
|
|
1497
|
+
this.hls?.swapAudioCodec();
|
|
1498
|
+
}
|
|
1499
|
+
this.hls?.recoverMediaError();
|
|
1500
|
+
return;
|
|
1375
1501
|
}
|
|
1376
1502
|
callbacks.onFatalError?.(data.details || "Fatal HLS error");
|
|
1377
1503
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1504
|
+
/** Pin the next load to the lowest ladder rung after MSE append failures. */
|
|
1505
|
+
lockPlaybackToSafeLevel() {
|
|
1506
|
+
if (!this.hls || this.qualities.length === 0) return;
|
|
1507
|
+
try {
|
|
1508
|
+
this.hls.currentLevel = 0;
|
|
1509
|
+
this.hls.loadLevel = 0;
|
|
1510
|
+
this.hls.nextLoadLevel = 0;
|
|
1511
|
+
} catch {
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
clampStartPosition(raw, videoEl) {
|
|
1515
|
+
let t = Math.max(0, raw);
|
|
1516
|
+
const duration = videoEl.duration;
|
|
1517
|
+
if (Number.isFinite(duration) && duration > 0) {
|
|
1518
|
+
t = Math.min(t, Math.max(0, duration - 0.35));
|
|
1519
|
+
}
|
|
1520
|
+
if (videoEl.seekable.length > 0) {
|
|
1521
|
+
const end = videoEl.seekable.end(videoEl.seekable.length - 1);
|
|
1522
|
+
if (Number.isFinite(end)) {
|
|
1523
|
+
t = Math.min(t, end);
|
|
1393
1524
|
}
|
|
1394
1525
|
}
|
|
1526
|
+
return t;
|
|
1527
|
+
}
|
|
1528
|
+
async applyStartPositionOnly() {
|
|
1529
|
+
if (this.destroyed || this.startPositionApplied) return;
|
|
1530
|
+
const { videoEl, startPosition } = this.options;
|
|
1531
|
+
this.startPositionApplied = true;
|
|
1532
|
+
if (typeof startPosition !== "number" || startPosition <= 0) return;
|
|
1533
|
+
const seek = () => {
|
|
1534
|
+
if (this.destroyed) return;
|
|
1535
|
+
try {
|
|
1536
|
+
videoEl.currentTime = this.clampStartPosition(startPosition, videoEl);
|
|
1537
|
+
} catch {
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
if (videoEl.seekable.length > 0 || Number.isFinite(videoEl.duration)) {
|
|
1541
|
+
seek();
|
|
1542
|
+
} else {
|
|
1543
|
+
videoEl.addEventListener("loadedmetadata", seek, { once: true });
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
async applyAutoplayOnly() {
|
|
1547
|
+
if (this.destroyed || this.autoplayApplied) return;
|
|
1548
|
+
this.autoplayApplied = true;
|
|
1549
|
+
const { videoEl, autoplay } = this.options;
|
|
1395
1550
|
if (!autoplay) return;
|
|
1396
1551
|
try {
|
|
1397
1552
|
await videoEl.play();
|
|
@@ -1406,6 +1561,10 @@ var HlsEngine = class {
|
|
|
1406
1561
|
}
|
|
1407
1562
|
}
|
|
1408
1563
|
}
|
|
1564
|
+
async applyStartPositionAndAutoplay() {
|
|
1565
|
+
await this.applyStartPositionOnly();
|
|
1566
|
+
await this.applyAutoplayOnly();
|
|
1567
|
+
}
|
|
1409
1568
|
bindMediaEvents() {
|
|
1410
1569
|
const { videoEl, callbacks } = this.options;
|
|
1411
1570
|
videoEl.addEventListener(
|
|
@@ -1649,11 +1808,20 @@ function createVideoPlayer(options) {
|
|
|
1649
1808
|
}
|
|
1650
1809
|
events.emit("progress", { bufferedEnd });
|
|
1651
1810
|
};
|
|
1811
|
+
const onVolumeChange = () => {
|
|
1812
|
+
if (destroyed) return;
|
|
1813
|
+
events.emit("volumechange", {
|
|
1814
|
+
volume: el.volume,
|
|
1815
|
+
muted: el.muted
|
|
1816
|
+
});
|
|
1817
|
+
};
|
|
1652
1818
|
el.addEventListener("timeupdate", onTimeUpdate);
|
|
1653
1819
|
el.addEventListener("progress", onProgress);
|
|
1820
|
+
el.addEventListener("volumechange", onVolumeChange);
|
|
1654
1821
|
mediaCleanup = () => {
|
|
1655
1822
|
el.removeEventListener("timeupdate", onTimeUpdate);
|
|
1656
1823
|
el.removeEventListener("progress", onProgress);
|
|
1824
|
+
el.removeEventListener("volumechange", onVolumeChange);
|
|
1657
1825
|
};
|
|
1658
1826
|
};
|
|
1659
1827
|
const authManager = new AuthManager(
|
|
@@ -1706,6 +1874,9 @@ function createVideoPlayer(options) {
|
|
|
1706
1874
|
isLiveStream = Boolean(source.live);
|
|
1707
1875
|
}
|
|
1708
1876
|
const el = ensureVideoElement();
|
|
1877
|
+
if (isLiveStream) {
|
|
1878
|
+
el.playbackRate = 1;
|
|
1879
|
+
}
|
|
1709
1880
|
bindMediaProgressEvents(el);
|
|
1710
1881
|
engine?.destroy();
|
|
1711
1882
|
engine = new HlsEngine({
|
|
@@ -1794,6 +1965,12 @@ function createVideoPlayer(options) {
|
|
|
1794
1965
|
}
|
|
1795
1966
|
});
|
|
1796
1967
|
await engine.start();
|
|
1968
|
+
if (!destroyed && videoEl) {
|
|
1969
|
+
events.emit("volumechange", {
|
|
1970
|
+
volume: videoEl.volume,
|
|
1971
|
+
muted: videoEl.muted
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1797
1974
|
if (!options.sourceUrl && options.playback) {
|
|
1798
1975
|
refreshManager?.stop();
|
|
1799
1976
|
refreshManager = new RefreshManager({
|
|
@@ -1913,7 +2090,8 @@ function createVideoPlayer(options) {
|
|
|
1913
2090
|
return videoEl?.muted ?? false;
|
|
1914
2091
|
},
|
|
1915
2092
|
setPlaybackRate(rate) {
|
|
1916
|
-
ensureVideoElement()
|
|
2093
|
+
const el = ensureVideoElement();
|
|
2094
|
+
el.playbackRate = isLiveStream ? 1 : rate;
|
|
1917
2095
|
},
|
|
1918
2096
|
getPlaybackRate() {
|
|
1919
2097
|
return videoEl?.playbackRate ?? 1;
|