@banou/media-player 0.10.18 → 0.10.19

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/dist/index.js CHANGED
@@ -5,10 +5,10 @@ import { createPlayer as v, useContainerAttach as y, useMediaAttach as b } from
5
5
  import { definePlayerFeature as x, videoFeatures as S } from "@videojs/core/dom";
6
6
  import { createMediaElementSource as C } from "@banou/ponyfill/web-audio";
7
7
  import { Fragment as w, jsx as T, jsxs as E } from "@emotion/react/jsx-runtime";
8
- import { Captions as D, CaptionsOff as O, Check as k, ChevronLeft as A, ChevronRight as j, Copy as M, Maximize as N, Minimize as P, Pause as F, Play as I, RotateCcw as L, Settings as R, TriangleAlert as z, Volume1 as ee, Volume2 as B, VolumeX as V } from "lucide-react";
9
- import { Tooltip as H } from "react-tooltip";
8
+ import { Captions as D, CaptionsOff as O, Check as k, ChevronLeft as A, ChevronRight as j, Copy as M, Maximize as N, Minimize as P, Pause as F, Play as I, RotateCcw as L, Settings as R, TriangleAlert as z, Volume1 as ee, Volume2 as te, VolumeX as B } from "lucide-react";
9
+ import { Tooltip as V } from "react-tooltip";
10
10
  //#region src/lib/react/source-feature.ts
