@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,91 +1,91 @@
|
|
|
1
|
-
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
-
|
|
3
|
-
import { ClassAttributes, ReactNode, useCallback, useContext, useEffect, useState } from 'react'
|
|
4
|
-
import { css } from '@emotion/react'
|
|
5
|
-
|
|
6
|
-
import { MediaMachineContext } from '../state-machines'
|
|
7
|
-
import { MediaPlayerContext } from '../utils/context'
|
|
8
|
-
import { fonts } from '../utils/fonts'
|
|
9
|
-
|
|
10
|
-
const style = css`
|
|
11
|
-
top: unset !important;
|
|
12
|
-
left: unset !important;
|
|
13
|
-
width: 100%;
|
|
14
|
-
height: 100%;
|
|
15
|
-
margin: auto;
|
|
16
|
-
pointer-events: none;
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
const titleStyle = css`
|
|
20
|
-
position: absolute;
|
|
21
|
-
top: 0;
|
|
22
|
-
left: 0;
|
|
23
|
-
width: 100%;
|
|
24
|
-
padding: 2.4rem;
|
|
25
|
-
${fonts.headings.small}
|
|
26
|
-
color: white;
|
|
27
|
-
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
28
|
-
z-index: 2;
|
|
29
|
-
pointer-events: none;
|
|
30
|
-
|
|
31
|
-
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%);
|
|
32
|
-
transition: opacity 0.1s cubic-bezier(.4,0,1,1);
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
const loadingInformationStyle = css`
|
|
36
|
-
position: absolute;
|
|
37
|
-
top: 0;
|
|
38
|
-
left: 0;
|
|
39
|
-
width: 100%;
|
|
40
|
-
height: 100%;
|
|
41
|
-
font-size: 2rem;
|
|
42
|
-
color: white;
|
|
43
|
-
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
44
|
-
z-index: 2;
|
|
45
|
-
display: flex;
|
|
46
|
-
justify-content: center;
|
|
47
|
-
align-items: center;
|
|
48
|
-
pointer-events: none;
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
export const Overlay = ({ loadingInformation }: { loadingInformation?: ReactNode }) => {
|
|
52
|
-
const mediaActor = MediaMachineContext.useActorRef()
|
|
53
|
-
const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
|
|
54
|
-
const mediaPlayerContext = useContext(MediaPlayerContext)
|
|
55
|
-
|
|
56
|
-
const [canvasElement, setCanvasElement] = useState<HTMLCanvasElement | null>()
|
|
57
|
-
const refFunction: ClassAttributes<HTMLCanvasElement>['ref'] = useCallback((element: HTMLCanvasElement | null) => {
|
|
58
|
-
setCanvasElement(element)
|
|
59
|
-
}, [])
|
|
60
|
-
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
if (!canvasElement) return
|
|
63
|
-
mediaActor.send({
|
|
64
|
-
type: 'SET_CANVAS_ELEMENT',
|
|
65
|
-
canvasElement,
|
|
66
|
-
})
|
|
67
|
-
}, [canvasElement])
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<>
|
|
71
|
-
{
|
|
72
|
-
mediaPlayerContext.title
|
|
73
|
-
? (
|
|
74
|
-
<div
|
|
75
|
-
css={titleStyle}
|
|
76
|
-
style={{ ...mediaPlayerContext.hideUI ? { opacity: '0', pointerEvents: 'none' } : {} }}
|
|
77
|
-
>
|
|
78
|
-
{mediaPlayerContext.title}
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
: undefined
|
|
82
|
-
}
|
|
83
|
-
{
|
|
84
|
-
duration === undefined && loadingInformation
|
|
85
|
-
? <div css={loadingInformationStyle}>{loadingInformation}</div>
|
|
86
|
-
: undefined
|
|
87
|
-
}
|
|
88
|
-
<canvas ref={refFunction} css={style}/>
|
|
89
|
-
</>
|
|
90
|
-
)
|
|
91
|
-
}
|
|
1
|
+
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
+
|
|
3
|
+
import { ClassAttributes, ReactNode, useCallback, useContext, useEffect, useState } from 'react'
|
|
4
|
+
import { css } from '@emotion/react'
|
|
5
|
+
|
|
6
|
+
import { MediaMachineContext } from '../state-machines'
|
|
7
|
+
import { MediaPlayerContext } from '../utils/context'
|
|
8
|
+
import { fonts } from '../utils/fonts'
|
|
9
|
+
|
|
10
|
+
const style = css`
|
|
11
|
+
top: unset !important;
|
|
12
|
+
left: unset !important;
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
margin: auto;
|
|
16
|
+
pointer-events: none;
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
const titleStyle = css`
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 0;
|
|
22
|
+
left: 0;
|
|
23
|
+
width: 100%;
|
|
24
|
+
padding: 2.4rem;
|
|
25
|
+
${fonts.headings.small}
|
|
26
|
+
color: white;
|
|
27
|
+
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
28
|
+
z-index: 2;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
|
|
31
|
+
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%);
|
|
32
|
+
transition: opacity 0.1s cubic-bezier(.4,0,1,1);
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
const loadingInformationStyle = css`
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 0;
|
|
38
|
+
left: 0;
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
41
|
+
font-size: 2rem;
|
|
42
|
+
color: white;
|
|
43
|
+
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
44
|
+
z-index: 2;
|
|
45
|
+
display: flex;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
align-items: center;
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
export const Overlay = ({ loadingInformation }: { loadingInformation?: ReactNode }) => {
|
|
52
|
+
const mediaActor = MediaMachineContext.useActorRef()
|
|
53
|
+
const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
|
|
54
|
+
const mediaPlayerContext = useContext(MediaPlayerContext)
|
|
55
|
+
|
|
56
|
+
const [canvasElement, setCanvasElement] = useState<HTMLCanvasElement | null>()
|
|
57
|
+
const refFunction: ClassAttributes<HTMLCanvasElement>['ref'] = useCallback((element: HTMLCanvasElement | null) => {
|
|
58
|
+
setCanvasElement(element)
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!canvasElement) return
|
|
63
|
+
mediaActor.send({
|
|
64
|
+
type: 'SET_CANVAS_ELEMENT',
|
|
65
|
+
canvasElement,
|
|
66
|
+
})
|
|
67
|
+
}, [canvasElement])
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
{
|
|
72
|
+
mediaPlayerContext.title
|
|
73
|
+
? (
|
|
74
|
+
<div
|
|
75
|
+
css={titleStyle}
|
|
76
|
+
style={{ ...mediaPlayerContext.hideUI ? { opacity: '0', pointerEvents: 'none' } : {} }}
|
|
77
|
+
>
|
|
78
|
+
{mediaPlayerContext.title}
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
: undefined
|
|
82
|
+
}
|
|
83
|
+
{
|
|
84
|
+
duration === undefined && loadingInformation
|
|
85
|
+
? <div css={loadingInformationStyle}>{loadingInformation}</div>
|
|
86
|
+
: undefined
|
|
87
|
+
}
|
|
88
|
+
<canvas ref={refFunction} css={style}/>
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -1,174 +1,174 @@
|
|
|
1
|
-
import { useRef, useState, useEffect, useMemo } from 'react'
|
|
2
|
-
import { css } from '@emotion/react'
|
|
3
|
-
|
|
4
|
-
import { MediaMachineContext } from '../state-machines'
|
|
5
|
-
|
|
6
|
-
const style = css`
|
|
7
|
-
display: flex;
|
|
8
|
-
align-items: center;
|
|
9
|
-
justify-content: center;
|
|
10
|
-
flex-direction: column;
|
|
11
|
-
width: 100%;
|
|
12
|
-
|
|
13
|
-
.playback-slider {
|
|
14
|
-
display: flex;
|
|
15
|
-
align-items: center;
|
|
16
|
-
|
|
17
|
-
padding: 8px 4px;
|
|
18
|
-
margin: 0 2px;
|
|
19
|
-
|
|
20
|
-
cursor: pointer;
|
|
21
|
-
|
|
22
|
-
> div {
|
|
23
|
-
position: relative;
|
|
24
|
-
|
|
25
|
-
background: #ccc;
|
|
26
|
-
border-radius: 4px;
|
|
27
|
-
|
|
28
|
-
width: 120px;
|
|
29
|
-
height: 3px;
|
|
30
|
-
@media (min-width: 768px) {
|
|
31
|
-
height: 4px;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.playback-handle {
|
|
36
|
-
position: absolute;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
background: #ffffff;
|
|
40
|
-
border-radius: 50%;
|
|
41
|
-
transform: translateX(-50%);
|
|
42
|
-
pointer-events: none;
|
|
43
|
-
|
|
44
|
-
width: 10px;
|
|
45
|
-
height: 10px;
|
|
46
|
-
top: -3.5px;
|
|
47
|
-
@media (min-width: 768px) {
|
|
48
|
-
width: 12px;
|
|
49
|
-
height: 12px;
|
|
50
|
-
top: -4px;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
const PlaybackSlider = () => {
|
|
57
|
-
const playbackRate = MediaMachineContext.useSelector((state) => state.context.media.playbackRate)
|
|
58
|
-
const mediaActor = MediaMachineContext.useActorRef()
|
|
59
|
-
const sliderRef = useRef<HTMLDivElement>(null)
|
|
60
|
-
const [isDragging, setIsDragging] = useState(false)
|
|
61
|
-
|
|
62
|
-
const MIN_RATE = 0.25
|
|
63
|
-
const MAX_RATE = 3.0
|
|
64
|
-
const RANGE = MAX_RATE - MIN_RATE
|
|
65
|
-
|
|
66
|
-
const roundToStep = (value: number, step: number = 0.05): number => {
|
|
67
|
-
return Math.round(value / step) * step
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const roundedRate = roundToStep(playbackRate || 1)
|
|
71
|
-
|
|
72
|
-
// Calculate position as percentage
|
|
73
|
-
const getHandlePosition = (rate: number): number => {
|
|
74
|
-
return ((rate - MIN_RATE) / RANGE) * 100
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const getPlaybackRateFromEvent = (clientX: number): number => {
|
|
78
|
-
if (!sliderRef.current) return roundedRate;
|
|
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
|
-
}
|
|
100
|
-
|
|
101
|
-
const handlePointerUp = () => {
|
|
102
|
-
setIsDragging(false)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const handleWheel = (e: WheelEvent) => {
|
|
106
|
-
e.preventDefault()
|
|
107
|
-
const step = 0.05
|
|
108
|
-
let newRate = roundedRate
|
|
109
|
-
|
|
110
|
-
if (e.deltaY < 0) {
|
|
111
|
-
newRate = Math.min(MAX_RATE, newRate + step)
|
|
112
|
-
} else {
|
|
113
|
-
newRate = Math.max(MIN_RATE, newRate - step)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
mediaActor.send({ type: 'SET_PLAYBACK_RATE', playbackRate: newRate })
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
window.addEventListener('mousemove', handlePointerMove)
|
|
121
|
-
window.addEventListener('mouseup', handlePointerUp)
|
|
122
|
-
return () => {
|
|
123
|
-
window.removeEventListener('mousemove', handlePointerMove)
|
|
124
|
-
window.removeEventListener('mouseup', handlePointerUp)
|
|
125
|
-
}
|
|
126
|
-
}, [isDragging])
|
|
127
|
-
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
const slider = sliderRef.current
|
|
130
|
-
if (slider) {
|
|
131
|
-
slider.addEventListener('wheel', handleWheel, { passive: false })
|
|
132
|
-
}
|
|
133
|
-
return () => {
|
|
134
|
-
if (slider) {
|
|
135
|
-
slider.removeEventListener('wheel', handleWheel)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}, [playbackRate])
|
|
139
|
-
|
|
140
|
-
const handlePosition = getHandlePosition(roundedRate)
|
|
141
|
-
const fillColor = '#fff'
|
|
142
|
-
const emptyColor = '#3A3A3A'
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<div css={style}>
|
|
146
|
-
<div>
|
|
147
|
-
{roundedRate.toFixed(2)}x
|
|
148
|
-
</div>
|
|
149
|
-
<div
|
|
150
|
-
className="playback-slider"
|
|
151
|
-
ref={sliderRef}
|
|
152
|
-
onMouseDown={handlePointerDown}
|
|
153
|
-
>
|
|
154
|
-
<div
|
|
155
|
-
style={{
|
|
156
|
-
background: `linear-gradient(to right,
|
|
157
|
-
${fillColor} 0% ${handlePosition}%,
|
|
158
|
-
${emptyColor} ${handlePosition}% 100%
|
|
159
|
-
)`
|
|
160
|
-
}}
|
|
161
|
-
>
|
|
162
|
-
<div
|
|
163
|
-
className="playback-handle"
|
|
164
|
-
style={{
|
|
165
|
-
left: `${handlePosition}%`
|
|
166
|
-
}}
|
|
167
|
-
/>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export default PlaybackSlider
|
|
1
|
+
import { useRef, useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import { css } from '@emotion/react'
|
|
3
|
+
|
|
4
|
+
import { MediaMachineContext } from '../state-machines'
|
|
5
|
+
|
|
6
|
+
const style = css`
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
width: 100%;
|
|
12
|
+
|
|
13
|
+
.playback-slider {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
|
|
17
|
+
padding: 8px 4px;
|
|
18
|
+
margin: 0 2px;
|
|
19
|
+
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
|
|
22
|
+
> div {
|
|
23
|
+
position: relative;
|
|
24
|
+
|
|
25
|
+
background: #ccc;
|
|
26
|
+
border-radius: 4px;
|
|
27
|
+
|
|
28
|
+
width: 120px;
|
|
29
|
+
height: 3px;
|
|
30
|
+
@media (min-width: 768px) {
|
|
31
|
+
height: 4px;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.playback-handle {
|
|
36
|
+
position: absolute;
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
background: #ffffff;
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
transform: translateX(-50%);
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
|
|
44
|
+
width: 10px;
|
|
45
|
+
height: 10px;
|
|
46
|
+
top: -3.5px;
|
|
47
|
+
@media (min-width: 768px) {
|
|
48
|
+
width: 12px;
|
|
49
|
+
height: 12px;
|
|
50
|
+
top: -4px;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
const PlaybackSlider = () => {
|
|
57
|
+
const playbackRate = MediaMachineContext.useSelector((state) => state.context.media.playbackRate)
|
|
58
|
+
const mediaActor = MediaMachineContext.useActorRef()
|
|
59
|
+
const sliderRef = useRef<HTMLDivElement>(null)
|
|
60
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
61
|
+
|
|
62
|
+
const MIN_RATE = 0.25
|
|
63
|
+
const MAX_RATE = 3.0
|
|
64
|
+
const RANGE = MAX_RATE - MIN_RATE
|
|
65
|
+
|
|
66
|
+
const roundToStep = (value: number, step: number = 0.05): number => {
|
|
67
|
+
return Math.round(value / step) * step
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const roundedRate = roundToStep(playbackRate || 1)
|
|
71
|
+
|
|
72
|
+
// Calculate position as percentage
|
|
73
|
+
const getHandlePosition = (rate: number): number => {
|
|
74
|
+
return ((rate - MIN_RATE) / RANGE) * 100
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getPlaybackRateFromEvent = (clientX: number): number => {
|
|
78
|
+
if (!sliderRef.current) return roundedRate;
|
|
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
|
+
}
|
|
100
|
+
|
|
101
|
+
const handlePointerUp = () => {
|
|
102
|
+
setIsDragging(false)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const handleWheel = (e: WheelEvent) => {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
const step = 0.05
|
|
108
|
+
let newRate = roundedRate
|
|
109
|
+
|
|
110
|
+
if (e.deltaY < 0) {
|
|
111
|
+
newRate = Math.min(MAX_RATE, newRate + step)
|
|
112
|
+
} else {
|
|
113
|
+
newRate = Math.max(MIN_RATE, newRate - step)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
mediaActor.send({ type: 'SET_PLAYBACK_RATE', playbackRate: newRate })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
window.addEventListener('mousemove', handlePointerMove)
|
|
121
|
+
window.addEventListener('mouseup', handlePointerUp)
|
|
122
|
+
return () => {
|
|
123
|
+
window.removeEventListener('mousemove', handlePointerMove)
|
|
124
|
+
window.removeEventListener('mouseup', handlePointerUp)
|
|
125
|
+
}
|
|
126
|
+
}, [isDragging])
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const slider = sliderRef.current
|
|
130
|
+
if (slider) {
|
|
131
|
+
slider.addEventListener('wheel', handleWheel, { passive: false })
|
|
132
|
+
}
|
|
133
|
+
return () => {
|
|
134
|
+
if (slider) {
|
|
135
|
+
slider.removeEventListener('wheel', handleWheel)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}, [playbackRate])
|
|
139
|
+
|
|
140
|
+
const handlePosition = getHandlePosition(roundedRate)
|
|
141
|
+
const fillColor = '#fff'
|
|
142
|
+
const emptyColor = '#3A3A3A'
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div css={style}>
|
|
146
|
+
<div>
|
|
147
|
+
{roundedRate.toFixed(2)}x
|
|
148
|
+
</div>
|
|
149
|
+
<div
|
|
150
|
+
className="playback-slider"
|
|
151
|
+
ref={sliderRef}
|
|
152
|
+
onMouseDown={handlePointerDown}
|
|
153
|
+
>
|
|
154
|
+
<div
|
|
155
|
+
style={{
|
|
156
|
+
background: `linear-gradient(to right,
|
|
157
|
+
${fillColor} 0% ${handlePosition}%,
|
|
158
|
+
${emptyColor} ${handlePosition}% 100%
|
|
159
|
+
)`
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<div
|
|
163
|
+
className="playback-handle"
|
|
164
|
+
style={{
|
|
165
|
+
left: `${handlePosition}%`
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default PlaybackSlider
|