@banou/media-player 0.8.2 → 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.
@@ -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,17 +23,51 @@ 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
 
29
38
  /**
30
- * The app's own readout, drawn at the right of the top bar beside `title`, for whatever it has to
31
- * say over the video. It shares the title's gradient and fades with the rest of the chrome, so a
32
- * running counter does not sit over the picture once the controls have hidden themselves.
39
+ * Draw the control bar. Defaults to true.
33
40
  *
34
- * The slot itself takes no pointer events, so a click still reaches the video and toggles
35
- * playback; content that needs a pointer (a tooltip anchor, a button) sets `pointer-events: auto`
36
- * on itself. `children` land next to the media instead, below the chrome.
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
+
50
+ /**
51
+ * The app's own content over the video: a download readout, a badge, a logo, anything the player
52
+ * itself has no opinion about.
53
+ *
54
+ * Pass one node, or several, and EACH TOP-LEVEL ITEM gets its own layer covering the whole player.
55
+ * That layer is the coordinate space, so an item is placed with ordinary CSS against the picture:
56
+ *
57
+ * ```tsx
58
+ * <MediaPlayer overlay={[
59
+ * <div key="stats" css={css`position: absolute; top: 0; right: 0;`}>82 peers</div>,
60
+ * <div key="badge" css={css`position: absolute; inset: auto auto 0 0;`}>4K</div>,
61
+ * ]} />
62
+ * ```
63
+ *
64
+ * A fragment works the same way. Items never share a containing block, so one item's CSS cannot
65
+ * move another, and each keeps its own DOM as the list changes.
66
+ *
67
+ * Items fade with the rest of the chrome, so a running counter does not sit over the picture once
68
+ * the controls have hidden themselves. A layer takes no pointer events, so a click still reaches
69
+ * the video and toggles playback; content that needs a pointer (a tooltip anchor, a button) sets
70
+ * `pointer-events: auto` on itself. `children` land next to the media instead, below the chrome.
37
71
  */
38
72
  overlay?: ReactNode
39
73
 
@@ -92,7 +126,16 @@ export type MediaPlayerLocalOptions =
92
126
  export type MediaPlayerRemoteOptions =
93
127
  & CommonOptions
94
128
  & {
95
- 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
96
139
  read?: never
97
140
  size?: never
98
141
 
@@ -164,19 +207,23 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
164
207
  // the document, and it renders the result itself.
165
208
  const subtitles = remote?.subtitles?.selection
166
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.
167
213
  useEffect(() => {
168
214
  if (!subtitles) return
169
215
  setSourceState({
170
- subtitleTracks: subtitles.options.map(({ id, label }) => ({ id, label })),
216
+ subtitleTracks: subtitles.options.map(({ id, label, disabled }) => ({ id, label, disabled })),
171
217
  selectedSubtitleTrack: subtitles.selectedId ?? undefined,
172
218
  selectSubtitleTrack: (id) => subtitles.select(id == null ? null : String(id)),
219
+ subtitleOffLabel: subtitles.offLabel,
173
220
  })
174
221
  }, [setSourceState, subtitles])
175
222
 
176
223
  useEffect(() => {
177
224
  if (!audio) return
178
225
  setSourceState({
179
- audioTracks: audio.options.map(({ id, label }) => ({ id, label })),
226
+ audioTracks: audio.options.map(({ id, label, disabled }) => ({ id, label, disabled })),
180
227
  selectedAudioTrack: audio.selectedId ?? undefined,
181
228
  selectAudioTrack: (id) => audio.select(String(id)),
182
229
  })
@@ -190,6 +237,7 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
190
237
  onVideoRef={remote ? undefined : setVideo}
191
238
  onCanvasRef={setCanvas}
192
239
  overlay={options.overlay}
240
+ controls={options.controls}
193
241
  >
194
242
  {children}
195
243
  </Chrome>