@banou/media-player 0.5.0 → 0.6.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 +3 -0
- package/build/components/playback-slider.d.ts +2 -0
- package/build/index.js +1244 -1072
- package/build/state-machines/index.d.ts +2503 -1838
- package/build/state-machines/media.d.ts +218 -51
- package/build/state-machines/subtitles.d.ts +3 -0
- package/build/utils/use-local-storage.d.ts +1 -1
- package/package.json +2 -2
- package/src/components/control-bar.tsx +8 -2
- package/src/components/overlay.tsx +15 -6
- package/src/components/playback-slider.tsx +174 -0
- package/src/components/settings.tsx +106 -53
- package/src/components/tooltip-display.tsx +1 -1
- package/src/index.tsx +2 -2
- package/src/main.tsx +2 -2
- package/src/state-machines/media-properties.ts +7 -3
- package/src/state-machines/media-source.ts +1 -1
- package/src/state-machines/media.ts +5 -2
- package/src/state-machines/subtitles.ts +16 -0
- package/src/utils/fonts.ts +4 -0
- package/src/utils/use-local-storage.ts +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export declare const useLocalStorage: <T>(name: string, defaultValue?: T) => readonly [string | T | undefined, (newValue: string) => void];
|
|
2
2
|
export default useLocalStorage;
|
|
3
|
-
export type
|
|
3
|
+
export type booleanType = 'true' | 'false' | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banou/media-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@xstate/react": "^5.0.1",
|
|
40
40
|
"ass-compiler": "^0.1.15",
|
|
41
41
|
"jassub": "^1.7.18",
|
|
42
|
-
"libav-wasm": "^0.
|
|
42
|
+
"libav-wasm": "^0.6.0",
|
|
43
43
|
"osra": "^0.1.2",
|
|
44
44
|
"p-queue": "^7.3.4",
|
|
45
45
|
"react": "^19.0.0",
|
|
@@ -14,6 +14,7 @@ import pictureInPicture from '../assets/picture-in-picture.svg'
|
|
|
14
14
|
import SettingsAction from './settings'
|
|
15
15
|
import colors from '../utils/colors'
|
|
16
16
|
import Sound from './sound'
|
|
17
|
+
import useLocalStorage, { booleanType } from '../utils/use-local-storage'
|
|
17
18
|
|
|
18
19
|
const style = css`
|
|
19
20
|
position: absolute;
|
|
@@ -107,10 +108,10 @@ const style = css`
|
|
|
107
108
|
.left {
|
|
108
109
|
.time {
|
|
109
110
|
${fonts.bMedium.regular}
|
|
111
|
+
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
.right {
|
|
113
|
-
|
|
114
115
|
.picture-in-picture {
|
|
115
116
|
img {
|
|
116
117
|
width: 22px;
|
|
@@ -138,6 +139,7 @@ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformatio
|
|
|
138
139
|
const currentTime = MediaMachineContext.useSelector((state) => state.context.media.currentTime)
|
|
139
140
|
const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
|
|
140
141
|
const [volumeElement, setVolumeElement] = useState<HTMLDivElement | undefined>()
|
|
142
|
+
const [hideMediaStats, _] = useLocalStorage('hideMediaStats', 'false') as [booleanType, (newValue: booleanType) => void]
|
|
141
143
|
|
|
142
144
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
143
145
|
|
|
@@ -274,7 +276,11 @@ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformatio
|
|
|
274
276
|
</div>
|
|
275
277
|
</div>
|
|
276
278
|
<div className='right'>
|
|
277
|
-
{
|
|
279
|
+
{
|
|
280
|
+
hideMediaStats !== 'true'
|
|
281
|
+
? mediaInformation
|
|
282
|
+
: null
|
|
283
|
+
}
|
|
278
284
|
<SettingsAction />
|
|
279
285
|
<TooltipDisplay
|
|
280
286
|
id='picture-in-picture'
|
|
@@ -5,6 +5,7 @@ import { css } from '@emotion/react'
|
|
|
5
5
|
|
|
6
6
|
import { MediaMachineContext } from '../state-machines'
|
|
7
7
|
import { MediaPlayerContext } from '../utils/context'
|
|
8
|
+
import { fonts } from '../utils/fonts'
|
|
8
9
|
|
|
9
10
|
const style = css`
|
|
10
11
|
top: unset !important;
|
|
@@ -20,14 +21,15 @@ const titleStyle = css`
|
|
|
20
21
|
top: 0;
|
|
21
22
|
left: 0;
|
|
22
23
|
width: 100%;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
margin-top: 2.5rem;
|
|
26
|
-
font-size: 2rem;
|
|
24
|
+
padding: 2.4rem;
|
|
25
|
+
${fonts.headings.small}
|
|
27
26
|
color: white;
|
|
28
27
|
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
29
28
|
z-index: 2;
|
|
30
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);
|
|
31
33
|
`
|
|
32
34
|
|
|
33
35
|
const loadingInformationStyle = css`
|
|
@@ -67,8 +69,15 @@ export const Overlay = ({ loadingInformation }: { loadingInformation?: ReactNode
|
|
|
67
69
|
return (
|
|
68
70
|
<>
|
|
69
71
|
{
|
|
70
|
-
mediaPlayerContext.title
|
|
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
|
+
)
|
|
72
81
|
: undefined
|
|
73
82
|
}
|
|
74
83
|
{
|
|
@@ -0,0 +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,10 +1,12 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
2
2
|
import { css } from "@emotion/react"
|
|
3
|
-
import { ChevronRight, Settings } from "react-feather"
|
|
3
|
+
import { ChevronLeft, ChevronRight, Settings } from "react-feather"
|
|
4
4
|
|
|
5
5
|
import { MediaMachineContext } from "../state-machines"
|
|
6
6
|
import { TooltipDisplay } from "./tooltip-display"
|
|
7
7
|
import { fonts } from "../utils/fonts"
|
|
8
|
+
import PlaybackSlider from "./playback-slider"
|
|
9
|
+
import useLocalStorage, { booleanType } from "../utils/use-local-storage"
|
|
8
10
|
|
|
9
11
|
const style = css`
|
|
10
12
|
position: relative;
|
|
@@ -14,6 +16,7 @@ position: relative;
|
|
|
14
16
|
right: 50%;
|
|
15
17
|
transform: translateX(50%);
|
|
16
18
|
|
|
19
|
+
overflow-y: auto;
|
|
17
20
|
width: 180px;
|
|
18
21
|
height: 160px;
|
|
19
22
|
top: -180px;
|
|
@@ -42,8 +45,6 @@ position: relative;
|
|
|
42
45
|
padding: 8px 6px 8px 12px;
|
|
43
46
|
|
|
44
47
|
width: 100%;
|
|
45
|
-
height: 100%;
|
|
46
|
-
cursor: pointer;
|
|
47
48
|
|
|
48
49
|
:first-of-type {
|
|
49
50
|
border-radius: 8px 8px 0 0;
|
|
@@ -51,7 +52,10 @@ position: relative;
|
|
|
51
52
|
:last-of-type {
|
|
52
53
|
border-radius: 0 0 8px 8px;
|
|
53
54
|
}
|
|
54
|
-
:hover {
|
|
55
|
+
:not(&.no-hover) {
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
}
|
|
58
|
+
:not(&.no-hover):hover {
|
|
55
59
|
background-color: rgba(255,255,255,.1);
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -66,6 +70,57 @@ position: relative;
|
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
}
|
|
73
|
+
|
|
74
|
+
.back {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: flex-start;
|
|
78
|
+
border-bottom: 1px solid #4D4D4E;
|
|
79
|
+
|
|
80
|
+
svg {
|
|
81
|
+
transform: translateX(-4px);
|
|
82
|
+
transition: transform 0.2s ease-in-out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
:hover {
|
|
86
|
+
svg {
|
|
87
|
+
transform: translateX(-8px);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.playback-rate {
|
|
94
|
+
.slider {
|
|
95
|
+
width: 100%;
|
|
96
|
+
}
|
|
97
|
+
.options {
|
|
98
|
+
display: grid;
|
|
99
|
+
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
100
|
+
align-items: center;
|
|
101
|
+
|
|
102
|
+
padding: 8px 12px;
|
|
103
|
+
|
|
104
|
+
> div {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
|
|
109
|
+
padding: 8px 6px;
|
|
110
|
+
height: 100%;
|
|
111
|
+
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
:hover {
|
|
114
|
+
background-color: rgba(255,255,255,.1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.subtitle {
|
|
121
|
+
.description {
|
|
122
|
+
word-break: break-word;
|
|
123
|
+
}
|
|
69
124
|
}
|
|
70
125
|
`
|
|
71
126
|
|
|
@@ -82,6 +137,7 @@ const SettingsAction = () => {
|
|
|
82
137
|
const playbackRate = MediaMachineContext.useSelector((state) => state.context.media.playbackRate)
|
|
83
138
|
const subtitleStreams = MediaMachineContext.useSelector((state) => state.context.subtitleStreams)
|
|
84
139
|
const selectedSubtitleStreamIndex = MediaMachineContext.useSelector((state) => state.context.selectedSubtitleStreamIndex)
|
|
140
|
+
const [hideMediaStats, setHideMediaStats] = useLocalStorage('hideMediaStats', 'false') as [booleanType, (newValue: booleanType) => void]
|
|
85
141
|
|
|
86
142
|
const [isOpenPopover, setIsOpenPopover] = useState(false)
|
|
87
143
|
const [popoverContent, setPopoverContent] = useState(PopoverContent.Default)
|
|
@@ -116,7 +172,7 @@ const SettingsAction = () => {
|
|
|
116
172
|
[subtitleStreams.length]
|
|
117
173
|
)
|
|
118
174
|
|
|
119
|
-
const setLanguage = (languageWithStreamIndex: { streamIndex: number, language
|
|
175
|
+
const setLanguage = (languageWithStreamIndex: { streamIndex: number | undefined, language?: string }) => () => {
|
|
120
176
|
mediaActor.send({ type: 'SELECT_SUBTITLE_STREAM', streamIndex: languageWithStreamIndex.streamIndex })
|
|
121
177
|
togglePopover()
|
|
122
178
|
}
|
|
@@ -147,19 +203,19 @@ const SettingsAction = () => {
|
|
|
147
203
|
/>
|
|
148
204
|
{
|
|
149
205
|
isOpenPopover && popoverContent === PopoverContent.Default && (
|
|
150
|
-
<div className='popover'>
|
|
206
|
+
<div className='popover menu'>
|
|
151
207
|
<div onClick={changePopoverContent(PopoverContent.Advanced)}>
|
|
152
208
|
<span>Advanced</span>
|
|
153
209
|
<div>
|
|
154
210
|
<ChevronRight />
|
|
155
211
|
</div>
|
|
156
212
|
</div>
|
|
157
|
-
<div onClick={changePopoverContent(PopoverContent.SelectNewSources)}>
|
|
213
|
+
{/* <div onClick={changePopoverContent(PopoverContent.SelectNewSources)}>
|
|
158
214
|
<div>Select new sources</div>
|
|
159
215
|
<div>
|
|
160
216
|
<ChevronRight />
|
|
161
217
|
</div>
|
|
162
|
-
</div>
|
|
218
|
+
</div> */}
|
|
163
219
|
<div onClick={changePopoverContent(PopoverContent.Subtitles)}>
|
|
164
220
|
<div>Subtitles</div>
|
|
165
221
|
<div>
|
|
@@ -184,38 +240,26 @@ const SettingsAction = () => {
|
|
|
184
240
|
}
|
|
185
241
|
{
|
|
186
242
|
isOpenPopover && popoverContent === PopoverContent.PlaybackRate && (
|
|
187
|
-
<div className='popover'>
|
|
188
|
-
<div onClick={changePopoverContent(PopoverContent.Default)}>
|
|
189
|
-
<
|
|
190
|
-
<
|
|
243
|
+
<div className='popover playback-rate'>
|
|
244
|
+
<div className="back" onClick={changePopoverContent(PopoverContent.Default)}>
|
|
245
|
+
<ChevronLeft />
|
|
246
|
+
<span>Plackback speed</span>
|
|
191
247
|
</div>
|
|
192
|
-
<div>
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
onClick={() => setPlaybackRate(1)}
|
|
204
|
-
>
|
|
205
|
-
1x
|
|
206
|
-
</button>
|
|
207
|
-
<button
|
|
208
|
-
type='button'
|
|
209
|
-
onClick={() => setPlaybackRate(1.5)}
|
|
210
|
-
>
|
|
248
|
+
<div className="slider no-hover">
|
|
249
|
+
<PlaybackSlider />
|
|
250
|
+
</div>
|
|
251
|
+
<div className="options no-hover">
|
|
252
|
+
<div onClick={() => setPlaybackRate(0.5)}>
|
|
253
|
+
0.5x
|
|
254
|
+
</div>
|
|
255
|
+
<div onClick={() => setPlaybackRate(1)}>
|
|
256
|
+
1x
|
|
257
|
+
</div>
|
|
258
|
+
<div onClick={() => setPlaybackRate(1.5)}>
|
|
211
259
|
1.5x
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
onClick={() => setPlaybackRate(2)}
|
|
216
|
-
>
|
|
217
|
-
2x
|
|
218
|
-
</button>
|
|
260
|
+
</div>
|
|
261
|
+
<div onClick={() => setPlaybackRate(2)}>
|
|
262
|
+
2x
|
|
219
263
|
</div>
|
|
220
264
|
</div>
|
|
221
265
|
</div>
|
|
@@ -223,42 +267,51 @@ const SettingsAction = () => {
|
|
|
223
267
|
}
|
|
224
268
|
{
|
|
225
269
|
isOpenPopover && popoverContent === PopoverContent.Advanced && (
|
|
226
|
-
<div className='popover'>
|
|
227
|
-
<div onClick={changePopoverContent(PopoverContent.Default)}>
|
|
228
|
-
<
|
|
229
|
-
<
|
|
270
|
+
<div className='popover advanced'>
|
|
271
|
+
<div className="back" onClick={changePopoverContent(PopoverContent.Default)}>
|
|
272
|
+
<ChevronLeft />
|
|
273
|
+
<span>Advanced</span>
|
|
274
|
+
</div>
|
|
275
|
+
<div onClick={() => setHideMediaStats(hideMediaStats === 'true' ? 'false' : 'true')}>
|
|
276
|
+
<span>Hide stats</span>
|
|
277
|
+
<span>{hideMediaStats === 'true' ? '✓' : ''}</span>
|
|
230
278
|
</div>
|
|
231
279
|
</div>
|
|
232
280
|
)
|
|
233
281
|
}
|
|
234
282
|
{
|
|
235
283
|
isOpenPopover && popoverContent === PopoverContent.SelectNewSources && (
|
|
236
|
-
<div className='popover'>
|
|
237
|
-
<div onClick={changePopoverContent(PopoverContent.Default)}>
|
|
238
|
-
<
|
|
239
|
-
<
|
|
284
|
+
<div className='popover sources'>
|
|
285
|
+
<div className="back" onClick={changePopoverContent(PopoverContent.Default)}>
|
|
286
|
+
<ChevronLeft />
|
|
287
|
+
<span>Select new sources</span>
|
|
240
288
|
</div>
|
|
241
289
|
</div>
|
|
242
290
|
)
|
|
243
291
|
}
|
|
244
292
|
{
|
|
245
293
|
isOpenPopover && popoverContent === PopoverContent.Subtitles && (
|
|
246
|
-
<div className='popover'>
|
|
294
|
+
<div className='popover subtitle'>
|
|
295
|
+
<div className="back" onClick={changePopoverContent(PopoverContent.Default)}>
|
|
296
|
+
<ChevronLeft />
|
|
297
|
+
<span>Subtitles</span>
|
|
298
|
+
</div>
|
|
299
|
+
<div onClick={setLanguage({ streamIndex: undefined })}>
|
|
300
|
+
<span>Disable</span>
|
|
301
|
+
<span>{selectedSubtitleStreamIndex === undefined ? '✓' : ''}</span>
|
|
302
|
+
</div>
|
|
247
303
|
{
|
|
248
304
|
languagesWithStreamIndex.map((languageWithStreamIndex) => (
|
|
249
305
|
<div
|
|
250
306
|
key={languageWithStreamIndex.streamIndex}
|
|
251
307
|
onClick={setLanguage(languageWithStreamIndex)}
|
|
308
|
+
className="description"
|
|
252
309
|
>
|
|
253
|
-
<
|
|
254
|
-
<
|
|
310
|
+
<span>{languageWithStreamIndex.language}</span>
|
|
311
|
+
<span>{selectedSubtitleStreamIndex === languageWithStreamIndex.streamIndex ? '✓' : ''}</span>
|
|
255
312
|
</div>
|
|
256
313
|
))
|
|
257
314
|
}
|
|
258
|
-
<div onClick={changePopoverContent(PopoverContent.Default)}>
|
|
259
|
-
<div>Back</div>
|
|
260
|
-
<div></div>
|
|
261
|
-
</div>
|
|
262
315
|
</div>
|
|
263
316
|
)
|
|
264
317
|
}
|
package/src/index.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { css } from '@emotion/react'
|
|
|
7
7
|
|
|
8
8
|
import { MediaMachineContext } from './state-machines'
|
|
9
9
|
import { MediaPlayerContext, DownloadedRange } from './utils/context'
|
|
10
|
-
import useLocalStorage, {
|
|
10
|
+
import useLocalStorage, { booleanType } from './utils/use-local-storage'
|
|
11
11
|
import Chrome from './components/chrome'
|
|
12
12
|
|
|
13
13
|
const BUFFER_SIZE = 2_500_000
|
|
@@ -49,7 +49,7 @@ export const FKNVideoRoot = (
|
|
|
49
49
|
const isReady = MediaMachineContext.useSelector((state) => state.context.isReady)
|
|
50
50
|
const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
|
|
51
51
|
const [mediaVolume, setMediaVolume] = useLocalStorage('mediaVolume', '1') as [string, (newValue: string) => void]
|
|
52
|
-
const [mediaMute, setMediaMute] = useLocalStorage('mediaMute', 'false') as [
|
|
52
|
+
const [mediaMute, setMediaMute] = useLocalStorage('mediaMute', 'false') as [booleanType, (newValue: booleanType) => void]
|
|
53
53
|
const [firstRender, setFirstRender] = useState(true)
|
|
54
54
|
|
|
55
55
|
useEffect(() => {
|
package/src/main.tsx
CHANGED
|
@@ -13,7 +13,7 @@ const mountStyle = css`
|
|
|
13
13
|
`
|
|
14
14
|
|
|
15
15
|
const BASE_BUFFER_SIZE = 2_500_000
|
|
16
|
-
const url = '/
|
|
16
|
+
const url = '/video8.mkv'
|
|
17
17
|
|
|
18
18
|
const Mount = () => {
|
|
19
19
|
const [contentLength, setContentLength] = useState<number>()
|
|
@@ -124,7 +124,7 @@ const globalStyle = css`
|
|
|
124
124
|
font-size: 1.6rem;
|
|
125
125
|
font-family: Fira Sans;
|
|
126
126
|
color: #fff;
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
font-family: Montserrat;
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -25,12 +25,16 @@ export default fromCallback<MediaPropertiesEvents, MediaPropertiesInput, MediaPr
|
|
|
25
25
|
|
|
26
26
|
receive((event) => {
|
|
27
27
|
if (event.type === 'PLAY') {
|
|
28
|
-
videoElement
|
|
29
|
-
|
|
28
|
+
videoElement
|
|
29
|
+
.play()
|
|
30
|
+
.then(() => handlePlay())
|
|
31
|
+
.catch((error) => {
|
|
32
|
+
console.error(error)
|
|
33
|
+
})
|
|
30
34
|
}
|
|
31
35
|
if (event.type === 'PAUSE') {
|
|
32
36
|
videoElement.pause()
|
|
33
|
-
|
|
37
|
+
handlePause()
|
|
34
38
|
}
|
|
35
39
|
if (event.type === 'SET_TIME') {
|
|
36
40
|
videoElement.currentTime = event.value
|
|
@@ -55,7 +55,7 @@ export default fromAsyncCallback<MediaSourceEvents, MediaSourceInput, MediaSourc
|
|
|
55
55
|
receive((event) => {
|
|
56
56
|
if (resolved) return
|
|
57
57
|
if (event.type === 'METADATA') {
|
|
58
|
-
const sourceBuffer = mediaSource.addSourceBuffer(`video/mp4; codecs="${event.info.
|
|
58
|
+
const sourceBuffer = mediaSource.addSourceBuffer(`video/mp4; codecs="${[event.info.output.videoMimeType, event.info.output.audioMimeType].filter(Boolean).join(',')}"`)
|
|
59
59
|
sourceBuffer.mode = 'segments'
|
|
60
60
|
mediaSource.duration = event.info.input.duration
|
|
61
61
|
headerBuffer = event.data
|
|
@@ -62,7 +62,7 @@ export default setup({
|
|
|
62
62
|
| { type: 'NEW_ATTACHMENTS', attachments: Attachment[] }
|
|
63
63
|
| { type: 'NEW_SUBTITLE_FRAGMENTS', subtitles: SubtitleFragment[] }
|
|
64
64
|
| { type: 'SUBTITLE_STREAMS_UPDATED', subtitlesStreams: SubtitleStream[] }
|
|
65
|
-
| { type: 'SELECT_SUBTITLE_STREAM', streamIndex: number }
|
|
65
|
+
| { type: 'SELECT_SUBTITLE_STREAM', streamIndex: number | undefined }
|
|
66
66
|
| { type: 'NEW_THUMBNAIL', thumbnail: Thumbnail }
|
|
67
67
|
| { type: 'DOWNLOADED_RANGES_UPDATED', downloadedRanges: DownloadedRange[] }
|
|
68
68
|
| { type: 'DESTROY' }
|
|
@@ -198,7 +198,10 @@ export default setup({
|
|
|
198
198
|
'PLAYING': { actions: assign({ media: ({ context }) => ({ ...context.media, paused: false }) }) },
|
|
199
199
|
'PAUSED': { actions: assign({ media: ({ context }) => ({ ...context.media, paused: true }) }) },
|
|
200
200
|
'ENDED': { actions: assign({ media: ({ context }) => ({ ...context.media, paused: true }) }) },
|
|
201
|
-
'TIME_UPDATE': { actions:
|
|
201
|
+
'TIME_UPDATE': { actions: [
|
|
202
|
+
assign({ media: ({ context, event }) => ({ ...context.media, currentTime: event.currentTime }) }),
|
|
203
|
+
sendTo('subtitles', ({ event }) => event)
|
|
204
|
+
] },
|
|
202
205
|
'VOLUME_UPDATE': { actions: assign({ media: ({ context, event }) => ({ ...context.media, muted: event.muted, volume: event.volume }) }) },
|
|
203
206
|
'SET_VOLUME': { actions: sendTo('media', ({ event }) => event) },
|
|
204
207
|
'PLAYBACK_RATE_UPDATE': { actions: assign({ media: ({ context, event }) => ({ ...context.media, playbackRate: event.playbackRate }) }) },
|