@codenkay/video-nsgplayer-core 3.0.4 → 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 +6 -0
- package/README.md +12 -0
- package/dist/index.cjs +215 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -11
- package/dist/index.d.ts +7 -11
- package/dist/index.js +215 -42
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
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
|
@@ -283,7 +283,8 @@ var DEFAULT_PLAYER_CONFIG = {
|
|
|
283
283
|
engine: "hls",
|
|
284
284
|
hls: {
|
|
285
285
|
enableWorker: true,
|
|
286
|
-
|
|
286
|
+
// VOD + AES-128: LL mode causes ABR thrash → bufferAppendError on MSE.
|
|
287
|
+
lowLatencyMode: false,
|
|
287
288
|
backBufferLength: 90,
|
|
288
289
|
fatalRecoveryLimit: 3
|
|
289
290
|
}
|
|
@@ -989,29 +990,52 @@ async function fetchEncryptionKeyBytes(url, accessToken, signal) {
|
|
|
989
990
|
};
|
|
990
991
|
}
|
|
991
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
|
+
}
|
|
992
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
|
+
}
|
|
993
1015
|
const stats = createLoaderStats();
|
|
994
1016
|
const end = performance.now();
|
|
995
1017
|
stats.loading.first = stats.loading.start;
|
|
996
1018
|
stats.loading.end = end;
|
|
997
|
-
stats.loaded =
|
|
998
|
-
stats.total =
|
|
1019
|
+
stats.loaded = key.byteLength;
|
|
1020
|
+
stats.total = key.byteLength;
|
|
999
1021
|
callbacks.onSuccess(
|
|
1000
1022
|
{
|
|
1001
1023
|
url: result.url,
|
|
1002
|
-
data:
|
|
1024
|
+
data: key,
|
|
1003
1025
|
code: result.response.status
|
|
1004
1026
|
},
|
|
1005
1027
|
stats,
|
|
1006
1028
|
context,
|
|
1007
1029
|
result.response
|
|
1008
1030
|
);
|
|
1031
|
+
return true;
|
|
1009
1032
|
}
|
|
1010
|
-
async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state) {
|
|
1033
|
+
async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state, lifecycle) {
|
|
1011
1034
|
const stats = createLoaderStats();
|
|
1035
|
+
const stillCurrent = () => lifecycle.isCurrent() && !signal.aborted;
|
|
1012
1036
|
const canTryProxy = Boolean(plan.proxyUrl) && !state.circuitOpen && state.proxyFailures < plan.maxProxyFailures;
|
|
1013
1037
|
if (canTryProxy && plan.proxyUrl) {
|
|
1014
|
-
while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures &&
|
|
1038
|
+
while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures && stillCurrent()) {
|
|
1015
1039
|
state.proxyFailures += 1;
|
|
1016
1040
|
try {
|
|
1017
1041
|
const response = await fetch(plan.proxyUrl, {
|
|
@@ -1021,18 +1045,33 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1021
1045
|
cache: "no-store",
|
|
1022
1046
|
signal
|
|
1023
1047
|
});
|
|
1048
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1024
1049
|
const contentType = response.headers.get("content-type");
|
|
1025
1050
|
const data = await response.arrayBuffer();
|
|
1051
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1026
1052
|
const head = new TextDecoder().decode(data.slice(0, 64));
|
|
1027
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
|
+
}
|
|
1028
1067
|
const end = performance.now();
|
|
1029
1068
|
stats.loading.end = end;
|
|
1030
|
-
stats.loaded =
|
|
1031
|
-
stats.total =
|
|
1069
|
+
stats.loaded = key.byteLength;
|
|
1070
|
+
stats.total = key.byteLength;
|
|
1032
1071
|
callbacks.onSuccess(
|
|
1033
1072
|
{
|
|
1034
1073
|
url: response.url || plan.proxyUrl,
|
|
1035
|
-
data,
|
|
1074
|
+
data: key,
|
|
1036
1075
|
code: response.status
|
|
1037
1076
|
},
|
|
1038
1077
|
stats,
|
|
@@ -1065,25 +1104,22 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1065
1104
|
return "ok";
|
|
1066
1105
|
} catch (error) {
|
|
1067
1106
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
1068
|
-
|
|
1069
|
-
{ code: 0, text: "enc.key proxy aborted/timeout" },
|
|
1070
|
-
context,
|
|
1071
|
-
null,
|
|
1072
|
-
stats
|
|
1073
|
-
);
|
|
1074
|
-
return "ok";
|
|
1107
|
+
return "aborted";
|
|
1075
1108
|
}
|
|
1076
1109
|
state.circuitOpen = true;
|
|
1077
1110
|
break;
|
|
1078
1111
|
}
|
|
1079
1112
|
}
|
|
1113
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1080
1114
|
state.circuitOpen = true;
|
|
1081
1115
|
}
|
|
1116
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1082
1117
|
const queryResult = await fetchEncryptionKeyBytes(
|
|
1083
1118
|
plan.signedKeyUrl,
|
|
1084
1119
|
null,
|
|
1085
1120
|
signal
|
|
1086
1121
|
);
|
|
1122
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1087
1123
|
if (queryResult.ok) {
|
|
1088
1124
|
reportKeySuccess(
|
|
1089
1125
|
{ ...context, url: plan.signedKeyUrl },
|
|
@@ -1092,6 +1128,9 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1092
1128
|
);
|
|
1093
1129
|
return "ok";
|
|
1094
1130
|
}
|
|
1131
|
+
if (queryResult.status === 0 && signal.aborted) {
|
|
1132
|
+
return "aborted";
|
|
1133
|
+
}
|
|
1095
1134
|
const needsBearer = Boolean(plan.accessToken) && (queryResult.status === 401 || queryResult.status === 403 || // Browser-direct: opaque CORS/network on unsigned GET — try Bearer once
|
|
1096
1135
|
// Appextech allows ACAO + Authorization (AUTH_MODE=browser, no BFF).
|
|
1097
1136
|
queryResult.cors || queryResult.status === 0);
|
|
@@ -1101,6 +1140,7 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1101
1140
|
plan.accessToken,
|
|
1102
1141
|
signal
|
|
1103
1142
|
);
|
|
1143
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1104
1144
|
if (bearerResult.ok) {
|
|
1105
1145
|
reportKeySuccess(
|
|
1106
1146
|
{ ...context, url: plan.signedKeyUrl },
|
|
@@ -1109,6 +1149,9 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1109
1149
|
);
|
|
1110
1150
|
return "ok";
|
|
1111
1151
|
}
|
|
1152
|
+
if (bearerResult.status === 0 && signal.aborted) {
|
|
1153
|
+
return "aborted";
|
|
1154
|
+
}
|
|
1112
1155
|
if (bearerResult.cors || bearerResult.status === 0) {
|
|
1113
1156
|
return "try_xhr";
|
|
1114
1157
|
}
|
|
@@ -1147,17 +1190,34 @@ function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
|
1147
1190
|
constructor(config) {
|
|
1148
1191
|
this.context = null;
|
|
1149
1192
|
this.keyAbort = null;
|
|
1193
|
+
/** Bumps on each key load so superseded aborts never call onError. */
|
|
1194
|
+
this.keyLoadGeneration = 0;
|
|
1195
|
+
this.pendingKeyCallbacks = null;
|
|
1150
1196
|
this.inner = new BaseLoader(config);
|
|
1151
1197
|
this.stats = this.inner.stats;
|
|
1152
1198
|
}
|
|
1153
|
-
|
|
1199
|
+
finishKeyAbort() {
|
|
1200
|
+
const cbs = this.pendingKeyCallbacks;
|
|
1201
|
+
this.pendingKeyCallbacks = null;
|
|
1154
1202
|
this.keyAbort?.abort();
|
|
1155
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();
|
|
1156
1216
|
this.inner.abort();
|
|
1157
1217
|
}
|
|
1158
1218
|
destroy() {
|
|
1159
|
-
this.
|
|
1160
|
-
this.
|
|
1219
|
+
this.keyLoadGeneration += 1;
|
|
1220
|
+
this.finishKeyAbort();
|
|
1161
1221
|
this.context = null;
|
|
1162
1222
|
this.inner.destroy();
|
|
1163
1223
|
}
|
|
@@ -1189,10 +1249,19 @@ function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
|
1189
1249
|
);
|
|
1190
1250
|
const proxyUrl = proxyConfigured && !keyProxyState.circuitOpen ? rewriteHlsRequestUrl(originalUrl, store, options) : null;
|
|
1191
1251
|
const accessToken = options.getKeyAccessToken?.() ?? null;
|
|
1192
|
-
this.keyAbort
|
|
1252
|
+
if (this.keyAbort || this.pendingKeyCallbacks) {
|
|
1253
|
+
this.keyLoadGeneration += 1;
|
|
1254
|
+
this.finishKeyAbort();
|
|
1255
|
+
}
|
|
1256
|
+
const generation = ++this.keyLoadGeneration;
|
|
1193
1257
|
this.keyAbort = new AbortController();
|
|
1258
|
+
this.pendingKeyCallbacks = callbacks;
|
|
1259
|
+
context.responseType = "arraybuffer";
|
|
1194
1260
|
const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
|
|
1195
1261
|
const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
|
|
1262
|
+
const lifecycle = {
|
|
1263
|
+
isCurrent: () => generation === this.keyLoadGeneration
|
|
1264
|
+
};
|
|
1196
1265
|
void loadEncryptionKeyEnterprise(
|
|
1197
1266
|
context,
|
|
1198
1267
|
callbacks,
|
|
@@ -1203,13 +1272,22 @@ function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
|
1203
1272
|
maxProxyFailures
|
|
1204
1273
|
},
|
|
1205
1274
|
this.keyAbort.signal,
|
|
1206
|
-
keyProxyState
|
|
1275
|
+
keyProxyState,
|
|
1276
|
+
lifecycle
|
|
1207
1277
|
).then((result) => {
|
|
1278
|
+
if (!lifecycle.isCurrent()) return;
|
|
1279
|
+
if (result === "aborted") {
|
|
1280
|
+
this.finishKeyAbort();
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
this.pendingKeyCallbacks = null;
|
|
1208
1284
|
if (result === "try_xhr") {
|
|
1209
1285
|
context.url = signedKeyUrl;
|
|
1210
1286
|
this.inner.load(context, config, callbacks);
|
|
1211
1287
|
}
|
|
1212
1288
|
}).catch((error) => {
|
|
1289
|
+
if (!lifecycle.isCurrent()) return;
|
|
1290
|
+
this.pendingKeyCallbacks = null;
|
|
1213
1291
|
callbacks.onError(
|
|
1214
1292
|
{
|
|
1215
1293
|
code: 0,
|
|
@@ -1240,6 +1318,10 @@ function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
|
1240
1318
|
function isAuthHttpStatus(code) {
|
|
1241
1319
|
return code === 401 || code === 403;
|
|
1242
1320
|
}
|
|
1321
|
+
function isGeckoBrowser() {
|
|
1322
|
+
if (typeof navigator === "undefined") return false;
|
|
1323
|
+
return /firefox|gecko\//i.test(navigator.userAgent);
|
|
1324
|
+
}
|
|
1243
1325
|
var HlsEngine = class {
|
|
1244
1326
|
constructor(options) {
|
|
1245
1327
|
this.options = options;
|
|
@@ -1255,6 +1337,8 @@ var HlsEngine = class {
|
|
|
1255
1337
|
this.audioTracks = [];
|
|
1256
1338
|
this.audioTrackSelection = 0;
|
|
1257
1339
|
this.nativeTextTrackCleanup = null;
|
|
1340
|
+
this.startPositionApplied = false;
|
|
1341
|
+
this.autoplayApplied = false;
|
|
1258
1342
|
}
|
|
1259
1343
|
async start() {
|
|
1260
1344
|
const { videoEl, playbackUrl, startMuted, volume } = this.options;
|
|
@@ -1301,10 +1385,27 @@ var HlsEngine = class {
|
|
|
1301
1385
|
getKeyAccessToken,
|
|
1302
1386
|
keyProxyMaxFailures: this.options.keyProxyMaxFailures
|
|
1303
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
|
+
}
|
|
1304
1394
|
this.hls = new Hls({
|
|
1305
|
-
enableWorker
|
|
1395
|
+
enableWorker,
|
|
1396
|
+
// LL mode on short encrypted VOD drives aggressive ABR thrash → MSE errors.
|
|
1306
1397
|
lowLatencyMode: hlsConfig.lowLatencyMode,
|
|
1307
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,
|
|
1308
1409
|
loader
|
|
1309
1410
|
});
|
|
1310
1411
|
this.hls.attachMedia(videoEl);
|
|
@@ -1322,7 +1423,11 @@ var HlsEngine = class {
|
|
|
1322
1423
|
this.syncHlsSubtitleTracks();
|
|
1323
1424
|
this.syncHlsAudioTracks();
|
|
1324
1425
|
callbacks.onStateChange?.("ready");
|
|
1325
|
-
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();
|
|
1326
1431
|
});
|
|
1327
1432
|
this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
|
|
1328
1433
|
if (this.destroyed) return;
|
|
@@ -1347,8 +1452,16 @@ var HlsEngine = class {
|
|
|
1347
1452
|
callbacks.onAuthRecoveryNeeded?.("key_error");
|
|
1348
1453
|
if (!data.fatal) return;
|
|
1349
1454
|
} else if (!data.fatal) {
|
|
1455
|
+
if (!status) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1350
1458
|
callbacks.onFatalError?.(
|
|
1351
|
-
|
|
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."
|
|
1352
1465
|
);
|
|
1353
1466
|
return;
|
|
1354
1467
|
}
|
|
@@ -1373,30 +1486,67 @@ var HlsEngine = class {
|
|
|
1373
1486
|
}
|
|
1374
1487
|
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
1375
1488
|
this.mediaRecoveryCount += 1;
|
|
1376
|
-
if (this.mediaRecoveryCount
|
|
1377
|
-
|
|
1489
|
+
if (this.mediaRecoveryCount > hlsConfig.fatalRecoveryLimit) {
|
|
1490
|
+
callbacks.onFatalError?.(data.details || "Fatal HLS error");
|
|
1378
1491
|
return;
|
|
1379
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;
|
|
1380
1501
|
}
|
|
1381
1502
|
callbacks.onFatalError?.(data.details || "Fatal HLS error");
|
|
1382
1503
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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);
|
|
1398
1524
|
}
|
|
1399
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;
|
|
1400
1550
|
if (!autoplay) return;
|
|
1401
1551
|
try {
|
|
1402
1552
|
await videoEl.play();
|
|
@@ -1411,6 +1561,10 @@ var HlsEngine = class {
|
|
|
1411
1561
|
}
|
|
1412
1562
|
}
|
|
1413
1563
|
}
|
|
1564
|
+
async applyStartPositionAndAutoplay() {
|
|
1565
|
+
await this.applyStartPositionOnly();
|
|
1566
|
+
await this.applyAutoplayOnly();
|
|
1567
|
+
}
|
|
1414
1568
|
bindMediaEvents() {
|
|
1415
1569
|
const { videoEl, callbacks } = this.options;
|
|
1416
1570
|
videoEl.addEventListener(
|
|
@@ -1654,11 +1808,20 @@ function createVideoPlayer(options) {
|
|
|
1654
1808
|
}
|
|
1655
1809
|
events.emit("progress", { bufferedEnd });
|
|
1656
1810
|
};
|
|
1811
|
+
const onVolumeChange = () => {
|
|
1812
|
+
if (destroyed) return;
|
|
1813
|
+
events.emit("volumechange", {
|
|
1814
|
+
volume: el.volume,
|
|
1815
|
+
muted: el.muted
|
|
1816
|
+
});
|
|
1817
|
+
};
|
|
1657
1818
|
el.addEventListener("timeupdate", onTimeUpdate);
|
|
1658
1819
|
el.addEventListener("progress", onProgress);
|
|
1820
|
+
el.addEventListener("volumechange", onVolumeChange);
|
|
1659
1821
|
mediaCleanup = () => {
|
|
1660
1822
|
el.removeEventListener("timeupdate", onTimeUpdate);
|
|
1661
1823
|
el.removeEventListener("progress", onProgress);
|
|
1824
|
+
el.removeEventListener("volumechange", onVolumeChange);
|
|
1662
1825
|
};
|
|
1663
1826
|
};
|
|
1664
1827
|
const authManager = new AuthManager(
|
|
@@ -1711,6 +1874,9 @@ function createVideoPlayer(options) {
|
|
|
1711
1874
|
isLiveStream = Boolean(source.live);
|
|
1712
1875
|
}
|
|
1713
1876
|
const el = ensureVideoElement();
|
|
1877
|
+
if (isLiveStream) {
|
|
1878
|
+
el.playbackRate = 1;
|
|
1879
|
+
}
|
|
1714
1880
|
bindMediaProgressEvents(el);
|
|
1715
1881
|
engine?.destroy();
|
|
1716
1882
|
engine = new HlsEngine({
|
|
@@ -1799,6 +1965,12 @@ function createVideoPlayer(options) {
|
|
|
1799
1965
|
}
|
|
1800
1966
|
});
|
|
1801
1967
|
await engine.start();
|
|
1968
|
+
if (!destroyed && videoEl) {
|
|
1969
|
+
events.emit("volumechange", {
|
|
1970
|
+
volume: videoEl.volume,
|
|
1971
|
+
muted: videoEl.muted
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1802
1974
|
if (!options.sourceUrl && options.playback) {
|
|
1803
1975
|
refreshManager?.stop();
|
|
1804
1976
|
refreshManager = new RefreshManager({
|
|
@@ -1918,7 +2090,8 @@ function createVideoPlayer(options) {
|
|
|
1918
2090
|
return videoEl?.muted ?? false;
|
|
1919
2091
|
},
|
|
1920
2092
|
setPlaybackRate(rate) {
|
|
1921
|
-
ensureVideoElement()
|
|
2093
|
+
const el = ensureVideoElement();
|
|
2094
|
+
el.playbackRate = isLiveStream ? 1 : rate;
|
|
1922
2095
|
},
|
|
1923
2096
|
getPlaybackRate() {
|
|
1924
2097
|
return videoEl?.playbackRate ?? 1;
|