@banou/media-player 0.10.18 → 0.10.20

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);
@@ -2331,6 +2351,7 @@ ${gt}
2331
2351
  e = Date.now() + zt;
2332
2352
  };
2333
2353
  }, [M]), d(() => () => clearInterval(S.current), []), /* @__PURE__ */ E("div", {
2354
+ className: "control-bar",
2334
2355
  css: Bt,
2335
2356
  style: { ...c ? {
2336
2357
  opacity: "0",
@@ -2461,7 +2482,7 @@ ${gt}
2461
2482
  color: #fff;
2462
2483
  }
2463
2484
  `, Wt = () => {
2464
- let e = G((e) => e.burnedInSubtitles), t = G((e) => e.pictureInPictureMode), [n, r] = m(!1);
2485
+ let e = W((e) => e.burnedInSubtitles), t = W((e) => e.pictureInPictureMode), [n, r] = m(!1);
2465
2486
  return d(() => {
2466
2487
  if (t !== "burn-in" || !e) {
2467
2488
  r(!1);
@@ -2541,7 +2562,7 @@ ${gt}
2541
2562
  }
2542
2563
  }
2543
2564
  `, 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(() => {
2565
+ 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
2566
  if (typeof t != "number") return;
2546
2567
  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
2568
  return r ? {
@@ -2658,50 +2679,54 @@ ${gt}
2658
2679
  item: e
2659
2680
  }];
2660
2681
  }), 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({
2682
+ 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
2683
  x: -1,
2663
2684
  y: -1
2664
- }), _ = p(null);
2685
+ }), _ = p(!1), v = p(null);
2665
2686
  d(() => () => clearTimeout(m.current), []), d(() => {
2666
2687
  u || (clearTimeout(m.current), l(!1));
2667
2688
  }, [u, l]);
2668
- let v = () => {
2689
+ let y = () => {
2669
2690
  u && l(!0);
2670
- }, y = (t) => {
2671
- _.current = t, typeof e == "function" ? e(t) : e && (e.current = t);
2672
- }, b = () => {
2691
+ }, b = (t) => {
2692
+ v.current = t, typeof e == "function" ? e(t) : e && (e.current = t);
2693
+ }, x = () => {
2673
2694
  if (s.passThroughPictureInPicture?.armed) return !0;
2674
2695
  if (h.current !== "mouse") return !1;
2675
2696
  let { x: e, y: t } = g.current;
2676
- if (!_.current || e < 0) return !1;
2697
+ if (!v.current || e < 0) return !1;
2677
2698
  let n = document.elementFromPoint(e, t);
2678
- return !!n && _.current.contains(n) && !n.closest(".video");
2679
- }, x = () => {
2680
- if (b()) {
2681
- m.current = setTimeout(x, Zt);
2699
+ return !n || !v.current.contains(n) || n.closest(".video") ? !1 : !n.closest(".control-bar") || _.current || !!v.current.querySelector(".control-bar [aria-expanded=\"true\"]");
2700
+ }, S = () => {
2701
+ if (x()) {
2702
+ m.current = setTimeout(S, Zt);
2682
2703
  return;
2683
2704
  }
2684
- v();
2685
- }, S = () => {
2686
- l(!1), clearTimeout(m.current), m.current = setTimeout(x, Zt);
2687
- };
2705
+ y();
2706
+ }, C = () => {
2707
+ l(!1), clearTimeout(m.current), m.current = setTimeout(S, Zt);
2708
+ }, w = (e) => {
2709
+ h.current = e.pointerType, e.pointerType === "mouse" && (g.current = {
2710
+ x: e.clientX,
2711
+ y: e.clientY
2712
+ }, C());
2713
+ }, D = (e) => {
2714
+ h.current = e.pointerType, _.current = !0;
2715
+ }, O = () => {
2716
+ _.current = !1;
2717
+ }, k = p(!0);
2688
2718
  return /* @__PURE__ */ E("div", {
2689
2719
  css: en,
2690
- ref: y,
2691
- onPointerMove: (e) => {
2692
- h.current = e.pointerType, e.pointerType === "mouse" && (g.current = {
2693
- x: e.clientX,
2694
- y: e.clientY
2695
- }, S());
2696
- },
2697
- onPointerDown: (e) => {
2698
- h.current = e.pointerType;
2699
- },
2720
+ ref: b,
2721
+ onPointerMove: w,
2722
+ onPointerDown: D,
2723
+ onPointerUp: O,
2724
+ onPointerCancel: O,
2700
2725
  onMouseOut: (e) => {
2701
2726
  let t = e.relatedTarget;
2702
2727
  if (t && e.currentTarget.contains(t)) return;
2703
2728
  let { left: n, right: r, top: i, bottom: a } = e.currentTarget.getBoundingClientRect();
2704
- e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(m.current), v());
2729
+ e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(m.current), y());
2705
2730
  },
2706
2731
  className: c ? "hide" : "",
2707
2732
  tabIndex: a === "player" ? -1 : void 0,
@@ -2718,23 +2743,27 @@ ${gt}
2718
2743
  }, e)),
