@banou/media-player 0.6.2 → 0.8.0
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 +149 -3
- package/dist/engine/index.d.ts +10 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/picture-in-picture.d.ts +27 -0
- package/dist/engine/playback.d.ts +45 -0
- package/dist/engine/source-buffer.d.ts +12 -0
- package/dist/engine/subtitles.d.ts +36 -0
- package/dist/engine/thumbnails.d.ts +22 -0
- package/dist/engine-Dmc7JSd9.js +470 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1610 -0
- package/dist/react/components/chrome.d.ts +12 -0
- package/dist/react/components/control-bar.d.ts +2 -0
- package/dist/react/components/overlay.d.ts +4 -0
- package/dist/react/components/playback-slider.d.ts +2 -0
- package/dist/react/components/progress-bar.d.ts +2 -0
- package/dist/react/components/settings.d.ts +2 -0
- package/dist/react/components/sound.d.ts +7 -0
- package/{build → dist/react}/components/tooltip-display.d.ts +8 -8
- package/dist/react/components/volume-slider.d.ts +6 -0
- package/dist/react/hooks/use-drag-value.d.ts +23 -0
- package/dist/react/hooks/use-picture-in-picture.d.ts +2 -0
- package/dist/react/hooks/use-playback.d.ts +8 -0
- package/dist/react/hooks/use-thumbnails.d.ts +12 -0
- package/dist/react/media.d.ts +98 -0
- package/dist/react/player.d.ts +26 -0
- package/dist/react/source-feature.d.ts +106 -0
- package/dist/react/video-player.d.ts +83 -0
- package/dist/utils/source.d.ts +30 -0
- package/dist/utils/track-label.d.ts +31 -0
- package/dist/utils/volume-utils.d.ts +6 -0
- package/package.json +58 -37
- package/src/lib/engine/index.ts +14 -0
- package/src/lib/engine/picture-in-picture.ts +250 -0
- package/src/lib/engine/playback.ts +352 -0
- package/src/lib/engine/source-buffer.ts +61 -0
- package/src/lib/engine/subtitles.ts +210 -0
- package/src/lib/engine/thumbnails.ts +137 -0
- package/src/lib/globals.d.ts +8 -0
- package/src/lib/index.tsx +51 -0
- package/src/lib/react/components/chrome.tsx +130 -0
- package/src/{components → lib/react/components}/control-bar.tsx +74 -96
- package/src/lib/react/components/overlay.tsx +125 -0
- package/src/{components → lib/react/components}/playback-slider.tsx +30 -52
- package/src/lib/react/components/progress-bar.tsx +341 -0
- package/src/lib/react/components/settings.tsx +384 -0
- package/src/lib/react/components/sound.tsx +133 -0
- package/src/{components → lib/react/components}/tooltip-display.tsx +15 -12
- package/src/{components → lib/react/components}/volume-slider.tsx +32 -46
- package/src/lib/react/hooks/use-drag-value.ts +73 -0
- package/src/lib/react/hooks/use-picture-in-picture.ts +36 -0
- package/src/lib/react/hooks/use-playback.ts +146 -0
- package/src/lib/react/hooks/use-thumbnails.ts +78 -0
- package/src/lib/react/media.ts +124 -0
- package/src/lib/react/player.ts +20 -0
- package/src/lib/react/source-feature.ts +108 -0
- package/src/lib/react/video-player.tsx +223 -0
- package/src/lib/utils/fonts.ts +160 -0
- package/src/lib/utils/source.ts +60 -0
- package/src/lib/utils/track-label.ts +51 -0
- package/src/lib/utils/volume-utils.ts +13 -0
- package/build/components/chrome.d.ts +0 -7
- package/build/components/control-bar.d.ts +0 -6
- package/build/components/overlay.d.ts +0 -4
- package/build/components/playback-slider.d.ts +0 -2
- package/build/components/progress-bar.d.ts +0 -1
- package/build/components/settings.d.ts +0 -2
- package/build/components/sound.d.ts +0 -4
- package/build/components/volume-slider.d.ts +0 -6
- package/build/index.d.ts +0 -26
- package/build/index.js +0 -3416
- package/build/main.d.ts +0 -1
- package/build/state-machines/data-source.d.ts +0 -27
- package/build/state-machines/index.d.ts +0 -34038
- package/build/state-machines/media-properties.d.ts +0 -45
- package/build/state-machines/media-source.d.ts +0 -34
- package/build/state-machines/media.d.ts +0 -8558
- package/build/state-machines/subtitles.d.ts +0 -56
- package/build/state-machines/thumbnails.d.ts +0 -24
- package/build/state-machines/utils.d.ts +0 -26
- package/build/utils/actor-utils.d.ts +0 -2
- package/build/utils/context.d.ts +0 -14
- package/build/utils/index.d.ts +0 -4
- package/build/utils/languages.d.ts +0 -260
- package/build/utils/mp4box.d.ts +0 -61
- package/build/utils/use-local-storage.d.ts +0 -3
- package/build/utils/use-scrub.d.ts +0 -11
- package/build/utils/volume-utils.d.ts +0 -17
- package/build/utils/window-height.d.ts +0 -2
- package/src/components/chrome.tsx +0 -95
- package/src/components/overlay.tsx +0 -91
- package/src/components/progress-bar.tsx +0 -251
- package/src/components/settings.tsx +0 -322
- package/src/components/sound.tsx +0 -101
- package/src/index.tsx +0 -195
- package/src/main.tsx +0 -169
- package/src/state-machines/data-source.ts +0 -84
- package/src/state-machines/index.ts +0 -5
- package/src/state-machines/media-properties.ts +0 -82
- package/src/state-machines/media-source.ts +0 -129
- package/src/state-machines/media.ts +0 -255
- package/src/state-machines/subtitles.ts +0 -285
- package/src/state-machines/thumbnails.ts +0 -123
- package/src/state-machines/utils.ts +0 -94
- package/src/utils/actor-utils.ts +0 -19
- package/src/utils/context.ts +0 -23
- package/src/utils/fonts.ts +0 -160
- package/src/utils/index.ts +0 -229
- package/src/utils/languages.ts +0 -262
- package/src/utils/mp4box.ts +0 -74
- package/src/utils/use-local-storage.ts +0 -30
- package/src/utils/use-scrub.ts +0 -40
- package/src/utils/volume-utils.ts +0 -39
- package/src/utils/window-height.ts +0 -19
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.json +0 -28
- package/tsconfig.node.json +0 -9
- /package/{build → dist}/utils/colors.d.ts +0 -0
- /package/{build → dist}/utils/fonts.d.ts +0 -0
- /package/{build → dist}/utils/time.d.ts +0 -0
- /package/src/{assets → lib/assets}/picture-in-picture.svg +0 -0
- /package/src/{utils → lib/utils}/colors.ts +0 -0
- /package/src/{utils → lib/utils}/time.ts +0 -0
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
3
|
import { css } from '@emotion/react'
|
|
3
4
|
import { Maximize, Minimize, Pause, Play, RotateCcw } from 'react-feather'
|
|
4
5
|
|
|
5
|
-
import { linearToLogVolume, logToLinearVolume } from '
|
|
6
|
-
import {
|
|
6
|
+
import { linearToLogVolume, logToLinearVolume } from '../../utils/volume-utils'
|
|
7
|
+
import { formatMediaTime } from '../../utils/time'
|
|
8
|
+
import { fonts } from '../../utils/fonts'
|
|
9
|
+
import { usePlayer } from '../player'
|
|
7
10
|
import { TooltipDisplay } from './tooltip-display'
|
|
8
|
-
import { togglePlay } from '../utils/actor-utils'
|
|
9
|
-
import { MediaPlayerContext } from '../utils/context'
|
|
10
11
|
import { ProgressBar } from './progress-bar'
|
|
11
|
-
import
|
|
12
|
-
import { fonts } from '../utils/fonts'
|
|
13
|
-
import pictureInPicture from '../assets/picture-in-picture.svg'
|
|
12
|
+
import pictureInPicture from '../../assets/picture-in-picture.svg'
|
|
14
13
|
import SettingsAction from './settings'
|
|
15
|
-
import colors from '
|
|
14
|
+
import colors from '../../utils/colors'
|
|
16
15
|
import Sound from './sound'
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
const VOLUME_STEP = 0.05
|
|
18
|
+
const SEEK_STEP = 5
|
|
18
19
|
|
|
19
20
|
const style = css`
|
|
20
21
|
position: absolute;
|
|
@@ -28,12 +29,14 @@ const style = css`
|
|
|
28
29
|
display: flex;
|
|
29
30
|
justify-content: space-between;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
/* repeated per breakpoint: nested at-rules are hoisted after the rule, so a shorthand inside a
|
|
33
|
+
media query would override a longhand above it and silently drop the safe-area inset */
|
|
34
|
+
padding: 6px calc(12px + env(safe-area-inset-right, 0px)) calc(6px + env(safe-area-inset-bottom, 0px)) calc(12px + env(safe-area-inset-left, 0px));
|
|
32
35
|
@media (min-width: 768px) {
|
|
33
|
-
padding: 8px 16px;
|
|
36
|
+
padding: 8px calc(16px + env(safe-area-inset-right, 0px)) calc(8px + env(safe-area-inset-bottom, 0px)) calc(16px + env(safe-area-inset-left, 0px));
|
|
34
37
|
}
|
|
35
38
|
@media (min-width: 2560px) {
|
|
36
|
-
padding: 8px 24px;
|
|
39
|
+
padding: 8px calc(24px + env(safe-area-inset-right, 0px)) calc(8px + env(safe-area-inset-bottom, 0px)) calc(24px + env(safe-area-inset-left, 0px));
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
.left, .right {
|
|
@@ -66,7 +69,6 @@ const style = css`
|
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
// Add basic styling for all the actions
|
|
70
72
|
.play, .sound, .time, .settings, .picture-in-picture, .full-screen {
|
|
71
73
|
display: flex;
|
|
72
74
|
align-items: center;
|
|
@@ -85,7 +87,6 @@ const style = css`
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
// Add styling to the actions that have some interactivity
|
|
89
90
|
.play, .sound, .settings, .picture-in-picture, .full-screen {
|
|
90
91
|
border-radius: 4px;
|
|
91
92
|
|
|
@@ -102,6 +103,16 @@ const style = css`
|
|
|
102
103
|
:hover {
|
|
103
104
|
background-color: ${colors.hover};
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
/* the hit area grows, the icon does not, keyed on the pointer because a narrow desktop
|
|
108
|
+
window still has a mouse */
|
|
109
|
+
@media (pointer: coarse) {
|
|
110
|
+
/* border-box explicitly, since a consumer reset cannot be assumed */
|
|
111
|
+
box-sizing: border-box;
|
|
112
|
+
min-width: 44px;
|
|
113
|
+
min-height: 44px;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
}
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -130,102 +141,73 @@ const style = css`
|
|
|
130
141
|
}
|
|
131
142
|
`
|
|
132
143
|
|
|
133
|
-
export const ControlBar = (
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const [volumeElement, setVolumeElement] = useState<
|
|
142
|
-
const [hideMediaStats, _] = useLocalStorage('hideMediaStats', 'false') as [booleanType, (newValue: booleanType) => void]
|
|
143
|
-
|
|
144
|
-
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
144
|
+
export const ControlBar = () => {
|
|
145
|
+
const player = usePlayer()
|
|
146
|
+
const paused = usePlayer((state) => state.paused)
|
|
147
|
+
const currentTime = usePlayer((state) => state.currentTime)
|
|
148
|
+
const duration = usePlayer((state) => state.duration)
|
|
149
|
+
const fullscreen = usePlayer((state) => state.fullscreen)
|
|
150
|
+
const hideUI = usePlayer((state) => state.hideUI)
|
|
151
|
+
const togglePictureInPicture = usePlayer((state) => state.togglePictureInPicture)
|
|
152
|
+
const [volumeElement, setVolumeElement] = useState<HTMLButtonElement | null>(null)
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
setIsFullscreen(!!document.fullscreenElement)
|
|
149
|
-
}
|
|
150
|
-
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
151
|
-
return () => {
|
|
152
|
-
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
|
153
|
-
}
|
|
154
|
-
}, [])
|
|
155
|
-
|
|
156
|
-
const toggleFullScreen = () => {
|
|
157
|
-
if (!document.fullscreenElement) {
|
|
158
|
-
if (containerRef.current) {
|
|
159
|
-
containerRef.current.requestFullscreen()
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
document.exitFullscreen()
|
|
163
|
-
}
|
|
164
|
-
}
|
|
154
|
+
// duration is 0 until metadata lands, so a bare equality would show replay before playback starts
|
|
155
|
+
const ended = duration > 0 && currentTime === duration
|
|
165
156
|
|
|
157
|
+
// Stepped in linear space and converted back, and the mute flag is restored afterwards because
|
|
158
|
+
// setVolume clears it for any value above zero.
|
|
166
159
|
const modifyVolume = useCallback(({ direction, stepSize }: { direction: 'up' | 'down', stepSize: number }) => {
|
|
167
|
-
|
|
168
|
-
const linearVolume = logToLinearVolume(volume)
|
|
160
|
+
const linearVolume = logToLinearVolume(player.volume)
|
|
169
161
|
const step = (direction === 'up' ? stepSize : -stepSize)
|
|
170
162
|
const newLinearVolume = Math.max(0, Math.min(1, linearVolume + step))
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
muted,
|
|
176
|
-
volume: newLogVolume
|
|
177
|
-
})
|
|
178
|
-
}, [mediaActor.id, muted, volume])
|
|
163
|
+
const wasMuted = player.muted
|
|
164
|
+
player.setVolume(linearToLogVolume(newLinearVolume))
|
|
165
|
+
if (wasMuted && !player.muted) player.toggleMuted()
|
|
166
|
+
}, [player])
|
|
179
167
|
|
|
180
168
|
useEffect(() => {
|
|
181
169
|
const eventListener = (ev: KeyboardEvent) => {
|
|
170
|
+
// on the window, so typing into a consumer's own input is the one case that opts out
|
|
171
|
+
const target = ev.target as HTMLElement | null
|
|
172
|
+
if (target?.isContentEditable || /^(input|textarea|select)$/i.test(target?.tagName ?? '')) return
|
|
173
|
+
|
|
182
174
|
let shouldPreventDefault = true
|
|
183
|
-
//
|
|
184
|
-
if (ev.key === 'f')
|
|
185
|
-
else if (ev.key === 'k')
|
|
186
|
-
else if (ev.key === ' ')
|
|
187
|
-
else if (ev.key === 'm')
|
|
188
|
-
mediaActor.send({ type: 'SET_VOLUME', muted: !muted, volume })
|
|
189
|
-
}
|
|
175
|
+
// space would otherwise reach the browser's own shortcut
|
|
176
|
+
if (ev.key === 'f') player.toggleFullscreen()
|
|
177
|
+
else if (ev.key === 'k') player.togglePaused()
|
|
178
|
+
else if (ev.key === ' ') player.togglePaused()
|
|
179
|
+
else if (ev.key === 'm') player.toggleMuted()
|
|
190
180
|
else if (ev.key === 'ArrowUp') {
|
|
191
|
-
modifyVolume({ direction: 'up', stepSize:
|
|
181
|
+
modifyVolume({ direction: 'up', stepSize: VOLUME_STEP })
|
|
192
182
|
}
|
|
193
183
|
else if (ev.key === 'ArrowDown') {
|
|
194
|
-
modifyVolume({ direction: 'down', stepSize:
|
|
184
|
+
modifyVolume({ direction: 'down', stepSize: VOLUME_STEP })
|
|
195
185
|
}
|
|
196
186
|
else if (ev.key === 'ArrowRight') {
|
|
197
|
-
if (!duration) return
|
|
198
|
-
|
|
199
|
-
mediaActor.send({ type: 'SET_TIME', value: duration })
|
|
200
|
-
} else {
|
|
201
|
-
mediaActor.send({ type: 'SET_TIME', value: currentTime + 5 })
|
|
202
|
-
}
|
|
187
|
+
if (!player.duration) return
|
|
188
|
+
player.seek(Math.min(player.currentTime + SEEK_STEP, player.duration))
|
|
203
189
|
}
|
|
204
190
|
else if (ev.key === 'ArrowLeft') {
|
|
205
|
-
|
|
206
|
-
mediaActor.send({ type: 'SET_TIME', value: 0 })
|
|
207
|
-
} else {
|
|
208
|
-
mediaActor.send({ type: 'SET_TIME', value: currentTime - 5 })
|
|
209
|
-
}
|
|
191
|
+
player.seek(Math.max(player.currentTime - SEEK_STEP, 0))
|
|
210
192
|
}
|
|
211
193
|
else {
|
|
212
194
|
shouldPreventDefault = false
|
|
213
195
|
}
|
|
214
|
-
|
|
196
|
+
|
|
215
197
|
if (shouldPreventDefault) {
|
|
216
198
|
ev.preventDefault()
|
|
217
199
|
}
|
|
218
200
|
}
|
|
219
201
|
window.addEventListener('keydown', eventListener)
|
|
220
202
|
return () => window.removeEventListener('keydown', eventListener)
|
|
221
|
-
}, [
|
|
203
|
+
}, [player, modifyVolume])
|
|
222
204
|
|
|
223
205
|
const handleWheel = useCallback((e: WheelEvent) => {
|
|
224
206
|
e.preventDefault()
|
|
225
207
|
if (e.deltaY < 0) { // scroll up
|
|
226
|
-
modifyVolume({ direction: 'up', stepSize:
|
|
208
|
+
modifyVolume({ direction: 'up', stepSize: VOLUME_STEP })
|
|
227
209
|
} else { // scroll down
|
|
228
|
-
modifyVolume({ direction: 'down', stepSize:
|
|
210
|
+
modifyVolume({ direction: 'down', stepSize: VOLUME_STEP })
|
|
229
211
|
}
|
|
230
212
|
}, [modifyVolume])
|
|
231
213
|
|
|
@@ -233,10 +215,10 @@ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformatio
|
|
|
233
215
|
if (!volumeElement) return
|
|
234
216
|
volumeElement.addEventListener('wheel', handleWheel, { passive: false })
|
|
235
217
|
return () => volumeElement.removeEventListener('wheel', handleWheel)
|
|
236
|
-
}, [volumeElement,
|
|
218
|
+
}, [volumeElement, handleWheel])
|
|
237
219
|
|
|
238
220
|
return (
|
|
239
|
-
<div css={style} style={{ ...
|
|
221
|
+
<div css={style} style={{ ...hideUI ? { opacity: '0', pointerEvents: 'none' } : {} }}>
|
|
240
222
|
<ProgressBar />
|
|
241
223
|
<div className='actions'>
|
|
242
224
|
<div className='left'>
|
|
@@ -247,12 +229,12 @@ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformatio
|
|
|
247
229
|
<button
|
|
248
230
|
className='play'
|
|
249
231
|
type='button'
|
|
250
|
-
onClick={() =>
|
|
232
|
+
onClick={() => player.togglePaused()}
|
|
251
233
|
>
|
|
252
234
|
{
|
|
253
|
-
|
|
235
|
+
ended
|
|
254
236
|
? <RotateCcw />
|
|
255
|
-
:
|
|
237
|
+
: paused
|
|
256
238
|
? <Play />
|
|
257
239
|
: <Pause />
|
|
258
240
|
}
|
|
@@ -261,26 +243,21 @@ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformatio
|
|
|
261
243
|
toolTipText={
|
|
262
244
|
<span>
|
|
263
245
|
{
|
|
264
|
-
|
|
246
|
+
ended
|
|
265
247
|
? 'Replay (k)'
|
|
266
|
-
:
|
|
248
|
+
: paused
|
|
267
249
|
? 'Play (k)'
|
|
268
250
|
: 'Pause (k)'
|
|
269
251
|
}
|
|
270
252
|
</span>
|
|
271
253
|
}
|
|
272
|
-
/>
|
|
254
|
+
/>
|
|
273
255
|
<Sound ref={setVolumeElement}/>
|
|
274
256
|
<div className='time'>
|
|
275
257
|
{formatMediaTime(currentTime, duration)}
|
|
276
258
|
</div>
|
|
277
259
|
</div>
|
|
278
260
|
<div className='right'>
|
|
279
|
-
{
|
|
280
|
-
hideMediaStats !== 'true'
|
|
281
|
-
? mediaInformation
|
|
282
|
-
: null
|
|
283
|
-
}
|
|
284
261
|
<SettingsAction />
|
|
285
262
|
<TooltipDisplay
|
|
286
263
|
id='picture-in-picture'
|
|
@@ -288,6 +265,7 @@ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformatio
|
|
|
288
265
|
<button
|
|
289
266
|
className='picture-in-picture'
|
|
290
267
|
type='button'
|
|
268
|
+
onClick={togglePictureInPicture}
|
|
291
269
|
>
|
|
292
270
|
<img src={pictureInPicture} />
|
|
293
271
|
</button>
|
|
@@ -305,10 +283,10 @@ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformatio
|
|
|
305
283
|
<button
|
|
306
284
|
className='full-screen'
|
|
307
285
|
type='button'
|
|
308
|
-
onClick={
|
|
286
|
+
onClick={() => player.toggleFullscreen()}
|
|
309
287
|
>
|
|
310
288
|
{
|
|
311
|
-
|
|
289
|
+
fullscreen
|
|
312
290
|
? <Minimize />
|
|
313
291
|
: <Maximize />
|
|
314
292
|
}
|
|
@@ -317,7 +295,7 @@ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformatio
|
|
|
317
295
|
toolTipText={
|
|
318
296
|
<span>
|
|
319
297
|
{
|
|
320
|
-
|
|
298
|
+
fullscreen
|
|
321
299
|
? 'Exit full screen (f)'
|
|
322
300
|
: 'Full screen (f)'
|
|
323
301
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
+
import { css, keyframes } from '@emotion/react'
|
|
3
|
+
|
|
4
|
+
import { usePlayer } from '../player'
|
|
5
|
+
import { fonts } from '../../utils/fonts'
|
|
6
|
+
|
|
7
|
+
const style = css`
|
|
8
|
+
top: unset !important;
|
|
9
|
+
left: unset !important;
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
margin: auto;
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
const titleStyle = css`
|
|
17
|
+
position: absolute;
|
|
18
|
+
top: 0;
|
|
19
|
+
left: 0;
|
|
20
|
+
width: 100%;
|
|
21
|
+
padding: calc(1.2 * var(--mp-unit)) calc(1.6 * var(--mp-unit));
|
|
22
|
+
/* clears a notch once the player is fullscreen; resolves to zero everywhere else */
|
|
23
|
+
padding-top: calc(calc(1.2 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
|
|
24
|
+
${fonts.headings.small}
|
|
25
|
+
color: white;
|
|
26
|
+
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
27
|
+
z-index: 2;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
|
|
30
|
+
/* one line with an ellipsis until there is width to wrap, since a release filename would
|
|
31
|
+
otherwise run to four lines on a phone */
|
|
32
|
+
white-space: nowrap;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
text-overflow: ellipsis;
|
|
35
|
+
|
|
36
|
+
@media (min-width: 768px) {
|
|
37
|
+
padding: calc(2.4 * var(--mp-unit));
|
|
38
|
+
padding-top: calc(calc(2.4 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
|
|
39
|
+
white-space: normal;
|
|
40
|
+
overflow: visible;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
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%);
|
|
44
|
+
transition: opacity 0.1s cubic-bezier(.4,0,1,1);
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
const spin = keyframes`
|
|
48
|
+
to { transform: rotate(360deg); }
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
const loadingStyle = css`
|
|
52
|
+
position: absolute;
|
|
53
|
+
inset: 0;
|
|
54
|
+
z-index: 2;
|
|
55
|
+
display: flex;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
align-items: center;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
|
|
60
|
+
::after {
|
|
61
|
+
content: '';
|
|
62
|
+
width: calc(4 * var(--mp-unit));
|
|
63
|
+
height: calc(4 * var(--mp-unit));
|
|
64
|
+
border-radius: 50%;
|
|
65
|
+
border: 3px solid rgba(255, 255, 255, 0.25);
|
|
66
|
+
border-top-color: #fff;
|
|
67
|
+
animation: ${spin} 0.8s linear infinite;
|
|
68
|
+
}
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
// a failure would otherwise be a black frame under a spinner that never resolves
|
|
72
|
+
const errorStyle = css`
|
|
73
|
+
position: absolute;
|
|
74
|
+
inset: auto 0 20%;
|
|
75
|
+
z-index: 3;
|
|
76
|
+
padding: 0 calc(2 * var(--mp-unit));
|
|
77
|
+
text-align: center;
|
|
78
|
+
color: #fff;
|
|
79
|
+
${fonts.bLarge.regular}
|
|
80
|
+
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
const errorMessage = (error: unknown) =>
|
|
85
|
+
error instanceof Error
|
|
86
|
+
? error.message
|
|
87
|
+
: typeof error === 'string' ? error : 'Playback failed'
|
|
88
|
+
|
|
89
|
+
export const Overlay = ({ onCanvasRef }: { onCanvasRef: (element: HTMLCanvasElement | null) => void }) => {
|
|
90
|
+
const title = usePlayer((state) => state.title)
|
|
91
|
+
const hideUI = usePlayer((state) => state.hideUI)
|
|
92
|
+
const playbackError = usePlayer((state) => state.playbackError)
|
|
93
|
+
const ready = usePlayer((state) => state.ready)
|
|
94
|
+
const size = usePlayer((state) => state.size)
|
|
95
|
+
// video.js's own: readyState below HAVE_FUTURE_DATA while not paused
|
|
96
|
+
const waiting = usePlayer((state) => state.waiting)
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
{title
|
|
101
|
+
? (
|
|
102
|
+
<div
|
|
103
|
+
css={titleStyle}
|
|
104
|
+
style={{ ...hideUI ? { opacity: '0', pointerEvents: 'none' } : {} }}
|
|
105
|
+
>
|
|
106
|
+
{title}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
: undefined}
|
|
110
|
+
{/* Two different waits, one spinner. With bytes it is pre-metadata rather than buffering: the
|
|
111
|
+
store reports 0 both before metadata and for a genuinely unknown duration, so `size` is what
|
|
112
|
+
tells whether a source was handed over at all. A media this player does not own has neither
|
|
113
|
+
`size` nor `ready`, and reports the ordinary `waiting` every element does. */}
|
|
114
|
+
{(size ? !ready : waiting) && !playbackError
|
|
115
|
+
? <div css={loadingStyle} />
|
|
116
|
+
: undefined}
|
|
117
|
+
{playbackError
|
|
118
|
+
? <div css={errorStyle}>{errorMessage(playbackError)}</div>
|
|
119
|
+
: undefined}
|
|
120
|
+
<canvas ref={onCanvasRef} css={style} />
|
|
121
|
+
</>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default Overlay
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
+
import { useRef, useEffect } from 'react'
|
|
2
3
|
import { css } from '@emotion/react'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { usePlayer } from '../player'
|
|
6
|
+
import { useDragValue } from '../hooks/use-drag-value'
|
|
5
7
|
|
|
6
8
|
const style = css`
|
|
7
9
|
display: flex;
|
|
@@ -16,6 +18,10 @@ width: 100%;
|
|
|
16
18
|
|
|
17
19
|
padding: 8px 4px;
|
|
18
20
|
margin: 0 2px;
|
|
21
|
+
/* a finger needs a 44px band to press, so the padding grows while the track stays 3px */
|
|
22
|
+
@media (pointer: coarse) {
|
|
23
|
+
padding: 20px 8px;
|
|
24
|
+
}
|
|
19
25
|
|
|
20
26
|
cursor: pointer;
|
|
21
27
|
|
|
@@ -26,6 +32,7 @@ width: 100%;
|
|
|
26
32
|
border-radius: 4px;
|
|
27
33
|
|
|
28
34
|
width: 120px;
|
|
35
|
+
max-width: 100%;
|
|
29
36
|
height: 3px;
|
|
30
37
|
@media (min-width: 768px) {
|
|
31
38
|
height: 4px;
|
|
@@ -39,7 +46,7 @@ width: 100%;
|
|
|
39
46
|
background: #ffffff;
|
|
40
47
|
border-radius: 50%;
|
|
41
48
|
transform: translateX(-50%);
|
|
42
|
-
pointer-events: none;
|
|
49
|
+
pointer-events: none;
|
|
43
50
|
|
|
44
51
|
width: 10px;
|
|
45
52
|
height: 10px;
|
|
@@ -53,77 +60,47 @@ width: 100%;
|
|
|
53
60
|
}
|
|
54
61
|
`
|
|
55
62
|
|
|
56
|
-
const PlaybackSlider = () => {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
63
|
+
export const PlaybackSlider = () => {
|
|
64
|
+
const player = usePlayer()
|
|
65
|
+
const playbackRate = usePlayer((state) => state.playbackRate)
|
|
59
66
|
const sliderRef = useRef<HTMLDivElement>(null)
|
|
60
|
-
const
|
|
67
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
61
68
|
|
|
62
69
|
const MIN_RATE = 0.25
|
|
63
70
|
const MAX_RATE = 3.0
|
|
64
71
|
const RANGE = MAX_RATE - MIN_RATE
|
|
65
|
-
|
|
72
|
+
|
|
66
73
|
const roundToStep = (value: number, step: number = 0.05): number => {
|
|
67
74
|
return Math.round(value / step) * step
|
|
68
75
|
}
|
|
69
|
-
|
|
76
|
+
|
|
70
77
|
const roundedRate = roundToStep(playbackRate || 1)
|
|
71
|
-
|
|
72
|
-
// Calculate position as percentage
|
|
78
|
+
|
|
73
79
|
const getHandlePosition = (rate: number): number => {
|
|
74
80
|
return ((rate - MIN_RATE) / RANGE) * 100
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const rect = sliderRef.current.getBoundingClientRect()
|
|
81
|
-
const xPos = clientX - rect.left
|
|
82
|
-
const percent = Math.max(0, Math.min(xPos, rect.width)) / rect.width
|
|
83
|
-
|
|
84
|
-
const rawRate = MIN_RATE + (percent * RANGE)
|
|
85
|
-
|
|
86
|
-
return roundToStep(rawRate)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const handlePointerDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
90
|
-
setIsDragging(true)
|
|
91
|
-
const newRate = getPlaybackRateFromEvent(e.clientX)
|
|
92
|
-
mediaActor.send({ type: 'SET_PLAYBACK_RATE', playbackRate: newRate })
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const handlePointerMove = (e: MouseEvent) => {
|
|
96
|
-
if (!isDragging) return;
|
|
97
|
-
const newRate = getPlaybackRateFromEvent(e.clientX)
|
|
98
|
-
mediaActor.send({ type: 'SET_PLAYBACK_RATE', playbackRate: newRate })
|
|
99
|
-
}
|
|
83
|
+
// the inverse of getHandlePosition, measured across the track rather than the padded row
|
|
84
|
+
const getPlaybackRateFromFraction = (fraction: number): number => roundToStep(MIN_RATE + (fraction * RANGE))
|
|
100
85
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
86
|
+
const { handlers } = useDragValue({
|
|
87
|
+
ref: trackRef,
|
|
88
|
+
onChange: (fraction) => player.setPlaybackRate(getPlaybackRateFromFraction(fraction))
|
|
89
|
+
})
|
|
104
90
|
|
|
105
91
|
const handleWheel = (e: WheelEvent) => {
|
|
106
92
|
e.preventDefault()
|
|
107
93
|
const step = 0.05
|
|
108
94
|
let newRate = roundedRate
|
|
109
|
-
|
|
95
|
+
|
|
110
96
|
if (e.deltaY < 0) {
|
|
111
97
|
newRate = Math.min(MAX_RATE, newRate + step)
|
|
112
98
|
} else {
|
|
113
99
|
newRate = Math.max(MIN_RATE, newRate - step)
|
|
114
100
|
}
|
|
115
|
-
|
|
116
|
-
mediaActor.send({ type: 'SET_PLAYBACK_RATE', playbackRate: newRate })
|
|
117
|
-
}
|
|
118
101
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
window.addEventListener('mouseup', handlePointerUp)
|
|
122
|
-
return () => {
|
|
123
|
-
window.removeEventListener('mousemove', handlePointerMove)
|
|
124
|
-
window.removeEventListener('mouseup', handlePointerUp)
|
|
125
|
-
}
|
|
126
|
-
}, [isDragging])
|
|
102
|
+
player.setPlaybackRate(newRate)
|
|
103
|
+
}
|
|
127
104
|
|
|
128
105
|
useEffect(() => {
|
|
129
106
|
const slider = sliderRef.current
|
|
@@ -149,12 +126,13 @@ const PlaybackSlider = () => {
|
|
|
149
126
|
<div
|
|
150
127
|
className="playback-slider"
|
|
151
128
|
ref={sliderRef}
|
|
152
|
-
|
|
129
|
+
{...handlers}
|
|
153
130
|
>
|
|
154
131
|
<div
|
|
132
|
+
ref={trackRef}
|
|
155
133
|
style={{
|
|
156
|
-
background: `linear-gradient(to right,
|
|
157
|
-
${fillColor} 0% ${handlePosition}%,
|
|
134
|
+
background: `linear-gradient(to right,
|
|
135
|
+
${fillColor} 0% ${handlePosition}%,
|
|
158
136
|
${emptyColor} ${handlePosition}% 100%
|
|
159
137
|
)`
|
|
160
138
|
}}
|