@banou/media-player 0.8.3 → 0.8.5

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.
@@ -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>
@@ -222,6 +257,45 @@ const rootStyle = css`
222
257
  */
223
258
  --mp-unit: 10px;
224
259
 
260
+ /**
261
+ * The chrome's own typeface, for exactly the reason the unit above exists.
262
+ *
263
+ * \`font-family\` inherits, and this library declared it nowhere, so every label drew in whatever the
264
+ * embedding document happened to set, and in the UA's serif where it set nothing. Not hypothetical:
265
+ * stub mounts the player in a second document, \`embed.html\`, which sets no font and loads no face,
266
+ * and the entire settings menu came out in Times New Roman.
267
+ *
268
+ * Named faces only, no bundled webfont. The chrome is a handful of short labels, so a font request
269
+ * on the critical path of a document whose job is to paint video buys a round trip and a flash of
270
+ * unstyled text for nothing.
271
+ *
272
+ * Read through \`var()\` so a host with a brand face sets \`--mp-font-family\` on any ancestor and it
273
+ * inherits in. A bare \`font-family\` on the host can no longer reach the chrome: this declaration is
274
+ * on the player element itself and beats anything inherited.
275
+ */
276
+ font-family: var(
277
+ --mp-font-family,
278
+ system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
279
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji'
280
+ );
281
+
282
+ /**
283
+ * The chrome's default ink, same argument again.
284
+ *
285
+ * Most text here names its own colour, but the two time readouts do not: the elapsed/duration pair
286
+ * and the seekbar's hover time carry a font token and a black text-shadow and nothing else.
287
+ * Inherited from a host that declares none that resolves to \`canvastext\`, which is black text under
288
+ * a black halo on a dark gradient.
289
+ */
290
+ color: #fff;
291
+
292
+ /**
293
+ * The settings menu scrolls, and a UA that has not been told the surface is dark paints that
294
+ * scrollbar in its light theme: a white track down the side of a #1c1c1c menu. Declared here rather
295
+ * than asked of the host, so it reaches the menu and nothing outside the player.
296
+ */
297
+ color-scheme: dark;
298
+
225
299
  /**
226
300
  * The other half of owning the chrome's own scale.
227
301
  *