2719
2744
  i === !1 ? null : /* @__PURE__ */ T(Vt, {
2720
2745
  shortcuts: a,
2721
- root: _
2746
+ root: v
2722
2747
  }),
2723
2748
  /* @__PURE__ */ T(Wt, {}),
2724
2749
  /* @__PURE__ */ T(Xt, {}),
2725
- f?.armed && _.current ? /* @__PURE__ */ T("div", {
2750
+ f?.armed && v.current ? /* @__PURE__ */ T("div", {
2726
2751
  className: "pass-through",
2727
- style: { clipPath: $t(f.armed, _.current.getBoundingClientRect()) },
2752
+ style: { clipPath: $t(f.armed, v.current.getBoundingClientRect()) },
2728
2753
  onPointerOver: f.disarm
2729
2754
  }) : null,
2730
2755
  /* @__PURE__ */ E("div", {
2731
2756
  className: "video",
2732
- onClick: () => {
2757
+ onClick: (e) => {
2733
2758
  if (h.current === "mouse") {
2734
- s.togglePaused();
2759
+ if (e.detail > 1) return;
2760
+ k.current = s.paused, s.togglePaused();
2735
2761
  return;
2736
2762
  }
2737
- c ? S() : (clearTimeout(m.current), v());
2763
+ c ? C() : (clearTimeout(m.current), y());
2764
+ },
2765
+ onDoubleClick: () => {
2766
+ h.current === "mouse" && (k.current ? s.pause() : s.play().catch(() => {}), s.toggleFullscreen());
2738
2767
  },
2739
2768
  children: [t ? /* @__PURE__ */ T("video", {
2740
2769
  ref: t,
@@ -2780,75 +2809,79 @@ ${gt}
2780
2809
  L,
2781
2810
  P,
2782
2811
  R
2783
- ]), { thumbnails: ee, requestThumbnail: B } = ue({
2812
+ ]), { thumbnails: ee, requestThumbnail: te } = ue({
2784
2813
  publicPath: c,
2785
2814
  workerUrl: l,
2786
2815
  length: g === !1 ? void 0 : o,
2787
2816
  read: h ?? u,
2788
2817
  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;
2818
+ }), 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
2819
  d(() => {
2791
- K({
2820
+ G({
2792
2821
  title: i,
2793
2822
  size: o,
2794
2823
  downloadedRanges: s,
2795
- hasMedia: q
2824
+ hasMedia: K
2796
2825
  });
2797
2826
  }, [
2798
- K,
2827
+ G,
2799
2828
  i,
2800
2829
  o,
2801
2830
  s,
2802
- q
2831
+ K
2803
2832
  ]);
2804
- let J = n?.thumbnails?.at;
2833
+ let q = n?.thumbnails?.at;
2805
2834
  d(() => {
2806
- K({
2807
- thumbnails: V,
2808
- thumbnailAt: J,
2809
- requestThumbnail: B,
2810
- togglePictureInPicture: H,
2811
- pictureInPictureMode: te,
2812
- burnedInSubtitles: U
2835
+ G({
2836
+ thumbnails: B,
2837
+ thumbnailAt: q,
2838
+ requestThumbnail: te,
2839
+ togglePictureInPicture: V,
2840
+ pictureInPictureMode: ne,
2841
+ burnedInSubtitles: H
2813
2842
  });
2814
2843
  }, [
2815
- K,
2816
- V,
2817
- J,
2844
+ G,
2818
2845
  B,
2819
- H,
2846
+ q,
2820
2847
  te,
2821
- U
2848
+ V,
2849
+ ne,
2850
+ H
2822
2851
  ]), 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 }) => ({
2852
+ G({ volumeNormalizer: z });
2853
+ }, [G, z]), d(() => {
2854
+ G({ passThroughPictureInPicture: U });
2855
+ }, [G, U]);
2856
+ let J = n?.subtitles?.selection, Y = n?.audioTracks?.selection;
2857
+ d(() => {
2858
+ J && G({
2859
+ subtitleTracks: J.options.map(({ id: e, label: t, disabled: n }) => ({
2831
2860
  id: e,
2832
2861
  label: t,
2833
2862
  disabled: n
2834
2863
  })),
2835
- selectedSubtitleTrack: Y.selectedId ?? void 0,
2836
- selectSubtitleTrack: (e) => Y.select(e == null ? null : String(e)),
2837
- subtitleOffLabel: Y.offLabel
2864
+ selectedSubtitleTrack: J.selectedId ?? void 0,
2865
+ selectSubtitleTrack: (e) => J.select(e == null ? null : String(e)),
2866
+ subtitleOffLabel: J.offLabel
2838
2867
  });
2839
- }, [K, Y]), d(() => {
2840
- X && K({
2841
- audioTracks: X.options.map(({ id: e, label: t, disabled: n }) => ({
2868
+ }, [G, J]), d(() => {
2869
+ Y && G({
2870
+ audioTracks: Y.options.map(({ id: e, label: t, disabled: n }) => ({
2842
2871
  id: e,
2843
2872
  label: t,
2844
2873
  disabled: n
2845
2874
  })),
2846
- selectedAudioTrack: X.selectedId ?? void 0,
2847
- selectAudioTrack: (e) => X.select(String(e))
2875
+ selectedAudioTrack: Y.selectedId ?? void 0,
2876
+ selectAudioTrack: (e) => Y.select(String(e))
2848
2877
  });
2849
- }, [K, X]), d(() => {
2850
- a && K({ chapters: a });
2851
- }, [K, a]), /* @__PURE__ */ T(nn, {
2878
+ }, [G, Y]);
2879
+ let X = n?.quality?.selection ?? null;
2880
+ return d(() => {
2881
+ G({ quality: X });
2882
+ }, [G, X]), d(() => {
2883
+ a && G({ chapters: a });
2884
+ }, [G, a]), /* @__PURE__ */ T(nn, {
2852
2885
  ref: v,
2853
2886
  onVideoRef: n ? void 0 : S,
2854
2887
  onSubtitleRef: w,
@@ -2929,7 +2962,7 @@ ${gt}
2929
2962
  overflow: hidden;
2930
2963
  `, on = (e) => /* @__PURE__ */ T("div", {
2931
2964
  css: an,
2932
- children: /* @__PURE__ */ T(W.Provider, { children: /* @__PURE__ */ T(rn, {
2965
+ children: /* @__PURE__ */ T(U.Provider, { children: /* @__PURE__ */ T(rn, {
2933
2966
  options: e,
2934
2967
  children: e.children
2935
2968
  }) })
@@ -2974,4 +3007,4 @@ ${gt}
2974
3007
  throw Error(`Unknown source type: ${e}`);
2975
3008
  };
2976
3009
  //#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 };
3010
+ 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.20",
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",
@@ -147,6 +147,8 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
147
147
  const lastPointerType = useRef<string>('mouse')
148
148
  // where the mouse last was, which is where it still is when nothing has moved
149
149
  const pointer = useRef({ x: -1, y: -1 })
150
+ // a mouse button held down with the mouse still is a drag on the seekbar or the volume, not yet over
151
+ const pressed = useRef(false)
150
152
  const root = useRef<HTMLDivElement | null>(null)
151
153
 
152
154
  useEffect(() => () => clearTimeout(autoHide.current), [])
@@ -189,9 +191,10 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
189
191
  * and a right click offers the browser's video menu rather than the control's. That is how a link
190
192
  * drawn in an overlay stops behaving like a link, which is what this exists to stop.
191
193
  *
192
- * Only what actually takes pointer events can be hit, so this needs no list of selectors: the
193
- * chrome's own layers opt out, an overlay item opts out until an app opts a child back in, and the
194
- * picture is excluded here because resting on the picture is exactly when hiding is right.
194
+ * Only what actually takes pointer events can be hit: the chrome's own layers opt out, and an
195
+ * overlay item opts out until an app opts a child back in. The picture and the control bar are
196
+ * excluded here because resting on them is exactly when hiding is right, unless a button is held
197
+ * on the bar or one of its menus is open, which are still in use.
195
198
  *
196
199
  * The mouse only. A finger has no resting position, and `elementFromPoint` on the last tap would
197
200
  * keep the controls up forever after one press.
@@ -205,7 +208,9 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
205
208
  const { x, y } = pointer.current
206
209
  if (!root.current || x < 0) return false
207
210
  const hit = document.elementFromPoint(x, y)
208
- return !!hit && root.current.contains(hit) && !hit.closest('.video')
211
+ if (!hit || !root.current.contains(hit) || hit.closest('.video')) return false
212
+ if (hit.closest('.control-bar')) return pressed.current || !!root.current.querySelector('.control-bar [aria-expanded="true"]')
213
+ return true
209
214
  }
210
215
 
211
216
  const hideUnlessTheMouseIsOnAControl = () => {
@@ -233,11 +238,20 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
233
238
 
234
239
  const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
235
240
  lastPointerType.current = event.pointerType
241
+ pressed.current = true
236
242
  }
237
243
 
244
+ const onPointerUp = () => {
245
+ pressed.current = false
246
+ }
247
+
248
+ const pausedBeforeClick = useRef(true)
249
+
238
250
  // play/pause with a mouse, show or hide the controls with a finger, which has no other way back
239
- const onVideoClick = () => {
251
+ const onVideoClick = (event: React.MouseEvent) => {
240
252
  if (lastPointerType.current === 'mouse') {
253
+ if (event.detail > 1) return
254
+ pausedBeforeClick.current = player.paused
241
255
  player.togglePaused()
242
256
  return
243
257
  }
@@ -248,6 +262,15 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
248
262
  }
249
263
  }
250
264
 
265
+ // The first click toggled playback, so it is put back rather than toggled again: a remote media
266
+ // reports its pause late, and a second toggle would read it as still playing and pause it twice.
267
+ const onVideoDoubleClick = () => {
268
+ if (lastPointerType.current !== 'mouse') return
269
+ if (pausedBeforeClick.current) player.pause()
270
+ else void player.play().catch(() => {})
271
+ void player.toggleFullscreen()
272
+ }
273
+
251
274
  // relatedTarget alone is not enough: the browser fires mouseout with a null one while the pointer
252
275
  // still sits over the video, so a null is confirmed against the pointer's own coordinates.
253
276
  const onMouseOut: React.DOMAttributes<HTMLDivElement>['onMouseOut'] = (ev) => {
@@ -266,6 +289,8 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
266
289
  ref={setRoot}
267
290
  onPointerMove={onPointerMove}
268
291
  onPointerDown={onPointerDown}
292
+ onPointerUp={onPointerUp}
293
+ onPointerCancel={onPointerUp}
269
294
  onMouseOut={onMouseOut}
270
295
  className={hideUI ? 'hide' : ''}
271
296
  // focusable, so a click on the picture puts the focus, and with it the shortcuts, inside the player
@@ -294,7 +319,7 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
294
319
  />
295
320
  )
296
321
  : null}
297
- <div className="video" onClick={onVideoClick}>
322
+ <div className="video" onClick={onVideoClick} onDoubleClick={onVideoDoubleClick}>
298
323
  {onVideoRef ? <video ref={onVideoRef} playsInline /> : null}
299
324
  {children}
300
325
  </div>
@@ -308,6 +308,7 @@ export const ControlBar = (
308
308
  // Armed, the whole bar takes no pointer events, or the rows around the control would still catch
309
309
  // the click at its box. The chrome's layer covers everything but the control meanwhile.
310
310
  <div
311
+ className='control-bar'
311
312
  css={style}
312
313
  style={{ ...hideUI ? { opacity: '0', pointerEvents: 'none' } : armed ? { pointerEvents: 'none' } : {} }}
313
314
  >
@@ -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
  *