@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 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.2";
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
- lowLatencyMode: true,
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(HlsClass) {
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 = result.data.byteLength;
996
- stats.total = result.data.byteLength;
1019
+ stats.loaded = key.byteLength;
1020
+ stats.total = key.byteLength;
997
1021
  callbacks.onSuccess(
998
1022
  {
999
1023
  url: result.url,
1000
- data: result.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 && !signal.aborted) {
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 = data.byteLength;
1029
- stats.total = data.byteLength;
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
- callbacks.onError(
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
- const needsBearer = Boolean(plan.accessToken) && (queryResult.status === 401 || queryResult.status === 403);
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.cors) {
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(HlsClass, store, options = {}) {
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
- abort() {
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.keyAbort?.abort();
1155
- this.keyAbort = null;
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?.abort();
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: hlsConfig.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.applyStartPositionAndAutoplay();
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
- 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."
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 <= hlsConfig.fatalRecoveryLimit) {
1372
- this.hls?.recoverMediaError();
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
- async applyStartPositionAndAutoplay() {
1379
- if (this.destroyed) return;
1380
- const { videoEl, autoplay, startPosition } = this.options;
1381
- if (typeof startPosition === "number" && startPosition > 0) {
1382
- const seek = () => {
1383
- if (this.destroyed) return;
1384
- try {
1385
- videoEl.currentTime = startPosition;
1386
- } catch {
1387
- }
1388
- };
1389
- if (videoEl.seekable.length > 0) {
1390
- seek();
1391
- } else {
1392
- videoEl.addEventListener("loadedmetadata", seek, { once: true });
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().playbackRate = rate;
2093
+ const el = ensureVideoElement();
2094
+ el.playbackRate = isLiveStream ? 1 : rate;
1917
2095
  },
1918
2096
  getPlaybackRate() {
1919
2097
  return videoEl?.playbackRate ?? 1;