@banou/media-player 0.8.3 → 0.8.4

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
@@ -4,10 +4,10 @@ import { css as p, keyframes as m } from "@emotion/react";
4
4
  import { createPlayer as h, useContainerAttach as g, useMediaAttach as _ } from "@videojs/react";
5
5
  import { definePlayerFeature as v, videoFeatures as y } from "@videojs/core/dom";
6
6
  import { Fragment as b, jsx as x, jsxs as S } from "@emotion/react/jsx-runtime";
7
- import { ChevronLeft as C, ChevronRight as w, Maximize as T, Minimize as E, Pause as D, Play as O, RotateCcw as k, Settings as A, Volume1 as j, Volume2 as ee, VolumeX as te } from "react-feather";
8
- import { Tooltip as ne } from "react-tooltip";
7
+ import { ChevronLeft as C, ChevronRight as w, Maximize as T, Minimize as E, Pause as D, Play as O, RotateCcw as k, Settings as A, Volume1 as j, Volume2 as M, VolumeX as ee } from "react-feather";
8
+ import { Tooltip as te } from "react-tooltip";
9
9
  //#region src/lib/react/source-feature.ts
10
- var re = {
10
+ var ne = {
11
11
  indexes: [],
12
12
  thumbnails: [],
13
13
  subtitleTracks: [],
@@ -22,19 +22,19 @@ var re = {
22
22
  playbackError: null,
23
23
  ready: !1,
24
24
  setSourceState: () => {}
25
- }, M = v({
25
+ }, N = v({
26
26
  name: "source",
27
- state: () => ({ ...re }),
27
+ state: () => ({ ...ne }),
28
28
  attach({ set: e }) {
29
29
  e({
30
30
  setSourceState: (t) => e(t),
31
31
  setHideUI: (t) => e({ hideUI: t })
32
32
  });
33
33
  }
34
- }), N = h({
35
- features: [...y, M],
34
+ }), P = h({
35
+ features: [...y, N],
36
36
  displayName: "MediaPlayer"
37
- }), P = N.usePlayer, ie = (e) => {
37
+ }), F = P.usePlayer, re = (e) => {
38
38
  if (!(!e || e === "und")) try {
39
39
  return new Intl.DisplayNames([navigator.language || "en"], {
40
40
  type: "language",
@@ -43,23 +43,23 @@ var re = {
43
43
  } catch {
44
44
  return;
45
45
  }
46
- }, F = (e) => e?.replace(/_/g, " ").trim() || void 0, ae = (e) => {
47
- let t = e.map((e) => ie(e.language) ?? F(e.title) ?? `Track ${e.streamIndex}`), n = t.reduce((e, t) => ({
46
+ }, I = (e) => e?.replace(/_/g, " ").trim() || void 0, L = (e) => {
47
+ let t = e.map((e) => re(e.language) ?? I(e.title) ?? `Track ${e.streamIndex}`), n = t.reduce((e, t) => ({
48
48
  ...e,
49
49
  [t]: (e[t] ?? 0) + 1
50
50
  }), {});
51
51
  return e.map((e, r) => {
52
- let i = t[r], a = F(e.title);
52
+ let i = t[r], a = I(e.title);
53
53
  return {
54
54
  track: e,
55
55
  label: (n[i] ?? 0) > 1 && a && a !== i ? `${i} (${a})` : i
56
56
  };
57
57
  });
58
- }, I = (e) => ae(e).map(({ track: e, label: t }) => ({
58
+ }, R = (e) => L(e).map(({ track: e, label: t }) => ({
59
59
  id: e.streamIndex,
60
60
  label: t
61
- })), oe = (t, n, r) => {
62
- let i = P(), { read: a, size: o, publicPath: s = "", libavWorkerUrl: u = "", jassubWorkerUrl: p = "", jassubWasmUrl: m = "", jassubLegacyWasmUrl: h, defaultFontUrl: g, bufferSize: _, autoplay: v = !1 } = r ?? {}, [y, b] = f(void 0), x = d(null), S = d(0), C = d(!1), w = d(a);
61
+ })), ie = (t, n, r) => {
62
+ let i = F(), { read: a, size: o, publicPath: s = "", libavWorkerUrl: u = "", jassubWorkerUrl: p = "", jassubWasmUrl: m = "", jassubLegacyWasmUrl: h, defaultFontUrl: g, bufferSize: _, autoplay: v = !1 } = r ?? {}, [y, b] = f(void 0), x = d(null), S = d(0), C = d(!1), w = d(a);
63
63
  w.current = a;
64
64
  let T = d(r?.onSeek);
65
65
  T.current = r?.onSeek;
@@ -70,7 +70,7 @@ var re = {
70
70
  C.current = !0, i.setSourceState({ selectedSubtitleTrack: t }), x.current?.selectSubtitleStream(t);
71
71
  }, [i]), O = c((e) => {
72
72
  typeof e == "number" && (i.setSourceState({ selectedAudioTrack: e }), b(e));
73
- }, [i]), k = P((e) => e.setSourceState);
73
+ }, [i]), k = F((e) => e.setSourceState);
74
74
  l(() => {
75
75
  k({
76
76
  selectSubtitleTrack: D,
@@ -115,13 +115,13 @@ var re = {
115
115
  onSeek: (e) => T.current?.(e),
116
116
  onSubtitleStreams: (e) => {
117
117
  r || i.setSourceState({
118
- subtitleTracks: I(e),
118
+ subtitleTracks: R(e),
119
119
  ...C.current ? {} : { selectedSubtitleTrack: e[0]?.streamIndex }
120
120
  });
121
121
  },
122
122
  onAudioStreams: (e, t) => {
123
123
  r || i.setSourceState({
124
- audioTracks: I(e),
124
+ audioTracks: R(e),
125
125
  selectedAudioTrack: t
126
126
  });
127
127
  }
@@ -154,13 +154,13 @@ var re = {
154
154
  y,
155
155
  v
156
156
  ]);
157
- }, se = 5e3, ce = 6e4, L = ({ publicPath: e, workerUrl: n, length: r, read: i, downloadedRanges: a }) => {
157
+ }, ae = 5e3, oe = 6e4, z = ({ publicPath: e, workerUrl: n, length: r, read: i, downloadedRanges: a }) => {
158
158
  let [o, s] = f([]), c = d(null), p = d(i);
159
159
  p.current = i;
160
160
  let m = u(() => a?.map(({ startByteOffset: e, endByteOffset: t }) => [e, t]), [a?.map(({ startByteOffset: e, endByteOffset: t }) => `${e}-${t}`).join(",")]), h = d(m);
161
161
  return h.current = m, l(() => {
162
162
  if (!r || !i || !e || !n) return;
163
- let a = !1, o = null, l, u = se, d = () => {
163
+ let a = !1, o = null, l, u = ae, d = () => {
164
164
  t({
165
165
  publicPath: e,
166
166
  workerUrl: n,
@@ -176,7 +176,7 @@ var re = {
176
176
  }
177
177
  o = e, c.current = e, e.update(h.current);
178
178
  }, () => {
179
- a || (l = setTimeout(d, u), u = Math.min(u * 2, ce));
179
+ a || (l = setTimeout(d, u), u = Math.min(u * 2, oe));
180
180
  });
181
181
  };
182
182
  return d(), () => {
@@ -189,8 +189,8 @@ var re = {
189
189
  ]), l(() => {
190
190
  c.current?.update(m);
191
191
  }, [m]), o;
192
- }, R = (e, t) => {
193
- let n = P(), r = d(null);
192
+ }, B = (e, t) => {
193
+ let n = F(), r = d(null);
194
194
  l(() => {
195
195
  if (!e || !t) return;
196
196
  let a = i({
@@ -212,7 +212,7 @@ var re = {
212
212
  });
213
213
  }, []);
214
214
  return e && t ? a : null;
215
- }, z = {
215
+ }, V = {
216
216
  headings: {
217
217
  large: "\n font-weight: 600;\n font-size: calc(2.8 * var(--mp-unit));\n line-height: calc(3.4 * var(--mp-unit));\n @media (min-width: 960px) {\n font-size: calc(3.4 * var(--mp-unit));\n line-height: calc(4.1 * var(--mp-unit));\n }\n ",
218
218
  medium: "\n font-weight: 600;\n font-size: calc(2.4 * var(--mp-unit));\n line-height: calc(2.9 * var(--mp-unit));\n @media (min-width: 960px) {\n font-size: calc(2.8 * var(--mp-unit));\n line-height: calc(3.4 * var(--mp-unit));\n }\n ",
@@ -239,14 +239,14 @@ var re = {
239
239
  medium: "\n font-weight: 500;\n font-size: calc(0.8 * var(--mp-unit));\n line-height: calc(1.1 * var(--mp-unit));\n @media (min-width: 960px) {\n font-size: calc(1 * var(--mp-unit));\n line-height: calc(1.4 * var(--mp-unit));\n }\n ",
240
240
  regular: "\n font-weight: 400;\n font-size: calc(0.8 * var(--mp-unit));\n line-height: calc(1.1 * var(--mp-unit));\n @media (min-width: 960px) {\n font-size: calc(1 * var(--mp-unit));\n line-height: calc(1.4 * var(--mp-unit));\n }\n "
241
241
  }
242
- }, le = p`
242
+ }, se = p`
243
243
  top: unset !important;
244
244
  left: unset !important;
245
245
  width: 100%;
246
246
  height: 100%;
247
247
  margin: auto;
248
248
  pointer-events: none;
249
- `, ue = p`
249
+ `, ce = p`
250
250
  position: absolute;
251
251
  top: 0;
252
252
  left: 0;
@@ -254,7 +254,7 @@ var re = {
254
254
  padding: calc(1.2 * var(--mp-unit)) calc(1.6 * var(--mp-unit));
255
255
  /* clears a notch once the player is fullscreen; resolves to zero everywhere else */
256
256
  padding-top: calc(calc(1.2 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
257
- ${z.headings.small}
257
+ ${V.headings.small}
258
258
  color: white;
259
259
  text-shadow: 0 0 4px rgba(0, 0, 0, 1);
260
260
  z-index: 2;
@@ -275,7 +275,7 @@ var re = {
275
275
 
276
276
  background: linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.3) 30%, rgba(0,0,0,0.2) 60%, rgba(0,0,0,0.1) 80%, transparent 100%);
277
277
  transition: opacity 0.1s cubic-bezier(.4,0,1,1);
278
- `, de = p`
278
+ `, le = p`
279
279
  position: absolute;
280
280
  inset: 0;
281
281
  z-index: 2;
@@ -295,43 +295,43 @@ var re = {
295
295
  to { transform: rotate(360deg); }
296
296
  `} 0.8s linear infinite;
297
297
  }
298
- `, fe = p`
298
+ `, ue = p`
299
299
  position: absolute;
300
300
  inset: auto 0 20%;
301
301
  z-index: 3;
302
302
  padding: 0 calc(2 * var(--mp-unit));
303
303
  text-align: center;
304
304
  color: #fff;
305
- ${z.bLarge.regular}
305
+ ${V.bLarge.regular}
306
306
  text-shadow: 0 0 4px rgba(0, 0, 0, 1);
307
307
  pointer-events: none;
308
- `, pe = (e) => e instanceof Error ? e.message : typeof e == "string" ? e : "Playback failed", B = ({ onCanvasRef: e }) => {
309
- let t = P((e) => e.title), n = P((e) => e.hideUI), r = P((e) => e.playbackError), i = P((e) => e.ready), a = P((e) => e.size), o = P((e) => e.waiting);
308
+ `, de = (e) => e instanceof Error ? e.message : typeof e == "string" ? e : "Playback failed", fe = ({ onCanvasRef: e }) => {
309
+ let t = F((e) => e.title), n = F((e) => e.hideUI), r = F((e) => e.playbackError), i = F((e) => e.ready), a = F((e) => e.size), o = F((e) => e.waiting);
310
310
  return /* @__PURE__ */ S(b, { children: [
311
311
  t ? /* @__PURE__ */ x("div", {
312
312
  className: "title",
313
- css: ue,
313
+ css: ce,
314
314
  style: { ...n ? {
315
315
  opacity: "0",
316
316
  pointerEvents: "none"
317
317
  } : {} },
318
318
  children: t
319
319
  }) : void 0,
320
- (a ? !i : o) && !r ? /* @__PURE__ */ x("div", { css: de }) : void 0,
320
+ (a ? !i : o) && !r ? /* @__PURE__ */ x("div", { css: le }) : void 0,
321
321
  r ? /* @__PURE__ */ x("div", {
322
- css: fe,
323
- children: pe(r)
322
+ css: ue,
323
+ children: de(r)
324
324
  }) : void 0,
325
325
  /* @__PURE__ */ x("canvas", {
326
326
  ref: e,
327
- css: le
327
+ css: se
328
328
  })
329
329
  ] });
330
- }, V = 1e-4, H = 1, U = (e) => Math.max(V, Math.min(H, e)) ** 2, W = (e) => Math.max(V, Math.min(H, e)) ** (H / 2), G = (e) => {
330
+ }, H = 1e-4, U = 1, W = (e) => Math.max(H, Math.min(U, e)) ** 2, G = (e) => Math.max(H, Math.min(U, e)) ** (U / 2), K = (e) => {
331
331
  if (e === void 0) return "0:00";
332
332
  let t = Math.floor(e / 3600), n = Math.floor(e % 3600 / 60), r = Math.floor(e % 60);
333
333
  return t > 0 ? `${t}:${n < 10 ? "0" : ""}${n}:${r < 10 ? "0" : ""}${r}` : `${n}:${r < 10 ? "0" : ""}${r}`;
334
- }, me = (e, t) => `${G(e)} / ${G(t)}`, he = (e) => p`
334
+ }, pe = (e, t) => `${K(e)} / ${K(t)}`, me = (e) => p`
335
335
  display: flex;
336
336
  justify-content: flex-end;
337
337
 
@@ -341,7 +341,7 @@ var re = {
341
341
  z-index: 3;
342
342
 
343
343
  * {
344
- ${z.bMedium.regular}
344
+ ${V.bMedium.regular}
345
345
  }
346
346
 
347
347
  ${e === "sm" && p`
@@ -353,7 +353,7 @@ var re = {
353
353
  ${e === "lg" && p`
354
354
  padding: calc(1.2 * var(--mp-unit))!important;
355
355
  `}
356
- `, K = ({ id: e, toolTipText: t, text: n, delayShow: r = 0, closeDelay: i = 0, offset: a = 20, tooltipPlace: o = "top", disabled: s = !1, size: c = "md" }) => /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x("div", {
356
+ `, q = ({ id: e, toolTipText: t, text: n, delayShow: r = 0, closeDelay: i = 0, offset: a = 20, tooltipPlace: o = "top", disabled: s = !1, size: c = "md" }) => /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x("div", {
357
357
  "data-tooltip-id": e,
358
358
  "data-open": !0,
359
359
  "data-tooltip-offset": a,
@@ -361,12 +361,12 @@ var re = {
361
361
  "data-tooltip-delay-hide": i,
362
362
  "data-tooltip-place": o,
363
363
  children: n
364
- }), !s && /* @__PURE__ */ x(ne, {
365
- css: he(c),
364
+ }), !s && /* @__PURE__ */ x(te, {
365
+ css: me(c),
366
366
  id: e,
367
367
  noArrow: !0,
368
368
  children: t
369
- })] }), q = ({ ref: e, onChange: t, orientation: n = "horizontal", disabled: r = !1 }) => {
369
+ })] }), J = ({ ref: e, onChange: t, orientation: n = "horizontal", disabled: r = !1 }) => {
370
370
  let [i, a] = f(!1), o = d(t);
371
371
  o.current = t;
372
372
  let s = d(null), u = c((t, r) => {
@@ -393,7 +393,7 @@ var re = {
393
393
  style: { touchAction: "none" }
394
394
  }
395
395
  };
396
- }, ge = p`
396
+ }, he = p`
397
397
  position: relative;
398
398
  height: calc(.4 * var(--mp-unit));
399
399
 
@@ -438,7 +438,7 @@ var re = {
438
438
  justify-content: center;
439
439
 
440
440
  text-shadow: 0 0 4px rgba(0, 0, 0, 1);
441
- ${z.bMedium.bold}
441
+ ${V.bMedium.bold}
442
442
 
443
443
  position: absolute;
444
444
  top: calc(-2.5 * var(--mp-unit));
@@ -550,8 +550,8 @@ var re = {
550
550
  --thumbnail-width: calc(18 * var(--mp-unit));
551
551
  --thumbnail-half: calc(9 * var(--mp-unit));
552
552
  }
553
- `, J = () => {
554
- let e = P(), t = P((e) => e.currentTime), n = P((e) => e.duration), r = P((e) => e.size), i = P((e) => e.downloadedRanges), a = P((e) => e.indexes), o = P((e) => e.thumbnails), s = P((e) => e.thumbnailAt), c = d(null), [p, m] = f(void 0), [h, g] = f(void 0), _ = d(void 0), { dragging: v, handlers: y } = q({
553
+ `, ge = () => {
554
+ let e = F(), t = F((e) => e.currentTime), n = F((e) => e.duration), r = F((e) => e.size), i = F((e) => e.downloadedRanges), a = F((e) => e.indexes), o = F((e) => e.thumbnails), s = F((e) => e.thumbnailAt), c = d(null), [p, m] = f(void 0), [h, g] = f(void 0), _ = d(void 0), { dragging: v, handlers: y } = J({
555
555
  ref: c,
556
556
  onChange: (e) => {
557
557
  m(e), _.current !== "mouse" && g(e * n);
@@ -609,7 +609,7 @@ var re = {
609
609
  h
610
610
  ]);
611
611
  return /* @__PURE__ */ S("div", {
612
- css: ge,
612
+ css: he,
613
613
  ref: c,
614
614
  className: v ? "progress-bar dragging" : "progress-bar",
615
615
  onMouseMove: T,
@@ -704,7 +704,7 @@ width: 100%;
704
704
  }
705
705
  }
706
706
  `, ye = () => {
707
- let e = P(), t = P((e) => e.playbackRate), n = d(null), r = d(null), i = .25, a = 3 - i, o = (e, t = .05) => Math.round(e / t) * t, s = o(t || 1), c = (e) => (e - i) / a * 100, u = (e) => o(i + e * a), { handlers: f } = q({
707
+ let e = F(), t = F((e) => e.playbackRate), n = d(null), r = d(null), i = .25, a = 3 - i, o = (e, t = .05) => Math.round(e / t) * t, s = o(t || 1), c = (e) => (e - i) / a * 100, u = (e) => o(i + e * a), { handlers: f } = J({
708
708
  ref: r,
709
709
  onChange: (t) => e.setPlaybackRate(u(t))
710
710
  }), p = (t) => {
@@ -776,7 +776,7 @@ position: relative;
776
776
 
777
777
  border-radius: 8px;
778
778
  background-color: rgba(28,28,28,0.95);
779
- ${z.bMedium.regular}
779
+ ${V.bMedium.regular}
780
780
 
781
781
  z-index: 4;
782
782
 
@@ -819,7 +819,7 @@ position: relative;
819
819
 
820
820
  .secondary {
821
821
  color: #eee;
822
- ${z.bSmall.regular}
822
+ ${V.bSmall.regular}
823
823
  }
824
824
  }
825
825
  }
@@ -886,72 +886,116 @@ position: relative;
886
886
  .description {
887
887
  word-break: break-word;
888
888
  }
889
+
890
+ /* Dimmed and inert, but still listed. A source that names a track it cannot serve right now is
891
+ saying the track exists, and dropping the row would read as the source having nothing. */
892
+ .unavailable {
893
+ opacity: 0.5;
894
+ cursor: default;
895
+ :hover {
896
+ background-color: transparent;
897
+ }
898
+ }
899
+
900
+ .failed {
901
+ border-bottom: 1px solid #4D4D4E;
902
+ color: #f66;
903
+ ${V.bSmall.regular}
904
+ }
889
905
  }
890
- `, Y = ({ title: e, tracks: t, selected: n, onSelect: r, onBack: i, allowDisable: a }) => /* @__PURE__ */ S("div", {
891
- className: "popover track-list",
892
- children: [
893
- /* @__PURE__ */ S("div", {
894
- className: "back",
895
- onClick: i,
896
- children: [/* @__PURE__ */ x(C, {}), /* @__PURE__ */ x("span", { children: e })]
897
- }),
898
- a ? /* @__PURE__ */ S("div", {
899
- onClick: () => r(void 0),
900
- children: [/* @__PURE__ */ x("span", { children: "Disable" }), /* @__PURE__ */ x("span", { children: n === void 0 ? "✓" : "" })]
901
- }) : null,
902
- t.map(({ id: e, label: t }) => /* @__PURE__ */ S("div", {
903
- onClick: () => r(e),
904
- className: "description",
905
- children: [/* @__PURE__ */ x("span", { children: t }), /* @__PURE__ */ x("span", { children: n === e ? "✓" : "" })]
906
- }, e))
907
- ]
908
- }), xe = () => {
909
- let e = P(), t = P((e) => e.playbackRate), n = P((e) => e.subtitleTracks), r = P((e) => e.selectedSubtitleTrack), i = P((e) => e.selectSubtitleTrack), a = P((e) => e.audioTracks), o = P((e) => e.selectedAudioTrack), s = P((e) => e.selectAudioTrack), [c, u] = f(!1), [p, m] = f(0), h = d(null), g = () => {
910
- u(!c), m(0);
911
- }, _ = (t) => {
912
- e.setPlaybackRate(t), m(0);
906
+ `, Y = ({ title: e, tracks: t, selected: n, onSelect: r, onBack: i, offLabel: a, pending: o, failed: s }) => {
907
+ let c = o !== void 0, l = (e, i, a) => {
908
+ let s = c && o === e, l = c || t.find((t) => t.id === e)?.disabled;
909
+ return /* @__PURE__ */ S("div", {
910
+ onClick: () => {
911
+ l || r(e ?? void 0);
912
+ },
913
+ className: [a, l ? "unavailable" : null].filter(Boolean).join(" ") || void 0,
914
+ children: [/* @__PURE__ */ x("span", { children: i }), /* @__PURE__ */ x("span", { children: s ? "…" : (n ?? null) === e ? "✓" : "" })]
915
+ }, e ?? "__off__");
916
+ };
917
+ return /* @__PURE__ */ S("div", {
918
+ className: "popover track-list",
919
+ children: [
920
+ /* @__PURE__ */ S("div", {
921
+ className: "back",
922
+ onClick: i,
923
+ children: [/* @__PURE__ */ x(C, {}), /* @__PURE__ */ x("span", { children: e })]
924
+ }),
925
+ s ? /* @__PURE__ */ x("div", {
926
+ className: "no-hover failed",
927
+ children: "Could not switch. Try again."
928
+ }) : null,
929
+ a ? l(null, a) : null,
930
+ t.map(({ id: e, label: t }) => l(e, t, "description"))
931
+ ]
932
+ });
933
+ }, xe = () => {
934
+ let e = F(), t = F((e) => e.playbackRate), n = F((e) => e.subtitleTracks), r = F((e) => e.selectedSubtitleTrack), i = F((e) => e.selectSubtitleTrack), a = F((e) => e.audioTracks), o = F((e) => e.selectedAudioTrack), s = F((e) => e.selectAudioTrack), c = F((e) => e.subtitleOffLabel), [u, p] = f(!1), [m, h] = f(0), g = d(null), [_, v] = f(void 0), [y, b] = f(!1), T = () => {
935
+ p(!u), h(0), b(!1);
936
+ }, E = () => {
937
+ p(!1), h(0);
938
+ }, D = (t) => {
939
+ e.setPlaybackRate(t), h(0);
913
940
  };
914
941
  l(() => {
915
942
  let e = (e) => {
916
- c && h.current && !h.current.contains(e.target) && g();
943
+ u && g.current && !g.current.contains(e.target) && T();
917
944
  };
918
945
  return document.addEventListener("pointerdown", e), () => document.removeEventListener("pointerdown", e);
919
- }, [c]);
920
- let v = (e) => {
921
- i(e), g();
922
- }, y = (e) => {
923
- e !== void 0 && s(e), g();
924
- }, b = (e) => () => {
925
- m(e);
946
+ }, [u]);
947
+ let O = (e, t) => {
948
+ if (_ !== void 0) return;
949
+ b(!1);
950
+ let n;
951
+ try {
952
+ n = t();
953
+ } catch (e) {
954
+ console.warn("[media-player] track selection failed:", e), b(!0);
955
+ return;
956
+ }
957
+ if (!(n instanceof Promise)) {
958
+ E();
959
+ return;
960
+ }
961
+ v(e), n.then(() => {
962
+ v(void 0), E();
963
+ }, (e) => {
964
+ console.warn("[media-player] track selection failed:", e), v(void 0), b(!0);
965
+ });
966
+ }, k = (e) => O(e ?? null, () => i(e)), j = (e) => {
967
+ e !== void 0 && O(e, () => s(e));
968
+ }, M = (e) => () => {
969
+ h(e);
926
970
  };
927
971
  return /* @__PURE__ */ S("div", {
928
972
  css: be,
929
- ref: h,
973
+ ref: g,
930
974
  children: [
931
- /* @__PURE__ */ x(K, {
975
+ /* @__PURE__ */ x(q, {
932
976
  id: "settings",
933
- disabled: c,
977
+ disabled: u,
934
978
  text: /* @__PURE__ */ x("button", {
935
979
  className: "settings",
936
980
  type: "button",
937
- onClick: g,
981
+ onClick: T,
938
982
  children: /* @__PURE__ */ x(A, {})
939
983
  }),
940
984
  toolTipText: /* @__PURE__ */ x("span", { children: "Settings" })
941
985
  }),
942
- c && p === 0 && /* @__PURE__ */ S("div", {
986
+ u && m === 0 && /* @__PURE__ */ S("div", {
943
987
  className: "popover menu",
944
988
  children: [
945
989
  a.length > 1 ? /* @__PURE__ */ S("div", {
946
- onClick: b(3),
990
+ onClick: M(3),
947
991
  children: [/* @__PURE__ */ x("div", { children: "Audio" }), /* @__PURE__ */ x("div", { children: /* @__PURE__ */ x(w, {}) })]
948
992
  }) : null,
949
993
  n.length > 0 ? /* @__PURE__ */ S("div", {
950
- onClick: b(2),
994
+ onClick: M(2),
951
995
  children: [/* @__PURE__ */ x("div", { children: "Subtitles" }), /* @__PURE__ */ x("div", { children: /* @__PURE__ */ x(w, {}) })]
952
996
  }) : null,
953
997
  /* @__PURE__ */ S("div", {
954
- onClick: b(1),
998
+ onClick: M(1),
955
999
  children: [/* @__PURE__ */ x("div", { children: "Playback speed" }), /* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("span", {
956
1000
  className: "secondary",
957
1001
  children: t === 1 ? "(Default)" : t.toFixed(2)
@@ -959,12 +1003,12 @@ position: relative;
959
1003
  })
960
1004
  ]
961
1005
  }),
962
- c && p === 1 && /* @__PURE__ */ S("div", {
1006
+ u && m === 1 && /* @__PURE__ */ S("div", {
963
1007
  className: "popover playback-rate",
964
1008
  children: [
965
1009
  /* @__PURE__ */ S("div", {
966
1010
  className: "back",
967
- onClick: b(0),
1011
+ onClick: M(0),
968
1012
  children: [/* @__PURE__ */ x(C, {}), /* @__PURE__ */ x("span", { children: "Playback speed" })]
969
1013
  }),
970
1014
  /* @__PURE__ */ x("div", {
@@ -975,39 +1019,43 @@ position: relative;
975
1019
  className: "options no-hover",
976
1020
  children: [
977
1021
  /* @__PURE__ */ x("div", {
978
- onClick: () => _(.5),
1022
+ onClick: () => D(.5),
979
1023
  children: "0.5x"
980
1024
  }),
981
1025
  /* @__PURE__ */ x("div", {
982
- onClick: () => _(1),
1026
+ onClick: () => D(1),
983
1027
  children: "1x"
984
1028
  }),
985
1029
  /* @__PURE__ */ x("div", {
986
- onClick: () => _(1.5),
1030
+ onClick: () => D(1.5),
987
1031
  children: "1.5x"
988
1032
  }),
989
1033
  /* @__PURE__ */ x("div", {
990
- onClick: () => _(2),
1034
+ onClick: () => D(2),
991
1035
  children: "2x"
992
1036
  })
993
1037
  ]
994
1038
  })
995
1039
  ]
996
1040
  }),
997
- c && p === 2 && /* @__PURE__ */ x(Y, {
1041
+ u && m === 2 && /* @__PURE__ */ x(Y, {
998
1042
  title: "Subtitles",
999
1043
  tracks: n,
1000
1044
  selected: r,
1001
- onSelect: v,
1002
- onBack: b(0),
1003
- allowDisable: !0
1045
+ onSelect: k,
1046
+ onBack: M(0),
1047
+ offLabel: c ?? "Disable",
1048
+ pending: _,
1049
+ failed: y
1004
1050
  }),
1005
- c && p === 3 && /* @__PURE__ */ x(Y, {
1051
+ u && m === 3 && /* @__PURE__ */ x(Y, {
1006
1052
  title: "Audio",
1007
1053
  tracks: a,
1008
1054
  selected: o,
1009
- onSelect: y,
1010
- onBack: b(0)
1055
+ onSelect: j,
1056
+ onBack: M(0),
1057
+ pending: _,
1058
+ failed: y
1011
1059
  })
1012
1060
  ]
1013
1061
  });
@@ -1061,7 +1109,7 @@ position: relative;
1061
1109
  height: 10px;
1062
1110
  }
1063
1111
  `, we = ({ value: e, onChange: t }) => {
1064
- let n = P((e) => e.volume), r = P((e) => e.muted), i = d(null), a = d(null), { handlers: o } = q({
1112
+ let n = F((e) => e.volume), r = F((e) => e.muted), i = d(null), a = d(null), { handlers: o } = J({
1065
1113
  ref: a,
1066
1114
  onChange: t
1067
1115
  }), s = (n) => {
@@ -1075,8 +1123,8 @@ position: relative;
1075
1123
  e && e.removeEventListener("wheel", s);
1076
1124
  };
1077
1125
  }, [e]);
1078
- let c = e * 100, f = u(() => Math.round(r ? 0 : W(n) * 100), [n, r]);
1079
- return /* @__PURE__ */ x(K, {
1126
+ let c = e * 100, f = u(() => Math.round(r ? 0 : G(n) * 100), [n, r]);
1127
+ return /* @__PURE__ */ x(q, {
1080
1128
  id: "volume-slider",
1081
1129
  text: /* @__PURE__ */ x("div", {
1082
1130
  ref: i,
@@ -1124,7 +1172,7 @@ position: relative;
1124
1172
  border-radius: 4px;
1125
1173
 
1126
1174
  .volume-value {
1127
- ${z.bMedium.regular};
1175
+ ${V.bMedium.regular};
1128
1176
  color: #ffffff;
1129
1177
  }
1130
1178
  }
@@ -1163,17 +1211,17 @@ position: relative;
1163
1211
  }
1164
1212
 
1165
1213
  `, Ee = ({ ref: e }) => {
1166
- let t = P(), n = P((e) => e.volume), r = P((e) => e.muted), i = u(() => W(n), [n]);
1214
+ let t = F(), n = F((e) => e.volume), r = F((e) => e.muted), i = u(() => G(n), [n]);
1167
1215
  return /* @__PURE__ */ S("div", {
1168
1216
  css: Te,
1169
- children: [/* @__PURE__ */ x(K, {
1217
+ children: [/* @__PURE__ */ x(q, {
1170
1218
  id: "sound",
1171
1219
  text: /* @__PURE__ */ x("button", {
1172
1220
  className: "sound",
1173
1221
  type: "button",
1174
1222
  onClick: () => t.toggleMuted(),
1175
1223
  ref: e,
1176
- children: x(r || n === 0 ? te : n <= .5 ? j : ee, {
1224
+ children: x(r || n === 0 ? ee : n <= .5 ? j : M, {
1177
1225
  size: 18,
1178
1226
  color: "#fff"
1179
1227
  })
@@ -1186,7 +1234,7 @@ position: relative;
1186
1234
  children: /* @__PURE__ */ x(we, {
1187
1235
  value: r ? 0 : i,
1188
1236
  onChange: (e) => {
1189
- t.setVolume(U(e));
1237
+ t.setVolume(W(e));
1190
1238
  }
1191
1239
  })
1192
1240
  })
@@ -1293,7 +1341,7 @@ position: relative;
1293
1341
 
1294
1342
  .left {
1295
1343
  .time {
1296
- ${z.bMedium.regular}
1344
+ ${V.bMedium.regular}
1297
1345
  text-shadow: 0 0 4px rgba(0, 0, 0, 1);
1298
1346
  }
1299
1347
  }
@@ -1315,9 +1363,9 @@ position: relative;
1315
1363
  }
1316
1364
  }
1317
1365
  `, Oe = () => {
1318
- let e = P(), t = P((e) => e.paused), n = P((e) => e.currentTime), r = P((e) => e.duration), i = P((e) => e.fullscreen), a = P((e) => e.hideUI), o = P((e) => e.togglePictureInPicture), [s, u] = f(null), d = r > 0 && n === r, p = c(({ direction: t, stepSize: n }) => {
1319
- let r = W(e.volume), i = t === "up" ? n : -n, a = Math.max(0, Math.min(1, r + i)), o = e.muted;
1320
- e.setVolume(U(a)), o && !e.muted && e.toggleMuted();
1366
+ let e = F(), t = F((e) => e.paused), n = F((e) => e.currentTime), r = F((e) => e.duration), i = F((e) => e.fullscreen), a = F((e) => e.hideUI), o = F((e) => e.togglePictureInPicture), [s, u] = f(null), d = r > 0 && n === r, p = c(({ direction: t, stepSize: n }) => {
1367
+ let r = G(e.volume), i = t === "up" ? n : -n, a = Math.max(0, Math.min(1, r + i)), o = e.muted;
1368
+ e.setVolume(W(a)), o && !e.muted && e.toggleMuted();
1321
1369
  }, [e]);
1322
1370
  l(() => {
1323
1371
  let t = (t) => {
@@ -1361,12 +1409,12 @@ position: relative;
1361
1409
  opacity: "0",
1362
1410
  pointerEvents: "none"
1363
1411
  } : {} },
1364
- children: [/* @__PURE__ */ x(J, {}), /* @__PURE__ */ S("div", {
1412
+ children: [/* @__PURE__ */ x(ge, {}), /* @__PURE__ */ S("div", {
1365
1413
  className: "actions",
1366
1414
  children: [/* @__PURE__ */ S("div", {
1367
1415
  className: "left",
1368
1416
  children: [
1369
- /* @__PURE__ */ x(K, {
1417
+ /* @__PURE__ */ x(q, {
1370
1418
  id: "play",
1371
1419
  tooltipPlace: "top-start",
1372
1420
  text: /* @__PURE__ */ x("button", {
@@ -1380,14 +1428,14 @@ position: relative;
1380
1428
  /* @__PURE__ */ x(Ee, { ref: u }),
1381
1429
  /* @__PURE__ */ x("div", {
1382
1430
  className: "time",
1383
- children: me(n, r)
1431
+ children: pe(n, r)
1384
1432
  })
1385
1433
  ]
1386
1434
  }), /* @__PURE__ */ S("div", {
1387
1435
  className: "right",
1388
1436
  children: [
1389
1437
  /* @__PURE__ */ x(xe, {}),
1390
- o ? /* @__PURE__ */ x(K, {
1438
+ o ? /* @__PURE__ */ x(q, {
1391
1439
  id: "picture-in-picture",
1392
1440
  text: /* @__PURE__ */ x("button", {
1393
1441
  className: "picture-in-picture",
@@ -1397,7 +1445,7 @@ position: relative;
1397
1445
  }),
1398
1446
  toolTipText: /* @__PURE__ */ x("span", { children: "Picture in picture" })
1399
1447
  }) : null,
1400
- /* @__PURE__ */ x(K, {
1448
+ /* @__PURE__ */ x(q, {
1401
1449
  id: "full-screen",
1402
1450
  tooltipPlace: "top-end",
1403
1451
  text: /* @__PURE__ */ x("button", {
@@ -1461,66 +1509,66 @@ position: relative;
1461
1509
  key: r,
1462
1510
  item: e
1463
1511
  }];
1464
- }), Me = ({ ref: e, onVideoRef: t, onCanvasRef: n, overlay: r, children: i }) => {
1465
- let a = P(), o = P((e) => e.hideUI), s = P((e) => e.setHideUI), c = d(void 0), u = d("mouse");
1466
- l(() => () => clearTimeout(c.current), []);
1467
- let f = () => {
1468
- s(!1), clearTimeout(c.current), c.current = setTimeout(() => s(!0), ke);
1512
+ }), Me = ({ ref: e, onVideoRef: t, onCanvasRef: n, overlay: r, controls: i, children: a }) => {
1513
+ let o = F(), s = F((e) => e.hideUI), c = F((e) => e.setHideUI), u = d(void 0), f = d("mouse");
1514
+ l(() => () => clearTimeout(u.current), []);
1515
+ let p = () => {
1516
+ c(!1), clearTimeout(u.current), u.current = setTimeout(() => c(!0), ke);
1469
1517
  };
1470
1518
  return /* @__PURE__ */ S("div", {
1471
1519
  css: je,
1472
1520
  ref: e,
1473
1521
  onPointerMove: (e) => {
1474
- u.current = e.pointerType, e.pointerType === "mouse" && f();
1522
+ f.current = e.pointerType, e.pointerType === "mouse" && p();
1475
1523
  },
1476
1524
  onPointerDown: (e) => {
1477
- u.current = e.pointerType;
1525
+ f.current = e.pointerType;
1478
1526
  },
1479
1527
  onMouseOut: (e) => {
1480
1528
  let t = e.relatedTarget;
1481
1529
  if (t && e.currentTarget.contains(t)) return;
1482
1530
  let { left: n, right: r, top: i, bottom: a } = e.currentTarget.getBoundingClientRect();
1483
- e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(c.current), s(!0));
1531
+ e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(u.current), c(!0));
1484
1532
  },
1485
- className: o ? "hide" : "",
1533
+ className: s ? "hide" : "",
1486
1534
  children: [
1487
- /* @__PURE__ */ x(B, { onCanvasRef: n }),
1535
+ /* @__PURE__ */ x(fe, { onCanvasRef: n }),
1488
1536
  Q(r).map(({ key: e, item: t }) => /* @__PURE__ */ x("div", {
1489
1537
  css: Ae,
1490
- style: { ...o ? {
1538
+ style: { ...s ? {
1491
1539
  opacity: "0",
1492
1540
  visibility: "hidden",
1493
1541
  pointerEvents: "none"
1494
1542
  } : {} },
1495
1543
  children: t
1496
1544
  }, e)),
1497
- /* @__PURE__ */ x(Oe, {}),
1545
+ i === !1 ? null : /* @__PURE__ */ x(Oe, {}),
1498
1546
  /* @__PURE__ */ S("div", {
1499
1547
  className: "video",
1500
1548
  onClick: () => {
1501
- if (u.current === "mouse") {
1502
- a.togglePaused();
1549
+ if (f.current === "mouse") {
1550
+ o.togglePaused();
1503
1551
  return;
1504
1552
  }
1505
- o ? f() : (clearTimeout(c.current), s(!0));
1553
+ s ? p() : (clearTimeout(u.current), c(!0));
1506
1554
  },
1507
1555
  children: [t ? /* @__PURE__ */ x("video", {
1508
1556
  ref: t,
1509
1557
  playsInline: !0
1510
- }) : null, i]
1558
+ }) : null, a]
1511
1559
  })
1512
1560
  ]
1513
1561
  });
1514
1562
  }, Ne = ({ options: e, children: t }) => {
1515
1563
  let n = "media" in e ? e : null, r = n ? null : e, { title: i } = e, { size: a, downloadedRanges: o, publicPath: s, libavWorkerUrl: c, read: u, thumbnailRead: d, thumbnailsEnabled: p } = r ?? {}, m = _(), h = g(), [v, y] = f(null), [b, S] = f(null), C = n?.media ?? v;
1516
- l(() => (m?.(C), () => m?.(null)), [C, m]), oe(v, b, r);
1517
- let w = L({
1564
+ l(() => (m?.(C), () => m?.(null)), [C, m]), ie(v, b, r);
1565
+ let w = z({
1518
1566
  publicPath: s,
1519
1567
  workerUrl: c,
1520
1568
  length: p === !1 ? void 0 : a,
1521
1569
  read: d ?? u,
1522
1570
  downloadedRanges: o
1523
- }), T = n?.thumbnails?.all ?? w, E = R(v, b), D = P((e) => e.setSourceState);
1571
+ }), T = n?.thumbnails?.all ?? w, E = B(v, b), D = F((e) => e.setSourceState);
1524
1572
  l(() => {
1525
1573
  D({
1526
1574
  title: i,
@@ -1549,18 +1597,21 @@ position: relative;
1549
1597
  let k = n?.subtitles?.selection, A = n?.audioTracks?.selection;
1550
1598
  return l(() => {
1551
1599
  k && D({
1552
- subtitleTracks: k.options.map(({ id: e, label: t }) => ({
1600
+ subtitleTracks: k.options.map(({ id: e, label: t, disabled: n }) => ({
1553
1601
  id: e,
1554
- label: t
1602
+ label: t,
1603
+ disabled: n
1555
1604
  })),
1556
1605
  selectedSubtitleTrack: k.selectedId ?? void 0,
1557
- selectSubtitleTrack: (e) => k.select(e == null ? null : String(e))
1606
+ selectSubtitleTrack: (e) => k.select(e == null ? null : String(e)),
1607
+ subtitleOffLabel: k.offLabel
1558
1608
  });
1559
1609
  }, [D, k]), l(() => {
1560
1610
  A && D({
1561
- audioTracks: A.options.map(({ id: e, label: t }) => ({
1611
+ audioTracks: A.options.map(({ id: e, label: t, disabled: n }) => ({
1562
1612
  id: e,
1563
- label: t
1613
+ label: t,
1614
+ disabled: n
1564
1615
  })),
1565
1616
  selectedAudioTrack: A.selectedId ?? void 0,
1566
1617
  selectAudioTrack: (e) => A.select(String(e))
@@ -1570,6 +1621,7 @@ position: relative;
1570
1621
  onVideoRef: n ? void 0 : y,
1571
1622
  onCanvasRef: S,
1572
1623
  overlay: e.overlay,
1624
+ controls: e.controls,
1573
1625
  children: t
1574
1626
  });
1575
1627
  }, Pe = p`
@@ -1605,7 +1657,7 @@ position: relative;
1605
1657
  overflow: hidden;
1606
1658
  `, $ = (e) => /* @__PURE__ */ x("div", {
1607
1659
  css: Pe,
1608
- children: /* @__PURE__ */ x(N.Provider, { children: /* @__PURE__ */ x(Ne, {
1660
+ children: /* @__PURE__ */ x(P.Provider, { children: /* @__PURE__ */ x(Ne, {
1609
1661
  options: e,
1610
1662
  children: e.children
1611
1663
  }) })
@@ -1648,4 +1700,4 @@ position: relative;
1648
1700
  throw Error(`Unknown source type: ${e}`);
1649
1701
  };
1650
1702
  //#endregion
1651
- export { $ as MediaPlayer, $ as default, N as Player, n as SUBTITLES_OFF, i as createPictureInPicture, r as createSubtitleRenderer, t as createThumbnailGenerator, Be as inputToRemuxerInput, Ie as isDelegatedTracks, Fe as isExternalThumbnails, M as sourceFeature, e as startPlayback, R as usePictureInPicture, P as usePlayer, L as useSeekThumbnails };
1703
+ export { $ as MediaPlayer, $ as default, P as Player, n as SUBTITLES_OFF, i as createPictureInPicture, r as createSubtitleRenderer, t as createThumbnailGenerator, Be as inputToRemuxerInput, Ie as isDelegatedTracks, Fe as isExternalThumbnails, N as sourceFeature, e as startPlayback, B as usePictureInPicture, F as usePlayer, z as useSeekThumbnails };
@@ -6,7 +6,9 @@ export type ChromeProps = {
6
6
  onCanvasRef: (element: HTMLCanvasElement | null) => void;
7
7
  /** The app's own content, over the video and outside the click-to-pause region, unlike `children`. */
8
8
  overlay?: ReactNode;
9
+ /** False draws no control bar at all, leaving the picture, the title and the overlay. */
10
+ controls?: boolean;
9
11
  children?: ReactNode;
10
12
  };
11
- export declare const Chrome: ({ ref, onVideoRef, onCanvasRef, overlay, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
13
+ export declare const Chrome: ({ ref, onVideoRef, onCanvasRef, overlay, controls, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
12
14
  export default Chrome;
@@ -7,10 +7,11 @@ export declare const Player: import("@videojs/react").CreatePlayerResult<import(
7
7
  thumbnailAt?: (time: number) => import("..").ThumbnailImage | undefined;
8
8
  subtitleTracks: import("./source-feature").TrackChoice[];
9
9
  selectedSubtitleTrack: string | number | undefined;
10
- selectSubtitleTrack: (id: string | number | undefined) => void;
10
+ selectSubtitleTrack: (id: string | number | undefined) => void | Promise<void>;
11
+ subtitleOffLabel?: string;
11
12
  audioTracks: import("./source-feature").TrackChoice[];
12
13
  selectedAudioTrack: string | number | undefined;
13
- selectAudioTrack: (id: string | number) => void;
14
+ selectAudioTrack: (id: string | number) => void | Promise<void>;
14
15
  hideUI: boolean;
15
16
  setHideUI: (hide: boolean) => void;
16
17
  togglePictureInPicture: (() => void) | null;
@@ -10,6 +10,13 @@ import type { MediaIndex, ThumbnailImage } from '../engine';
10
10
  export type TrackChoice = {
11
11
  id: string | number;
12
12
  label: string;
13
+ /**
14
+ * Offered but not selectable, so the menu shows it and refuses the click.
15
+ *
16
+ * Hiding it instead would be worse: a source that lists a dub it cannot currently serve is telling
17
+ * the viewer the dub exists, and a menu that silently omits it looks like the source has nothing.
18
+ */
19
+ disabled?: boolean;
13
20
  };
14
21
  /**
15
22
  * A byte span of the file the consumer has in hand, mapped onto the timeline through the keyframe
@@ -40,13 +47,24 @@ export type SourceState = {
40
47
  * Falls back to scanning `thumbnails` when absent, which is what the engine's generator fills.
41
48
  */
42
49
  thumbnailAt?: (time: number) => ThumbnailImage | undefined;
50
+ /**
51
+ * Both selectors may answer with a promise, and the menu waits on it.
52
+ *
53
+ * The engine switches a track by pointing the pipeline at another stream, which is immediate and
54
+ * cannot fail, so locally these return nothing. A source that owns its own player is the opposite
55
+ * case: the switch is a round trip through somebody else's UI and takes seconds, and it can lose.
56
+ * A menu that closed on the click would report a selection that has not happened, and a rejection
57
+ * with nothing awaiting it is an unhandled rejection rather than an error the viewer ever sees.
58
+ */
43
59
  subtitleTracks: TrackChoice[];
44
60
  /** undefined means subtitles are off. */
45
61
  selectedSubtitleTrack: string | number | undefined;
46
- selectSubtitleTrack: (id: string | number | undefined) => void;
62
+ selectSubtitleTrack: (id: string | number | undefined) => void | Promise<void>;
63
+ /** What the row that turns subtitles off is called, when the source would rather name it itself. */
64
+ subtitleOffLabel?: string;
47
65
  audioTracks: TrackChoice[];
48
66
  selectedAudioTrack: string | number | undefined;
49
- selectAudioTrack: (id: string | number) => void;
67
+ selectAudioTrack: (id: string | number) => void | Promise<void>;
50
68
  /** Chrome auto-hide. True means the controls, title and cursor are hidden. */
51
69
  hideUI: boolean;
52
70
  setHideUI: (hide: boolean) => void;
@@ -87,13 +105,24 @@ export declare const sourceFeature: import("@videojs/react").PlayerFeature<{
87
105
  * Falls back to scanning `thumbnails` when absent, which is what the engine's generator fills.
88
106
  */
89
107
  thumbnailAt?: (time: number) => ThumbnailImage | undefined;
108
+ /**
109
+ * Both selectors may answer with a promise, and the menu waits on it.
110
+ *
111
+ * The engine switches a track by pointing the pipeline at another stream, which is immediate and
112
+ * cannot fail, so locally these return nothing. A source that owns its own player is the opposite
113
+ * case: the switch is a round trip through somebody else's UI and takes seconds, and it can lose.
114
+ * A menu that closed on the click would report a selection that has not happened, and a rejection
115
+ * with nothing awaiting it is an unhandled rejection rather than an error the viewer ever sees.
116
+ */
90
117
  subtitleTracks: TrackChoice[];
91
118
  /** undefined means subtitles are off. */
92
119
  selectedSubtitleTrack: string | number | undefined;
93
- selectSubtitleTrack: (id: string | number | undefined) => void;
120
+ selectSubtitleTrack: (id: string | number | undefined) => void | Promise<void>;
121
+ /** What the row that turns subtitles off is called, when the source would rather name it itself. */
122
+ subtitleOffLabel?: string;
94
123
  audioTracks: TrackChoice[];
95
124
  selectedAudioTrack: string | number | undefined;
96
- selectAudioTrack: (id: string | number) => void;
125
+ selectAudioTrack: (id: string | number) => void | Promise<void>;
97
126
  /** Chrome auto-hide. True means the controls, title and cursor are hidden. */
98
127
  hideUI: boolean;
99
128
  setHideUI: (hide: boolean) => void;
@@ -14,8 +14,28 @@ export type MediaPlayerSource = {
14
14
  };
15
15
  /** Shared by both arms: nothing here depends on who owns the media. */
16
16
  type CommonOptions = {
17
+ /**
18
+ * Drawn across the top of the picture, in a layer of its own.
19
+ *
20
+ * That layer cannot see an `overlay` item, so an app that also places something along the top has
21
+ * two elements competing for one band of screen: on a narrow viewport the title ellipsizes against
22
+ * the FULL width and then runs underneath whatever is painted over it. An app in that position
23
+ * should leave this unset and draw the title as part of its own overlay row, where the two can
24
+ * share a flex line and the filename can be the one that gives way.
25
+ */
17
26
  title?: string;
18
27
  autoplay?: boolean;
28
+ /**
29
+ * Draw the control bar. Defaults to true.
30
+ *
31
+ * False leaves the picture, the title and the overlay and takes away only the chrome, for a host
32
+ * that has to put its own interactive UI over the media for a while. A source whose sign-in form
33
+ * lives inside its own document is the case this exists for: the form is the thing the viewer has
34
+ * to reach, and a control bar for a media that has not loaded yet sits on top of it and eats the
35
+ * clicks. Hiding it from the host side is not an option, since `setHideUI` is installed by the
36
+ * store's `attach` and does nothing at all until a media arrives.
37
+ */
38
+ controls?: boolean;
19
39
  /**
20
40
  * The app's own content over the video: a download readout, a badge, a logo, anything the player
21
41
  * itself has no opinion about.
@@ -85,7 +105,16 @@ export type MediaPlayerLocalOptions = CommonOptions & MediaPlayerSource & {
85
105
  * works: the key session belongs to whoever owns the element.
86
106
  */
87
107
  export type MediaPlayerRemoteOptions = CommonOptions & {
88
- media: PlayerMedia;
108
+ /**
109
+ * Null means the media has not been found yet: the chrome renders and attaches when it arrives.
110
+ *
111
+ * A source usually has to mount its own document before it can hand over an element, and that
112
+ * document is passed as `children`, so it can only exist once this component has rendered. The
113
+ * key is still REQUIRED even when the value is null, because its presence is what selects this
114
+ * arm: omitting it falls through to the local arm, which draws an idle `<video>` over whatever
115
+ * the children put there.
116
+ */
117
+ media: PlayerMedia | null;
89
118
  read?: never;
90
119
  size?: never;
91
120
  /** The source's own storyboard, since there are no bytes to generate previews from. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
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",
@@ -95,10 +95,12 @@ export type ChromeProps = {
95
95
  onCanvasRef: (element: HTMLCanvasElement | null) => void
96
96
  /** The app's own content, over the video and outside the click-to-pause region, unlike `children`. */
97
97
  overlay?: ReactNode
98
+ /** False draws no control bar at all, leaving the picture, the title and the overlay. */
99
+ controls?: boolean
98
100
  children?: ReactNode
99
101
  }
100
102
 
101
- export const Chrome = ({ ref, onVideoRef, onCanvasRef, overlay, children }: ChromeProps) => {
103
+ export const Chrome = ({ ref, onVideoRef, onCanvasRef, overlay, controls, children }: ChromeProps) => {
102
104
  const player = usePlayer()
103
105
  const hideUI = usePlayer((state) => state.hideUI)
104
106
  const setHideUI = usePlayer((state) => state.setHideUI)
@@ -170,7 +172,7 @@ export const Chrome = ({ ref, onVideoRef, onCanvasRef, overlay, children }: Chro
170
172
  {item}
171
173
  </div>
172
174
  ))}
173
- <ControlBar />
175
+ {controls === false ? null : <ControlBar />}
174
176
  <div className="video" onClick={onVideoClick}>
175
177
  {onVideoRef ? <video ref={onVideoRef} playsInline /> : null}
176
178
  {children}
@@ -158,6 +158,22 @@ position: relative;
158
158
  .description {
159
159
  word-break: break-word;
160
160
  }
161
+
162
+ /* Dimmed and inert, but still listed. A source that names a track it cannot serve right now is
163
+ saying the track exists, and dropping the row would read as the source having nothing. */
164
+ .unavailable {
165
+ opacity: 0.5;
166
+ cursor: default;
167
+ :hover {
168
+ background-color: transparent;
169
+ }
170
+ }
171
+
172
+ .failed {
173
+ border-bottom: 1px solid #4D4D4E;
174
+ color: #f66;
175
+ ${fonts.bSmall.regular}
176
+ }
161
177
  }
162
178
  `
163
179
 
@@ -168,46 +184,55 @@ enum PopoverContent {
168
184
  Audio
169
185
  }
170
186
 
171
- /** One track picker, shared by the subtitle and audio menus. */
187
+ /**
188
+ * One track picker, shared by the subtitle and audio menus.
189
+ *
190
+ * `pending` and `failed` describe the switch the viewer just asked for, not the menu: while a source
191
+ * is working, every row is inert and the one being switched to says so, because the selection has
192
+ * not moved yet and a tick next to it would be a lie.
193
+ */
172
194
  const TrackMenu = (
173
- { title, tracks, selected, onSelect, onBack, allowDisable }: {
195
+ { title, tracks, selected, onSelect, onBack, offLabel, pending, failed }: {
174
196
  title: string
175
197
  tracks: TrackChoice[]
176
198
  selected: string | number | undefined
177
199
  onSelect: (id: string | number | undefined) => void
178
200
  onBack: () => void
179
- allowDisable?: boolean
201
+ /** Absent means the menu offers no way off at all, which is what audio wants. */
202
+ offLabel?: string
203
+ /** The id currently being switched to, where `null` is the off row and undefined means idle. */
204
+ pending?: string | number | null
205
+ failed?: boolean
206
+ }
207
+ ) => {
208
+ const busy = pending !== undefined
209
+ const row = (id: string | number | null, label: string, className?: string) => {
210
+ const isPending = busy && pending === id
211
+ const disabled = busy || tracks.find((track) => track.id === id)?.disabled
212
+ return (
213
+ <div
214
+ key={id ?? '__off__'}
215
+ onClick={() => { if (!disabled) onSelect(id ?? undefined) }}
216
+ className={[className, disabled ? 'unavailable' : null].filter(Boolean).join(' ') || undefined}
217
+ >
218
+ <span>{label}</span>
219
+ <span>{isPending ? '…' : (selected ?? null) === id ? '✓' : ''}</span>
220
+ </div>
221
+ )
180
222
  }
181
- ) => (
182
- <div className='popover track-list'>
183
- <div className="back" onClick={onBack}>
184
- <ChevronLeft />
185
- <span>{title}</span>
223
+
224
+ return (
225
+ <div className='popover track-list'>
226
+ <div className="back" onClick={onBack}>
227
+ <ChevronLeft />
228
+ <span>{title}</span>
229
+ </div>
230
+ {failed ? <div className="no-hover failed">Could not switch. Try again.</div> : null}
231
+ {offLabel ? row(null, offLabel) : null}
232
+ {tracks.map(({ id, label }) => row(id, label, 'description'))}
186
233
  </div>
187
- {
188
- allowDisable
189
- ? (
190
- <div onClick={() => onSelect(undefined)}>
191
- <span>Disable</span>
192
- <span>{selected === undefined ? '✓' : ''}</span>
193
- </div>
194
- )
195
- : null
196
- }
197
- {
198
- tracks.map(({ id, label }) => (
199
- <div
200
- key={id}
201
- onClick={() => onSelect(id)}
202
- className="description"
203
- >
204
- <span>{label}</span>
205
- <span>{selected === id ? '✓' : ''}</span>
206
- </div>
207
- ))
208
- }
209
- </div>
210
- )
234
+ )
235
+ }
211
236
 
212
237
  export const SettingsAction = () => {
213
238
  const player = usePlayer()
@@ -219,13 +244,27 @@ export const SettingsAction = () => {
219
244
  const selectedAudioTrack = usePlayer((state) => state.selectedAudioTrack)
220
245
  const selectAudioTrack = usePlayer((state) => state.selectAudioTrack)
221
246
 
247
+ const subtitleOffLabel = usePlayer((state) => state.subtitleOffLabel)
248
+
222
249
  const [isOpenPopover, setIsOpenPopover] = useState(false)
223
250
  const [popoverContent, setPopoverContent] = useState(PopoverContent.Default)
224
251
  const settingsContainerRef = useRef<HTMLDivElement>(null)
252
+ // The id being switched to, or undefined when nothing is in flight. Null is the row that turns
253
+ // subtitles off, so this cannot be a plain id: undefined has to keep meaning idle.
254
+ const [pending, setPending] = useState<string | number | null | undefined>(undefined)
255
+ const [failed, setFailed] = useState(false)
225
256
 
226
257
  const togglePopover = () => {
227
258
  setIsOpenPopover(!isOpenPopover)
228
259
  setPopoverContent(PopoverContent.Default)
260
+ setFailed(false)
261
+ }
262
+
263
+ // Not `togglePopover`: this runs from a promise, where the captured `isOpenPopover` is whatever it
264
+ // was at click time, so a toggle could reopen a menu the viewer has already dismissed.
265
+ const closePopover = () => {
266
+ setIsOpenPopover(false)
267
+ setPopoverContent(PopoverContent.Default)
229
268
  }
230
269
 
231
270
  const setPlaybackRate = (rate: number) => {
@@ -248,15 +287,50 @@ export const SettingsAction = () => {
248
287
  return () => document.removeEventListener('pointerdown', handleClickOutside)
249
288
  }, [isOpenPopover])
250
289
 
251
- const chooseSubtitle = (id: string | number | undefined) => {
252
- selectSubtitleTrack(id)
253
- togglePopover()
290
+ /**
291
+ * Runs a track switch and decides when the menu may close.
292
+ *
293
+ * A selector that returns nothing switched synchronously, so the click closes the menu as it always
294
+ * has. One that returns a promise owns a player this one cannot reach, and the menu stays open and
295
+ * inert until it answers: closing first would show a selection that has not happened yet, and
296
+ * dropping the promise would turn a failed switch into an unhandled rejection nobody ever sees.
297
+ */
298
+ const runSelect = (id: string | number | null, select: () => void | Promise<void>) => {
299
+ if (pending !== undefined) return
300
+ setFailed(false)
301
+ let result: void | Promise<void>
302
+ try {
303
+ result = select()
304
+ } catch (err) {
305
+ console.warn('[media-player] track selection failed:', err)
306
+ setFailed(true)
307
+ return
308
+ }
309
+ if (!(result instanceof Promise)) {
310
+ closePopover()
311
+ return
312
+ }
313
+ setPending(id)
314
+ result.then(
315
+ () => {
316
+ setPending(undefined)
317
+ closePopover()
318
+ },
319
+ (err) => {
320
+ console.warn('[media-player] track selection failed:', err)
321
+ setPending(undefined)
322
+ setFailed(true)
323
+ },
324
+ )
254
325
  }
255
326
 
327
+ const chooseSubtitle = (id: string | number | undefined) =>
328
+ runSelect(id ?? null, () => selectSubtitleTrack(id))
329
+
330
+ // the audio menu offers no way off, so there is no undefined case to answer for
256
331
  const chooseAudio = (id: string | number | undefined) => {
257
- // the audio menu never offers "Disable"
258
- if (id !== undefined) selectAudioTrack(id)
259
- togglePopover()
332
+ if (id === undefined) return
333
+ runSelect(id, () => selectAudioTrack(id))
260
334
  }
261
335
 
262
336
  const changePopoverContent = (newPopoverContent: PopoverContent) => () => {
@@ -362,7 +436,9 @@ export const SettingsAction = () => {
362
436
  selected={selectedSubtitleTrack}
363
437
  onSelect={chooseSubtitle}
364
438
  onBack={changePopoverContent(PopoverContent.Default)}
365
- allowDisable
439
+ offLabel={subtitleOffLabel ?? 'Disable'}
440
+ pending={pending}
441
+ failed={failed}
366
442
  />
367
443
  )
368
444
  }
@@ -374,6 +450,8 @@ export const SettingsAction = () => {
374
450
  selected={selectedAudioTrack}
375
451
  onSelect={chooseAudio}
376
452
  onBack={changePopoverContent(PopoverContent.Default)}
453
+ pending={pending}
454
+ failed={failed}
377
455
  />
378
456
  )
379
457
  }
@@ -13,6 +13,13 @@ import { definePlayerFeature } from '@videojs/core/dom'
13
13
  export type TrackChoice = {
14
14
  id: string | number
15
15
  label: string
16
+ /**
17
+ * Offered but not selectable, so the menu shows it and refuses the click.
18
+ *
19
+ * Hiding it instead would be worse: a source that lists a dub it cannot currently serve is telling
20
+ * the viewer the dub exists, and a menu that silently omits it looks like the source has nothing.
21
+ */
22
+ disabled?: boolean
16
23
  }
17
24
 
18
25
  /**
@@ -47,14 +54,25 @@ export type SourceState = {
47
54
  */
48
55
  thumbnailAt?: (time: number) => ThumbnailImage | undefined
49
56
 
57
+ /**
58
+ * Both selectors may answer with a promise, and the menu waits on it.
59
+ *
60
+ * The engine switches a track by pointing the pipeline at another stream, which is immediate and
61
+ * cannot fail, so locally these return nothing. A source that owns its own player is the opposite
62
+ * case: the switch is a round trip through somebody else's UI and takes seconds, and it can lose.
63
+ * A menu that closed on the click would report a selection that has not happened, and a rejection
64
+ * with nothing awaiting it is an unhandled rejection rather than an error the viewer ever sees.
65
+ */
50
66
  subtitleTracks: TrackChoice[]
51
67
  /** undefined means subtitles are off. */
52
68
  selectedSubtitleTrack: string | number | undefined
53
- selectSubtitleTrack: (id: string | number | undefined) => void
69
+ selectSubtitleTrack: (id: string | number | undefined) => void | Promise<void>
70
+ /** What the row that turns subtitles off is called, when the source would rather name it itself. */
71
+ subtitleOffLabel?: string
54
72
 
55
73
  audioTracks: TrackChoice[]
56
74
  selectedAudioTrack: string | number | undefined
57
- selectAudioTrack: (id: string | number) => void
75
+ selectAudioTrack: (id: string | number) => void | Promise<void>
58
76
 
59
77
  /** Chrome auto-hide. True means the controls, title and cursor are hidden. */
60
78
  hideUI: boolean
@@ -23,9 +23,30 @@ export type MediaPlayerSource =
23
23
 
24
24
  /** Shared by both arms: nothing here depends on who owns the media. */
25
25
  type CommonOptions = {
26
+ /**
27
+ * Drawn across the top of the picture, in a layer of its own.
28
+ *
29
+ * That layer cannot see an `overlay` item, so an app that also places something along the top has
30
+ * two elements competing for one band of screen: on a narrow viewport the title ellipsizes against
31
+ * the FULL width and then runs underneath whatever is painted over it. An app in that position
32
+ * should leave this unset and draw the title as part of its own overlay row, where the two can
33
+ * share a flex line and the filename can be the one that gives way.
34
+ */
26
35
  title?: string
27
36
  autoplay?: boolean
28
37
 
38
+ /**
39
+ * Draw the control bar. Defaults to true.
40
+ *
41
+ * False leaves the picture, the title and the overlay and takes away only the chrome, for a host
42
+ * that has to put its own interactive UI over the media for a while. A source whose sign-in form
43
+ * lives inside its own document is the case this exists for: the form is the thing the viewer has
44
+ * to reach, and a control bar for a media that has not loaded yet sits on top of it and eats the
45
+ * clicks. Hiding it from the host side is not an option, since `setHideUI` is installed by the
46
+ * store's `attach` and does nothing at all until a media arrives.
47
+ */
48
+ controls?: boolean
49
+
29
50
  /**
30
51
  * The app's own content over the video: a download readout, a badge, a logo, anything the player
31
52
  * itself has no opinion about.
@@ -105,7 +126,16 @@ export type MediaPlayerLocalOptions =
105
126
  export type MediaPlayerRemoteOptions =
106
127
  & CommonOptions
107
128
  & {
108
- media: PlayerMedia
129
+ /**
130
+ * Null means the media has not been found yet: the chrome renders and attaches when it arrives.
131
+ *
132
+ * A source usually has to mount its own document before it can hand over an element, and that
133
+ * document is passed as `children`, so it can only exist once this component has rendered. The
134
+ * key is still REQUIRED even when the value is null, because its presence is what selects this
135
+ * arm: omitting it falls through to the local arm, which draws an idle `<video>` over whatever
136
+ * the children put there.
137
+ */
138
+ media: PlayerMedia | null
109
139
  read?: never
110
140
  size?: never
111
141
 
@@ -177,19 +207,23 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
177
207
  // the document, and it renders the result itself.
178
208
  const subtitles = remote?.subtitles?.selection
179
209
  const audio = remote?.audioTracks?.selection
210
+ // `select` is published as-is rather than wrapped, so its promise reaches the menu: the menu is the
211
+ // only place that can hold the popover open, keep the tick off an unfinished switch, and show a
212
+ // failure, and a promise nothing awaits is an unhandled rejection.
180
213
  useEffect(() => {
181
214
  if (!subtitles) return
182
215
  setSourceState({
183
- subtitleTracks: subtitles.options.map(({ id, label }) => ({ id, label })),
216
+ subtitleTracks: subtitles.options.map(({ id, label, disabled }) => ({ id, label, disabled })),
184
217
  selectedSubtitleTrack: subtitles.selectedId ?? undefined,
185
218
  selectSubtitleTrack: (id) => subtitles.select(id == null ? null : String(id)),
219
+ subtitleOffLabel: subtitles.offLabel,
186
220
  })
187
221
  }, [setSourceState, subtitles])
188
222
 
189
223
  useEffect(() => {
190
224
  if (!audio) return
191
225
  setSourceState({
192
- audioTracks: audio.options.map(({ id, label }) => ({ id, label })),
226
+ audioTracks: audio.options.map(({ id, label, disabled }) => ({ id, label, disabled })),
193
227
  selectedAudioTrack: audio.selectedId ?? undefined,
194
228
  selectAudioTrack: (id) => audio.select(String(id)),
195
229
  })
@@ -203,6 +237,7 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
203
237
  onVideoRef={remote ? undefined : setVideo}
204
238
  onCanvasRef={setCanvas}
205
239
  overlay={options.overlay}
240
+ controls={options.controls}
206
241
  >
207
242
  {children}
208
243
  </Chrome>