@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.
Files changed (37) hide show
  1. package/README.md +6 -6
  2. package/package.json +51 -51
  3. package/src/assets/picture-in-picture.svg +5 -5
  4. package/src/components/chrome.tsx +95 -95
  5. package/src/components/control-bar.tsx +333 -333
  6. package/src/components/overlay.tsx +91 -91
  7. package/src/components/playback-slider.tsx +174 -174
  8. package/src/components/progress-bar.tsx +251 -251
  9. package/src/components/settings.tsx +321 -321
  10. package/src/components/sound.tsx +100 -100
  11. package/src/components/tooltip-display.tsx +83 -83
  12. package/src/components/volume-slider.tsx +156 -156
  13. package/src/index.tsx +195 -195
  14. package/src/main.tsx +169 -169
  15. package/src/state-machines/data-source.ts +84 -84
  16. package/src/state-machines/index.ts +5 -5
  17. package/src/state-machines/media-properties.ts +82 -82
  18. package/src/state-machines/media-source.ts +129 -129
  19. package/src/state-machines/media.ts +255 -255
  20. package/src/state-machines/subtitles.ts +285 -285
  21. package/src/state-machines/thumbnails.ts +123 -123
  22. package/src/state-machines/utils.ts +94 -94
  23. package/src/utils/actor-utils.ts +19 -19
  24. package/src/utils/colors.ts +8 -8
  25. package/src/utils/context.ts +23 -23
  26. package/src/utils/fonts.ts +159 -159
  27. package/src/utils/index.ts +229 -229
  28. package/src/utils/languages.ts +262 -262
  29. package/src/utils/mp4box.ts +74 -74
  30. package/src/utils/time.ts +15 -15
  31. package/src/utils/use-local-storage.ts +30 -30
  32. package/src/utils/use-scrub.ts +40 -40
  33. package/src/utils/volume-utils.ts +39 -39
  34. package/src/utils/window-height.ts +19 -19
  35. package/src/vite-env.d.ts +1 -1
  36. package/tsconfig.json +28 -28
  37. 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
+ }