@banou/media-player 0.7.0 → 0.8.1
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/README.md +3 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +335 -289
- package/dist/react/components/chrome.d.ts +5 -2
- package/dist/react/hooks/use-picture-in-picture.d.ts +1 -1
- package/dist/react/hooks/use-playback.d.ts +4 -2
- package/dist/react/hooks/use-thumbnails.d.ts +3 -2
- package/dist/react/media.d.ts +98 -0
- package/dist/react/player.d.ts +8 -7
- package/dist/react/source-feature.d.ts +55 -17
- package/dist/react/video-player.d.ts +45 -5
- package/dist/utils/track-label.d.ts +12 -0
- package/package.json +13 -4
- package/src/lib/index.tsx +16 -1
- package/src/lib/react/components/chrome.tsx +7 -3
- package/src/lib/react/components/control-bar.tsx +21 -17
- package/src/lib/react/components/overlay.tsx +14 -10
- package/src/lib/react/components/progress-bar.tsx +29 -26
- package/src/lib/react/components/settings.tsx +22 -29
- package/src/lib/react/components/sound.tsx +1 -1
- package/src/lib/react/components/tooltip-display.tsx +4 -4
- package/src/lib/react/hooks/use-picture-in-picture.ts +5 -1
- package/src/lib/react/hooks/use-playback.ts +34 -22
- package/src/lib/react/hooks/use-thumbnails.ts +4 -3
- package/src/lib/react/media.ts +124 -0
- package/src/lib/react/source-feature.ts +42 -16
- package/src/lib/react/video-player.tsx +118 -14
- package/src/lib/utils/fonts.ts +66 -66
- package/src/lib/utils/track-label.ts +15 -0
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MediaIndex, ThumbnailImage } from '../engine'
|
|
2
2
|
|
|
3
3
|
import { definePlayerFeature } from '@videojs/core/dom'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* One row of a track menu, already named.
|
|
7
|
+
*
|
|
8
|
+
* Labelled by whoever writes it rather than by the menu, because the two writers know different
|
|
9
|
+
* things: the engine has a language tag and a title to disambiguate between, while a source that
|
|
10
|
+
* owns its own player hands over a label it has already decided on. The id is opaque here for the
|
|
11
|
+
* same reason, a libav stream index one way and a site's own track id the other.
|
|
12
|
+
*/
|
|
13
|
+
export type TrackChoice = {
|
|
14
|
+
id: string | number
|
|
15
|
+
label: string
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
/**
|
|
6
19
|
* A byte span of the file the consumer has in hand, mapped onto the timeline through the keyframe
|
|
7
20
|
* index, because a file's download percentage is not its playback percentage.
|
|
@@ -28,22 +41,35 @@ export type SourceState = {
|
|
|
28
41
|
/** Keyframe index of the input, which turns a downloaded byte range into a time range. */
|
|
29
42
|
indexes: MediaIndex[]
|
|
30
43
|
thumbnails: ThumbnailImage[]
|
|
44
|
+
/**
|
|
45
|
+
* Answers for one time directly, when the source has a storyboard it can index but not enumerate.
|
|
46
|
+
* Falls back to scanning `thumbnails` when absent, which is what the engine's generator fills.
|
|
47
|
+
*/
|
|
48
|
+
thumbnailAt?: (time: number) => ThumbnailImage | undefined
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
subtitleTracks: TrackChoice[]
|
|
33
51
|
/** undefined means subtitles are off. */
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
selectedSubtitleTrack: string | number | undefined
|
|
53
|
+
selectSubtitleTrack: (id: string | number | undefined) => void
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
audioTracks: TrackChoice[]
|
|
56
|
+
selectedAudioTrack: string | number | undefined
|
|
57
|
+
selectAudioTrack: (id: string | number) => void
|
|
40
58
|
|
|
41
59
|
/** Chrome auto-hide. True means the controls, title and cursor are hidden. */
|
|
42
60
|
hideUI: boolean
|
|
43
61
|
setHideUI: (hide: boolean) => void
|
|
44
62
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Owned here, not by `pip`: that watches the media element, and the window holds a mirror.
|
|
65
|
+
*
|
|
66
|
+
* null means the control is not offered at all. Locally that is "no element yet". For a media the
|
|
67
|
+
* player does not own it means the source cannot do it, and the difference matters: the compositing
|
|
68
|
+
* this does needs a local element to draw, and even a source that forwards the request cannot
|
|
69
|
+
* report the resulting STATE back, since `document.pictureInPictureElement` is never a proxy. A
|
|
70
|
+
* button that toggles nothing and never lights up is worse than no button.
|
|
71
|
+
*/
|
|
72
|
+
togglePictureInPicture: (() => void) | null
|
|
47
73
|
|
|
48
74
|
/** Set when the pipeline fails. Cleared when it recovers. */
|
|
49
75
|
playbackError: unknown
|
|
@@ -63,15 +89,15 @@ export type SourceState = {
|
|
|
63
89
|
const initialState: SourceState = {
|
|
64
90
|
indexes: [],
|
|
65
91
|
thumbnails: [],
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
92
|
+
subtitleTracks: [],
|
|
93
|
+
selectedSubtitleTrack: undefined,
|
|
94
|
+
selectSubtitleTrack: () => {},
|
|
95
|
+
audioTracks: [],
|
|
96
|
+
selectedAudioTrack: undefined,
|
|
97
|
+
selectAudioTrack: () => {},
|
|
72
98
|
hideUI: false,
|
|
73
99
|
setHideUI: () => {},
|
|
74
|
-
togglePictureInPicture:
|
|
100
|
+
togglePictureInPicture: null,
|
|
75
101
|
playbackError: null,
|
|
76
102
|
ready: false,
|
|
77
103
|
setSourceState: () => {},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
import type { DownloadedRange } from './source-feature'
|
|
4
|
+
import type { DelegatedTracks, ExternalThumbnails, PlayerMedia } from './media'
|
|
4
5
|
|
|
5
6
|
import { useEffect, useState } from 'react'
|
|
6
7
|
import { css } from '@emotion/react'
|
|
@@ -20,7 +21,26 @@ export type MediaPlayerSource =
|
|
|
20
21
|
| { read: (offset: number, size: number) => Promise<ArrayBuffer>, size: number }
|
|
21
22
|
| { read?: undefined, size?: undefined }
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
/** Shared by both arms: nothing here depends on who owns the media. */
|
|
25
|
+
type CommonOptions = {
|
|
26
|
+
title?: string
|
|
27
|
+
autoplay?: boolean
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Drawn above the control bar and outside the click-to-pause region, for whatever the app has to
|
|
31
|
+
* say over the video. `children` land next to the media instead, below the chrome.
|
|
32
|
+
*/
|
|
33
|
+
overlay?: ReactNode
|
|
34
|
+
|
|
35
|
+
onSeek?: (fraction: number) => void
|
|
36
|
+
onPlaybackError?: (error: unknown) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Bytes in: the player owns the `<video>`, and libav feeds it a fragment at a time.
|
|
41
|
+
*/
|
|
42
|
+
export type MediaPlayerLocalOptions =
|
|
43
|
+
& CommonOptions
|
|
24
44
|
& MediaPlayerSource
|
|
25
45
|
& {
|
|
26
46
|
/**
|
|
@@ -41,18 +61,57 @@ export type MediaPlayerOptions =
|
|
|
41
61
|
/** Fallback face for `liberation sans`, used when a subtitle track names a font the file does not carry. */
|
|
42
62
|
defaultFontUrl?: string
|
|
43
63
|
bufferSize?: number
|
|
44
|
-
autoplay?: boolean
|
|
45
64
|
|
|
46
|
-
title?: string
|
|
47
65
|
/** Byte spans available, painted on the seekbar and informing the thumbnail generator. */
|
|
48
66
|
downloadedRanges?: DownloadedRange[]
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Reader for the thumbnail engine, when it should differ from playback's.
|
|
70
|
+
*
|
|
71
|
+
* They are the same by default so generation shares playback's fetch order. A consumer whose
|
|
72
|
+
* reads are not free wants them apart: a torrent hands this a non-prioritising, fail-fast reader
|
|
73
|
+
* so generating previews cannot steal download order from the bytes playback is blocked on.
|
|
74
|
+
*/
|
|
75
|
+
thumbnailRead?: (offset: number, size: number) => Promise<ArrayBuffer>
|
|
76
|
+
/** Off entirely. A second wasm worker during pipeline boot is worth avoiding on a slow source. */
|
|
77
|
+
thumbnailsEnabled?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A media the caller owns and the player only drives.
|
|
82
|
+
*
|
|
83
|
+
* No bytes, so no libav, no MediaSource and no thumbnail generation. For a source whose video lives
|
|
84
|
+
* in a document this one cannot reach into, which is also the only arrangement under which its DRM
|
|
85
|
+
* works: the key session belongs to whoever owns the element.
|
|
86
|
+
*/
|
|
87
|
+
export type MediaPlayerRemoteOptions =
|
|
88
|
+
& CommonOptions
|
|
89
|
+
& {
|
|
90
|
+
media: PlayerMedia
|
|
91
|
+
read?: never
|
|
92
|
+
size?: never
|
|
93
|
+
|
|
94
|
+
/** The source's own storyboard, since there are no bytes to generate previews from. */
|
|
95
|
+
thumbnails?: ExternalThumbnails
|
|
96
|
+
/** The source renders these; the player draws the menu and reports the pick. */
|
|
97
|
+
subtitles?: DelegatedTracks
|
|
98
|
+
audioTracks?: DelegatedTracks
|
|
52
99
|
}
|
|
53
100
|
|
|
101
|
+
export type MediaPlayerOptions = MediaPlayerLocalOptions | MediaPlayerRemoteOptions
|
|
102
|
+
|
|
54
103
|
const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, children?: ReactNode }) => {
|
|
55
|
-
|
|
104
|
+
// Narrowed on the field that carries the difference: a remote arm brings its own media and, by
|
|
105
|
+
// construction, no bytes. Both halves stay null-safe so the engine hooks below can be called
|
|
106
|
+
// unconditionally, which they have to be.
|
|
107
|
+
const remote = 'media' in options ? options : null
|
|
108
|
+
const local = remote ? null : options as MediaPlayerLocalOptions
|
|
109
|
+
// `title` is common to both arms, so it is read off `options`. Everything else here belongs to the
|
|
110
|
+
// local arm and is absent when the media is remote.
|
|
111
|
+
const { title } = options
|
|
112
|
+
const {
|
|
113
|
+
size, downloadedRanges, publicPath, libavWorkerUrl, read, thumbnailRead, thumbnailsEnabled,
|
|
114
|
+
} = local ?? ({} as Partial<MediaPlayerLocalOptions>)
|
|
56
115
|
|
|
57
116
|
const setMedia = useMediaAttach()
|
|
58
117
|
// Mandatory, not an optimisation: without it requestFullscreen falls through to the bare <video>
|
|
@@ -62,17 +121,24 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
|
|
|
62
121
|
const [video, setVideo] = useState<HTMLVideoElement | null>(null)
|
|
63
122
|
const [canvas, setCanvas] = useState<HTMLCanvasElement | null>(null)
|
|
64
123
|
|
|
65
|
-
|
|
124
|
+
// Attaching is not optional in either arm: the store installs `setSourceState` in `attach`, and
|
|
125
|
+
// video.js only runs attach once media is non-null, so skipping it would leave every write below a
|
|
126
|
+
// permanent no-op.
|
|
127
|
+
const media = remote?.media ?? video
|
|
128
|
+
useEffect(() => { setMedia?.(media); return () => setMedia?.(null) }, [media, setMedia])
|
|
66
129
|
|
|
67
|
-
|
|
130
|
+
// Each of these no-ops on null inputs, which is what a remote arm supplies: it renders no <video>,
|
|
131
|
+
// so there is nothing for them to attach to and nothing to guard at the call site.
|
|
132
|
+
usePlayback(video, canvas, local)
|
|
68
133
|
|
|
69
|
-
const
|
|
134
|
+
const generatedThumbnails = useSeekThumbnails({
|
|
70
135
|
publicPath,
|
|
71
136
|
workerUrl: libavWorkerUrl,
|
|
72
|
-
length: size,
|
|
73
|
-
read,
|
|
137
|
+
length: thumbnailsEnabled === false ? undefined : size,
|
|
138
|
+
read: thumbnailRead ?? read,
|
|
74
139
|
downloadedRanges,
|
|
75
140
|
})
|
|
141
|
+
const thumbnails = remote?.thumbnails?.all ?? generatedThumbnails
|
|
76
142
|
const togglePictureInPicture = usePictureInPicture(video, canvas)
|
|
77
143
|
|
|
78
144
|
// Subscribed rather than read off the store, because it is a no-op until the media element
|
|
@@ -83,15 +149,42 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
|
|
|
83
149
|
setSourceState({ title, size, downloadedRanges })
|
|
84
150
|
}, [setSourceState, title, size, downloadedRanges])
|
|
85
151
|
|
|
152
|
+
const thumbnailAt = remote?.thumbnails?.at
|
|
86
153
|
useEffect(() => {
|
|
87
|
-
setSourceState({ thumbnails, togglePictureInPicture })
|
|
88
|
-
}, [setSourceState, thumbnails, togglePictureInPicture])
|
|
154
|
+
setSourceState({ thumbnails, thumbnailAt, togglePictureInPicture })
|
|
155
|
+
}, [setSourceState, thumbnails, thumbnailAt, togglePictureInPicture])
|
|
156
|
+
|
|
157
|
+
// A delegated track list writes the same store fields the engine writes, so the menus never learn
|
|
158
|
+
// which arm they are showing. Only the writer differs: here the pick is forwarded to whoever owns
|
|
159
|
+
// the document, and it renders the result itself.
|
|
160
|
+
const subtitles = remote?.subtitles?.selection
|
|
161
|
+
const audio = remote?.audioTracks?.selection
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!subtitles) return
|
|
164
|
+
setSourceState({
|
|
165
|
+
subtitleTracks: subtitles.options.map(({ id, label }) => ({ id, label })),
|
|
166
|
+
selectedSubtitleTrack: subtitles.selectedId ?? undefined,
|
|
167
|
+
selectSubtitleTrack: (id) => subtitles.select(id == null ? null : String(id)),
|
|
168
|
+
})
|
|
169
|
+
}, [setSourceState, subtitles])
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!audio) return
|
|
173
|
+
setSourceState({
|
|
174
|
+
audioTracks: audio.options.map(({ id, label }) => ({ id, label })),
|
|
175
|
+
selectedAudioTrack: audio.selectedId ?? undefined,
|
|
176
|
+
selectAudioTrack: (id) => audio.select(String(id)),
|
|
177
|
+
})
|
|
178
|
+
}, [setSourceState, audio])
|
|
89
179
|
|
|
90
180
|
return (
|
|
91
181
|
<Chrome
|
|
92
182
|
ref={setContainer}
|
|
93
|
-
|
|
183
|
+
// No element in the remote arm: the media is somebody else's, and whatever renders it is
|
|
184
|
+
// passed in as children. Rendering an idle <video> here would sit over it.
|
|
185
|
+
onVideoRef={remote ? undefined : setVideo}
|
|
94
186
|
onCanvasRef={setCanvas}
|
|
187
|
+
overlay={options.overlay}
|
|
95
188
|
>
|
|
96
189
|
{children}
|
|
97
190
|
</Chrome>
|
|
@@ -100,6 +193,17 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
|
|
|
100
193
|
|
|
101
194
|
// #111 against the black inside is deliberate: this is the letterbox around the player box.
|
|
102
195
|
const rootStyle = css`
|
|
196
|
+
/**
|
|
197
|
+
* The chrome's whole scale, in one place.
|
|
198
|
+
*
|
|
199
|
+
* Everything inside is sized against this rather than against \`rem\`, because \`rem\` is root
|
|
200
|
+
* relative and a library cannot own the host page's root font. The old contract was that the host
|
|
201
|
+
* set \`html { font-size: 62.5% }\`, which silently rendered every control 1.6x too large in any app
|
|
202
|
+
* that did not, and could not be met by an app whose own screens are sized against the default.
|
|
203
|
+
* Override it on the player element to rescale the whole chrome.
|
|
204
|
+
*/
|
|
205
|
+
--mp-unit: 10px;
|
|
206
|
+
|
|
103
207
|
display: flex;
|
|
104
208
|
justify-content: center;
|
|
105
209
|
background-color: #111;
|
package/src/lib/utils/fonts.ts
CHANGED
|
@@ -2,158 +2,158 @@ export const fonts = {
|
|
|
2
2
|
headings: {
|
|
3
3
|
large: `
|
|
4
4
|
font-weight: 600;
|
|
5
|
-
font-size: 2.
|
|
6
|
-
line-height: 3.
|
|
5
|
+
font-size: calc(2.8 * var(--mp-unit));
|
|
6
|
+
line-height: calc(3.4 * var(--mp-unit));
|
|
7
7
|
@media (min-width: 960px) {
|
|
8
|
-
font-size: 3.
|
|
9
|
-
line-height: 4.
|
|
8
|
+
font-size: calc(3.4 * var(--mp-unit));
|
|
9
|
+
line-height: calc(4.1 * var(--mp-unit));
|
|
10
10
|
}
|
|
11
11
|
`,
|
|
12
12
|
medium: `
|
|
13
13
|
font-weight: 600;
|
|
14
|
-
font-size: 2.
|
|
15
|
-
line-height: 2.
|
|
14
|
+
font-size: calc(2.4 * var(--mp-unit));
|
|
15
|
+
line-height: calc(2.9 * var(--mp-unit));
|
|
16
16
|
@media (min-width: 960px) {
|
|
17
|
-
font-size: 2.
|
|
18
|
-
line-height: 3.
|
|
17
|
+
font-size: calc(2.8 * var(--mp-unit));
|
|
18
|
+
line-height: calc(3.4 * var(--mp-unit));
|
|
19
19
|
}
|
|
20
20
|
`,
|
|
21
21
|
small: `
|
|
22
22
|
font-weight: 500;
|
|
23
|
-
font-size: 1.
|
|
24
|
-
line-height: 1.
|
|
23
|
+
font-size: calc(1.8 * var(--mp-unit));
|
|
24
|
+
line-height: calc(1.9 * var(--mp-unit));
|
|
25
25
|
@media (min-width: 960px) {
|
|
26
|
-
font-size: 1.
|
|
27
|
-
line-height: 2.
|
|
26
|
+
font-size: calc(1.8 * var(--mp-unit));
|
|
27
|
+
line-height: calc(2.2 * var(--mp-unit));
|
|
28
28
|
}
|
|
29
29
|
@media (min-width: 2560px) {
|
|
30
|
-
font-size: 2.
|
|
31
|
-
line-height: 2.
|
|
30
|
+
font-size: calc(2.2 * var(--mp-unit));
|
|
31
|
+
line-height: calc(2.6 * var(--mp-unit));
|
|
32
32
|
}
|
|
33
33
|
`,
|
|
34
34
|
extraSmall: `
|
|
35
35
|
font-weight: 500;
|
|
36
|
-
font-size: 1.
|
|
37
|
-
line-height: 1.
|
|
36
|
+
font-size: calc(1.4 * var(--mp-unit));
|
|
37
|
+
line-height: calc(1.7 * var(--mp-unit));
|
|
38
38
|
@media (min-width: 960px) {
|
|
39
|
-
font-size: 1.
|
|
40
|
-
line-height: 1.
|
|
39
|
+
font-size: calc(1.6 * var(--mp-unit));
|
|
40
|
+
line-height: calc(1.9 * var(--mp-unit));
|
|
41
41
|
}
|
|
42
42
|
`
|
|
43
43
|
},
|
|
44
44
|
bLarge: {
|
|
45
45
|
bold: `
|
|
46
46
|
font-weight: 600;
|
|
47
|
-
font-size: 1.
|
|
48
|
-
line-height:
|
|
47
|
+
font-size: calc(1.4 * var(--mp-unit));
|
|
48
|
+
line-height: calc(2 * var(--mp-unit));
|
|
49
49
|
@media (min-width: 960px) {
|
|
50
|
-
font-size: 1.
|
|
51
|
-
line-height: 2.
|
|
50
|
+
font-size: calc(1.6 * var(--mp-unit));
|
|
51
|
+
line-height: calc(2.2 * var(--mp-unit));
|
|
52
52
|
}
|
|
53
53
|
`,
|
|
54
54
|
medium: `
|
|
55
55
|
font-weight: 500;
|
|
56
|
-
font-size: 1.
|
|
57
|
-
line-height:
|
|
56
|
+
font-size: calc(1.4 * var(--mp-unit));
|
|
57
|
+
line-height: calc(2 * var(--mp-unit));
|
|
58
58
|
@media (min-width: 960px) {
|
|
59
|
-
font-size: 1.
|
|
60
|
-
line-height: 2.
|
|
59
|
+
font-size: calc(1.6 * var(--mp-unit));
|
|
60
|
+
line-height: calc(2.2 * var(--mp-unit));
|
|
61
61
|
}
|
|
62
62
|
`,
|
|
63
63
|
regular: `
|
|
64
64
|
font-weight: 400;
|
|
65
|
-
font-size: 1.
|
|
66
|
-
line-height:
|
|
65
|
+
font-size: calc(1.4 * var(--mp-unit));
|
|
66
|
+
line-height: calc(2 * var(--mp-unit));
|
|
67
67
|
@media (min-width: 960px) {
|
|
68
|
-
font-size: 1.
|
|
69
|
-
line-height: 2.
|
|
68
|
+
font-size: calc(1.6 * var(--mp-unit));
|
|
69
|
+
line-height: calc(2.2 * var(--mp-unit));
|
|
70
70
|
}
|
|
71
71
|
`,
|
|
72
72
|
},
|
|
73
73
|
bMedium: {
|
|
74
74
|
bold: `
|
|
75
75
|
font-weight: 600;
|
|
76
|
-
font-size: 1.
|
|
77
|
-
line-height: 1.
|
|
76
|
+
font-size: calc(1.2 * var(--mp-unit));
|
|
77
|
+
line-height: calc(1.7 * var(--mp-unit));
|
|
78
78
|
@media (min-width: 960px) {
|
|
79
|
-
font-size: 1.
|
|
80
|
-
line-height:
|
|
79
|
+
font-size: calc(1.4 * var(--mp-unit));
|
|
80
|
+
line-height: calc(2 * var(--mp-unit));
|
|
81
81
|
}
|
|
82
82
|
`,
|
|
83
83
|
medium: `
|
|
84
84
|
font-weight: 500;
|
|
85
|
-
font-size: 1.
|
|
86
|
-
line-height: 1.
|
|
85
|
+
font-size: calc(1.2 * var(--mp-unit));
|
|
86
|
+
line-height: calc(1.7 * var(--mp-unit));
|
|
87
87
|
@media (min-width: 960px) {
|
|
88
|
-
font-size: 1.
|
|
89
|
-
line-height:
|
|
88
|
+
font-size: calc(1.4 * var(--mp-unit));
|
|
89
|
+
line-height: calc(2 * var(--mp-unit));
|
|
90
90
|
}
|
|
91
91
|
`,
|
|
92
92
|
regular: `
|
|
93
93
|
font-weight: 400;
|
|
94
|
-
font-size: 1.
|
|
95
|
-
line-height: 1.
|
|
94
|
+
font-size: calc(1.2 * var(--mp-unit));
|
|
95
|
+
line-height: calc(1.7 * var(--mp-unit));
|
|
96
96
|
@media (min-width: 960px) {
|
|
97
|
-
font-size: 1.
|
|
98
|
-
line-height:
|
|
97
|
+
font-size: calc(1.4 * var(--mp-unit));
|
|
98
|
+
line-height: calc(2 * var(--mp-unit));
|
|
99
99
|
}
|
|
100
100
|
`,
|
|
101
101
|
},
|
|
102
102
|
bSmall: {
|
|
103
103
|
bold: `
|
|
104
104
|
font-weight: 600;
|
|
105
|
-
font-size:
|
|
106
|
-
line-height: 1.
|
|
105
|
+
font-size: calc(1 * var(--mp-unit));
|
|
106
|
+
line-height: calc(1.4 * var(--mp-unit));
|
|
107
107
|
@media (min-width: 960px) {
|
|
108
|
-
font-size: 1.
|
|
109
|
-
line-height: 1.
|
|
108
|
+
font-size: calc(1.2 * var(--mp-unit));
|
|
109
|
+
line-height: calc(1.7 * var(--mp-unit));
|
|
110
110
|
}
|
|
111
111
|
`,
|
|
112
112
|
medium: `
|
|
113
113
|
font-weight: 500;
|
|
114
|
-
font-size:
|
|
115
|
-
line-height: 1.
|
|
114
|
+
font-size: calc(1 * var(--mp-unit));
|
|
115
|
+
line-height: calc(1.4 * var(--mp-unit));
|
|
116
116
|
@media (min-width: 960px) {
|
|
117
|
-
font-size: 1.
|
|
118
|
-
line-height: 1.
|
|
117
|
+
font-size: calc(1.2 * var(--mp-unit));
|
|
118
|
+
line-height: calc(1.7 * var(--mp-unit));
|
|
119
119
|
}
|
|
120
120
|
`,
|
|
121
121
|
regular: `
|
|
122
122
|
font-weight: 400;
|
|
123
|
-
font-size:
|
|
124
|
-
line-height: 1.
|
|
123
|
+
font-size: calc(1 * var(--mp-unit));
|
|
124
|
+
line-height: calc(1.4 * var(--mp-unit));
|
|
125
125
|
@media (min-width: 960px) {
|
|
126
|
-
font-size: 1.
|
|
127
|
-
line-height: 1.
|
|
126
|
+
font-size: calc(1.2 * var(--mp-unit));
|
|
127
|
+
line-height: calc(1.7 * var(--mp-unit));
|
|
128
128
|
}
|
|
129
129
|
`,
|
|
130
130
|
},
|
|
131
131
|
bExtraSmall: {
|
|
132
132
|
bold: `
|
|
133
133
|
font-weight: 600;
|
|
134
|
-
font-size: 0.
|
|
135
|
-
line-height: 1.
|
|
134
|
+
font-size: calc(0.8 * var(--mp-unit));
|
|
135
|
+
line-height: calc(1.1 * var(--mp-unit));
|
|
136
136
|
@media (min-width: 960px) {
|
|
137
|
-
font-size:
|
|
138
|
-
line-height: 1.
|
|
137
|
+
font-size: calc(1 * var(--mp-unit));
|
|
138
|
+
line-height: calc(1.4 * var(--mp-unit));
|
|
139
139
|
}
|
|
140
140
|
`,
|
|
141
141
|
medium: `
|
|
142
142
|
font-weight: 500;
|
|
143
|
-
font-size: 0.
|
|
144
|
-
line-height: 1.
|
|
143
|
+
font-size: calc(0.8 * var(--mp-unit));
|
|
144
|
+
line-height: calc(1.1 * var(--mp-unit));
|
|
145
145
|
@media (min-width: 960px) {
|
|
146
|
-
font-size:
|
|
147
|
-
line-height: 1.
|
|
146
|
+
font-size: calc(1 * var(--mp-unit));
|
|
147
|
+
line-height: calc(1.4 * var(--mp-unit));
|
|
148
148
|
}
|
|
149
149
|
`,
|
|
150
150
|
regular: `
|
|
151
151
|
font-weight: 400;
|
|
152
|
-
font-size: 0.
|
|
153
|
-
line-height: 1.
|
|
152
|
+
font-size: calc(0.8 * var(--mp-unit));
|
|
153
|
+
line-height: calc(1.1 * var(--mp-unit));
|
|
154
154
|
@media (min-width: 960px) {
|
|
155
|
-
font-size:
|
|
156
|
-
line-height: 1.
|
|
155
|
+
font-size: calc(1 * var(--mp-unit));
|
|
156
|
+
line-height: calc(1.4 * var(--mp-unit));
|
|
157
157
|
}
|
|
158
158
|
`,
|
|
159
159
|
},
|
|
@@ -4,6 +4,12 @@ export type LabelledTrack = {
|
|
|
4
4
|
language: string
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/** One row of a track menu, already named. Mirrors `TrackChoice` without importing the store. */
|
|
8
|
+
export type NamedTrack = {
|
|
9
|
+
id: string | number
|
|
10
|
+
label: string
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Human name for a track's language tag. `fallback: 'none'` is what makes an unknown tag come back
|
|
9
15
|
* undefined instead of echoing itself, so the caller can fall through to the title.
|
|
@@ -34,3 +40,12 @@ export const labelTracks = <T extends LabelledTrack>(tracks: T[]): { track: T, l
|
|
|
34
40
|
return { track, label: ambiguous && detail && detail !== base ? `${base} (${detail})` : base }
|
|
35
41
|
})
|
|
36
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Names the engine's streams once, on the way into the store, so the menu only ever reads rows.
|
|
46
|
+
*
|
|
47
|
+
* The store's ids are opaque so a source that owns its own player can use its own track ids; the id
|
|
48
|
+
* here is the libav stream index, which is what this half of the code has to say.
|
|
49
|
+
*/
|
|
50
|
+
export const toNamedTracks = <T extends LabelledTrack>(tracks: T[]): NamedTrack[] =>
|
|
51
|
+
labelTracks(tracks).map(({ track, label }) => ({ id: track.streamIndex, label }))
|