@banou/media-player 0.2.6 → 0.4.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.
Files changed (76) hide show
  1. package/README.md +0 -10
  2. package/build/components/chrome.d.ts +4 -0
  3. package/build/components/control-bar.d.ts +5 -0
  4. package/build/components/overlay.d.ts +1 -0
  5. package/build/components/progress-bar.d.ts +1 -0
  6. package/build/components/settings.d.ts +2 -0
  7. package/build/components/sound.d.ts +4 -0
  8. package/build/components/tooltip-display.d.ts +20 -0
  9. package/build/components/volume-slider.d.ts +6 -0
  10. package/build/index.d.ts +23 -48
  11. package/build/index.js +2593 -2230
  12. package/build/main.d.ts +1 -1
  13. package/build/state-machines/data-source.d.ts +27 -0
  14. package/build/state-machines/index.d.ts +33185 -0
  15. package/build/state-machines/media-properties.d.ts +45 -0
  16. package/build/state-machines/media-source.d.ts +34 -0
  17. package/build/state-machines/media.d.ts +8344 -0
  18. package/build/state-machines/subtitles.d.ts +53 -0
  19. package/build/state-machines/thumbnails.d.ts +24 -0
  20. package/build/state-machines/utils.d.ts +26 -0
  21. package/build/utils/actor-utils.d.ts +2 -0
  22. package/build/utils/colors.d.ts +8 -0
  23. package/build/utils/context.d.ts +14 -0
  24. package/build/utils/fonts.d.ts +28 -0
  25. package/build/{utils.d.ts → utils/index.d.ts} +4 -12
  26. package/build/utils/languages.d.ts +260 -0
  27. package/build/{mp4box.d.ts → utils/mp4box.d.ts} +61 -61
  28. package/build/utils/time.d.ts +2 -0
  29. package/build/utils/use-local-storage.d.ts +3 -0
  30. package/build/{use-scrub.d.ts → utils/use-scrub.d.ts} +11 -10
  31. package/build/utils/volume-utils.d.ts +17 -0
  32. package/build/utils/window-height.d.ts +2 -0
  33. package/package.json +15 -12
  34. package/src/assets/picture-in-picture.svg +5 -0
  35. package/src/components/chrome.tsx +89 -0
  36. package/src/components/control-bar.tsx +330 -0
  37. package/src/components/overlay.tsx +28 -0
  38. package/src/components/progress-bar.tsx +252 -0
  39. package/src/components/settings.tsx +269 -0
  40. package/src/components/sound.tsx +101 -0
  41. package/src/components/tooltip-display.tsx +83 -0
  42. package/src/components/volume-slider.tsx +156 -0
  43. package/src/index.tsx +145 -434
  44. package/src/main.tsx +66 -39
  45. package/src/state-machines/data-source.ts +83 -0
  46. package/src/state-machines/index.ts +5 -0
  47. package/src/state-machines/media-properties.ts +78 -0
  48. package/src/state-machines/media-source.ts +129 -0
  49. package/src/state-machines/media.ts +245 -0
  50. package/src/state-machines/subtitles.ts +247 -0
  51. package/src/state-machines/thumbnails.ts +123 -0
  52. package/src/state-machines/utils.ts +94 -0
  53. package/src/utils/actor-utils.ts +19 -0
  54. package/src/utils/colors.ts +9 -0
  55. package/src/utils/context.ts +23 -0
  56. package/src/utils/fonts.ts +156 -0
  57. package/src/{utils.ts → utils/index.ts} +2 -26
  58. package/src/utils/time.ts +16 -0
  59. package/src/utils/use-local-storage.ts +30 -0
  60. package/src/{use-scrub.ts → utils/use-scrub.ts} +2 -1
  61. package/src/utils/volume-utils.ts +39 -0
  62. package/src/utils/window-height.ts +19 -0
  63. package/src/vite-env.d.ts +1 -1
  64. package/tsconfig.json +4 -2
  65. package/tsconfig.node.json +9 -9
  66. package/build/chrome/bottom.d.ts +0 -20
  67. package/build/chrome/index.d.ts +0 -27
  68. package/build/chrome/overlay.d.ts +0 -9
  69. package/build/languages.d.ts +0 -260
  70. package/build/use-local-storage.d.ts +0 -6
  71. package/src/chrome/bottom.tsx +0 -651
  72. package/src/chrome/index.tsx +0 -199
  73. package/src/chrome/overlay.tsx +0 -104
  74. package/src/use-local-storage.ts +0 -32
  75. /package/src/{languages.ts → utils/languages.ts} +0 -0
  76. /package/src/{mp4box.ts → utils/mp4box.ts} +0 -0
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-picture-in-picture">
2
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
3
+ <path d="M11 19h-6a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v4" />
4
+ <path d="M14 14m0 1a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z" />
5
+ </svg>
@@ -0,0 +1,89 @@
1
+ /// <reference types="@emotion/react/types/css-prop" />
2
+ import { MouseEventHandler, useContext, useRef, type HTMLAttributes } from 'react'
3
+
4
+ import { css } from '@emotion/react'
5
+
6
+ import { MediaMachineContext } from '../state-machines'
7
+ import { MediaPlayerContext } from '../utils/context'
8
+ import { togglePlay } from '../utils/actor-utils'
9
+ import { Overlay } from './overlay'
10
+ import ControlBar from './control-bar'
11
+
12
+ const style = css`
13
+ position: relative;
14
+ background-color: black;
15
+ display: flex;
16
+ justify-content: center;
17
+ align-items: center;
18
+
19
+ & > div:not(:last-of-type) {
20
+ position: absolute;
21
+ z-index: 2;
22
+ }
23
+
24
+ canvas {
25
+ height: 100%;
26
+ width: 100%;
27
+ pointer-events: none;
28
+ position: absolute;
29
+ z-index: 1;
30
+ }
31
+
32
+ .video, video {
33
+ height: 100%;
34
+ width: 100%;
35
+ background-color: black;
36
+ }
37
+
38
+ &.hide {
39
+ cursor: none;
40
+ }
41
+ `
42
+
43
+ export type ChromeOptions = {} & HTMLAttributes<HTMLDivElement>
44
+
45
+ export const Chrome = ({ children }: ChromeOptions) => {
46
+ const mediaActor = MediaMachineContext.useActorRef()
47
+ const mediaPlayerContext = useContext(MediaPlayerContext)
48
+ const isPaused = MediaMachineContext.useSelector((state) => state.context.media.paused)
49
+ const currentTime = MediaMachineContext.useSelector((state) => state.context.media.currentTime)
50
+ const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
51
+
52
+ const autoHide = useRef<number>()
53
+ const containerRef = useRef<HTMLDivElement>(null)
54
+
55
+ const mouseMove: MouseEventHandler<HTMLDivElement> = (ev) => {
56
+ mediaPlayerContext.update(({ ...mediaPlayerContext, hideUI: false }))
57
+ if (autoHide.current) clearInterval(autoHide.current)
58
+ const timeout = setTimeout(() => {
59
+ mediaPlayerContext.update(({ ...mediaPlayerContext, hideUI: true }))
60
+ }, 3_000) as unknown as number
61
+ autoHide.current = timeout
62
+ }
63
+
64
+ const mouseOut: React.DOMAttributes<HTMLDivElement>['onMouseOut'] = (ev) => {
65
+ if (ev.currentTarget.parentElement !== ev.relatedTarget && ev.relatedTarget !== null) return
66
+ mediaPlayerContext.update(({ ...mediaPlayerContext, hideUI: true }))
67
+ }
68
+
69
+ return (
70
+ <div
71
+ css={style}
72
+ ref={containerRef}
73
+ onMouseMove={mouseMove}
74
+ onMouseOut={mouseOut}
75
+ className={mediaPlayerContext.hideUI ? 'hide' : ''}
76
+ >
77
+ <Overlay />
78
+ <ControlBar containerRef={containerRef} />
79
+ <div
80
+ className='video'
81
+ onClick={() => togglePlay(mediaActor, isPaused, duration, currentTime)}
82
+ >
83
+ {children}
84
+ </div>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ export default Chrome
@@ -0,0 +1,330 @@
1
+ import { RefObject, useCallback, useContext, useEffect, useRef, useState } from 'react'
2
+ import { css } from '@emotion/react'
3
+ import { Maximize, Minimize, Pause, Play, RotateCcw } from 'react-feather'
4
+
5
+ import { MediaMachineContext } from '../state-machines'
6
+ import { TooltipDisplay } from './tooltip-display'
7
+ import { togglePlay } from '../utils/actor-utils'
8
+ import { MediaPlayerContext } from '../utils/context'
9
+ import { ProgressBar } from './progress-bar'
10
+ import { formatMediaTime } from '../utils/time'
11
+ import { fonts } from '../utils/fonts'
12
+ import pictureInPicture from '../assets/picture-in-picture.svg'
13
+ import SettingsAction from './settings'
14
+ import colors from '../utils/colors'
15
+ import Sound from './sound'
16
+ import { linearToLogVolume, logToLinearVolume } from '../utils/volume-utils'
17
+
18
+ const style = css`
19
+ position: absolute;
20
+ bottom: 0;
21
+ width: 100%;
22
+
23
+ background: linear-gradient(0deg,#000 0,rgba(0,0,0,.4) 60%,transparent);
24
+ transition: opacity 0.1s cubic-bezier(.4,0,1,1);
25
+
26
+ .actions {
27
+ display: flex;
28
+ justify-content: space-between;
29
+
30
+ padding: 6px 12px;
31
+ @media (min-width: 768px) {
32
+ padding: 8px 16px;
33
+ }
34
+ @media (min-width: 2560px) {
35
+ padding: 8px 24px;
36
+ }
37
+
38
+ .left, .right {
39
+ display: flex;
40
+ align-items: center;
41
+
42
+ button {
43
+ display: flex;
44
+ align-items: center;
45
+
46
+ outline: none;
47
+ border: none;
48
+ background: none;
49
+
50
+ svg {
51
+ width: 18px;
52
+ height: 18px;
53
+ @media (min-width: 768px) {
54
+ width: 24px;
55
+ height: 24px;
56
+ }
57
+ @media (min-width: 2560px) {
58
+ width: 28px;
59
+ height: 28px;
60
+ }
61
+ }
62
+
63
+ svg {
64
+ stroke: #fff;
65
+ }
66
+ }
67
+
68
+ // Add basic styling for all the actions
69
+ .play, .sound, .time, .settings, .picture-in-picture, .full-screen {
70
+ display: flex;
71
+ align-items: center;
72
+
73
+ height: 100%;
74
+
75
+ border-radius: 4px;
76
+ user-select: none;
77
+
78
+ padding: 8px;
79
+ @media (min-width: 768px) {
80
+ padding: 8px 12px;
81
+ }
82
+ @media (min-width: 2560px) {
83
+ padding: 8px 12px;
84
+ }
85
+ }
86
+
87
+ // Add styling to the actions that have some interactivity
88
+ .play, .sound, .settings, .picture-in-picture, .full-screen {
89
+ border-radius: 4px;
90
+
91
+ padding: 8px;
92
+ @media (min-width: 768px) {
93
+ padding: 8px 12px;
94
+ }
95
+ @media (min-width: 2560px) {
96
+ padding: 8px 12px;
97
+ }
98
+
99
+ cursor: pointer;
100
+
101
+ :hover {
102
+ background-color: ${colors.hover};
103
+ }
104
+ }
105
+ }
106
+
107
+ .left {
108
+ .time {
109
+ ${fonts.bMedium.regular}
110
+ }
111
+ }
112
+ .right {
113
+
114
+ .picture-in-picture {
115
+ img {
116
+ width: 22px;
117
+ height: 22px;
118
+ @media (min-width: 768px) {
119
+ width: 28px;
120
+ height: 28px;
121
+ }
122
+ @media (min-width: 2560px) {
123
+ width: 32px;
124
+ height: 32px;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ `
131
+
132
+ export const ControlBar = ({
133
+ containerRef
134
+ }: {
135
+ containerRef: RefObject<HTMLDivElement>
136
+ }) => {
137
+ const mediaActor = MediaMachineContext.useActorRef()
138
+ const chromeContext = useContext(MediaPlayerContext)
139
+ const isPaused = MediaMachineContext.useSelector((state) => state.context.media.paused)
140
+ const volume = MediaMachineContext.useSelector((state) => state.context.media.volume)
141
+ const muted = MediaMachineContext.useSelector((state) => state.context.media.muted)
142
+ const currentTime = MediaMachineContext.useSelector((state) => state.context.media.currentTime)
143
+ const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
144
+ const [volumeElement, setVolumeElement] = useState<HTMLDivElement | undefined>()
145
+
146
+ const [isFullscreen, setIsFullscreen] = useState(false)
147
+
148
+ useEffect(() => {
149
+ const handleFullscreenChange = () => {
150
+ setIsFullscreen(!!document.fullscreenElement)
151
+ }
152
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
153
+ return () => {
154
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
155
+ }
156
+ }, [])
157
+
158
+ const toggleFullScreen = () => {
159
+ if (!document.fullscreenElement) {
160
+ if (containerRef.current) {
161
+ containerRef.current.requestFullscreen()
162
+ }
163
+ } else {
164
+ document.exitFullscreen()
165
+ }
166
+ }
167
+
168
+ const modifyVolume = useCallback(({ direction, stepSize }: { direction: 'up' | 'down', stepSize: number }) => {
169
+ if (!mediaActor) return
170
+ const linearVolume = logToLinearVolume(volume)
171
+ const step = (direction === 'up' ? stepSize : -stepSize)
172
+ const newLinearVolume = Math.max(0, Math.min(1, linearVolume + step))
173
+ const newLogVolume = linearToLogVolume(newLinearVolume)
174
+
175
+ mediaActor.send({
176
+ type: 'SET_VOLUME',
177
+ muted,
178
+ volume: newLogVolume
179
+ })
180
+ }, [mediaActor.id, muted, volume])
181
+
182
+ useEffect(() => {
183
+ const eventListener = (ev: KeyboardEvent) => {
184
+ let shouldPreventDefault = true
185
+ // avoid triggering the browser's default behavior (e.g space for pause it opens the full screen)
186
+ if (ev.key === 'f') toggleFullScreen()
187
+ else if (ev.key === 'k') togglePlay(mediaActor, isPaused, duration, currentTime)
188
+ else if (ev.key === ' ') togglePlay(mediaActor, isPaused, duration, currentTime)
189
+ else if (ev.key === 'm') {
190
+ mediaActor.send({ type: 'SET_VOLUME', muted: !muted, volume })
191
+ }
192
+ else if (ev.key === 'ArrowUp') {
193
+ modifyVolume({ direction: 'up', stepSize: 0.05 })
194
+ }
195
+ else if (ev.key === 'ArrowDown') {
196
+ modifyVolume({ direction: 'down', stepSize: 0.05 })
197
+ }
198
+ else if (ev.key === 'ArrowRight') {
199
+ if (!duration) return
200
+ if (currentTime + 5 >= duration) {
201
+ mediaActor.send({ type: 'SET_TIME', value: duration })
202
+ } else {
203
+ mediaActor.send({ type: 'SET_TIME', value: currentTime + 5 })
204
+ }
205
+ }
206
+ else if (ev.key === 'ArrowLeft') {
207
+ if (currentTime - 5 < 0) {
208
+ mediaActor.send({ type: 'SET_TIME', value: 0 })
209
+ } else {
210
+ mediaActor.send({ type: 'SET_TIME', value: currentTime - 5 })
211
+ }
212
+ }
213
+ else {
214
+ shouldPreventDefault = false
215
+ }
216
+
217
+ if (shouldPreventDefault) {
218
+ ev.preventDefault()
219
+ }
220
+ }
221
+ window.addEventListener('keydown', eventListener)
222
+ return () => window.removeEventListener('keydown', eventListener)
223
+ }, [mediaActor, volume, muted, isPaused, currentTime, duration])
224
+
225
+ const handleWheel = useCallback((e: WheelEvent) => {
226
+ e.preventDefault()
227
+ if (e.deltaY < 0) { // scroll up
228
+ modifyVolume({ direction: 'up', stepSize: 0.05 })
229
+ } else { // scroll down
230
+ modifyVolume({ direction: 'down', stepSize: 0.05 })
231
+ }
232
+ }, [modifyVolume])
233
+
234
+ useEffect(() => {
235
+ if (!volumeElement) return
236
+ volumeElement.addEventListener('wheel', handleWheel, { passive: false })
237
+ return () => volumeElement.removeEventListener('wheel', handleWheel)
238
+ }, [volumeElement, modifyVolume])
239
+
240
+ return (
241
+ <div css={style} style={{ ...chromeContext.hideUI ? { opacity: '0', pointerEvents: 'none' } : {} }}>
242
+ <ProgressBar />
243
+ <div className='actions'>
244
+ <div className='left'>
245
+ <TooltipDisplay
246
+ id='play'
247
+ tooltipPlace='top-start'
248
+ text={
249
+ <button
250
+ className='play'
251
+ type='button'
252
+ onClick={() => togglePlay(mediaActor, isPaused, duration, currentTime)}
253
+ >
254
+ {
255
+ duration === currentTime
256
+ ? <RotateCcw />
257
+ : isPaused
258
+ ? <Play />
259
+ : <Pause />
260
+ }
261
+ </button>
262
+ }
263
+ toolTipText={
264
+ <span>
265
+ {
266
+ duration === currentTime
267
+ ? 'Replay (k)'
268
+ : isPaused
269
+ ? 'Play (k)'
270
+ : 'Pause (k)'
271
+ }
272
+ </span>
273
+ }
274
+ />
275
+ <Sound ref={setVolumeElement}/>
276
+ <div className='time'>
277
+ {formatMediaTime(currentTime, duration)}
278
+ </div>
279
+ </div>
280
+ <div className='right'>
281
+ <SettingsAction />
282
+ <TooltipDisplay
283
+ id='picture-in-picture'
284
+ text={
285
+ <button
286
+ className='picture-in-picture'
287
+ type='button'
288
+ >
289
+ <img src={pictureInPicture} />
290
+ </button>
291
+ }
292
+ toolTipText={
293
+ <span>
294
+ Picture in picture
295
+ </span>
296
+ }
297
+ />
298
+ <TooltipDisplay
299
+ id='full-screen'
300
+ tooltipPlace='top-end'
301
+ text={
302
+ <button
303
+ className='full-screen'
304
+ type='button'
305
+ onClick={toggleFullScreen}
306
+ >
307
+ {
308
+ isFullscreen
309
+ ? <Minimize />
310
+ : <Maximize />
311
+ }
312
+ </button>
313
+ }
314
+ toolTipText={
315
+ <span>
316
+ {
317
+ isFullscreen
318
+ ? 'Exit full screen (f)'
319
+ : 'Full screen (f)'
320
+ }
321
+ </span>
322
+ }
323
+ />
324
+ </div>
325
+ </div>
326
+ </div>
327
+ )
328
+ }
329
+
330
+ export default ControlBar
@@ -0,0 +1,28 @@
1
+ /// <reference types="@emotion/react/types/css-prop" />
2
+
3
+ import { ClassAttributes, useCallback, useEffect, useState } from 'react'
4
+
5
+ import { MediaMachineContext } from '../state-machines'
6
+
7
+ export const Overlay = () => {
8
+ const mediaActor = MediaMachineContext.useActorRef()
9
+
10
+ const [canvasElement, setCanvasElement] = useState<HTMLCanvasElement | null>()
11
+ const refFunction: ClassAttributes<HTMLCanvasElement>['ref'] = useCallback((element: HTMLCanvasElement | null) => {
12
+ setCanvasElement(element)
13
+ }, [])
14
+
15
+ useEffect(() => {
16
+ if (!canvasElement) return
17
+ mediaActor.send({
18
+ type: 'SET_CANVAS_ELEMENT',
19
+ canvasElement,
20
+ })
21
+ }, [canvasElement])
22
+
23
+ return (
24
+ <>
25
+ <canvas ref={refFunction}/>
26
+ </>
27
+ )
28
+ }
@@ -0,0 +1,252 @@
1
+ import { DOMAttributes, useContext, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { css } from '@emotion/react'
3
+
4
+ import { MediaMachineContext } from '../state-machines'
5
+ import { MediaPlayerContext } from '../utils/context'
6
+ import useScrub from '../utils/use-scrub'
7
+ import { fonts } from '../utils/fonts'
8
+
9
+ const style = css`
10
+ position: relative;
11
+ height: .4rem;
12
+
13
+ transition: transform .2s ease-in-out;
14
+
15
+ margin: 0 12px;
16
+ @media (min-width: 768px) {
17
+ margin: 0 16px;
18
+ }
19
+ @media (min-width: 2560px) {
20
+ margin: 0 24px;
21
+ }
22
+
23
+ user-select: none;
24
+ cursor: pointer;
25
+
26
+ :hover {
27
+ .background-bar {
28
+ transform: scaleY(1.5);
29
+ }
30
+ .loaded {
31
+ transform: scaleY(1.5);
32
+ top: -.1rem;
33
+ }
34
+ .play-container {
35
+ transform: scaleY(1.5);
36
+ }
37
+ }
38
+
39
+ .background-bar {
40
+ position: absolute;
41
+ inset: 0;
42
+ background-color: hsla(0, 100%, 100%, .2);
43
+ }
44
+
45
+ .cursor-time {
46
+ display: flex;
47
+ justify-content: center;
48
+
49
+ text-shadow: 0 0 4px rgba(0, 0, 0, 1);
50
+ ${fonts.bMedium.bold}
51
+
52
+ position: absolute;
53
+ top: -2.5rem;
54
+ width: 5rem;
55
+
56
+ margin-left: -2.5rem;
57
+ pointer-events: none;
58
+ }
59
+
60
+ .loaded {
61
+ transform-origin: 0 0;
62
+ position: absolute;
63
+ bottom: 0;
64
+ height: .4rem;
65
+ width: 100%;
66
+ div {
67
+ width: 100%;
68
+ transform-origin: 0 0;
69
+ height: .4rem;
70
+ background-color: hsla(0, 100%, 100%, .4);
71
+ }
72
+ }
73
+
74
+ .play-container {
75
+ position: absolute;
76
+ bottom: 0;
77
+ width: 100%;
78
+ height: .4rem;
79
+ }
80
+
81
+ .play {
82
+ transform-origin: 0 0;
83
+ background-color: #f03;
84
+ position: absolute;
85
+ bottom: 0;
86
+ height: .4rem;
87
+ width: 100%;
88
+ }
89
+
90
+ .padding {
91
+ position: absolute;
92
+ bottom: -7.5px;
93
+ height: 2rem;
94
+ width: 100%;
95
+ }
96
+
97
+ .thumbnail {
98
+ display: flex;
99
+ justify-content: center;
100
+
101
+ position: absolute;
102
+ top: -17.5rem;
103
+ height: calc(25rem * 9/16);
104
+ width: 25rem;
105
+
106
+ margin-left: -12.5rem;
107
+ pointer-events: none;
108
+
109
+ img {
110
+ border-radius: .4rem;
111
+ box-shadow: 0 0 1rem rgba(0, 0, 0, .5);
112
+
113
+ width: 100%;
114
+ height: 100%;
115
+ object-fit: cover;
116
+ }
117
+ }
118
+
119
+ .loaded-part {
120
+ transform-origin: 0 0;
121
+ position: absolute;
122
+ background-color: hsla(0, 100%, 100%, .4);
123
+ }
124
+ `
125
+
126
+ export const ProgressBar = () => {
127
+ const mediaActor = MediaMachineContext.useActorRef()
128
+ const currentTime = MediaMachineContext.useSelector((state) => state.context.media.currentTime)
129
+ const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
130
+ const thumbnails = MediaMachineContext.useSelector((state) => state.context.thumbnails)
131
+
132
+ const mediaPlayerContext = useContext(MediaPlayerContext)
133
+
134
+ const progressBarRef = useRef<HTMLDivElement>(null)
135
+ const { scrub: seekScrub, value: seekScrubValue, setValue: setSeekValue } = useScrub({ ref: progressBarRef })
136
+
137
+ const [progressBarHoverTime, setProgressBarOverTime] = useState<number | undefined>(undefined)
138
+
139
+ const onProgressBarOver: DOMAttributes<HTMLDivElement>['onMouseMove'] = (ev) => {
140
+ const percentage = ev.nativeEvent.offsetX / ev.currentTarget.offsetWidth
141
+ const time = percentage * (duration ?? 0)
142
+ setProgressBarOverTime(time)
143
+ }
144
+
145
+ const hideProgressBarTime = () => {
146
+ if (!progressBarRef.current) return
147
+ setProgressBarOverTime(undefined)
148
+ }
149
+
150
+ const loadedParts = useMemo(() =>
151
+ mediaPlayerContext
152
+ .downloadedRanges
153
+ ?.map((range, i) => {
154
+ if ('startByteOffset' in range && 'endByteOffset' in range && mediaPlayerContext.size) {
155
+ const start = range.startByteOffset / mediaPlayerContext.size
156
+ const end = range.endByteOffset / mediaPlayerContext.size
157
+ const duration = end - start
158
+ const left = start * 100
159
+ return (
160
+ <div key={i} className="loaded-part" style={{ transform: `scaleX(${duration})`, marginLeft: `${left}%` }}></div>
161
+ )
162
+ } else {
163
+ return null
164
+ }
165
+ })
166
+ ?? [],
167
+ [mediaPlayerContext.downloadedRanges?.map(({ startByteOffset, endByteOffset }) => `${startByteOffset}/${endByteOffset}`).join(',')]
168
+ )
169
+
170
+ const cusorTimeString = useMemo(() => {
171
+ if (!progressBarHoverTime || progressBarHoverTime < 0) return undefined
172
+ const hours = Math.floor(progressBarHoverTime! / 3600)
173
+ const minutes = Math.floor((progressBarHoverTime! - hours * 3600) / 60)
174
+ const seconds = Math.floor(progressBarHoverTime! - hours * 3600 - minutes * 60)
175
+ const hoursString =
176
+ hours > 0
177
+ ? `${hours}:`
178
+ : ''
179
+ return `${hoursString}${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
180
+ }, [progressBarHoverTime])
181
+
182
+ useEffect(() => {
183
+ if (!mediaActor || seekScrubValue === undefined || !duration) return
184
+ const timestamp = seekScrubValue * duration
185
+ mediaActor.send({ type: 'SET_TIME', value: timestamp })
186
+ }, [mediaActor, seekScrubValue, duration])
187
+
188
+ const scaleX = useMemo(() => {
189
+ return typeof duration !== 'number' || typeof currentTime !== 'number'
190
+ ? 0
191
+ : 1 / ((duration ?? 0) / (currentTime ?? 0))
192
+ }, [duration, currentTime])
193
+
194
+ const thumbnail = useMemo(() => {
195
+ if (!thumbnails.length || !progressBarHoverTime) return undefined
196
+ return (
197
+ thumbnails
198
+ .find(({ timestamp, duration }) =>
199
+ timestamp <= progressBarHoverTime && progressBarHoverTime <= timestamp + duration
200
+ )
201
+ )
202
+ }, [thumbnails.length, progressBarHoverTime])
203
+
204
+ return (
205
+ <div
206
+ css={style}
207
+ ref={progressBarRef}
208
+ className="progress-bar"
209
+ onMouseMove={onProgressBarOver}
210
+ onMouseOut={hideProgressBarTime}
211
+ >
212
+ <div className="background-bar" />
213
+ {
214
+ progressBarHoverTime
215
+ ? (
216
+ <div
217
+ className="cursor-time"
218
+ style={{ left: `clamp(1.8rem, ${1 / ((duration ?? 0) / (progressBarHoverTime ?? 1)) * 100}%, calc(100% - 1.8rem))` }}
219
+ >
220
+ {cusorTimeString}
221
+ </div>
222
+ )
223
+ : undefined
224
+ }
225
+ <div className="progress"></div>
226
+ {/* bar showing the currently loaded progress */}
227
+ <div className="loaded">
228
+ {loadedParts}
229
+ </div>
230
+ {/* <div className="load" style={{ transform: `scaleX(${1 / ((duration ?? 0) / (loadedTime?.[1] ?? 0))})` }}></div> */}
231
+ {/* bar to show when hovering to potentially seek */}
232
+ <div className="hover"></div>
233
+ {/* bar displaying the current playback progress */}
234
+ <div className='play-container'>
235
+ <div className="play" style={{ transform: `scaleX(${scaleX})` }}></div>
236
+ </div>
237
+ <div className="chapters"></div>
238
+ <div className="scrubber"></div>
239
+ <div className="padding" onMouseDown={seekScrub}></div>
240
+ <div
241
+ className="thumbnail"
242
+ style={{ left: `clamp(12.5rem, ${1 / ((duration ?? 0) / (progressBarHoverTime ?? 1)) * 100}%, calc(100% - 12.5rem))` }}
243
+ >
244
+ {
245
+ thumbnail
246
+ ? <img src={thumbnail.url}/>
247
+ : undefined
248
+ }
249
+ </div>
250
+ </div>
251
+ )
252
+ }