@banou/media-player 0.3.0 → 0.4.1

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 (74) 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 +2576 -2250
  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 -11
  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 -435
  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/utils/volume-utils.ts +39 -0
  61. package/src/utils/window-height.ts +19 -0
  62. package/tsconfig.json +4 -2
  63. package/build/chrome/bottom.d.ts +0 -22
  64. package/build/chrome/index.d.ts +0 -28
  65. package/build/chrome/overlay.d.ts +0 -9
  66. package/build/languages.d.ts +0 -260
  67. package/build/use-local-storage.d.ts +0 -6
  68. package/src/chrome/bottom.tsx +0 -697
  69. package/src/chrome/index.tsx +0 -203
  70. package/src/chrome/overlay.tsx +0 -104
  71. package/src/use-local-storage.ts +0 -32
  72. /package/src/{languages.ts → utils/languages.ts} +0 -0
  73. /package/src/{mp4box.ts → utils/mp4box.ts} +0 -0
  74. /package/src/{use-scrub.ts → utils/use-scrub.ts} +0 -0
@@ -1,697 +0,0 @@
1
- /// <reference types="@emotion/react/types/css-prop" />
2
- import type { Dispatch, DOMAttributes, HTMLAttributes, MouseEvent, MouseEventHandler, MutableRefObject, SetStateAction } from 'react'
3
-
4
- import type { ChromeOptions } from '.'
5
- import type { FKNVideoControl, Subtitle, TransmuxError } from '..'
6
-
7
- import { css } from '@emotion/react'
8
- import { useEffect, useMemo, useRef, useState } from 'react'
9
- import { Volume, Volume1, Volume2, VolumeX, AlertTriangle } from 'react-feather'
10
-
11
- import useScrub from '../use-scrub'
12
- import { tagToLanguage } from '../languages'
13
- import useNamespacedLocalStorage from '../use-local-storage'
14
- import { Tooltip } from 'react-tooltip'
15
-
16
- const style = css`
17
- position: relative;
18
- grid-column: 1;
19
- grid-row: 1;
20
- align-self: end;
21
- height: calc(4.8rem + var(--background-padding));
22
- width: 100%;
23
- padding: 0 2rem;
24
- margin: 0 auto;
25
-
26
- /* subtle background black gradient for scenes with high brightness to properly display white progress bar */
27
- padding-top: var(--background-padding);
28
- background: linear-gradient(0deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.1) calc(100% - 1rem), rgba(0,0,0,0) 100%);
29
-
30
- .progress-bar {
31
- position: relative;
32
- height: .4rem;
33
- cursor: pointer;
34
- user-select: none;
35
- transition: transform .2s ease-in-out;
36
-
37
-
38
- &:hover {
39
- .background-bar {
40
- transform: scaleY(1.5);
41
- }
42
- }
43
- .background-bar {
44
- position: absolute;
45
- inset: 0;
46
- background-color: hsla(0, 100%, 100%, .2);
47
- }
48
-
49
- .cursor-time {
50
- pointer-events: none;
51
- position: absolute;
52
- top: -2rem;
53
- width: 5rem;
54
- margin-left: -2.5rem;
55
- display: flex;
56
- justify-content: center;
57
- }
58
-
59
- .load {
60
- transform-origin: 0 0;
61
- background-color: hsla(0, 100%, 100%, .4);
62
- position: absolute;
63
- bottom: 0;
64
- height: .4rem;
65
- width: 100%;
66
- }
67
- .play {
68
- transform-origin: 0 0;
69
- background-color: hsla(0, 100%, 50%, .8);
70
- position: absolute;
71
- bottom: 0;
72
- height: .4rem;
73
- width: 100%;
74
- }
75
- .padding {
76
- position: absolute;
77
- bottom: -4px;
78
- height: 1.6rem;
79
- width: 100%;
80
- }
81
- }
82
-
83
- .controls {
84
- height: 100%;
85
- display: grid;
86
- align-items: center;
87
- grid-template-columns: 5rem fit-content(4.8rem) 20rem auto fit-content(10rem) 5rem 5rem 5rem;
88
- grid-gap: 1rem;
89
- color: #fff;
90
- user-select: none;
91
-
92
- .picture-in-picture, .fullscreen, .play-button {
93
- height: 100%;
94
- color: #fff;
95
- background-color: transparent;
96
- border: none;
97
- cursor: pointer;
98
- }
99
-
100
- .volume-area {
101
- display: flex;
102
- /* grid-template-columns: 4.8rem fit-content(0rem); */
103
- height: 100%;
104
- cursor: pointer;
105
-
106
- .mute-button {
107
- color: #fff;
108
- border: none;
109
- background: none;
110
- height: 100%;
111
- width: 4.8rem;
112
- cursor: pointer;
113
- }
114
-
115
- .volume-panel {
116
- display: inline-block;
117
- width: 0;
118
- /* width: 100%; */
119
- /* width: 12rem; */
120
- height: 100%;
121
- -webkit-transition: margin .2s cubic-bezier(0.4,0,1,1),width .2s cubic-bezier(0.4,0,1,1);
122
- transition: margin .2s cubic-bezier(0.4,0,1,1),width .2s cubic-bezier(0.4,0,1,1);
123
- cursor: pointer;
124
- outline: 0;
125
-
126
- &.volume-control-hover {
127
- width: 6rem;
128
- /* width: 52px; */
129
- margin-right: 3px;
130
- -webkit-transition: margin .2s cubic-bezier(0,0,0.2,1),width .2s cubic-bezier(0,0,0.2,1);
131
- transition: margin .2s cubic-bezier(0,0,0.2,1),width .2s cubic-bezier(0,0,0.2,1);
132
- }
133
-
134
- .slider {
135
- height: 100%;
136
- min-height: 36px;
137
- position: relative;
138
- overflow: hidden;
139
-
140
- .slider-handle {
141
- /* left: 40px; */
142
- position: absolute;
143
- top: 50%;
144
- width: 12px;
145
- height: 12px;
146
- border-radius: 6px;
147
- margin-top: -6px;
148
- margin-left: -5px;
149
- /* background: #fff; */
150
- }
151
- .slider-handle::before, .slider-handle::after {
152
- content: "";
153
- position: absolute;
154
- display: block;
155
- top: 50%;
156
- left: 0;
157
- height: 3px;
158
- margin-top: -2px;
159
- width: 64px;
160
- }
161
- .slider-handle::before {
162
- left: -58px;
163
- background: #fff;
164
- }
165
- .slider-handle::after {
166
- left: 6px;
167
- background: rgba(255,255,255,.2);
168
- }
169
- }
170
- }
171
- }
172
-
173
- .time {
174
- font-family: Roboto;
175
- padding-bottom: .5rem;
176
- }
177
-
178
- .custom-controls {
179
- display: flex;
180
- justify-content: end;
181
- }
182
-
183
- .error-area {
184
- position: relative;
185
- height: 3.8rem;
186
-
187
- .error-menu {
188
- position: absolute;
189
- bottom: 4.8rem;
190
- left: -10rem;
191
- width: 25rem;
192
- /* background: rgba(0, 0, 0, .2); */
193
- /* background: rgb(35, 35, 35); */
194
- padding: 1rem;
195
- background-color: rgb(51, 51, 51);
196
-
197
- &.hide {
198
- display: none;
199
- }
200
-
201
- .error-menu-message {
202
-
203
- }
204
-
205
- button {
206
- width: 100%;
207
- padding: 0.5rem 0;
208
- background: none;
209
- border: none;
210
- color: #fff;
211
- cursor: pointer;
212
- text-shadow: 2px 2px #000;
213
- }
214
- }
215
-
216
- .error-menu-button {
217
- display: grid;
218
- grid-auto-flow: column;
219
- align-items: center;
220
- height: 100%;
221
- width: 100%;
222
- cursor: pointer;
223
- color: #eb1010;
224
- font-weight: bold;
225
- background: none;
226
- border: none;
227
- }
228
- }
229
-
230
- .subtitle-area {
231
- position: relative;
232
- height: 3.8rem;
233
-
234
- .subtitle-menu {
235
- position: absolute;
236
- bottom: 4.8rem;
237
- left: -1rem;
238
- /* background: rgba(0, 0, 0, .2); */
239
- /* background: rgb(35, 35, 35); */
240
- padding: 1rem 0;
241
- background-color: rgb(17, 17, 17);
242
- /* background-color: rgb(51, 51, 51); */
243
- border-radius: .3rem;
244
-
245
- &.hide {
246
- display: none;
247
- }
248
-
249
- button {
250
- width: 100%;
251
- padding: 1rem;
252
- /* padding: 0.5rem 0; */
253
- background: none;
254
- border: none;
255
- color: #fff;
256
- cursor: pointer;
257
- text-shadow: 2px 2px #000;
258
-
259
- &:hover {
260
- background-color: rgb(51, 51, 51);
261
- }
262
- }
263
- }
264
-
265
- .subtitle-menu-button {
266
- height: 100%;
267
- width: 100%;
268
- cursor: pointer;
269
- color: #fff;
270
- background: none;
271
- border: none;
272
- }
273
- }
274
- }
275
- `
276
-
277
- export type BottomOptions = {
278
- video: MutableRefObject<HTMLVideoElement | undefined>
279
- videoReactive: HTMLVideoElement | undefined,
280
- duration?: number
281
- currentTime?: number
282
- togglePlay: () => void
283
- isSubtitleMenuHidden: boolean
284
- setIsSubtitleMenuHidden: Dispatch<SetStateAction<boolean>>
285
- isErrorMenuHidden: boolean
286
- setIsErrorMenuHidden: Dispatch<SetStateAction<boolean>>
287
- setCurrentSubtitleTrack: Dispatch<SetStateAction<number | undefined>>
288
- isFullscreen: boolean
289
- toggleFullscreen: () => void
290
- subtitleTrack: Subtitle | undefined
291
- errors: TransmuxError[]
292
- customControls?: FKNVideoControl[]
293
- } &
294
- Pick<ChromeOptions, 'seek' | 'setVolume' | 'loadedTime' | 'isPlaying' | 'tracks' | 'pictureInPicture'> &
295
- HTMLAttributes<HTMLDivElement>
296
-
297
- export default ({
298
- video,
299
- videoReactive,
300
- duration,
301
- currentTime,
302
- togglePlay,
303
- seek,
304
- setVolume,
305
- isSubtitleMenuHidden,
306
- setIsSubtitleMenuHidden,
307
- isErrorMenuHidden,
308
- setIsErrorMenuHidden,
309
- setCurrentSubtitleTrack,
310
- loadedTime,
311
- isPlaying,
312
- tracks,
313
- isFullscreen,
314
- toggleFullscreen,
315
- subtitleTrack,
316
- pictureInPicture,
317
- errors,
318
- customControls,
319
- ...rest
320
- }: BottomOptions) => {
321
- const progressBarRef = useRef<HTMLDivElement>(null)
322
- const volumeBarRef = useRef<HTMLDivElement>(null)
323
- const [hiddenVolumeArea, setHiddenVolumeArea] = useState(false)
324
- const { scrub: seekScrub, value: seekScrubValue, setValue: setSeekValue } = useScrub({ ref: progressBarRef })
325
- const isPictureInPictureEnabled = useMemo(() => document.pictureInPictureEnabled, [])
326
- const [hasShownErrorPopup, setHasShownErrorPopup] = useState(false)
327
-
328
- const useStoredValue = useNamespacedLocalStorage<{ volume: number, muted: boolean }>('fkn-media-player')
329
- const [volume, setStoredVolume] = useStoredValue('volume', 1)
330
- const [isMuted, setStoredMuted] = useStoredValue('muted', false)
331
- const { scrub: volumeScrub, value: volumeScrubValue, setValue: updateVolume } = useScrub({ ref: volumeBarRef, defaultValue: volume })
332
-
333
- useEffect(() => {
334
- if (hasShownErrorPopup) return
335
- setHasShownErrorPopup(true)
336
- errorMenuButtonClick(undefined)
337
- }, [hasShownErrorPopup, errors.length])
338
-
339
- useEffect(() => {
340
- if (seekScrubValue === undefined) return
341
- seek(seekScrubValue * (duration ?? 0))
342
- }, [seekScrubValue])
343
-
344
- useEffect(() => {
345
- if (isMuted) {
346
- setVolume(0)
347
- setStoredMuted(isMuted)
348
- if (volumeScrubValue) setStoredVolume(volumeScrubValue)
349
- return
350
- }
351
- if (volumeScrubValue === undefined) return
352
- setVolume(volumeScrubValue ** 2)
353
- setStoredVolume(volumeScrubValue)
354
- setStoredMuted(isMuted)
355
- }, [volumeScrubValue, isMuted])
356
-
357
- const hoverVolumeArea: React.DOMAttributes<HTMLDivElement>['onMouseOver'] = () => {
358
- setHiddenVolumeArea(false)
359
- }
360
-
361
- const mouseOutBottom: React.DOMAttributes<HTMLDivElement>['onMouseOut'] = (ev) => {
362
- if (ev.currentTarget !== ev.relatedTarget && ev.relatedTarget !== null) return
363
- setHiddenVolumeArea(true)
364
- }
365
-
366
- const toggleMuteButton = () => {
367
- setStoredMuted(value => !value)
368
- }
369
-
370
- const setSubtitleTrack = (ev: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>, track: Subtitle | undefined, trackIndex: number) => {
371
- setCurrentSubtitleTrack(track === undefined ? track : trackIndex)
372
- }
373
-
374
- const subtitleMenuButtonClick: MouseEventHandler<HTMLButtonElement> = (ev) => {
375
- if (!isSubtitleMenuHidden && ev.relatedTarget === null) {
376
- setIsSubtitleMenuHidden(value => !value)
377
- return
378
- }
379
- setIsSubtitleMenuHidden(false)
380
- const clickListener = (ev: globalThis.MouseEvent) => {
381
- ev.stopPropagation()
382
- setIsSubtitleMenuHidden(true)
383
- document.removeEventListener('click', clickListener)
384
- }
385
- setTimeout(() => {
386
- document.addEventListener('click', clickListener)
387
- }, 0)
388
- }
389
-
390
- const errorMenuButtonClick = (ev: MouseEvent<HTMLDivElement, globalThis.MouseEvent> | undefined) => {
391
- if (!isErrorMenuHidden && ev?.relatedTarget === null) {
392
- setIsErrorMenuHidden(value => !value)
393
- return
394
- }
395
- setIsErrorMenuHidden(false)
396
- const clickListener = (ev: globalThis.MouseEvent) => {
397
- ev.stopPropagation()
398
- setIsErrorMenuHidden(true)
399
- document.removeEventListener('click', clickListener)
400
- }
401
- setTimeout(() => {
402
- document.addEventListener('click', clickListener)
403
- }, 0)
404
- }
405
-
406
- const SubtitleTrackToName = ({ subtitleTrack: subtitle }: { subtitleTrack: Subtitle | undefined }) => {
407
- if (!subtitle) return <>Disabled</>
408
-
409
- if (!subtitle.title && !subtitle.language) return <>Default</>
410
- const language =
411
- subtitle.language === 'jpn' ? 'ja'
412
- : tagToLanguage(subtitle.language) ? tagToLanguage(subtitle.language)
413
- : subtitle.language
414
-
415
- if (subtitle.title) {
416
- return (
417
- <>
418
- {subtitle.title?.replace('subs', '')}{language ? `(${language})` : ''}
419
- </>
420
- )
421
- }
422
-
423
- return (
424
- <>
425
- {language}
426
- </>
427
- )
428
- }
429
-
430
- const shownErrors = useMemo(() =>
431
- [
432
- ...new Set(
433
- errors
434
- .map(error =>
435
- error.message === 'non monotonicaly increasing DTS values'
436
- ? (
437
- error.count > 2
438
- ? 'The video seems malformatted and might have issues(skipped frames) during playback'
439
- : null
440
- )
441
- : 'An unexpected error occured while trying to play the video'
442
- )
443
- )
444
- ]
445
- .filter(Boolean)
446
- .map(str => <div key={str} className="error-menu-message">{str}</div>)
447
- , [errors])
448
-
449
- useEffect(() => {
450
- const eventListener = (ev: KeyboardEvent) => {
451
- if (ev.key === 'f') toggleFullscreen()
452
- if (ev.key === 'k') togglePlay()
453
- if (ev.key === ' ') togglePlay()
454
- if (ev.key === 'm') toggleMuteButton()
455
- if (ev.key === 'ArrowUp') {
456
- if (volume <= 0.95) {
457
- updateVolume(volume + 0.05)
458
- } else {
459
- updateVolume(1)
460
- }
461
- }
462
- if (ev.key === 'ArrowDown') {
463
- if (volume >= 0.05) {
464
- updateVolume(volume - 0.05)
465
- } else {
466
- updateVolume(0)
467
- }
468
- }
469
- if (ev.key === 'ArrowLeft') {
470
- if (!videoReactive) return
471
- videoReactive.currentTime -= (
472
- ev.shiftKey ? 20
473
- : ev.ctrlKey ? 2
474
- : 5
475
- )
476
- }
477
- if (ev.key === 'ArrowRight') {
478
- if (!videoReactive) return
479
- videoReactive.currentTime += (
480
- ev.shiftKey ? 20
481
- : ev.ctrlKey ? 2
482
- : 5
483
- )
484
- }
485
- }
486
- window.addEventListener('keydown', eventListener)
487
- return () => window.removeEventListener('keydown', eventListener)
488
- }, [videoReactive, toggleFullscreen, togglePlay, toggleMuteButton])
489
-
490
- const [progressBarOverTime, setProgressBarOverTime] = useState<number | undefined>(undefined)
491
-
492
- const onProgressBarOver: DOMAttributes<HTMLDivElement>['onMouseMove'] = (ev) => {
493
- const percentage = ev.nativeEvent.offsetX / ev.currentTarget.offsetWidth
494
- const time = percentage * (duration ?? 0)
495
- setProgressBarOverTime(time)
496
- }
497
-
498
- const hideProgressBarTime = () => {
499
- if (!progressBarRef.current) return
500
- setProgressBarOverTime(undefined)
501
- }
502
-
503
- return (
504
- <div css={style} onMouseOut={mouseOutBottom} {...rest}>
505
- <div className="preview"></div>
506
- <div
507
- ref={progressBarRef}
508
- className="progress-bar"
509
- onMouseMove={onProgressBarOver}
510
- onMouseOut={hideProgressBarTime}
511
- >
512
- <div className="background-bar"></div>
513
- {
514
- progressBarOverTime
515
- ? (
516
- <div className="cursor-time" data-tip={progressBarOverTime} style={{ left: `${1 / ((duration ?? 0) / (progressBarOverTime ?? 1)) * 100}%` }}>
517
- {
518
- new Date((progressBarOverTime ?? 0) * 1000)
519
- .toISOString()
520
- .substr(
521
- (duration ?? 0) >= 3600
522
- ? 11
523
- : 14,
524
- (duration ?? 0) >= 3600
525
- ? 8
526
- : 5
527
-
528
- )
529
- }
530
- </div>
531
- )
532
- : undefined
533
- }
534
- <div className="progress"></div>
535
- {/* bar showing the currently loaded progress */}
536
- <div className="load" style={{ transform: `scaleX(${1 / ((duration ?? 0) / (loadedTime?.[1] ?? 0))})` }}></div>
537
- {/* bar to show when hovering to potentially seek */}
538
- <div className="hover"></div>
539
- {/* bar displaying the current playback progress */}
540
- <div className="play" style={{
541
- transform: `scaleX(${
542
- typeof duration !== 'number' || typeof currentTime !== 'number'
543
- ? 0
544
- : 1 / ((duration ?? 0) / (currentTime ?? 0))
545
- })`
546
- }}>
547
- </div>
548
- <div className="chapters"></div>
549
- <div className="scrubber"></div>
550
- <div className="padding" onMouseDown={seekScrub}></div>
551
- </div>
552
- <div className="controls">
553
- <Tooltip id="play-button-tooltip" place="top-start" >
554
- {
555
- isPlaying
556
- ? 'Pause (k)'
557
- : 'Play (k)'
558
- }
559
- </Tooltip>
560
- <button className="play-button" type="button" data-tip data-tooltip-id="play-button-tooltip" onClick={togglePlay}>
561
- {
562
- isPlaying
563
- ? <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-pause"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>
564
- : <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-play"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
565
- }
566
- </button>
567
- <div className="volume-area" onMouseOver={hoverVolumeArea}>
568
- <Tooltip
569
- id="mute-button-tooltip"
570
- place="top"
571
- >
572
- {
573
- isMuted
574
- ? 'Unmute (m)'
575
- : 'Mute (m)'
576
- }
577
- </Tooltip>
578
- <button className="mute-button" data-tip data-tooltip-id="mute-button-tooltip" onClick={toggleMuteButton}>
579
- {
580
- isMuted ? <VolumeX/>
581
- : (volume ?? 0) > 0.66 ? <Volume2/>
582
- : (volume ?? 0) > 0.33 ? <Volume1/>
583
- : (volume ?? 0) > 0 ? <Volume/>
584
- : <VolumeX/>
585
- }
586
- </button>
587
- <div ref={volumeBarRef} className={`volume-panel${hiddenVolumeArea ? '' : ' volume-control-hover'}`} onMouseDown={volumeScrub}>
588
- <div className="slider">
589
- <div className="slider-handle" style={{ left: `${(volume ?? 0) * 100}%` }}></div>
590
- </div>
591
- </div>
592
- </div>
593
- <div className="time">
594
- <span>{new Date((currentTime ?? 0) * 1000).toISOString().substr(11, 8)}</span>
595
- <span> / </span>
596
- <span>{duration ? new Date(duration * 1000).toISOString().substr(11, 8) : ''}</span>
597
- </div>
598
- <div className="custom-controls">
599
- {
600
- customControls?.length
601
- ? customControls.map((Control, i) => <Control key={i}/>)
602
- : null
603
- }
604
- </div>
605
- <div className="error-area">
606
- {
607
- shownErrors.length
608
- ? (
609
- <>
610
- <div className={`error-menu ${isErrorMenuHidden ? 'hide' : ''}`}>
611
- {
612
- isErrorMenuHidden
613
- ? null
614
- : shownErrors
615
- }
616
- </div>
617
- <div className="error-menu-button" onClick={errorMenuButtonClick}>
618
- <AlertTriangle/>
619
- Error
620
- </div>
621
- </>
622
- )
623
- : null
624
- }
625
- </div>
626
- <Tooltip
627
- id="subtitle-button-tooltip"
628
- globalEventOff="click"
629
-
630
- disable={!isSubtitleMenuHidden}
631
- class="tooltip"
632
- >
633
- <span>
634
- Subtitles
635
- </span>
636
- </Tooltip>
637
- <div className="subtitle-area">
638
- {
639
- tracks.length
640
- ? (
641
- <>
642
- <div className={`subtitle-menu ${isSubtitleMenuHidden ? 'hide' : ''}`}>
643
- {
644
- isSubtitleMenuHidden
645
- ? null
646
- : (
647
- [...tracks, undefined].map((track, i) =>
648
- <button key={!track ? 'disabled' : i} onClick={ev => setSubtitleTrack(ev, track, i)}>
649
- <SubtitleTrackToName subtitleTrack={track}/>
650
- </button>
651
- )
652
- )
653
- }
654
- </div>
655
- <button className="subtitle-menu-button" data-tip data-tooltip-id="subtitle-button-tooltip" onClick={subtitleMenuButtonClick}>
656
- <SubtitleTrackToName subtitleTrack={subtitleTrack}/>
657
- </button>
658
- </>
659
- )
660
- : null
661
- }
662
- </div>
663
- <Tooltip
664
- id="pip-button-tooltip"
665
-
666
- place="top"
667
- >
668
- Picture in Picture
669
- </Tooltip>
670
- {
671
- isPictureInPictureEnabled
672
- ? (
673
- <button className="picture-in-picture" data-tip data-tooltip-id="pip-button-tooltip" onClick={pictureInPicture}>
674
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="21" height="14" rx="2" ry="2"></rect><rect x="12.5" y="8.5" width="8" height="6" rx="2" ry="2"></rect></svg>
675
- </button>
676
- )
677
- : null
678
- }
679
- <Tooltip id="fullscreen-button-tooltip" place="top-end">
680
- {
681
- isFullscreen
682
- ? 'Exit full screen (f)'
683
- : 'Full screen (f)'
684
- }
685
- </Tooltip>
686
- <button className="fullscreen" data-tip data-tooltip-id="fullscreen-button-tooltip" onClick={toggleFullscreen}>
687
- {
688
- isFullscreen
689
- ? <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-minimize"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path></svg>
690
- : <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-maximize"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>
691
- }
692
- </button>
693
- </div>
694
- </div>
695
- )
696
- }
697
-