@banou/media-player 0.6.0 → 0.6.2
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 +6 -6
- package/package.json +51 -51
- package/src/assets/picture-in-picture.svg +5 -5
- package/src/components/chrome.tsx +95 -95
- package/src/components/control-bar.tsx +333 -333
- package/src/components/overlay.tsx +91 -91
- package/src/components/playback-slider.tsx +174 -174
- package/src/components/progress-bar.tsx +251 -251
- package/src/components/settings.tsx +321 -321
- package/src/components/sound.tsx +100 -100
- package/src/components/tooltip-display.tsx +83 -83
- package/src/components/volume-slider.tsx +156 -156
- package/src/index.tsx +195 -195
- package/src/main.tsx +169 -169
- package/src/state-machines/data-source.ts +84 -84
- package/src/state-machines/index.ts +5 -5
- package/src/state-machines/media-properties.ts +82 -82
- package/src/state-machines/media-source.ts +129 -129
- package/src/state-machines/media.ts +255 -255
- package/src/state-machines/subtitles.ts +285 -285
- package/src/state-machines/thumbnails.ts +123 -123
- package/src/state-machines/utils.ts +94 -94
- package/src/utils/actor-utils.ts +19 -19
- package/src/utils/colors.ts +8 -8
- package/src/utils/context.ts +23 -23
- package/src/utils/fonts.ts +159 -159
- package/src/utils/index.ts +229 -229
- package/src/utils/languages.ts +262 -262
- package/src/utils/mp4box.ts +74 -74
- package/src/utils/time.ts +15 -15
- package/src/utils/use-local-storage.ts +30 -30
- package/src/utils/use-scrub.ts +40 -40
- package/src/utils/volume-utils.ts +39 -39
- package/src/utils/window-height.ts +19 -19
- package/src/vite-env.d.ts +1 -1
- package/tsconfig.json +28 -28
- package/tsconfig.node.json +9 -9
|
@@ -1,251 +1,251 @@
|
|
|
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
|
-
.loaded-part {
|
|
67
|
-
transform-origin: 0 0;
|
|
68
|
-
bottom: 0;
|
|
69
|
-
height: .4rem;
|
|
70
|
-
width: 100%;
|
|
71
|
-
position: absolute;
|
|
72
|
-
background-color: hsla(0, 100%, 100%, .4);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.play-container {
|
|
77
|
-
position: absolute;
|
|
78
|
-
bottom: 0;
|
|
79
|
-
width: 100%;
|
|
80
|
-
height: .4rem;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.play {
|
|
84
|
-
transform-origin: 0 0;
|
|
85
|
-
background-color: #f03;
|
|
86
|
-
position: absolute;
|
|
87
|
-
bottom: 0;
|
|
88
|
-
height: .4rem;
|
|
89
|
-
width: 100%;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.padding {
|
|
93
|
-
position: absolute;
|
|
94
|
-
bottom: -7.5px;
|
|
95
|
-
height: 2rem;
|
|
96
|
-
width: 100%;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.thumbnail {
|
|
100
|
-
display: flex;
|
|
101
|
-
justify-content: center;
|
|
102
|
-
|
|
103
|
-
position: absolute;
|
|
104
|
-
top: -17.5rem;
|
|
105
|
-
height: calc(25rem * 9/16);
|
|
106
|
-
width: 25rem;
|
|
107
|
-
|
|
108
|
-
margin-left: -12.5rem;
|
|
109
|
-
pointer-events: none;
|
|
110
|
-
|
|
111
|
-
img {
|
|
112
|
-
border-radius: .4rem;
|
|
113
|
-
box-shadow: 0 0 1rem rgba(0, 0, 0, .5);
|
|
114
|
-
|
|
115
|
-
width: 100%;
|
|
116
|
-
height: 100%;
|
|
117
|
-
object-fit: cover;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
`
|
|
121
|
-
|
|
122
|
-
export const ProgressBar = () => {
|
|
123
|
-
const mediaActor = MediaMachineContext.useActorRef()
|
|
124
|
-
const currentTime = MediaMachineContext.useSelector((state) => state.context.media.currentTime)
|
|
125
|
-
const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
|
|
126
|
-
const indexes = MediaMachineContext.useSelector((state) => state.context.indexes)
|
|
127
|
-
const thumbnails = MediaMachineContext.useSelector((state) => state.context.thumbnails)
|
|
128
|
-
|
|
129
|
-
const mediaPlayerContext = useContext(MediaPlayerContext)
|
|
130
|
-
|
|
131
|
-
const progressBarRef = useRef<HTMLDivElement>(null)
|
|
132
|
-
const { scrub: seekScrub, value: seekScrubValue, setValue: setSeekValue } = useScrub({ ref: progressBarRef })
|
|
133
|
-
|
|
134
|
-
const [progressBarHoverTime, setProgressBarOverTime] = useState<number | undefined>(undefined)
|
|
135
|
-
|
|
136
|
-
const onProgressBarOver: DOMAttributes<HTMLDivElement>['onMouseMove'] = (ev) => {
|
|
137
|
-
const percentage = ev.nativeEvent.offsetX / ev.currentTarget.offsetWidth
|
|
138
|
-
const time = percentage * (duration ?? 0)
|
|
139
|
-
setProgressBarOverTime(time)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const hideProgressBarTime = () => {
|
|
143
|
-
if (!progressBarRef.current) return
|
|
144
|
-
setProgressBarOverTime(undefined)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// file download % does not equal video time %, as the video contains sometimes, big headers including fonts, which might be ~50mb
|
|
148
|
-
const loadedParts = useMemo(() =>
|
|
149
|
-
mediaPlayerContext
|
|
150
|
-
.downloadedRanges
|
|
151
|
-
?.map((range, i) => {
|
|
152
|
-
if (!mediaPlayerContext.size || !duration) return null
|
|
153
|
-
const matchingIndexes = indexes.filter(index => range.startByteOffset <= index.pos && index.pos <= range.endByteOffset)
|
|
154
|
-
const firstIndex = matchingIndexes.at(0)
|
|
155
|
-
const lastIndex = matchingIndexes.at(-1)
|
|
156
|
-
if (!firstIndex || !lastIndex) return null
|
|
157
|
-
const start = firstIndex.timestamp / duration
|
|
158
|
-
const end = lastIndex.timestamp / duration
|
|
159
|
-
const rangeDuration = Number((end - start).toFixed(2)) // to prevent cases like `0.9995140832939585`
|
|
160
|
-
const left = start * 100
|
|
161
|
-
return (
|
|
162
|
-
<div key={i} className="loaded-part" style={{ transform: `scaleX(${rangeDuration})`, marginLeft: `${left}%` }}></div>
|
|
163
|
-
)
|
|
164
|
-
})
|
|
165
|
-
?? [],
|
|
166
|
-
[duration, indexes.length, mediaPlayerContext.downloadedRanges?.map(({ startByteOffset, endByteOffset }) => `${startByteOffset}/${endByteOffset}`).join(',')]
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
const cusorTimeString = useMemo(() => {
|
|
170
|
-
if (!progressBarHoverTime || progressBarHoverTime < 0) return undefined
|
|
171
|
-
const hours = Math.floor(progressBarHoverTime! / 3600)
|
|
172
|
-
const minutes = Math.floor((progressBarHoverTime! - hours * 3600) / 60)
|
|
173
|
-
const seconds = Math.floor(progressBarHoverTime! - hours * 3600 - minutes * 60)
|
|
174
|
-
const hoursString =
|
|
175
|
-
hours > 0
|
|
176
|
-
? `${hours}:`
|
|
177
|
-
: ''
|
|
178
|
-
return `${hoursString}${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
|
179
|
-
}, [progressBarHoverTime])
|
|
180
|
-
|
|
181
|
-
useEffect(() => {
|
|
182
|
-
if (!mediaActor || seekScrubValue === undefined || !duration) return
|
|
183
|
-
const timestamp = seekScrubValue * duration
|
|
184
|
-
mediaActor.send({ type: 'SET_TIME', value: timestamp })
|
|
185
|
-
}, [mediaActor, seekScrubValue, duration])
|
|
186
|
-
|
|
187
|
-
const scaleX = useMemo(() => {
|
|
188
|
-
return typeof duration !== 'number' || typeof currentTime !== 'number'
|
|
189
|
-
? 0
|
|
190
|
-
: 1 / ((duration ?? 0) / (currentTime ?? 0))
|
|
191
|
-
}, [duration, currentTime])
|
|
192
|
-
|
|
193
|
-
const thumbnail = useMemo(() => {
|
|
194
|
-
if (!thumbnails.length || !progressBarHoverTime) return undefined
|
|
195
|
-
return (
|
|
196
|
-
thumbnails
|
|
197
|
-
.find(({ timestamp, duration }) =>
|
|
198
|
-
timestamp <= progressBarHoverTime && progressBarHoverTime <= timestamp + duration
|
|
199
|
-
)
|
|
200
|
-
)
|
|
201
|
-
}, [thumbnails.length, progressBarHoverTime])
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<div
|
|
205
|
-
css={style}
|
|
206
|
-
ref={progressBarRef}
|
|
207
|
-
className="progress-bar"
|
|
208
|
-
onMouseMove={onProgressBarOver}
|
|
209
|
-
onMouseOut={hideProgressBarTime}
|
|
210
|
-
>
|
|
211
|
-
<div className="background-bar" />
|
|
212
|
-
{
|
|
213
|
-
progressBarHoverTime
|
|
214
|
-
? (
|
|
215
|
-
<div
|
|
216
|
-
className="cursor-time"
|
|
217
|
-
style={{ left: `clamp(1.8rem, ${1 / ((duration ?? 0) / (progressBarHoverTime ?? 1)) * 100}%, calc(100% - 1.8rem))` }}
|
|
218
|
-
>
|
|
219
|
-
{cusorTimeString}
|
|
220
|
-
</div>
|
|
221
|
-
)
|
|
222
|
-
: undefined
|
|
223
|
-
}
|
|
224
|
-
<div className="progress"></div>
|
|
225
|
-
{/* bar showing the currently loaded progress */}
|
|
226
|
-
<div className="loaded">
|
|
227
|
-
{loadedParts}
|
|
228
|
-
</div>
|
|
229
|
-
{/* <div className="load" style={{ transform: `scaleX(${1 / ((duration ?? 0) / (loadedTime?.[1] ?? 0))})` }}></div> */}
|
|
230
|
-
{/* bar to show when hovering to potentially seek */}
|
|
231
|
-
<div className="hover"></div>
|
|
232
|
-
{/* bar displaying the current playback progress */}
|
|
233
|
-
<div className='play-container'>
|
|
234
|
-
<div className="play" style={{ transform: `scaleX(${scaleX})` }}></div>
|
|
235
|
-
</div>
|
|
236
|
-
<div className="chapters"></div>
|
|
237
|
-
<div className="scrubber"></div>
|
|
238
|
-
<div className="padding" onMouseDown={seekScrub}></div>
|
|
239
|
-
<div
|
|
240
|
-
className="thumbnail"
|
|
241
|
-
style={{ left: `clamp(12.5rem, ${1 / ((duration ?? 0) / (progressBarHoverTime ?? 1)) * 100}%, calc(100% - 12.5rem))` }}
|
|
242
|
-
>
|
|
243
|
-
{
|
|
244
|
-
thumbnail
|
|
245
|
-
? <img src={thumbnail.url}/>
|
|
246
|
-
: undefined
|
|
247
|
-
}
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
)
|
|
251
|
-
}
|
|
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
|
+
.loaded-part {
|
|
67
|
+
transform-origin: 0 0;
|
|
68
|
+
bottom: 0;
|
|
69
|
+
height: .4rem;
|
|
70
|
+
width: 100%;
|
|
71
|
+
position: absolute;
|
|
72
|
+
background-color: hsla(0, 100%, 100%, .4);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.play-container {
|
|
77
|
+
position: absolute;
|
|
78
|
+
bottom: 0;
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: .4rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.play {
|
|
84
|
+
transform-origin: 0 0;
|
|
85
|
+
background-color: #f03;
|
|
86
|
+
position: absolute;
|
|
87
|
+
bottom: 0;
|
|
88
|
+
height: .4rem;
|
|
89
|
+
width: 100%;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.padding {
|
|
93
|
+
position: absolute;
|
|
94
|
+
bottom: -7.5px;
|
|
95
|
+
height: 2rem;
|
|
96
|
+
width: 100%;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.thumbnail {
|
|
100
|
+
display: flex;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
|
|
103
|
+
position: absolute;
|
|
104
|
+
top: -17.5rem;
|
|
105
|
+
height: calc(25rem * 9/16);
|
|
106
|
+
width: 25rem;
|
|
107
|
+
|
|
108
|
+
margin-left: -12.5rem;
|
|
109
|
+
pointer-events: none;
|
|
110
|
+
|
|
111
|
+
img {
|
|
112
|
+
border-radius: .4rem;
|
|
113
|
+
box-shadow: 0 0 1rem rgba(0, 0, 0, .5);
|
|
114
|
+
|
|
115
|
+
width: 100%;
|
|
116
|
+
height: 100%;
|
|
117
|
+
object-fit: cover;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
`
|
|
121
|
+
|
|
122
|
+
export const ProgressBar = () => {
|
|
123
|
+
const mediaActor = MediaMachineContext.useActorRef()
|
|
124
|
+
const currentTime = MediaMachineContext.useSelector((state) => state.context.media.currentTime)
|
|
125
|
+
const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
|
|
126
|
+
const indexes = MediaMachineContext.useSelector((state) => state.context.indexes)
|
|
127
|
+
const thumbnails = MediaMachineContext.useSelector((state) => state.context.thumbnails)
|
|
128
|
+
|
|
129
|
+
const mediaPlayerContext = useContext(MediaPlayerContext)
|
|
130
|
+
|
|
131
|
+
const progressBarRef = useRef<HTMLDivElement>(null)
|
|
132
|
+
const { scrub: seekScrub, value: seekScrubValue, setValue: setSeekValue } = useScrub({ ref: progressBarRef })
|
|
133
|
+
|
|
134
|
+
const [progressBarHoverTime, setProgressBarOverTime] = useState<number | undefined>(undefined)
|
|
135
|
+
|
|
136
|
+
const onProgressBarOver: DOMAttributes<HTMLDivElement>['onMouseMove'] = (ev) => {
|
|
137
|
+
const percentage = ev.nativeEvent.offsetX / ev.currentTarget.offsetWidth
|
|
138
|
+
const time = percentage * (duration ?? 0)
|
|
139
|
+
setProgressBarOverTime(time)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const hideProgressBarTime = () => {
|
|
143
|
+
if (!progressBarRef.current) return
|
|
144
|
+
setProgressBarOverTime(undefined)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// file download % does not equal video time %, as the video contains sometimes, big headers including fonts, which might be ~50mb
|
|
148
|
+
const loadedParts = useMemo(() =>
|
|
149
|
+
mediaPlayerContext
|
|
150
|
+
.downloadedRanges
|
|
151
|
+
?.map((range, i) => {
|
|
152
|
+
if (!mediaPlayerContext.size || !duration) return null
|
|
153
|
+
const matchingIndexes = indexes.filter(index => range.startByteOffset <= index.pos && index.pos <= range.endByteOffset)
|
|
154
|
+
const firstIndex = matchingIndexes.at(0)
|
|
155
|
+
const lastIndex = matchingIndexes.at(-1)
|
|
156
|
+
if (!firstIndex || !lastIndex) return null
|
|
157
|
+
const start = firstIndex.timestamp / duration
|
|
158
|
+
const end = lastIndex.timestamp / duration
|
|
159
|
+
const rangeDuration = Number((end - start).toFixed(2)) // to prevent cases like `0.9995140832939585`
|
|
160
|
+
const left = start * 100
|
|
161
|
+
return (
|
|
162
|
+
<div key={i} className="loaded-part" style={{ transform: `scaleX(${rangeDuration})`, marginLeft: `${left}%` }}></div>
|
|
163
|
+
)
|
|
164
|
+
})
|
|
165
|
+
?? [],
|
|
166
|
+
[duration, indexes.length, mediaPlayerContext.downloadedRanges?.map(({ startByteOffset, endByteOffset }) => `${startByteOffset}/${endByteOffset}`).join(',')]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const cusorTimeString = useMemo(() => {
|
|
170
|
+
if (!progressBarHoverTime || progressBarHoverTime < 0) return undefined
|
|
171
|
+
const hours = Math.floor(progressBarHoverTime! / 3600)
|
|
172
|
+
const minutes = Math.floor((progressBarHoverTime! - hours * 3600) / 60)
|
|
173
|
+
const seconds = Math.floor(progressBarHoverTime! - hours * 3600 - minutes * 60)
|
|
174
|
+
const hoursString =
|
|
175
|
+
hours > 0
|
|
176
|
+
? `${hours}:`
|
|
177
|
+
: ''
|
|
178
|
+
return `${hoursString}${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
|
179
|
+
}, [progressBarHoverTime])
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!mediaActor || seekScrubValue === undefined || !duration) return
|
|
183
|
+
const timestamp = seekScrubValue * duration
|
|
184
|
+
mediaActor.send({ type: 'SET_TIME', value: timestamp })
|
|
185
|
+
}, [mediaActor, seekScrubValue, duration])
|
|
186
|
+
|
|
187
|
+
const scaleX = useMemo(() => {
|
|
188
|
+
return typeof duration !== 'number' || typeof currentTime !== 'number'
|
|
189
|
+
? 0
|
|
190
|
+
: 1 / ((duration ?? 0) / (currentTime ?? 0))
|
|
191
|
+
}, [duration, currentTime])
|
|
192
|
+
|
|
193
|
+
const thumbnail = useMemo(() => {
|
|
194
|
+
if (!thumbnails.length || !progressBarHoverTime) return undefined
|
|
195
|
+
return (
|
|
196
|
+
thumbnails
|
|
197
|
+
.find(({ timestamp, duration }) =>
|
|
198
|
+
timestamp <= progressBarHoverTime && progressBarHoverTime <= timestamp + duration
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
}, [thumbnails.length, progressBarHoverTime])
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
css={style}
|
|
206
|
+
ref={progressBarRef}
|
|
207
|
+
className="progress-bar"
|
|
208
|
+
onMouseMove={onProgressBarOver}
|
|
209
|
+
onMouseOut={hideProgressBarTime}
|
|
210
|
+
>
|
|
211
|
+
<div className="background-bar" />
|
|
212
|
+
{
|
|
213
|
+
progressBarHoverTime
|
|
214
|
+
? (
|
|
215
|
+
<div
|
|
216
|
+
className="cursor-time"
|
|
217
|
+
style={{ left: `clamp(1.8rem, ${1 / ((duration ?? 0) / (progressBarHoverTime ?? 1)) * 100}%, calc(100% - 1.8rem))` }}
|
|
218
|
+
>
|
|
219
|
+
{cusorTimeString}
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
: undefined
|
|
223
|
+
}
|
|
224
|
+
<div className="progress"></div>
|
|
225
|
+
{/* bar showing the currently loaded progress */}
|
|
226
|
+
<div className="loaded">
|
|
227
|
+
{loadedParts}
|
|
228
|
+
</div>
|
|
229
|
+
{/* <div className="load" style={{ transform: `scaleX(${1 / ((duration ?? 0) / (loadedTime?.[1] ?? 0))})` }}></div> */}
|
|
230
|
+
{/* bar to show when hovering to potentially seek */}
|
|
231
|
+
<div className="hover"></div>
|
|
232
|
+
{/* bar displaying the current playback progress */}
|
|
233
|
+
<div className='play-container'>
|
|
234
|
+
<div className="play" style={{ transform: `scaleX(${scaleX})` }}></div>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="chapters"></div>
|
|
237
|
+
<div className="scrubber"></div>
|
|
238
|
+
<div className="padding" onMouseDown={seekScrub}></div>
|
|
239
|
+
<div
|
|
240
|
+
className="thumbnail"
|
|
241
|
+
style={{ left: `clamp(12.5rem, ${1 / ((duration ?? 0) / (progressBarHoverTime ?? 1)) * 100}%, calc(100% - 12.5rem))` }}
|
|
242
|
+
>
|
|
243
|
+
{
|
|
244
|
+
thumbnail
|
|
245
|
+
? <img src={thumbnail.url}/>
|
|
246
|
+
: undefined
|
|
247
|
+
}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
251
|
+
}
|