@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.
@@ -10,6 +10,9 @@ type SubtitlesEvents = {
10
10
  } | {
11
11
  type: 'SELECT_SUBTITLE_STREAM';
12
12
  streamIndex: number;
13
+ } | {
14
+ type: 'TIME_UPDATE';
15
+ currentTime: number;
13
16
  };
14
17
  type SubtitlesEmittedEvents = {
15
18
  type: 'WAITING';
@@ -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 mediaMutedType = 'true' | 'false' | undefined;
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.5.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.5.5",
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
- {mediaInformation}
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
- height: 100%;
24
- margin-left: 2.5rem;
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 && !mediaPlayerContext.hideUI
71
- ? <div css={titleStyle}>{mediaPlayerContext.title}</div>
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: string }) => () => {
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
- <div>Back</div>
190
- <div></div>
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
- <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
- >
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
- </button>
213
- <button
214
- type='button'
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
- <div>Back</div>
229
- <div></div>
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
- <div>Back</div>
239
- <div></div>
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
- <div>{languageWithStreamIndex.language}</div>
254
- <div>{selectedSubtitleStreamIndex === languageWithStreamIndex.streamIndex ? '✓' : ''}</div>
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
  }
@@ -14,7 +14,7 @@ const style = (size: buttonSize) => css`
14
14
  z-index: 3;
15
15
 
16
16
  * {
17
- ${fonts.bSmall.regular}
17
+ ${fonts.bMedium.regular}
18
18
  }
19
19
 
20
20
  ${size === buttonSize.sm && css`
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, { mediaMutedType } from './utils/use-local-storage'
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 [mediaMutedType, (newValue: mediaMutedType) => void]
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 = '/video2.mkv'
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.play()
29
- handlePlay()
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
- handlePlay()
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.input.videoMimeType},${event.info.input.audioMimeType}"`)
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: assign({ media: ({ context, event }) => ({ ...context.media, currentTime: event.currentTime }) }) },
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 }) }) },