@banou/media-player 0.2.6 → 0.4.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.
Files changed (76) 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 +2593 -2230
  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 -10
  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 -434
  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/{use-scrub.ts → utils/use-scrub.ts} +2 -1
  61. package/src/utils/volume-utils.ts +39 -0
  62. package/src/utils/window-height.ts +19 -0
  63. package/src/vite-env.d.ts +1 -1
  64. package/tsconfig.json +4 -2
  65. package/tsconfig.node.json +9 -9
  66. package/build/chrome/bottom.d.ts +0 -20
  67. package/build/chrome/index.d.ts +0 -27
  68. package/build/chrome/overlay.d.ts +0 -9
  69. package/build/languages.d.ts +0 -260
  70. package/build/use-local-storage.d.ts +0 -6
  71. package/src/chrome/bottom.tsx +0 -651
  72. package/src/chrome/index.tsx +0 -199
  73. package/src/chrome/overlay.tsx +0 -104
  74. package/src/use-local-storage.ts +0 -32
  75. /package/src/{languages.ts → utils/languages.ts} +0 -0
  76. /package/src/{mp4box.ts → utils/mp4box.ts} +0 -0
@@ -0,0 +1,269 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react"
2
+ import { css } from "@emotion/react"
3
+ import { ChevronRight, Settings } from "react-feather"
4
+
5
+ import { MediaMachineContext } from "../state-machines"
6
+ import { TooltipDisplay } from "./tooltip-display"
7
+ import { fonts } from "../utils/fonts"
8
+
9
+ const style = css`
10
+ position: relative;
11
+
12
+ .popover {
13
+ position: absolute;
14
+ right: 50%;
15
+ transform: translateX(50%);
16
+
17
+ width: 180px;
18
+ height: 160px;
19
+ top: -180px;
20
+ @media (min-width: 768px) {
21
+ width: 250px;
22
+ height: 160px;
23
+ }
24
+
25
+ display: flex;
26
+ flex-direction: column;
27
+
28
+ border-radius: 8px;
29
+ background-color: rgba(28,28,28,0.95);
30
+ ${fonts.bMedium.regular}
31
+
32
+ z-index: 4;
33
+
34
+ > div {
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: space-between;
38
+
39
+ color: #fff;
40
+ outline: none;
41
+
42
+ padding: 8px 6px 8px 12px;
43
+
44
+ width: 100%;
45
+ height: 100%;
46
+ cursor: pointer;
47
+
48
+ :first-of-type {
49
+ border-radius: 8px 8px 0 0;
50
+ }
51
+ :last-of-type {
52
+ border-radius: 0 0 8px 8px;
53
+ }
54
+ :hover {
55
+ background-color: rgba(255,255,255,.1);
56
+ }
57
+
58
+ > div {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+
63
+ .secondary {
64
+ color: #eee;
65
+ ${fonts.bSmall.regular}
66
+ }
67
+ }
68
+ }
69
+ }
70
+ `
71
+
72
+ enum PopoverContent {
73
+ Default,
74
+ PlaybackRate,
75
+ Advanced,
76
+ SelectNewSources,
77
+ Subtitles
78
+ }
79
+
80
+ const SettingsAction = () => {
81
+ const mediaActor = MediaMachineContext.useActorRef()
82
+ const playbackRate = MediaMachineContext.useSelector((state) => state.context.media.playbackRate)
83
+ const subtitleStreams = MediaMachineContext.useSelector((state) => state.context.subtitleStreams)
84
+ const selectedSubtitleStreamIndex = MediaMachineContext.useSelector((state) => state.context.selectedSubtitleStreamIndex)
85
+
86
+ const [isOpenPopover, setIsOpenPopover] = useState(false)
87
+ const [popoverContent, setPopoverContent] = useState(PopoverContent.Default)
88
+ const settingsContainerRef = useRef<HTMLDivElement>(null)
89
+
90
+ const togglePopover = () => {
91
+ setIsOpenPopover(!isOpenPopover)
92
+ setPopoverContent(PopoverContent.Default)
93
+ }
94
+
95
+ const setPlaybackRate = (rate: number) => {
96
+ mediaActor.send({ type: 'SET_PLAYBACK_RATE', playbackRate: rate })
97
+ setPopoverContent(PopoverContent.Default)
98
+ }
99
+
100
+ useEffect(() => {
101
+ const handleClickOutside = (ev: MouseEvent) => {
102
+ if (
103
+ isOpenPopover &&
104
+ settingsContainerRef.current &&
105
+ !settingsContainerRef.current.contains(ev.target as Node)
106
+ ) {
107
+ togglePopover()
108
+ }
109
+ }
110
+ document.addEventListener('mousedown', handleClickOutside)
111
+ return () => document.removeEventListener('mousedown', handleClickOutside)
112
+ }, [isOpenPopover])
113
+
114
+ const languagesWithStreamIndex = useMemo(
115
+ () => subtitleStreams.map(({ header }) => ({ streamIndex: header.streamIndex, language: header.parsed.info.Title })),
116
+ [subtitleStreams.length]
117
+ )
118
+
119
+ const setLanguage = (languageWithStreamIndex: { streamIndex: number, language: string }) => () => {
120
+ mediaActor.send({ type: 'SELECT_SUBTITLE_STREAM', streamIndex: languageWithStreamIndex.streamIndex })
121
+ togglePopover()
122
+ }
123
+
124
+ const changePopoverContent = (newPopoverContent: PopoverContent) => () => {
125
+ setPopoverContent(newPopoverContent)
126
+ }
127
+
128
+ return (
129
+ <div css={style} ref={settingsContainerRef}>
130
+ <TooltipDisplay
131
+ id='settings'
132
+ disabled={isOpenPopover}
133
+ text={
134
+ <button
135
+ className='settings'
136
+ type='button'
137
+ onClick={togglePopover}
138
+ >
139
+ <Settings />
140
+ </button>
141
+ }
142
+ toolTipText={
143
+ <span>
144
+ Settings
145
+ </span>
146
+ }
147
+ />
148
+ {
149
+ isOpenPopover && popoverContent === PopoverContent.Default && (
150
+ <div className='popover'>
151
+ <div onClick={changePopoverContent(PopoverContent.Advanced)}>
152
+ <span>Advanced</span>
153
+ <div>
154
+ <ChevronRight />
155
+ </div>
156
+ </div>
157
+ <div onClick={changePopoverContent(PopoverContent.SelectNewSources)}>
158
+ <div>Select new sources</div>
159
+ <div>
160
+ <ChevronRight />
161
+ </div>
162
+ </div>
163
+ <div onClick={changePopoverContent(PopoverContent.Subtitles)}>
164
+ <div>Subtitles</div>
165
+ <div>
166
+ <ChevronRight />
167
+ </div>
168
+ </div>
169
+ <div onClick={changePopoverContent(PopoverContent.PlaybackRate)}>
170
+ <div>Playback speed</div>
171
+ <div>
172
+ <span className='secondary'>
173
+ {
174
+ playbackRate === 1
175
+ ? '(Default)'
176
+ : playbackRate.toFixed(2)
177
+ }
178
+ </span>
179
+ <ChevronRight />
180
+ </div>
181
+ </div>
182
+ </div>
183
+ )
184
+ }
185
+ {
186
+ isOpenPopover && popoverContent === PopoverContent.PlaybackRate && (
187
+ <div className='popover'>
188
+ <div onClick={changePopoverContent(PopoverContent.Default)}>
189
+ <div>Back</div>
190
+ <div></div>
191
+ </div>
192
+ <div>
193
+ <div>Playback speed</div>
194
+ <div>
195
+ <button
196
+ type='button'
197
+ onClick={() => setPlaybackRate(0.5)}
198
+ >
199
+ 0.5x
200
+ </button>
201
+ <button
202
+ type='button'
203
+ onClick={() => setPlaybackRate(1)}
204
+ >
205
+ 1x
206
+ </button>
207
+ <button
208
+ type='button'
209
+ onClick={() => setPlaybackRate(1.5)}
210
+ >
211
+ 1.5x
212
+ </button>
213
+ <button
214
+ type='button'
215
+ onClick={() => setPlaybackRate(2)}
216
+ >
217
+ 2x
218
+ </button>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ )
223
+ }
224
+ {
225
+ isOpenPopover && popoverContent === PopoverContent.Advanced && (
226
+ <div className='popover'>
227
+ <div onClick={changePopoverContent(PopoverContent.Default)}>
228
+ <div>Back</div>
229
+ <div></div>
230
+ </div>
231
+ </div>
232
+ )
233
+ }
234
+ {
235
+ isOpenPopover && popoverContent === PopoverContent.SelectNewSources && (
236
+ <div className='popover'>
237
+ <div onClick={changePopoverContent(PopoverContent.Default)}>
238
+ <div>Back</div>
239
+ <div></div>
240
+ </div>
241
+ </div>
242
+ )
243
+ }
244
+ {
245
+ isOpenPopover && popoverContent === PopoverContent.Subtitles && (
246
+ <div className='popover'>
247
+ {
248
+ languagesWithStreamIndex.map((languageWithStreamIndex) => (
249
+ <div
250
+ key={languageWithStreamIndex.streamIndex}
251
+ onClick={setLanguage(languageWithStreamIndex)}
252
+ >
253
+ <div>{languageWithStreamIndex.language}</div>
254
+ <div>{selectedSubtitleStreamIndex === languageWithStreamIndex.streamIndex ? '✓' : ''}</div>
255
+ </div>
256
+ ))
257
+ }
258
+ <div onClick={changePopoverContent(PopoverContent.Default)}>
259
+ <div>Back</div>
260
+ <div></div>
261
+ </div>
262
+ </div>
263
+ )
264
+ }
265
+ </div>
266
+ )
267
+ }
268
+
269
+ export default SettingsAction
@@ -0,0 +1,101 @@
1
+ import { Volume1, Volume2, VolumeX } from "react-feather"
2
+ import { css } from "@emotion/react"
3
+
4
+ import { MediaMachineContext } from "../state-machines"
5
+ import { TooltipDisplay } from "./tooltip-display"
6
+ import { fonts } from "../utils/fonts"
7
+ import VolumeSlider from "./volume-slider"
8
+ import { MutableRefObject, useMemo } from 'react'
9
+ import { linearToLogVolume, logToLinearVolume } from "../utils/volume-utils"
10
+
11
+ const style = css`
12
+ position: relative;
13
+ display: flex;
14
+ align-items: center;
15
+
16
+ .volume-slider-container {
17
+ display: flex;
18
+ align-items: center;
19
+
20
+ width: 0;
21
+ margin-right: 0;
22
+ overflow: hidden;
23
+ pointer-events: none;
24
+
25
+ transition: margin .2s cubic-bezier(0.4,0,1,1), width .2s cubic-bezier(0.4,0,1,1);
26
+ -webkit-transition: margin .2s cubic-bezier(0.4,0,1,1), width .2s cubic-bezier(0.4,0,1,1);
27
+
28
+ .volume-slider-background {
29
+ display: flex;
30
+ flex-direction: row;
31
+ align-items: center;
32
+
33
+ border-radius: 4px;
34
+
35
+ .volume-value {
36
+ ${fonts.bMedium.regular};
37
+ color: #ffffff;
38
+ }
39
+ }
40
+ }
41
+
42
+ &:hover .volume-slider-container {
43
+ width: 9rem;
44
+
45
+ transition: margin .2s cubic-bezier(0,0,0.2,1), width .2s cubic-bezier(0,0,0.2,1);
46
+ -webkit-transition: margin .2s cubic-bezier(0,0,0.2,1), width .2s cubic-bezier(0,0,0.2,1);
47
+
48
+ pointer-events: auto;
49
+ }
50
+ `
51
+
52
+ // todo: try to find a better type for `ref`
53
+ const Sound = ({ ref }: { ref: any }) => {
54
+ const mediaActor = MediaMachineContext.useActorRef()
55
+ const volume = MediaMachineContext.useSelector((state) => state.context.media.volume)
56
+ const muted = MediaMachineContext.useSelector((state) => state.context.media.muted)
57
+
58
+ const linearVolume = useMemo(() => logToLinearVolume(volume), [volume])
59
+ const setVolume = (newLinearVolume: number, muted: boolean) =>
60
+ mediaActor.send({
61
+ type: 'SET_VOLUME',
62
+ muted,
63
+ volume: linearToLogVolume(newLinearVolume)
64
+ })
65
+
66
+ return (
67
+ <div css={style}>
68
+ <TooltipDisplay
69
+ id='sound'
70
+ text={
71
+ <button
72
+ className='sound'
73
+ type='button'
74
+ onClick={() => setVolume(linearVolume, !muted)}
75
+ ref={ref}
76
+ >
77
+ {muted || volume === 0
78
+ ? <VolumeX size={18} color='#fff' />
79
+ : volume <= 0.5
80
+ ? <Volume1 size={18} color='#fff' />
81
+ : <Volume2 size={18} color='#fff' />
82
+ }
83
+ </button>
84
+ }
85
+ toolTipText={
86
+ <span>{muted ? 'Unmute (m)' : 'Mute (m)'}</span>
87
+ }
88
+ />
89
+ <div className='volume-slider-container'>
90
+ <div className='volume-slider-background'>
91
+ <VolumeSlider
92
+ value={muted ? 0 : linearVolume}
93
+ onChange={linearValue => setVolume(linearValue, false)}
94
+ />
95
+ </div>
96
+ </div>
97
+ </div>
98
+ )
99
+ }
100
+
101
+ export default Sound
@@ -0,0 +1,83 @@
1
+ import { ReactNode } from 'react'
2
+ import { css } from '@emotion/react'
3
+ import { PlacesType, Tooltip } from 'react-tooltip'
4
+
5
+ import { fonts } from '../utils/fonts'
6
+
7
+ const style = (size: buttonSize) => css`
8
+ display: flex;
9
+ justify-content: flex-end;
10
+
11
+ border-radius: 0.4rem;
12
+ user-select: none;
13
+
14
+ z-index: 3;
15
+
16
+ * {
17
+ ${fonts.bSmall.regular}
18
+ }
19
+
20
+ ${size === buttonSize.sm && css`
21
+ padding: 0.4rem!important;
22
+ `}
23
+ ${size === buttonSize.md && css`
24
+ padding: 0.6rem!important;
25
+ `}
26
+ ${size === buttonSize.lg && css`
27
+ padding: 1.2rem!important;
28
+ `}
29
+ `
30
+
31
+ interface TooltipDisplayProps {
32
+ id: string
33
+ toolTipText: ReactNode
34
+ text: ReactNode
35
+ delayShow?: number
36
+ closeDelay?: number
37
+ offset?: number
38
+ tooltipPlace?: PlacesType
39
+ size?: buttonSize
40
+ disabled?: boolean
41
+ }
42
+
43
+ export enum buttonSize {
44
+ sm = 'sm',
45
+ md = 'md',
46
+ lg = 'lg'
47
+ }
48
+
49
+ export const TooltipDisplay = ({
50
+ id,
51
+ toolTipText,
52
+ text,
53
+ delayShow = 0,
54
+ closeDelay = 0,
55
+ offset = 20,
56
+ tooltipPlace = 'top',
57
+ disabled = false,
58
+ size = buttonSize.md
59
+ }: TooltipDisplayProps) => (
60
+ <>
61
+ <div
62
+ data-tooltip-id={id}
63
+ data-open={true}
64
+ data-tooltip-offset={offset}
65
+ data-tooltip-delay-show={delayShow}
66
+ data-tooltip-delay-hide={closeDelay}
67
+ data-tooltip-place={tooltipPlace}
68
+ >
69
+ {text}
70
+ </div>
71
+ {
72
+ !disabled && (
73
+ <Tooltip
74
+ css={style(size)}
75
+ id={id}
76
+ noArrow={true}
77
+ >
78
+ {toolTipText}
79
+ </Tooltip>
80
+ )
81
+ }
82
+ </>
83
+ )
@@ -0,0 +1,156 @@
1
+ import { useRef, useState, useEffect, useMemo } from 'react'
2
+ import { css } from '@emotion/react'
3
+
4
+ import { TooltipDisplay } from './tooltip-display'
5
+ import { MediaMachineContext } from '../state-machines'
6
+ import { logToLinearVolume } from '../utils/volume-utils'
7
+
8
+ const style = css`
9
+ display: flex;
10
+ align-items: center;
11
+
12
+ padding: 16px 4px;
13
+ margin: 0 2px;
14
+
15
+ cursor: pointer;
16
+
17
+ > div {
18
+ position: relative;
19
+
20
+ background: #ccc;
21
+ border-radius: 4px;
22
+
23
+ width: 80px;
24
+ height: 3px;
25
+ }
26
+
27
+ .volume-handle {
28
+ position: absolute;
29
+ top: -3.5px;
30
+
31
+ background: #ffffff;
32
+ border-radius: 50%;
33
+ transform: translateX(-50%);
34
+ pointer-events: none;
35
+
36
+ width: 10px;
37
+ height: 10px;
38
+ }
39
+ `
40
+
41
+ type VolumeSliderType = {
42
+ value: number
43
+ onChange: (v: number) => void
44
+ }
45
+
46
+ const VolumeSlider = ({ value, onChange }: VolumeSliderType) => {
47
+ const volume = MediaMachineContext.useSelector((state) => state.context.media.volume)
48
+ const muted = MediaMachineContext.useSelector((state) => state.context.media.muted)
49
+
50
+ const sliderRef = useRef<HTMLDivElement>(null)
51
+ const [isDragging, setIsDragging] = useState(false)
52
+
53
+ const getNewVolumeFromEvent = (clientX: number): number => {
54
+ if (!sliderRef.current) return value
55
+ const rect = sliderRef.current.getBoundingClientRect()
56
+ const xPos = clientX - rect.left
57
+ const clamped = Math.max(0, Math.min(xPos, rect.width))
58
+ return clamped / rect.width
59
+ }
60
+
61
+ const handlePointerDown = (e: React.MouseEvent<HTMLDivElement>) => {
62
+ setIsDragging(true)
63
+ const newVolume = getNewVolumeFromEvent(e.clientX)
64
+ onChange(newVolume)
65
+ }
66
+
67
+ const handlePointerMove = (e: MouseEvent) => {
68
+ if (!isDragging) return
69
+ const newVolume = getNewVolumeFromEvent(e.clientX)
70
+ onChange(newVolume)
71
+ }
72
+
73
+ const handlePointerUp = () => {
74
+ setIsDragging(false)
75
+ }
76
+
77
+ const handleWheel = (e: WheelEvent) => {
78
+ e.preventDefault()
79
+ const step = 0.05
80
+ let newVol = value
81
+ if (e.deltaY < 0) {
82
+ // scroll up
83
+ newVol = Math.min(1, newVol + step)
84
+ } else {
85
+ // scroll down
86
+ newVol = Math.max(0, newVol - step)
87
+ }
88
+ onChange(newVol)
89
+ }
90
+
91
+ useEffect(() => {
92
+ window.addEventListener('mousemove', handlePointerMove)
93
+ window.addEventListener('mouseup', handlePointerUp)
94
+ return () => {
95
+ window.removeEventListener('mousemove', handlePointerMove)
96
+ window.removeEventListener('mouseup', handlePointerUp)
97
+ }
98
+ }, [isDragging])
99
+
100
+ useEffect(() => {
101
+ const slider = sliderRef.current
102
+ if (slider) {
103
+ slider.addEventListener('wheel', handleWheel, { passive: false })
104
+ }
105
+ return () => {
106
+ if (slider) {
107
+ slider.removeEventListener('wheel', handleWheel)
108
+ }
109
+ }
110
+ }, [value])
111
+
112
+ const fillPercent = value * 100
113
+ const fillColor = '#fff'
114
+ const emptyColor = '#3A3A3A'
115
+
116
+ const displayVolumePercent = useMemo(
117
+ () => Math.round(muted ? 0 : logToLinearVolume(volume) * 100),
118
+ [volume]
119
+ )
120
+
121
+ return (
122
+ <TooltipDisplay
123
+ id='volume-slider'
124
+ text={
125
+ <div
126
+ ref={sliderRef}
127
+ css={style}
128
+ onMouseDown={handlePointerDown}
129
+ >
130
+ <div
131
+ style={{
132
+ background: `linear-gradient(to right,
133
+ ${fillColor} 0% ${fillPercent}%,
134
+ ${emptyColor} ${fillPercent}% 100%
135
+ )`
136
+ }}
137
+ >
138
+ <div
139
+ className="volume-handle"
140
+ style={{
141
+ left: `${value * 100}%`
142
+ }}
143
+ />
144
+ </div>
145
+ </div>
146
+ }
147
+ toolTipText={
148
+ <div className='volume-value'>
149
+ {displayVolumePercent}%
150
+ </div>
151
+ }
152
+ />
153
+ )
154
+ }
155
+
156
+ export default VolumeSlider