11
- var te = {
11
+ var ne = {
12
12
  indexes: [],
13
13
  chapters: [],
14
14
  thumbnails: [],
@@ -20,6 +20,7 @@ var te = {
20
20
  audioTracks: [],
21
21
  selectedAudioTrack: void 0,
22
22
  selectAudioTrack: () => {},
23
+ quality: null,
23
24
  hideUI: !1,
24
25
  setHideUI: () => {},
25
26
  hasMedia: !1,
@@ -32,19 +33,19 @@ var te = {
32
33
  playbackErrors: [],
33
34
  ready: !1,
34
35
  setSourceState: () => {}
35
- }, U = x({
36
+ }, H = x({
36
37
  name: "source",
37
- state: () => ({ ...te }),
38
+ state: () => ({ ...ne }),
38
39
  attach({ set: e }) {
39
40
  e({
40
41
  setSourceState: (t) => e(t),
41
42
  setHideUI: (t) => e({ hideUI: t })
42
43
  });
43
44
  }
44
- }), W = v({
45
- features: [...S, U],
45
+ }), U = v({
46
+ features: [...S, H],
46
47
  displayName: "MediaPlayer"
47
- }), G = W.usePlayer, K = (e) => {
48
+ }), W = U.usePlayer, G = (e) => {
48
49
  if (e && e !== "und") try {
49
50
  return new Intl.DisplayNames([navigator.language || "en"], {
50
51
  type: "language",
@@ -53,28 +54,28 @@ var te = {
53
54
  } catch {
54
55
  return;
55
56
  }
56
- }, q = (e) => e?.replace(/_/g, " ").trim() || void 0, J = (e) => {
57
- let t = e.map((e) => K(e.language) ?? q(e.title) ?? `Track ${e.streamIndex}`), n = t.reduce((e, t) => ({
57
+ }, K = (e) => e?.replace(/_/g, " ").trim() || void 0, q = (e) => {
58
+ let t = e.map((e) => G(e.language) ?? K(e.title) ?? `Track ${e.streamIndex}`), n = t.reduce((e, t) => ({
58
59
  ...e,
59
60
  [t]: (e[t] ?? 0) + 1
60
61
  }), {});
61
62
  return e.map((e, r) => {
62
- let i = t[r], a = q(e.title);
63
+ let i = t[r], a = K(e.title);
63
64
  return {
64
65
  track: e,
65
66
  label: (n[i] ?? 0) > 1 && a && a !== i ? `${i} (${a})` : i
66
67
  };
67
68
  });
68
- }, Y = (e) => J(e).map(({ track: e, label: t }) => ({
69
+ }, J = (e) => q(e).map(({ track: e, label: t }) => ({
69
70
  id: e.streamIndex,
70
71
  label: t
71
- })), X = [
72
+ })), Y = [
72
73
  0,
73
74
  250,
74
75
  1e3,
75
76
  3e3,
76
77
  1e4
77
- ], ne = 6e4, re = 500, ie = 15e3, ae = (e) => e instanceof Error ? e.message : String(e), oe = (e) => {
78
+ ], X = 6e4, re = 500, ie = 15e3, ae = (e) => e instanceof Error ? e.message : String(e), oe = (e) => {
78
79
  let t = [], n = e?.cause;
79
80
  for (let e = 0; n != null && e < 8; e++) {
80
81
  if (typeof MediaError < "u" && n instanceof MediaError) {
@@ -85,7 +86,7 @@ var te = {
85
86
  }
86
87
  return t.length ? t.join("\n") : void 0;
87
88
  }, se = (e, t, r) => {
88
- let a = G(), { read: o, size: s, publicPath: c = "", libavWorkerUrl: l = "", jassubWorkerUrl: f = "", jassubWasmUrl: h = "", jassubLegacyWasmUrl: g, defaultFontUrl: _, bufferSize: v, autoplay: y = !1, chapters: b, seekPrepareBudgetMs: x = re } = r ?? {}, S = p(b);
89
+ let a = W(), { read: o, size: s, publicPath: c = "", libavWorkerUrl: l = "", jassubWorkerUrl: f = "", jassubWasmUrl: h = "", jassubLegacyWasmUrl: g, defaultFontUrl: _, bufferSize: v, autoplay: y = !1, chapters: b, seekPrepareBudgetMs: x = re } = r ?? {}, S = p(b);
89
90
  S.current = b;
90
91
  let [C, w] = m(void 0), [T, E] = m(0), D = p({
91
92
  count: 0,
@@ -132,7 +133,7 @@ var te = {
132
133
  });
133
134
  }, [a, x]), L = u((e) => {
134
135
  typeof e == "number" && (a.setSourceState({ selectedAudioTrack: e }), w(e));
135
- }, [a]), R = G((e) => e.setSourceState);
136
+ }, [a]), R = W((e) => e.setSourceState);
136
137
  d(() => {
137
138
  R({
138
139
  selectSubtitleTrack: F,
@@ -177,11 +178,11 @@ var te = {
177
178
  let t = n(e);
178
179
  if (u(e, t), t) {
179
180
  let t = performance.now();
180
- t - D.current.at > ne && (D.current = {
181
+ t - D.current.at > X && (D.current = {
181
182
  count: 0,
182
183
  at: t
183
184
  });
184
- let n = X[Math.min(D.current.count, X.length - 1)];
185
+ let n = Y[Math.min(D.current.count, Y.length - 1)];
185
186
  D.current = {
186
187
  count: D.current.count + 1,
187
188
  at: t
@@ -218,13 +219,13 @@ var te = {
218
219
  onSeek: (e) => N.current?.(e),
219
220
  onSubtitleStreams: (e) => {
220
221
  r || a.setSourceState({
221
- subtitleTracks: Y(e),
222
+ subtitleTracks: J(e),
222
223
  ...j.current ? {} : { selectedSubtitleTrack: e[0]?.streamIndex }
223
224
  });
224
225
  },
225
226
  onAudioStreams: (e, t) => {
226
227
  r || a.setSourceState({
227
- audioTracks: Y(e),
228
+ audioTracks: J(e),
228
229
  selectedAudioTrack: t
229
230
  });
230
231
  }
@@ -572,7 +573,7 @@ var te = {
572
573
  text-shadow: 0 0 4px rgba(0, 0, 0, 1);
573
574
  pointer-events: none;
574
575
  `, Oe = (e) => e instanceof Error ? e.message : typeof e == "string" ? e : "Playback failed", ke = ({ onSubtitleRef: e }) => {
575
- let t = G((e) => e.title), n = G((e) => e.hideUI), r = G((e) => e.playbackError), i = G((e) => e.ready), a = G((e) => e.size), o = G((e) => e.waiting), s = G((e) => e.seekingTo);
576
+ let t = W((e) => e.title), n = W((e) => e.hideUI), r = W((e) => e.playbackError), i = W((e) => e.ready), a = W((e) => e.size), o = W((e) => e.waiting), s = W((e) => e.seekingTo);
576
577
  return /* @__PURE__ */ E(w, { children: [
577
578
  t ? /* @__PURE__ */ T("div", {
578
579
  className: "title",
@@ -676,7 +677,7 @@ var te = {
676
677
  "data-tooltip-delay-hide": i,
677
678
  "data-tooltip-place": o,
678
679
  children: n
679
- }), !s && /* @__PURE__ */ T(H, {
680
+ }), !s && /* @__PURE__ */ T(V, {
680
681
  ref: u,
681
682
  css: Ie(c),
682
683
  id: e,
@@ -1053,7 +1054,7 @@ var te = {
1053
1054
  --thumbnail-half: calc(9 * var(--mp-unit));
1054
1055
  }
1055
1056
  `, ut = 4, dt = () => {
1056
- let e = G(), t = G((e) => e.currentTime), n = G((e) => e.requestSeek), r = G((e) => e.duration), i = G((e) => e.size), a = G((e) => e.downloadedRanges), o = G((e) => e.buffered), s = G((e) => e.indexes), c = G((e) => e.thumbnails), l = G((e) => e.thumbnailAt), u = G((e) => e.requestThumbnail), h = G((e) => e.chapters), g = G((e) => e.seekingTo), _ = G((e) => e.setSourceState), v = G((e) => e.hideUI), y = p(null), [b, x] = m(void 0), [S, C] = m(void 0), w = (e) => {
1057
+ let e = W(), t = W((e) => e.currentTime), n = W((e) => e.requestSeek), r = W((e) => e.duration), i = W((e) => e.size), a = W((e) => e.downloadedRanges), o = W((e) => e.buffered), s = W((e) => e.indexes), c = W((e) => e.thumbnails), l = W((e) => e.thumbnailAt), u = W((e) => e.requestThumbnail), h = W((e) => e.chapters), g = W((e) => e.seekingTo), _ = W((e) => e.setSourceState), v = W((e) => e.hideUI), y = p(null), [b, x] = m(void 0), [S, C] = m(void 0), w = (e) => {
1057
1058
  C(e), u(e);
1058
1059
  }, D = !!S;
1059
1060
  d(() => {
@@ -1089,9 +1090,9 @@ var te = {
1089
1090
  R.current = e.pointerType;
1090
1091
  }, ee = (e) => {
1091
1092
  R.current !== "touch" && w(L(e.clientX));
1092
- }, B = () => {
1093
+ }, te = () => {
1093
1094
  y.current && w(void 0);
1094
- }, V = (e) => r ? e / r * 100 : 0, H = (e, t, n) => {
1095
+ }, B = (e) => r ? e / r * 100 : 0, V = (e, t, n) => {
1095
1096
  let r = Number((n - t).toFixed(4)), i = t * 100;
1096
1097
  return /* @__PURE__ */ T("div", {
1097
1098
  className: "loaded-part",
@@ -1100,10 +1101,10 @@ var te = {
1100
1101
  marginLeft: `${i}%`
1101
1102
  }
1102
1103
  }, e);
1103
- }, te = f(() => i === void 0 ? r ? o.map(([e, t], n) => H(n, e / r, t / r)) : [] : a?.map((e, t) => {
1104
+ }, ne = f(() => i === void 0 ? r ? o.map(([e, t], n) => V(n, e / r, t / r)) : [] : a?.map((e, t) => {
1104
1105
  if (!i || !r) return null;
1105
1106
  let n = s.filter((t) => e.startByteOffset <= t.pos && t.pos <= e.endByteOffset), a = n.at(0), o = n.at(-1);
1106
- return !a || !o ? null : H(t, a.timestamp / r, o.timestamp / r);
1107
+ return !a || !o ? null : V(t, a.timestamp / r, o.timestamp / r);
1107
1108
  }) ?? [], [
1108
1109
  r,
1109
1110
  i,
@@ -1119,7 +1120,7 @@ var te = {
1119
1120
  b,
1120
1121
  r
1121
1122
  ]);
1122
- let U = f(() => {
1123
+ let H = f(() => {
1123
1124
  if (!r) return 0;
1124
1125
  if (N && b !== void 0) return b;
1125
1126
  let e = g ?? t;
@@ -1130,22 +1131,22 @@ var te = {
1130
1131
  N,
1131
1132
  b,
1132
1133
  g
1133
- ]), W = f(() => We(h, r), [h, r]), K = W.length > 0, q = f(() => {
1134
- if (!K || S === void 0 || !r) return -1;
1135
- let e = S / r * 100, t = W.findIndex((t, n) => n < W.length - 1 && e >= t && e < W[n + 1]);
1136
- return t >= 0 ? t : W.length - 2;
1134
+ ]), U = f(() => We(h, r), [h, r]), G = U.length > 0, K = f(() => {
1135
+ if (!G || S === void 0 || !r) return -1;
1136
+ let e = S / r * 100, t = U.findIndex((t, n) => n < U.length - 1 && e >= t && e < U[n + 1]);
1137
+ return t >= 0 ? t : U.length - 2;
1137
1138
  }, [
1138
- K,
1139
- W,
1139
+ G,
1140
+ U,
1140
1141
  S,
1141
1142
  r
1142
- ]), J = f(() => K ? Ge(W, (e) => e !== q) : void 0, [
1143
- K,
1144
- W,
1145
- q
1146
- ]), Y = f(() => q >= 0 ? Ge(W, (e) => e === q) : void 0, [W, q]), X = f(() => {
1143
+ ]), q = f(() => G ? Ge(U, (e) => e !== K) : void 0, [
1144
+ G,
1145
+ U,
1146
+ K
1147
+ ]), J = f(() => K >= 0 ? Ge(U, (e) => e === K) : void 0, [U, K]), Y = f(() => {
1147
1148
  if (S !== void 0) return h.find(({ start: e, end: t }) => e <= S && S < t);
1148
- }, [h, S]), ne = f(() => {
1149
+ }, [h, S]), X = f(() => {
1149
1150
  if (S) {
1150
1151
  if (l) return l(S);
1151
1152
  if (c.length) return c.find(({ startTime: e, endTime: t }) => e <= S && S < t);
@@ -1161,13 +1162,13 @@ var te = {
1161
1162
  /* @__PURE__ */ T("div", { className: "background-bar" }),
1162
1163
  /* @__PURE__ */ T("div", {
1163
1164
  className: "loaded",
1164
- children: te
1165
+ children: ne
1165
1166
  }),
1166
1167
  /* @__PURE__ */ T("div", {
1167
1168
  className: "play-container",
1168
1169
  children: /* @__PURE__ */ T("div", {
1169
1170
  className: "play",
1170
- style: { transform: `scaleX(${U})` }
1171
+ style: { transform: `scaleX(${H})` }
1171
1172
  })
1172
1173
  })
1173
1174
  ]
@@ -1178,31 +1179,31 @@ var te = {
1178
1179
  className: [
1179
1180
  "progress-bar",
1180
1181
  N ? "dragging" : "",
1181
- K ? "segmented" : ""
1182
+ G ? "segmented" : ""
1182
1183
  ].filter(Boolean).join(" "),
1183
1184
  onPointerDown: z,
1184
1185
  onPointerMove: z,
1185
1186
  onMouseMove: ee,
1186
- onMouseOut: B,
1187
+ onMouseOut: te,
1187
1188
  children: [
1188
- re(void 0, J),
1189
+ re(void 0, q),
1189
1190
  S ? /* @__PURE__ */ E("div", {
1190
1191
  className: "cursor-time",
1191
1192
  style: {
1192
- left: X ? `clamp(var(--thumbnail-half), ${V(S)}%, calc(100% - var(--thumbnail-half)))` : `clamp(calc(3 * var(--mp-unit)), ${V(S)}%, calc(100% - calc(3 * var(--mp-unit))))`,
1193
- maxWidth: X ? "var(--thumbnail-width)" : void 0
1193
+ left: Y ? `clamp(var(--thumbnail-half), ${B(S)}%, calc(100% - var(--thumbnail-half)))` : `clamp(calc(3 * var(--mp-unit)), ${B(S)}%, calc(100% - calc(3 * var(--mp-unit))))`,
1194
+ maxWidth: Y ? "var(--thumbnail-width)" : void 0
1194
1195
  },
1195
1196
  children: [/* @__PURE__ */ T("span", {
1196
1197
  className: "cursor-time-value",
1197
1198
  children: Pe(S)
1198
- }), X ? /* @__PURE__ */ T("span", {
1199
+ }), Y ? /* @__PURE__ */ T("span", {
1199
1200
  className: "chapter-title",
1200
- children: X.title
1201
+ children: Y.title
1201
1202
  }) : void 0]
1202
1203
  }) : void 0,
1203
1204
  /* @__PURE__ */ T("div", { className: "progress" }),
1204
1205
  /* @__PURE__ */ T("div", { className: "hover" }),
1205
- Y ? re("focus", Y) : void 0,
1206
+ J ? re("focus", J) : void 0,
1206
1207
  /* @__PURE__ */ T("div", { className: "scrubber" }),
1207
1208
  /* @__PURE__ */ T("div", {
1208
1209
  className: "padding",
@@ -1213,8 +1214,8 @@ var te = {
1213
1214
  }),
1214
1215
  /* @__PURE__ */ T("div", {
1215
1216
  className: "thumbnail",
1216
- style: { left: `clamp(var(--thumbnail-half), ${V(S ?? 1)}%, calc(100% - var(--thumbnail-half)))` },
1217
- children: ne?.url ? /* @__PURE__ */ T("img", { src: ne.url }) : void 0
1217
+ style: { left: `clamp(var(--thumbnail-half), ${B(S ?? 1)}%, calc(100% - var(--thumbnail-half)))` },
1218
+ children: X?.url ? /* @__PURE__ */ T("img", { src: X.url }) : void 0
1218
1219
  })
1219
1220
  ]
1220
1221
  });
@@ -1549,7 +1550,7 @@ ${gt}
1549
1550
  e.detail
1550
1551
  ].filter(Boolean).join("\n");
1551
1552
  }).join("\n\n"), wt = () => {
1552
- let e = G((e) => e.playbackErrors), { open: t, toggle: n, containerRef: r } = vt(), [i, a] = m(!1);
1553
+ let e = W((e) => e.playbackErrors), { open: t, toggle: n, containerRef: r } = vt(), [i, a] = m(!1);
1553
1554
  if (e.length === 0) return null;
1554
1555
  let o = () => {
1555
1556
  navigator.clipboard?.writeText(Ct(e)).then(() => {
@@ -1664,7 +1665,7 @@ width: 100%;
1664
1665
  }
1665
1666
  }
1666
1667
  `, Et = () => {
1667
- let e = G(), t = G((e) => e.playbackRate), n = p(null), r = p(null), i = .25, a = 2.75, o = (e, t = .05) => Math.round(e / t) * t, s = o(t || 1), c = (e) => (e - i) / a * 100, l = (e) => o(i + e * a), { handlers: u } = ze({
1668
+ let e = W(), t = W((e) => e.playbackRate), n = p(null), r = p(null), i = .25, a = 2.75, o = (e, t = .05) => Math.round(e / t) * t, s = o(t || 1), c = (e) => (e - i) / a * 100, l = (e) => o(i + e * a), { handlers: u } = ze({
1668
1669
  ref: r,
1669
1670
  onChange: (t) => e.setPlaybackRate(l(t))
1670
1671
  }), f = (t) => {
@@ -1793,65 +1794,74 @@ ${gt}
1793
1794
  }
1794
1795
 
1795
1796
  `, Ot = () => {
1796
- let e = G(), t = G((e) => e.playbackRate), n = G((e) => e.audioTracks), r = G((e) => e.selectedAudioTrack), i = G((e) => e.selectAudioTrack), a = G((e) => e.volumeNormalizer), [o, s] = m(0), { open: c, toggle: l, containerRef: u, pending: d, failed: f, runSelect: p } = vt({ onReset: () => s(0) }), h = (t) => {
1797
- e.setPlaybackRate(t), s(0);
1798
- }, g = (e) => {
1799
- e !== void 0 && p(e, () => i(e));
1800
- }, _ = (e) => () => {
1801
- s(e);
1797
+ let e = W(), t = W((e) => e.playbackRate), n = W((e) => e.audioTracks), r = W((e) => e.selectedAudioTrack), i = W((e) => e.selectAudioTrack), a = W((e) => e.quality), o = W((e) => e.volumeNormalizer), [s, c] = m(0), { open: l, toggle: u, containerRef: d, pending: f, failed: p, runSelect: h } = vt({ onReset: () => c(0) }), g = (t) => {
1798
+ e.setPlaybackRate(t), c(0);
1799
+ }, _ = (e) => {
1800
+ e !== void 0 && h(e, () => i(e));
1801
+ }, v = (e) => {
1802
+ a && h(e ?? null, () => a.select(e === void 0 ? null : String(e)));
1803
+ }, y = a ? a.options.length + +!!a.offLabel : 0, b = a?.selectedId == null ? a?.offLabel : a.options.find(({ id: e }) => e === a.selectedId)?.label, x = (e) => () => {
1804
+ c(e);
1802
1805
  };
1803
1806
  return /* @__PURE__ */ E("div", {
1804
1807
  css: Dt,
1805
- ref: u,
1808
+ ref: d,
1806
1809
  children: [
1807
1810
  /* @__PURE__ */ T($, {
1808
1811
  id: "settings",
1809
- disabled: c,
1812
+ disabled: l,
1810
1813
  text: /* @__PURE__ */ T("button", {
1811
1814
  className: "settings",
1812
1815
  type: "button",
1813
- onClick: l,
1816
+ onClick: u,
1814
1817
  "aria-label": "Settings",
1815
- "aria-expanded": c,
1818
+ "aria-expanded": l,
1816
1819
  children: /* @__PURE__ */ T(R, {})
1817
1820
  }),
1818
1821
  toolTipText: /* @__PURE__ */ T("span", { children: "Settings" })
1819
1822
  }),
1820
- c && o === 0 && /* @__PURE__ */ E("div", {
1823
+ l && s === 0 && /* @__PURE__ */ E("div", {
1821
1824
  className: "popover menu",
1822
1825
  children: [
1823
1826
  n.length > 1 ? /* @__PURE__ */ E("div", {
1824
- onClick: _(2),
1827
+ onClick: x(2),
1825
1828
  children: [/* @__PURE__ */ T("div", { children: "Audio" }), /* @__PURE__ */ T("div", { children: /* @__PURE__ */ T(j, {}) })]
1826
1829
  }) : null,
1830
+ y > 1 ? /* @__PURE__ */ E("div", {
1831
+ onClick: x(3),
1832
+ children: [/* @__PURE__ */ T("div", { children: "Quality" }), /* @__PURE__ */ E("div", { children: [b ? /* @__PURE__ */ T("span", {
1833
+ className: "secondary",
1834
+ children: b
1835
+ }) : null, /* @__PURE__ */ T(j, {})] })]
1836
+ }) : null,
1827
1837
  /* @__PURE__ */ E("div", {
1828
- onClick: _(1),
1838
+ onClick: x(1),
1829
1839
  children: [/* @__PURE__ */ T("div", { children: "Playback speed" }), /* @__PURE__ */ E("div", { children: [/* @__PURE__ */ T("span", {
1830
1840
  className: "secondary",
1831
1841
  children: t === 1 ? "(Default)" : t.toFixed(2)
1832
1842
  }), /* @__PURE__ */ T(j, {})] })]
1833
1843
  }),
1834
- a ? /* @__PURE__ */ E("div", {
1844
+ o ? /* @__PURE__ */ E("div", {
1835
1845
  role: "switch",
1836
- "aria-checked": a.available && a.enabled,
1837
- "aria-disabled": !a.available,
1838
- className: a.available ? void 0 : "no-hover unavailable",
1846
+ "aria-checked": o.available && o.enabled,
1847
+ "aria-disabled": !o.available,
1848
+ className: o.available ? void 0 : "no-hover unavailable",
1839
1849
  onClick: () => {
1840
- a.available && a.setEnabled(!a.enabled);
1850
+ o.available && o.setEnabled(!o.enabled);
1841
1851
  },
1842
- children: [/* @__PURE__ */ T("div", { children: "Normalize volume" }), /* @__PURE__ */ T("div", { children: a.available ? /* @__PURE__ */ T("span", { className: a.enabled ? "switch on" : "switch" }) : /* @__PURE__ */ T("span", {
1852
+ children: [/* @__PURE__ */ T("div", { children: "Normalize volume" }), /* @__PURE__ */ T("div", { children: o.available ? /* @__PURE__ */ T("span", { className: o.enabled ? "switch on" : "switch" }) : /* @__PURE__ */ T("span", {
1843
1853
  className: "secondary",
1844
1854
  children: "Unavailable"
1845
1855
  }) })]
1846
1856
  }) : null
1847
1857
  ]
1848
1858
  }),
1849
- c && o === 1 && /* @__PURE__ */ E("div", {
1859
+ l && s === 1 && /* @__PURE__ */ E("div", {
1850
1860
  className: "popover playback-rate",
1851
1861
  children: [
1852
1862
  /* @__PURE__ */ E("div", {
1853
1863
  className: "back",
1854
- onClick: _(0),
1864
+ onClick: x(0),
1855
1865
  children: [/* @__PURE__ */ T(A, {}), /* @__PURE__ */ T("span", { children: "Playback speed" })]
1856
1866
  }),
1857
1867
  /* @__PURE__ */ T("div", {
@@ -1862,33 +1872,43 @@ ${gt}
1862
1872
  className: "options no-hover",
1863
1873
  children: [
1864
1874
  /* @__PURE__ */ T("div", {
1865
- onClick: () => h(.5),
1875
+ onClick: () => g(.5),
1866
1876
  children: "0.5x"
1867
1877
  }),
1868
1878
  /* @__PURE__ */ T("div", {
1869
- onClick: () => h(1),
1879
+ onClick: () => g(1),
1870
1880
  children: "1x"
1871
1881
  }),
1872
1882
  /* @__PURE__ */ T("div", {
1873
- onClick: () => h(1.5),
1883
+ onClick: () => g(1.5),
1874
1884
  children: "1.5x"
1875
1885
  }),
1876
1886
  /* @__PURE__ */ T("div", {
1877
- onClick: () => h(2),
1887
+ onClick: () => g(2),
1878
1888
  children: "2x"
1879
1889
  })
1880
1890
  ]
1881
1891
  })
1882
1892
  ]
1883
1893
  }),
1884
- c && o === 2 && /* @__PURE__ */ T(_t, {
1894
+ l && s === 2 && /* @__PURE__ */ T(_t, {
1885
1895
  title: "Audio",
1886
1896
  tracks: n,
1887
1897
  selected: r,
1888
- onSelect: g,
1889
- onBack: _(0),
1890
- pending: d,
1891
- failed: f
1898
+ onSelect: _,
1899
+ onBack: x(0),
1900
+ pending: f,
1901
+ failed: p
1902
+ }),
1903
+ l && s === 3 && a && /* @__PURE__ */ T(_t, {
1904
+ title: "Quality",
1905
+ tracks: a.options,
1906
+ selected: a.selectedId ?? void 0,
1907
+ onSelect: v,
1908
+ onBack: x(0),
1909
+ offLabel: a.offLabel,
1910
+ pending: f,
1911
+ failed: p
1892
1912
  })
1893
1913
  ]
1894
1914
  });
@@ -1910,7 +1930,7 @@ position: static;
1910
1930
 
1911
1931
  ${gt}
1912
1932
  `, At = () => {
1913
- let e = G((e) => e.subtitleTracks), t = G((e) => e.selectedSubtitleTrack), n = G((e) => e.selectSubtitleTrack), r = G((e) => e.subtitleOffLabel), { open: i, toggle: a, containerRef: o, pending: s, failed: c, runSelect: l } = vt();
1933
+ let e = W((e) => e.subtitleTracks), t = W((e) => e.selectedSubtitleTrack), n = W((e) => e.selectSubtitleTrack), r = W((e) => e.subtitleOffLabel), { open: i, toggle: a, containerRef: o, pending: s, failed: c, runSelect: l } = vt();
1914
1934
  if (e.length === 0) return null;
1915
1935
  let u = t !== void 0;
1916
1936
  return /* @__PURE__ */ E("div", {
@@ -1989,7 +2009,7 @@ ${gt}
1989
2009
  height: 10px;
1990
2010
  }
1991
2011
  `, Nt = ({ value: e, onChange: t }) => {
1992
- let n = G((e) => e.volume), r = G((e) => e.muted), i = p(null), a = p(null), { handlers: o } = ze({
2012
+ let n = W((e) => e.volume), r = W((e) => e.muted), i = p(null), a = p(null), { handlers: o } = ze({
1993
2013
  ref: a,
1994
2014
  onChange: t
1995
2015
  }), s = (n) => {
@@ -2091,7 +2111,7 @@ ${gt}
2091
2111
  }
2092
2112
 
2093
2113
  `, Ft = ({ ref: e }) => {
2094
- let t = G(), n = G((e) => e.volume), r = G((e) => e.muted), i = f(() => Ne(n), [n]);
2114
+ let t = W(), n = W((e) => e.volume), r = W((e) => e.muted), i = f(() => Ne(n), [n]);
2095
2115
  return /* @__PURE__ */ E("div", {
2096
2116
  css: Pt,
2097
2117
  children: [/* @__PURE__ */ T($, {
@@ -2103,7 +2123,7 @@ ${gt}
2103
2123
  ref: e,
2104
2124
  "aria-label": "Mute",
2105
2125
  "aria-pressed": r || n === 0,
2106
- children: T(r || n === 0 ? V : n <= .5 ? ee : B, {
2126
+ children: T(r || n === 0 ? B : n <= .5 ? ee : te, {
2107
2127
  size: 18,
2108
2128
  color: "#fff"
2109
2129
  })
@@ -2253,7 +2273,7 @@ ${gt}
2253
2273
  }
2254
2274
  }
2255
2275
  `, Vt = ({ shortcuts: e = "window", root: t }) => {
2256
- let n = G(), r = G((e) => e.paused), i = G((e) => e.currentTime), a = G((e) => e.duration), o = G((e) => e.seekingTo), s = G((e) => e.fullscreen), c = G((e) => e.hideUI), l = G((e) => e.togglePictureInPicture), f = G((e) => e.pictureInPictureMode), h = G((e) => e.burnedInSubtitles), g = G((e) => e.passThroughPictureInPicture), _ = G((e) => e.subtitleTracks), v = G((e) => e.requestSeek), [y, b] = m(null), x = p(null), S = p(void 0), C = f === "burn-in", w = _.length > 0, D = a > 0 && i === a, O = u(({ direction: e, stepSize: t }) => {
2276
+ let n = W(), r = W((e) => e.paused), i = W((e) => e.currentTime), a = W((e) => e.duration), o = W((e) => e.seekingTo), s = W((e) => e.fullscreen), c = W((e) => e.hideUI), l = W((e) => e.togglePictureInPicture), f = W((e) => e.pictureInPictureMode), h = W((e) => e.burnedInSubtitles), g = W((e) => e.passThroughPictureInPicture), _ = W((e) => e.subtitleTracks), v = W((e) => e.requestSeek), [y, b] = m(null), x = p(null), S = p(void 0), C = f === "burn-in", w = _.length > 0, D = a > 0 && i === a, O = u(({ direction: e, stepSize: t }) => {
2257
2277
  let r = Ne(n.volume), i = e === "up" ? t : -t, a = Math.max(0, Math.min(1, r + i)), o = n.muted;
2258
2278
  n.setVolume(Me(a)), o && !n.muted && n.toggleMuted();
2259
2279
  }, [n]), k = p(!1);
@@ -2461,7 +2481,7 @@ ${gt}
2461
2481
  color: #fff;
2462
2482
  }
2463
2483
  `, Wt = () => {
2464
- let e = G((e) => e.burnedInSubtitles), t = G((e) => e.pictureInPictureMode), [n, r] = m(!1);
2484
+ let e = W((e) => e.burnedInSubtitles), t = W((e) => e.pictureInPictureMode), [n, r] = m(!1);
2465
2485
  return d(() => {
2466
2486
  if (t !== "burn-in" || !e) {
2467
2487
  r(!1);
@@ -2541,7 +2561,7 @@ ${gt}
2541
2561
  }
2542
2562
  }
2543
2563
  `, Xt = () => {
2544
- let e = G((e) => e.chapters), t = G((e) => e.currentTime), n = G((e) => e.requestSeek), r = G((e) => e.seekPreview), i = G(), a = f(() => ct(e), [e]), o = f(() => {
2564
+ let e = W((e) => e.chapters), t = W((e) => e.currentTime), n = W((e) => e.requestSeek), r = W((e) => e.seekPreview), i = W(), a = f(() => ct(e), [e]), o = f(() => {
2545
2565
  if (typeof t != "number") return;
2546
2566
  let n = e.findIndex((e, n) => a[n] !== void 0 && e.start - Gt <= t && t < e.end), r = n >= 0 ? a[n] : void 0;
2547
2567
  return r ? {
@@ -2658,7 +2678,7 @@ ${gt}
2658
2678
  item: e
2659
2679
  }];
2660
2680
  }), nn = ({ ref: e, onVideoRef: t, onSubtitleRef: n, overlay: r, controls: i, shortcuts: a, children: o }) => {
2661
- let s = G(), c = G((e) => e.hideUI), l = G((e) => e.setHideUI), u = G((e) => e.hasMedia), f = G((e) => e.passThroughPictureInPicture), m = p(void 0), h = p("mouse"), g = p({
2681
+ let s = W(), c = W((e) => e.hideUI), l = W((e) => e.setHideUI), u = W((e) => e.hasMedia), f = W((e) => e.passThroughPictureInPicture), m = p(void 0), h = p("mouse"), g = p({
2662
2682
  x: -1,
2663
2683
  y: -1
2664
2684
  }), _ = p(null);
@@ -2780,75 +2800,79 @@ ${gt}
2780
2800
  L,
2781
2801
  P,
2782
2802
  R
2783
- ]), { thumbnails: ee, requestThumbnail: B } = ue({
2803
+ ]), { thumbnails: ee, requestThumbnail: te } = ue({
2784
2804
  publicPath: c,
2785
2805
  workerUrl: l,
2786
2806
  length: g === !1 ? void 0 : o,
2787
2807
  read: h ?? u,
2788
2808
  downloadedRanges: s
2789
- }), V = n?.thumbnails?.all ?? ee, { toggle: H, mode: te, burnedIn: U } = de(x, C), W = _e(n?.media ?? null, n?.pictureInPicture), K = G((e) => e.setSourceState), q = n ? !!n.media : o !== void 0;
2809
+ }), B = n?.thumbnails?.all ?? ee, { toggle: V, mode: ne, burnedIn: H } = de(x, C), U = _e(n?.media ?? null, n?.pictureInPicture), G = W((e) => e.setSourceState), K = n ? !!n.media : o !== void 0;
2790
2810
  d(() => {
2791
- K({
2811
+ G({
2792
2812
  title: i,
2793
2813
  size: o,
2794
2814
  downloadedRanges: s,
2795
- hasMedia: q
2815
+ hasMedia: K
2796
2816
  });
2797
2817
  }, [
2798
- K,
2818
+ G,
2799
2819
  i,
2800
2820
  o,
2801
2821
  s,
2802
- q
2822
+ K
2803
2823
  ]);
2804
- let J = n?.thumbnails?.at;
2824
+ let q = n?.thumbnails?.at;
2805
2825
  d(() => {
2806
- K({
2807
- thumbnails: V,
2808
- thumbnailAt: J,
2809
- requestThumbnail: B,
2810
- togglePictureInPicture: H,
2811
- pictureInPictureMode: te,
2812
- burnedInSubtitles: U
2826
+ G({
2827
+ thumbnails: B,
2828
+ thumbnailAt: q,
2829
+ requestThumbnail: te,
2830
+ togglePictureInPicture: V,
2831
+ pictureInPictureMode: ne,
2832
+ burnedInSubtitles: H
2813
2833
  });
2814
2834
  }, [
2815
- K,
2816
- V,
2817
- J,
2835
+ G,
2818
2836
  B,
2819
- H,
2837
+ q,
2820
2838
  te,
2821
- U
2839
+ V,
2840
+ ne,
2841
+ H
2822
2842
  ]), d(() => {
2823
- K({ volumeNormalizer: z });
2824
- }, [K, z]), d(() => {
2825
- K({ passThroughPictureInPicture: W });
2826
- }, [K, W]);
2827
- let Y = n?.subtitles?.selection, X = n?.audioTracks?.selection;
2828
- return d(() => {
2829
- Y && K({
2830
- subtitleTracks: Y.options.map(({ id: e, label: t, disabled: n }) => ({
2843
+ G({ volumeNormalizer: z });
2844
+ }, [G, z]), d(() => {
2845
+ G({ passThroughPictureInPicture: U });
2846
+ }, [G, U]);
2847
+ let J = n?.subtitles?.selection, Y = n?.audioTracks?.selection;
2848
+ d(() => {
2849
+ J && G({
2850
+ subtitleTracks: J.options.map(({ id: e, label: t, disabled: n }) => ({
2831
2851
  id: e,
2832
2852
  label: t,
2833
2853
  disabled: n
2834
2854
  })),
2835
- selectedSubtitleTrack: Y.selectedId ?? void 0,
2836
- selectSubtitleTrack: (e) => Y.select(e == null ? null : String(e)),
2837
- subtitleOffLabel: Y.offLabel
2855
+ selectedSubtitleTrack: J.selectedId ?? void 0,
2856
+ selectSubtitleTrack: (e) => J.select(e == null ? null : String(e)),
2857
+ subtitleOffLabel: J.offLabel
2838
2858
  });
2839
- }, [K, Y]), d(() => {
2840
- X && K({
2841
- audioTracks: X.options.map(({ id: e, label: t, disabled: n }) => ({
2859
+ }, [G, J]), d(() => {
2860
+ Y && G({
2861
+ audioTracks: Y.options.map(({ id: e, label: t, disabled: n }) => ({
2842
2862
  id: e,
2843
2863
  label: t,
2844
2864
  disabled: n
2845
2865
  })),
2846
- selectedAudioTrack: X.selectedId ?? void 0,
2847
- selectAudioTrack: (e) => X.select(String(e))
2866
+ selectedAudioTrack: Y.selectedId ?? void 0,
2867
+ selectAudioTrack: (e) => Y.select(String(e))
2848
2868
  });
2849
- }, [K, X]), d(() => {
2850
- a && K({ chapters: a });
2851
- }, [K, a]), /* @__PURE__ */ T(nn, {
2869
+ }, [G, Y]);
2870
+ let X = n?.quality?.selection ?? null;
2871
+ return d(() => {
2872
+ G({ quality: X });
2873
+ }, [G, X]), d(() => {
2874
+ a && G({ chapters: a });
2875
+ }, [G, a]), /* @__PURE__ */ T(nn, {
2852
2876
  ref: v,
2853
2877
  onVideoRef: n ? void 0 : S,
2854
2878
  onSubtitleRef: w,
@@ -2929,7 +2953,7 @@ ${gt}
2929
2953
  overflow: hidden;
2930
2954
  `, on = (e) => /* @__PURE__ */ T("div", {
2931
2955
  css: an,
2932
- children: /* @__PURE__ */ T(W.Provider, { children: /* @__PURE__ */ T(rn, {
2956
+ children: /* @__PURE__ */ T(U.Provider, { children: /* @__PURE__ */ T(rn, {
2933
2957
  options: e,
2934
2958
  children: e.children
2935
2959
  }) })
@@ -2974,4 +2998,4 @@ ${gt}
2974
2998
  throw Error(`Unknown source type: ${e}`);
2975
2999
  };
2976
3000
  //#endregion
2977
- export { on as MediaPlayer, on as default, W as Player, e as SUBTITLES_OFF, a as createPictureInPicture, o as createSubtitleRenderer, r as createThumbnailGenerator, fn as inputToRemuxerInput, cn as isDelegatedTracks, sn as isExternalThumbnails, U as sourceFeature, i as startPlayback, de as usePictureInPicture, G as usePlayer, ue as useSeekThumbnails };
3001
+ export { on as MediaPlayer, on as default, U as Player, e as SUBTITLES_OFF, a as createPictureInPicture, o as createSubtitleRenderer, r as createThumbnailGenerator, fn as inputToRemuxerInput, cn as isDelegatedTracks, sn as isExternalThumbnails, H as sourceFeature, i as startPlayback, de as usePictureInPicture, W as usePlayer, ue as useSeekThumbnails };
@@ -17,7 +17,7 @@ export declare const popoverStyle: import("@emotion/utils").SerializedStyles;
17
17
  */
18
18
  export declare const TrackMenu: ({ title, tracks, selected, onSelect, onBack, offLabel, pending, failed }: {
19
19
  title: string;
20
- tracks: TrackChoice[];
20
+ tracks: readonly TrackChoice[];
21
21
  selected: string | number | undefined;
22
22
  onSelect: (id: string | number | undefined) => void;
23
23
  /** Absent means the menu was opened straight from the bar, so the header is a label and no more. */
@@ -15,6 +15,7 @@ export declare const Player: import("@videojs/react").CreatePlayerResult<import(
15
15
  audioTracks: import("./source-feature").TrackChoice[];
16
16
  selectedAudioTrack: string | number | undefined;
17
17
  selectAudioTrack: (id: string | number) => void | Promise<void>;
18
+ quality: import("./media").DelegatedSelection | null;
18
19
  hideUI: boolean;
19
20
  setHideUI: (hide: boolean) => void;
20
21
  hasMedia: boolean;
@@ -1,4 +1,5 @@
1
1
  import type { MediaChapter, MediaIndex, PictureInPictureMode, ThumbnailImage } from '../engine';
2
+ import type { DelegatedSelection } from './media';
2
3
  /**
3
4
  * One row of a track menu, already named.
4
5
  *
@@ -162,6 +163,8 @@ export type SourceState = {
162
163
  audioTracks: TrackChoice[];
163
164
  selectedAudioTrack: string | number | undefined;
164
165
  selectAudioTrack: (id: string | number) => void | Promise<void>;
166
+ /** The settings menu's quality choices, from a media the player does not own. null when none are offered. */
167
+ quality: DelegatedSelection | null;
165
168
  /** Chrome auto-hide. True means the controls, title and cursor are hidden. */
166
169
  hideUI: boolean;
167
170
  setHideUI: (hide: boolean) => void;
@@ -307,6 +310,8 @@ export declare const sourceFeature: import("@videojs/react").PlayerFeature<{
307
310
  audioTracks: TrackChoice[];
308
311
  selectedAudioTrack: string | number | undefined;
309
312
  selectAudioTrack: (id: string | number) => void | Promise<void>;
313
+ /** The settings menu's quality choices, from a media the player does not own. null when none are offered. */
314
+ quality: DelegatedSelection | null;
310
315
  /** Chrome auto-hide. True means the controls, title and cursor are hidden. */
311
316
  hideUI: boolean;
312
317
  setHideUI: (hide: boolean) => void;
@@ -193,6 +193,12 @@ export type MediaPlayerRemoteOptions = CommonOptions & {
193
193
  /** The source renders these; the player draws the menu and reports the pick. */
194
194
  subtitles?: DelegatedTracks;
195
195
  audioTracks?: DelegatedTracks;
196
+ /**
197
+ * The source's own quality choices, as a row of the settings menu that shows the selected one.
198
+ * Offered only with two or more rows to choose between; `offLabel` names a row with no quality
199
+ * picked, such as an automatic one, whose id is null.
200
+ */
201
+ quality?: DelegatedTracks;
196
202
  /**
197
203
  * Offer picture in picture, entered by the host from inside the media's own document. Off by
198
204
  * default, and then there is no control at all. See `PassThroughPictureInPicture` for what the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.10.18",
3
+ "version": "0.10.19",
4
4
  "description": "A video player for containers and codecs the browser cannot play natively, remuxed on the fly",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -107,7 +107,8 @@ ${popoverStyle}
107
107
  enum PopoverContent {
108
108
  Default,
109
109
  PlaybackRate,
110
- Audio
110
+ Audio,
111
+ Quality
111
112
  }
112
113
 
113
114
  export const SettingsAction = () => {
@@ -116,6 +117,7 @@ export const SettingsAction = () => {
116
117
  const audioTracks = usePlayer((state) => state.audioTracks)
117
118
  const selectedAudioTrack = usePlayer((state) => state.selectedAudioTrack)
118
119
  const selectAudioTrack = usePlayer((state) => state.selectAudioTrack)
120
+ const quality = usePlayer((state) => state.quality)
119
121
  const volumeNormalizer = usePlayer((state) => state.volumeNormalizer)
120
122
 
121
123
  const [popoverContent, setPopoverContent] = useState(PopoverContent.Default)
@@ -134,6 +136,15 @@ export const SettingsAction = () => {
134
136
  runSelect(id, () => selectAudioTrack(id))
135
137
  }
136
138
 
139
+ const chooseQuality = (id: string | number | undefined) => {
140
+ if (!quality) return
141
+ runSelect(id ?? null, () => quality.select(id === undefined ? null : String(id)))
142
+ }
143
+ const qualityRows = quality ? quality.options.length + (quality.offLabel ? 1 : 0) : 0
144
+ const qualityLabel = quality?.selectedId == null
145
+ ? quality?.offLabel
146
+ : quality.options.find(({ id }) => id === quality.selectedId)?.label
147
+
137
148
  const changePopoverContent = (newPopoverContent: PopoverContent) => () => {
138
149
  setPopoverContent(newPopoverContent)
139
150
  }
@@ -179,6 +190,17 @@ export const SettingsAction = () => {
179
190
  </div>
180
191
  )
181
192
  : null}
193
+ {qualityRows > 1
194
+ ? (
195
+ <div onClick={changePopoverContent(PopoverContent.Quality)}>
196
+ <div>Quality</div>
197
+ <div>
198
+ {qualityLabel ? <span className='secondary'>{qualityLabel}</span> : null}
199
+ <ChevronRight />
200
+ </div>
201
+ </div>
202
+ )
203
+ : null}
182
204
  <div onClick={changePopoverContent(PopoverContent.PlaybackRate)}>
183
205
  <div>Playback speed</div>
184
206
  <div>
@@ -255,6 +277,20 @@ export const SettingsAction = () => {
255
277
  />
256
278
  )
257
279
  }
280
+ {
281
+ open && popoverContent === PopoverContent.Quality && quality && (
282
+ <TrackMenu
283
+ title='Quality'
284
+ tracks={quality.options}
285
+ selected={quality.selectedId ?? undefined}
286
+ onSelect={chooseQuality}
287
+ onBack={changePopoverContent(PopoverContent.Default)}
288
+ offLabel={quality.offLabel}
289
+ pending={pending}
290
+ failed={failed}
291
+ />
292
+ )
293
+ }
258
294
  </div>
259
295
  )
260
296
  }
@@ -154,7 +154,7 @@ export const popoverStyle = css`
154
154
  export const TrackMenu = (
155
155
  { title, tracks, selected, onSelect, onBack, offLabel, pending, failed }: {
156
156
  title: string
157
- tracks: TrackChoice[]
157
+ tracks: readonly TrackChoice[]
158
158
  selected: string | number | undefined
159
159
  onSelect: (id: string | number | undefined) => void
160
160
  /** Absent means the menu was opened straight from the bar, so the header is a label and no more. */
@@ -1,4 +1,5 @@
1
1
  import type { MediaChapter, MediaIndex, PictureInPictureMode, ThumbnailImage } from '../engine'
2
+ import type { DelegatedSelection } from './media'
2
3
 
3
4
  import { definePlayerFeature } from '@videojs/core/dom'
4
5
 
@@ -175,6 +176,9 @@ export type SourceState = {
175
176
  selectedAudioTrack: string | number | undefined
176
177
  selectAudioTrack: (id: string | number) => void | Promise<void>
177
178
 
179
+ /** The settings menu's quality choices, from a media the player does not own. null when none are offered. */
180
+ quality: DelegatedSelection | null
181
+
178
182
  /** Chrome auto-hide. True means the controls, title and cursor are hidden. */
179
183
  hideUI: boolean
180
184
  setHideUI: (hide: boolean) => void
@@ -283,6 +287,7 @@ const initialState: SourceState = {
283
287
  audioTracks: [],
284
288
  selectedAudioTrack: undefined,
285
289
  selectAudioTrack: () => {},
290
+ quality: null,
286
291
  hideUI: false,
287
292
  setHideUI: () => {},
288
293
  hasMedia: false,
@@ -220,6 +220,12 @@ export type MediaPlayerRemoteOptions =
220
220
  /** The source renders these; the player draws the menu and reports the pick. */
221
221
  subtitles?: DelegatedTracks
222
222
  audioTracks?: DelegatedTracks
223
+ /**
224
+ * The source's own quality choices, as a row of the settings menu that shows the selected one.
225
+ * Offered only with two or more rows to choose between; `offLabel` names a row with no quality
226
+ * picked, such as an automatic one, whose id is null.
227
+ */
228
+ quality?: DelegatedTracks
223
229
  /**
224
230
  * Offer picture in picture, entered by the host from inside the media's own document. Off by
225
231
  * default, and then there is no control at all. See `PassThroughPictureInPicture` for what the
@@ -397,6 +403,11 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
397
403
  })
398
404
  }, [setSourceState, audio])
399
405
 
406
+ const quality = remote?.quality?.selection ?? null
407
+ useEffect(() => {
408
+ setSourceState({ quality })
409
+ }, [setSourceState, quality])
410
+
400
411
  /*
401
412
  * Guarded, and its own effect, for the reason the ones above are.
402
413
  *