@codenkay/video-nsgplayer-core 3.0.4 → 5.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 +12 -0
- package/README.md +14 -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/dist/index.d.cts
CHANGED
|
@@ -452,17 +452,6 @@ type HlsEngineOptions = {
|
|
|
452
452
|
logger: Logger;
|
|
453
453
|
callbacks: HlsEngineCallbacks;
|
|
454
454
|
};
|
|
455
|
-
/**
|
|
456
|
-
* Dual-path HLS engine: hls.js when supported, otherwise native Safari HLS.
|
|
457
|
-
*
|
|
458
|
-
* Signed CDN playback requires auth query params on every HLS request
|
|
459
|
-
* (playlists, segments, and AES `enc.key` on CDN hosts).
|
|
460
|
-
*
|
|
461
|
-
* Optional `streaming.keyProxyUrlBuilder` (same-origin BFF) or
|
|
462
|
-
* `streaming.getKeyAccessToken` (Bearer only after signed-query 401/403).
|
|
463
|
-
* CDN keys prefer signed query without Authorization (no CORS preflight).
|
|
464
|
-
* when query-signed CDN access is insufficient.
|
|
465
|
-
*/
|
|
466
455
|
declare class HlsEngine {
|
|
467
456
|
private readonly options;
|
|
468
457
|
private hls;
|
|
@@ -477,10 +466,17 @@ declare class HlsEngine {
|
|
|
477
466
|
private audioTracks;
|
|
478
467
|
private audioTrackSelection;
|
|
479
468
|
private nativeTextTrackCleanup;
|
|
469
|
+
private startPositionApplied;
|
|
470
|
+
private autoplayApplied;
|
|
480
471
|
constructor(options: HlsEngineOptions);
|
|
481
472
|
start(): Promise<void>;
|
|
482
473
|
private startWithHlsJs;
|
|
483
474
|
private handleHlsError;
|
|
475
|
+
/** Pin the next load to the lowest ladder rung after MSE append failures. */
|
|
476
|
+
private lockPlaybackToSafeLevel;
|
|
477
|
+
private clampStartPosition;
|
|
478
|
+
private applyStartPositionOnly;
|
|
479
|
+
private applyAutoplayOnly;
|
|
484
480
|
private applyStartPositionAndAutoplay;
|
|
485
481
|
private bindMediaEvents;
|
|
486
482
|
updateAuthParams(): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -452,17 +452,6 @@ type HlsEngineOptions = {
|
|
|
452
452
|
logger: Logger;
|
|
453
453
|
callbacks: HlsEngineCallbacks;
|
|
454
454
|
};
|
|
455
|
-
/**
|
|
456
|
-
* Dual-path HLS engine: hls.js when supported, otherwise native Safari HLS.
|
|
457
|
-
*
|
|
458
|
-
* Signed CDN playback requires auth query params on every HLS request
|
|
459
|
-
* (playlists, segments, and AES `enc.key` on CDN hosts).
|
|
460
|
-
*
|
|
461
|
-
* Optional `streaming.keyProxyUrlBuilder` (same-origin BFF) or
|
|
462
|
-
* `streaming.getKeyAccessToken` (Bearer only after signed-query 401/403).
|
|
463
|
-
* CDN keys prefer signed query without Authorization (no CORS preflight).
|
|
464
|
-
* when query-signed CDN access is insufficient.
|
|
465
|
-
*/
|
|
466
455
|
declare class HlsEngine {
|
|
467
456
|
private readonly options;
|
|
468
457
|
private hls;
|
|
@@ -477,10 +466,17 @@ declare class HlsEngine {
|
|
|
477
466
|
private audioTracks;
|
|
478
467
|
private audioTrackSelection;
|
|
479
468
|
private nativeTextTrackCleanup;
|
|
469
|
+
private startPositionApplied;
|
|
470
|
+
private autoplayApplied;
|
|
480
471
|
constructor(options: HlsEngineOptions);
|
|
481
472
|
start(): Promise<void>;
|
|
482
473
|
private startWithHlsJs;
|
|
483
474
|
private handleHlsError;
|
|
475
|
+
/** Pin the next load to the lowest ladder rung after MSE append failures. */
|
|
476
|
+
private lockPlaybackToSafeLevel;
|
|
477
|
+
private clampStartPosition;
|
|
478
|
+
private applyStartPositionOnly;
|
|
479
|
+
private applyAutoplayOnly;
|
|
484
480
|
private applyStartPositionAndAutoplay;
|
|
485
481
|
private bindMediaEvents;
|
|
486
482
|
updateAuthParams(): void;
|
package/dist/index.js
CHANGED
|
@@ -216,7 +216,8 @@ var DEFAULT_PLAYER_CONFIG = {
|
|
|
216
216
|
engine: "hls",
|
|
217
217
|
hls: {
|
|
218
218
|
enableWorker: true,
|
|
219
|
-
|
|
219
|
+
// VOD + AES-128: LL mode causes ABR thrash → bufferAppendError on MSE.
|
|
220
|
+
lowLatencyMode: false,
|
|
220
221
|
backBufferLength: 90,
|
|
221
222
|
fatalRecoveryLimit: 3
|
|
222
223
|
}
|
|
@@ -922,29 +923,52 @@ async function fetchEncryptionKeyBytes(url, accessToken, signal) {
|
|
|
922
923
|
};
|
|
923
924
|
}
|
|
924
925
|
}
|
|
926
|
+
var AES_128_KEY_BYTES = 16;
|
|
927
|
+
function normalizeAes128Key(data) {
|
|
928
|
+
if (data.byteLength === AES_128_KEY_BYTES) {
|
|
929
|
+
return data.slice(0);
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
925
933
|
function reportKeySuccess(context, callbacks, result) {
|
|
934
|
+
const key = normalizeAes128Key(result.data);
|
|
935
|
+
if (!key) {
|
|
936
|
+
const stats2 = createLoaderStats();
|
|
937
|
+
callbacks.onError(
|
|
938
|
+
{
|
|
939
|
+
code: 422,
|
|
940
|
+
text: `enc.key must be ${AES_128_KEY_BYTES} bytes (got ${result.data.byteLength})`
|
|
941
|
+
},
|
|
942
|
+
context,
|
|
943
|
+
result.response,
|
|
944
|
+
stats2
|
|
945
|
+
);
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
926
948
|
const stats = createLoaderStats();
|
|
927
949
|
const end = performance.now();
|
|
928
950
|
stats.loading.first = stats.loading.start;
|
|
929
951
|
stats.loading.end = end;
|
|
930
|
-
stats.loaded =
|
|
931
|
-
stats.total =
|
|
952
|
+
stats.loaded = key.byteLength;
|
|
953
|
+
stats.total = key.byteLength;
|
|
932
954
|
callbacks.onSuccess(
|
|
933
955
|
{
|
|
934
956
|
url: result.url,
|
|
935
|
-
data:
|
|
957
|
+
data: key,
|
|
936
958
|
code: result.response.status
|
|
937
959
|
},
|
|
938
960
|
stats,
|
|
939
961
|
context,
|
|
940
962
|
result.response
|
|
941
963
|
);
|
|
964
|
+
return true;
|
|
942
965
|
}
|
|
943
|
-
async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state) {
|
|
966
|
+
async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, state, lifecycle) {
|
|
944
967
|
const stats = createLoaderStats();
|
|
968
|
+
const stillCurrent = () => lifecycle.isCurrent() && !signal.aborted;
|
|
945
969
|
const canTryProxy = Boolean(plan.proxyUrl) && !state.circuitOpen && state.proxyFailures < plan.maxProxyFailures;
|
|
946
970
|
if (canTryProxy && plan.proxyUrl) {
|
|
947
|
-
while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures &&
|
|
971
|
+
while (!state.circuitOpen && state.proxyFailures < plan.maxProxyFailures && stillCurrent()) {
|
|
948
972
|
state.proxyFailures += 1;
|
|
949
973
|
try {
|
|
950
974
|
const response = await fetch(plan.proxyUrl, {
|
|
@@ -954,18 +978,33 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
954
978
|
cache: "no-store",
|
|
955
979
|
signal
|
|
956
980
|
});
|
|
981
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
957
982
|
const contentType = response.headers.get("content-type");
|
|
958
983
|
const data = await response.arrayBuffer();
|
|
984
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
959
985
|
const head = new TextDecoder().decode(data.slice(0, 64));
|
|
960
986
|
if (response.ok && !isProxyUpstreamBlocked(response.status, contentType, head)) {
|
|
987
|
+
const key = normalizeAes128Key(data);
|
|
988
|
+
if (!key) {
|
|
989
|
+
callbacks.onError(
|
|
990
|
+
{
|
|
991
|
+
code: 422,
|
|
992
|
+
text: `enc.key proxy returned ${data.byteLength} bytes (need ${AES_128_KEY_BYTES})`
|
|
993
|
+
},
|
|
994
|
+
{ ...context, url: plan.proxyUrl },
|
|
995
|
+
response,
|
|
996
|
+
stats
|
|
997
|
+
);
|
|
998
|
+
return "ok";
|
|
999
|
+
}
|
|
961
1000
|
const end = performance.now();
|
|
962
1001
|
stats.loading.end = end;
|
|
963
|
-
stats.loaded =
|
|
964
|
-
stats.total =
|
|
1002
|
+
stats.loaded = key.byteLength;
|
|
1003
|
+
stats.total = key.byteLength;
|
|
965
1004
|
callbacks.onSuccess(
|
|
966
1005
|
{
|
|
967
1006
|
url: response.url || plan.proxyUrl,
|
|
968
|
-
data,
|
|
1007
|
+
data: key,
|
|
969
1008
|
code: response.status
|
|
970
1009
|
},
|
|
971
1010
|
stats,
|
|
@@ -998,25 +1037,22 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
998
1037
|
return "ok";
|
|
999
1038
|
} catch (error) {
|
|
1000
1039
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
1001
|
-
|
|
1002
|
-
{ code: 0, text: "enc.key proxy aborted/timeout" },
|
|
1003
|
-
context,
|
|
1004
|
-
null,
|
|
1005
|
-
stats
|
|
1006
|
-
);
|
|
1007
|
-
return "ok";
|
|
1040
|
+
return "aborted";
|
|
1008
1041
|
}
|
|
1009
1042
|
state.circuitOpen = true;
|
|
1010
1043
|
break;
|
|
1011
1044
|
}
|
|
1012
1045
|
}
|
|
1046
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1013
1047
|
state.circuitOpen = true;
|
|
1014
1048
|
}
|
|
1049
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1015
1050
|
const queryResult = await fetchEncryptionKeyBytes(
|
|
1016
1051
|
plan.signedKeyUrl,
|
|
1017
1052
|
null,
|
|
1018
1053
|
signal
|
|
1019
1054
|
);
|
|
1055
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1020
1056
|
if (queryResult.ok) {
|
|
1021
1057
|
reportKeySuccess(
|
|
1022
1058
|
{ ...context, url: plan.signedKeyUrl },
|
|
@@ -1025,6 +1061,9 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1025
1061
|
);
|
|
1026
1062
|
return "ok";
|
|
1027
1063
|
}
|
|
1064
|
+
if (queryResult.status === 0 && signal.aborted) {
|
|
1065
|
+
return "aborted";
|
|
1066
|
+
}
|
|
1028
1067
|
const needsBearer = Boolean(plan.accessToken) && (queryResult.status === 401 || queryResult.status === 403 || // Browser-direct: opaque CORS/network on unsigned GET — try Bearer once
|
|
1029
1068
|
// Appextech allows ACAO + Authorization (AUTH_MODE=browser, no BFF).
|
|
1030
1069
|
queryResult.cors || queryResult.status === 0);
|
|
@@ -1034,6 +1073,7 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1034
1073
|
plan.accessToken,
|
|
1035
1074
|
signal
|
|
1036
1075
|
);
|
|
1076
|
+
if (!lifecycle.isCurrent()) return "aborted";
|
|
1037
1077
|
if (bearerResult.ok) {
|
|
1038
1078
|
reportKeySuccess(
|
|
1039
1079
|
{ ...context, url: plan.signedKeyUrl },
|
|
@@ -1042,6 +1082,9 @@ async function loadEncryptionKeyEnterprise(context, callbacks, plan, signal, sta
|
|
|
1042
1082
|
);
|
|
1043
1083
|
return "ok";
|
|
1044
1084
|
}
|
|
1085
|
+
if (bearerResult.status === 0 && signal.aborted) {
|
|
1086
|
+
return "aborted";
|
|
1087
|
+
}
|
|
1045
1088
|
if (bearerResult.cors || bearerResult.status === 0) {
|
|
1046
1089
|
return "try_xhr";
|
|
1047
1090
|
}
|
|
@@ -1080,17 +1123,34 @@ function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
|
1080
1123
|
constructor(config) {
|
|
1081
1124
|
this.context = null;
|
|
1082
1125
|
this.keyAbort = null;
|
|
1126
|
+
/** Bumps on each key load so superseded aborts never call onError. */
|
|
1127
|
+
this.keyLoadGeneration = 0;
|
|
1128
|
+
this.pendingKeyCallbacks = null;
|
|
1083
1129
|
this.inner = new BaseLoader(config);
|
|
1084
1130
|
this.stats = this.inner.stats;
|
|
1085
1131
|
}
|
|
1086
|
-
|
|
1132
|
+
finishKeyAbort() {
|
|
1133
|
+
const cbs = this.pendingKeyCallbacks;
|
|
1134
|
+
this.pendingKeyCallbacks = null;
|
|
1087
1135
|
this.keyAbort?.abort();
|
|
1088
1136
|
this.keyAbort = null;
|
|
1137
|
+
if (cbs?.onAbort && this.context) {
|
|
1138
|
+
const stats = createLoaderStats();
|
|
1139
|
+
stats.aborted = true;
|
|
1140
|
+
try {
|
|
1141
|
+
cbs.onAbort(stats, this.context, null);
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
abort() {
|
|
1147
|
+
this.keyLoadGeneration += 1;
|
|
1148
|
+
this.finishKeyAbort();
|
|
1089
1149
|
this.inner.abort();
|
|
1090
1150
|
}
|
|
1091
1151
|
destroy() {
|
|
1092
|
-
this.
|
|
1093
|
-
this.
|
|
1152
|
+
this.keyLoadGeneration += 1;
|
|
1153
|
+
this.finishKeyAbort();
|
|
1094
1154
|
this.context = null;
|
|
1095
1155
|
this.inner.destroy();
|
|
1096
1156
|
}
|
|
@@ -1122,10 +1182,19 @@ function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
|
1122
1182
|
);
|
|
1123
1183
|
const proxyUrl = proxyConfigured && !keyProxyState.circuitOpen ? rewriteHlsRequestUrl(originalUrl, store, options) : null;
|
|
1124
1184
|
const accessToken = options.getKeyAccessToken?.() ?? null;
|
|
1125
|
-
this.keyAbort
|
|
1185
|
+
if (this.keyAbort || this.pendingKeyCallbacks) {
|
|
1186
|
+
this.keyLoadGeneration += 1;
|
|
1187
|
+
this.finishKeyAbort();
|
|
1188
|
+
}
|
|
1189
|
+
const generation = ++this.keyLoadGeneration;
|
|
1126
1190
|
this.keyAbort = new AbortController();
|
|
1191
|
+
this.pendingKeyCallbacks = callbacks;
|
|
1192
|
+
context.responseType = "arraybuffer";
|
|
1127
1193
|
const timeoutMs = typeof config?.timeout === "number" && config.timeout > 0 ? config.timeout : 2e4;
|
|
1128
1194
|
const timer = setTimeout(() => this.keyAbort?.abort(), timeoutMs);
|
|
1195
|
+
const lifecycle = {
|
|
1196
|
+
isCurrent: () => generation === this.keyLoadGeneration
|
|
1197
|
+
};
|
|
1129
1198
|
void loadEncryptionKeyEnterprise(
|
|
1130
1199
|
context,
|
|
1131
1200
|
callbacks,
|
|
@@ -1136,13 +1205,22 @@ function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
|
1136
1205
|
maxProxyFailures
|
|
1137
1206
|
},
|
|
1138
1207
|
this.keyAbort.signal,
|
|
1139
|
-
keyProxyState
|
|
1208
|
+
keyProxyState,
|
|
1209
|
+
lifecycle
|
|
1140
1210
|
).then((result) => {
|
|
1211
|
+
if (!lifecycle.isCurrent()) return;
|
|
1212
|
+
if (result === "aborted") {
|
|
1213
|
+
this.finishKeyAbort();
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
this.pendingKeyCallbacks = null;
|
|
1141
1217
|
if (result === "try_xhr") {
|
|
1142
1218
|
context.url = signedKeyUrl;
|
|
1143
1219
|
this.inner.load(context, config, callbacks);
|
|
1144
1220
|
}
|
|
1145
1221
|
}).catch((error) => {
|
|
1222
|
+
if (!lifecycle.isCurrent()) return;
|
|
1223
|
+
this.pendingKeyCallbacks = null;
|
|
1146
1224
|
callbacks.onError(
|
|
1147
1225
|
{
|
|
1148
1226
|
code: 0,
|
|
@@ -1173,6 +1251,10 @@ function createAuthParamsLoader(HlsModule, store, options = {}) {
|
|
|
1173
1251
|
function isAuthHttpStatus(code) {
|
|
1174
1252
|
return code === 401 || code === 403;
|
|
1175
1253
|
}
|
|
1254
|
+
function isGeckoBrowser() {
|
|
1255
|
+
if (typeof navigator === "undefined") return false;
|
|
1256
|
+
return /firefox|gecko\//i.test(navigator.userAgent);
|
|
1257
|
+
}
|
|
1176
1258
|
var HlsEngine = class {
|
|
1177
1259
|
constructor(options) {
|
|
1178
1260
|
this.options = options;
|
|
@@ -1188,6 +1270,8 @@ var HlsEngine = class {
|
|
|
1188
1270
|
this.audioTracks = [];
|
|
1189
1271
|
this.audioTrackSelection = 0;
|
|
1190
1272
|
this.nativeTextTrackCleanup = null;
|
|
1273
|
+
this.startPositionApplied = false;
|
|
1274
|
+
this.autoplayApplied = false;
|
|
1191
1275
|
}
|
|
1192
1276
|
async start() {
|
|
1193
1277
|
const { videoEl, playbackUrl, startMuted, volume } = this.options;
|
|
@@ -1234,10 +1318,27 @@ var HlsEngine = class {
|
|
|
1234
1318
|
getKeyAccessToken,
|
|
1235
1319
|
keyProxyMaxFailures: this.options.keyProxyMaxFailures
|
|
1236
1320
|
});
|
|
1321
|
+
const enableWorker = hlsConfig.enableWorker && !isGeckoBrowser();
|
|
1322
|
+
if (hlsConfig.enableWorker && !enableWorker) {
|
|
1323
|
+
this.options.logger.warn(
|
|
1324
|
+
"Disabling hls.js worker on Firefox to avoid MSE bufferAppendError"
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1237
1327
|
this.hls = new Hls({
|
|
1238
|
-
enableWorker
|
|
1328
|
+
enableWorker,
|
|
1329
|
+
// LL mode on short encrypted VOD drives aggressive ABR thrash → MSE errors.
|
|
1239
1330
|
lowLatencyMode: hlsConfig.lowLatencyMode,
|
|
1240
1331
|
backBufferLength: hlsConfig.backBufferLength,
|
|
1332
|
+
// Stabilize ABR: avoid first-frag-only bandwidth test that immediately
|
|
1333
|
+
// hops levels on single-segment Appextech VODs (HAR showed truncated
|
|
1334
|
+
// 720p body vs Content-Length during level switches).
|
|
1335
|
+
testBandwidth: false,
|
|
1336
|
+
abrEwmaDefaultEstimate: 5e5,
|
|
1337
|
+
abrBandWidthFactor: 0.8,
|
|
1338
|
+
abrBandWidthUpFactor: 0.5,
|
|
1339
|
+
maxStarvationDelay: 4,
|
|
1340
|
+
capLevelToPlayerSize: true,
|
|
1341
|
+
startLevel: -1,
|
|
1241
1342
|
loader
|
|
1242
1343
|
});
|
|
1243
1344
|
this.hls.attachMedia(videoEl);
|
|
@@ -1255,7 +1356,11 @@ var HlsEngine = class {
|
|
|
1255
1356
|
this.syncHlsSubtitleTracks();
|
|
1256
1357
|
this.syncHlsAudioTracks();
|
|
1257
1358
|
callbacks.onStateChange?.("ready");
|
|
1258
|
-
void this.
|
|
1359
|
+
void this.applyAutoplayOnly();
|
|
1360
|
+
});
|
|
1361
|
+
this.hls.on(Hls.Events.FRAG_BUFFERED, () => {
|
|
1362
|
+
if (this.destroyed || this.startPositionApplied) return;
|
|
1363
|
+
void this.applyStartPositionOnly();
|
|
1259
1364
|
});
|
|
1260
1365
|
this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
|
|
1261
1366
|
if (this.destroyed) return;
|
|
@@ -1280,8 +1385,16 @@ var HlsEngine = class {
|
|
|
1280
1385
|
callbacks.onAuthRecoveryNeeded?.("key_error");
|
|
1281
1386
|
if (!data.fatal) return;
|
|
1282
1387
|
} else if (!data.fatal) {
|
|
1388
|
+
if (!status) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1283
1391
|
callbacks.onFatalError?.(
|
|
1284
|
-
|
|
1392
|
+
`Encryption key load failed (HTTP ${status}).`
|
|
1393
|
+
);
|
|
1394
|
+
return;
|
|
1395
|
+
} else if (!status) {
|
|
1396
|
+
callbacks.onFatalError?.(
|
|
1397
|
+
"Encryption key load aborted or failed. Retry playback; if it persists, check the key proxy / CDN path."
|
|
1285
1398
|
);
|
|
1286
1399
|
return;
|
|
1287
1400
|
}
|
|
@@ -1306,30 +1419,67 @@ var HlsEngine = class {
|
|
|
1306
1419
|
}
|
|
1307
1420
|
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
1308
1421
|
this.mediaRecoveryCount += 1;
|
|
1309
|
-
if (this.mediaRecoveryCount
|
|
1310
|
-
|
|
1422
|
+
if (this.mediaRecoveryCount > hlsConfig.fatalRecoveryLimit) {
|
|
1423
|
+
callbacks.onFatalError?.(data.details || "Fatal HLS error");
|
|
1311
1424
|
return;
|
|
1312
1425
|
}
|
|
1426
|
+
if (data.details === Hls.ErrorDetails.BUFFER_APPEND_ERROR || data.details === Hls.ErrorDetails.BUFFER_FULL_ERROR) {
|
|
1427
|
+
this.lockPlaybackToSafeLevel();
|
|
1428
|
+
}
|
|
1429
|
+
if (this.mediaRecoveryCount === 2) {
|
|
1430
|
+
this.hls?.swapAudioCodec();
|
|
1431
|
+
}
|
|
1432
|
+
this.hls?.recoverMediaError();
|
|
1433
|
+
return;
|
|
1313
1434
|
}
|
|
1314
1435
|
callbacks.onFatalError?.(data.details || "Fatal HLS error");
|
|
1315
1436
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1437
|
+
/** Pin the next load to the lowest ladder rung after MSE append failures. */
|
|
1438
|
+
lockPlaybackToSafeLevel() {
|
|
1439
|
+
if (!this.hls || this.qualities.length === 0) return;
|
|
1440
|
+
try {
|
|
1441
|
+
this.hls.currentLevel = 0;
|
|
1442
|
+
this.hls.loadLevel = 0;
|
|
1443
|
+
this.hls.nextLoadLevel = 0;
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
clampStartPosition(raw, videoEl) {
|
|
1448
|
+
let t = Math.max(0, raw);
|
|
1449
|
+
const duration = videoEl.duration;
|
|
1450
|
+
if (Number.isFinite(duration) && duration > 0) {
|
|
1451
|
+
t = Math.min(t, Math.max(0, duration - 0.35));
|
|
1452
|
+
}
|
|
1453
|
+
if (videoEl.seekable.length > 0) {
|
|
1454
|
+
const end = videoEl.seekable.end(videoEl.seekable.length - 1);
|
|
1455
|
+
if (Number.isFinite(end)) {
|
|
1456
|
+
t = Math.min(t, end);
|
|
1331
1457
|
}
|
|
1332
1458
|
}
|
|
1459
|
+
return t;
|
|
1460
|
+
}
|
|
1461
|
+
async applyStartPositionOnly() {
|
|
1462
|
+
if (this.destroyed || this.startPositionApplied) return;
|
|
1463
|
+
const { videoEl, startPosition } = this.options;
|
|
1464
|
+
this.startPositionApplied = true;
|
|
1465
|
+
if (typeof startPosition !== "number" || startPosition <= 0) return;
|
|
1466
|
+
const seek = () => {
|
|
1467
|
+
if (this.destroyed) return;
|
|
1468
|
+
try {
|
|
1469
|
+
videoEl.currentTime = this.clampStartPosition(startPosition, videoEl);
|
|
1470
|
+
} catch {
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
if (videoEl.seekable.length > 0 || Number.isFinite(videoEl.duration)) {
|
|
1474
|
+
seek();
|
|
1475
|
+
} else {
|
|
1476
|
+
videoEl.addEventListener("loadedmetadata", seek, { once: true });
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async applyAutoplayOnly() {
|
|
1480
|
+
if (this.destroyed || this.autoplayApplied) return;
|
|
1481
|
+
this.autoplayApplied = true;
|
|
1482
|
+
const { videoEl, autoplay } = this.options;
|
|
1333
1483
|
if (!autoplay) return;
|
|
1334
1484
|
try {
|
|
1335
1485
|
await videoEl.play();
|
|
@@ -1344,6 +1494,10 @@ var HlsEngine = class {
|
|
|
1344
1494
|
}
|
|
1345
1495
|
}
|
|
1346
1496
|
}
|
|
1497
|
+
async applyStartPositionAndAutoplay() {
|
|
1498
|
+
await this.applyStartPositionOnly();
|
|
1499
|
+
await this.applyAutoplayOnly();
|
|
1500
|
+
}
|
|
1347
1501
|
bindMediaEvents() {
|
|
1348
1502
|
const { videoEl, callbacks } = this.options;
|
|
1349
1503
|
videoEl.addEventListener(
|
|
@@ -1587,11 +1741,20 @@ function createVideoPlayer(options) {
|
|
|
1587
1741
|
}
|
|
1588
1742
|
events.emit("progress", { bufferedEnd });
|
|
1589
1743
|
};
|
|
1744
|
+
const onVolumeChange = () => {
|
|
1745
|
+
if (destroyed) return;
|
|
1746
|
+
events.emit("volumechange", {
|
|
1747
|
+
volume: el.volume,
|
|
1748
|
+
muted: el.muted
|
|
1749
|
+
});
|
|
1750
|
+
};
|
|
1590
1751
|
el.addEventListener("timeupdate", onTimeUpdate);
|
|
1591
1752
|
el.addEventListener("progress", onProgress);
|
|
1753
|
+
el.addEventListener("volumechange", onVolumeChange);
|
|
1592
1754
|
mediaCleanup = () => {
|
|
1593
1755
|
el.removeEventListener("timeupdate", onTimeUpdate);
|
|
1594
1756
|
el.removeEventListener("progress", onProgress);
|
|
1757
|
+
el.removeEventListener("volumechange", onVolumeChange);
|
|
1595
1758
|
};
|
|
1596
1759
|
};
|
|
1597
1760
|
const authManager = new AuthManager(
|
|
@@ -1644,6 +1807,9 @@ function createVideoPlayer(options) {
|
|
|
1644
1807
|
isLiveStream = Boolean(source.live);
|
|
1645
1808
|
}
|
|
1646
1809
|
const el = ensureVideoElement();
|
|
1810
|
+
if (isLiveStream) {
|
|
1811
|
+
el.playbackRate = 1;
|
|
1812
|
+
}
|
|
1647
1813
|
bindMediaProgressEvents(el);
|
|
1648
1814
|
engine?.destroy();
|
|
1649
1815
|
engine = new HlsEngine({
|
|
@@ -1732,6 +1898,12 @@ function createVideoPlayer(options) {
|
|
|
1732
1898
|
}
|
|
1733
1899
|
});
|
|
1734
1900
|
await engine.start();
|
|
1901
|
+
if (!destroyed && videoEl) {
|
|
1902
|
+
events.emit("volumechange", {
|
|
1903
|
+
volume: videoEl.volume,
|
|
1904
|
+
muted: videoEl.muted
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1735
1907
|
if (!options.sourceUrl && options.playback) {
|
|
1736
1908
|
refreshManager?.stop();
|
|
1737
1909
|
refreshManager = new RefreshManager({
|
|
@@ -1851,7 +2023,8 @@ function createVideoPlayer(options) {
|
|
|
1851
2023
|
return videoEl?.muted ?? false;
|
|
1852
2024
|
},
|
|
1853
2025
|
setPlaybackRate(rate) {
|
|
1854
|
-
ensureVideoElement()
|
|
2026
|
+
const el = ensureVideoElement();
|
|
2027
|
+
el.playbackRate = isLiveStream ? 1 : rate;
|
|
1855
2028
|
},
|
|
1856
2029
|
getPlaybackRate() {
|
|
1857
2030
|
return videoEl?.playbackRate ?? 1;
|