@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.
- package/dist/index.js +430 -393
- package/dist/react/components/chrome.d.ts +4 -2
- package/dist/react/components/overlay.d.ts +2 -6
- package/dist/react/player.d.ts +3 -2
- package/dist/react/source-feature.d.ts +33 -4
- package/dist/react/video-player.d.ts +49 -7
- package/package.json +1 -1
- package/src/lib/react/components/chrome.tsx +60 -5
- package/src/lib/react/components/overlay.tsx +22 -67
- package/src/lib/react/components/settings.tsx +117 -39
- package/src/lib/react/source-feature.ts +20 -2
- package/src/lib/react/video-player.tsx +57 -9
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
|
|
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>
|