@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
package/src/index.tsx CHANGED
@@ -1,475 +1,186 @@
1
1
  /// <reference types="@emotion/react/types/css-prop" />
2
- import type { ClassAttributes, ReactNode, SyntheticEvent, VideoHTMLAttributes } from 'react'
2
+ import type { ClassAttributes, MutableRefObject, ReactNode, RefCallback } from 'react'
3
+ import type { MediaPlayerContextType } from './utils/context'
3
4
 
4
- import { forwardRef, useEffect, useRef, useState } from 'react'
5
+ import { useCallback, useContext, useEffect, useState } from 'react'
5
6
  import { css } from '@emotion/react'
6
- import { makeRemuxer as libavMakeRemuxer } from 'libav-wasm'
7
7
 
8
- import { debounceImmediateAndLatest, queuedDebounceWithLastCall, toBufferedStream, toStreamChunkSize } from './utils'
9
- import Chrome from './chrome'
10
- import PQueue from 'p-queue'
8
+ import { MediaMachineContext } from './state-machines'
9
+ import { MediaPlayerContext, DownloadedRange } from './utils/context'
10
+ import useLocalStorage, { mediaMutedType } from './utils/use-local-storage'
11
+ import Chrome from './components/chrome'
11
12
 
12
- export type TransmuxError = {
13
- critical: boolean
14
- message: string
15
- count: number
16
- }
17
-
18
- export type Subtitle = {
19
- title: string
20
- language: string
21
- data: string
22
- }
23
-
24
- export type Attachment = {
25
- filename: string
26
- mimetype: string
27
- data: Uint8Array
28
- }
29
-
30
- type Chunk = {
31
- offset: number
32
- buffer: Uint8Array
33
- pts: number
34
- duration: number
35
- pos: number
36
- }
13
+ const BUFFER_SIZE = 2_500_000
37
14
 
38
- const BASE_BUFFER_SIZE = 2_500_000
39
-
40
- const style = css`
41
- display: grid;
15
+ const FKNVideoRootStyle = css`
16
+ display: flex;
42
17
  justify-content: center;
43
18
  background-color: #111;
44
-
45
- video {
46
- pointer-events: none;
47
- grid-column: 1;
48
- grid-row: 1;
49
-
50
- height: 100%;
51
- max-height: 100vh;
52
- max-width: 100%;
53
- background-color: black;
54
- }
55
-
56
- .chrome {
57
- grid-column: 1;
58
- grid-row: 1;
59
- }
19
+ height: 100%;
20
+ overflow: hidden;
60
21
  `
61
22
 
62
- export type FKNVideoControlOptions = {
63
-
64
- }
65
-
66
- export type FKNVideoControl = (args: FKNVideoControlOptions) => JSX.Element
67
-
68
23
  export type FKNVideoOptions = {
69
- customOverlay?: ReactNode
70
- baseBufferSize?: number
24
+ title?: string
25
+ downloadedRanges?: DownloadedRange[]
26
+ read?: (offset: number, size: number) => Promise<ArrayBuffer>
71
27
  size?: number
72
- fetch: (offset: number, size: number | undefined) => Promise<Response>
73
- customControls?: FKNVideoControl[]
28
+ bufferSize?: number
74
29
  publicPath: string
75
- wasmUrl: string
30
+ jassubWorkerUrl: string
31
+ jassubWasmUrl: string
32
+ jassubModernWasmUrl: string
76
33
  libavWorkerUrl: string
77
- libavWorkerOptions?: WorkerOptions
78
- libassWorkerUrl: string
79
- makeTransmuxer?: typeof libavMakeRemuxer
80
34
  }
81
35
 
82
- export type HeaderChunk = Chunk & { buffer: { buffer: { fileStart: number } } }
83
-
84
- const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputElement> & FKNVideoOptions>(({
85
- customOverlay,
86
- baseBufferSize = BASE_BUFFER_SIZE,
87
- size: contentLength,
88
- fetch,
89
- customControls,
90
- publicPath,
91
- wasmUrl,
92
- libavWorkerUrl,
93
- libavWorkerOptions,
94
- libassWorkerUrl,
95
- makeTransmuxer = libavMakeRemuxer
96
- }, ref) => {
97
- const [loading, setLoading] = useState(true)
98
- const containerRef = useRef<HTMLDivElement>(null)
99
- const videoRef = useRef<HTMLVideoElement>()
100
- const [videoElement, setVideoElement] = useState<HTMLVideoElement>()
101
- const [isPlaying, setIsPlaying] = useState(!(videoRef?.current?.paused ?? true))
102
- const [currentTime, setCurrentTime] = useState(0)
103
- const [attachments, setAttachments] = useState<Attachment[] | undefined>(undefined)
104
- const [tracks, setTracks] = useState<Subtitle[]>([])
105
- const [errors, setErrors] = useState<TransmuxError[]>([])
106
- const [duration, setDuration] = useState<number>()
107
- const [currentLoadedRange, setCurrentLoadedRange] = useState<[number, number]>([0, 0])
108
- const [needsInitialInteraction, setNeedsInitialInteraction] = useState(false)
36
+ export const FKNVideoRoot = (
37
+ { options, videoElement, children }:
38
+ { options: FKNVideoOptions, videoElement: HTMLVideoElement | undefined, children: ReactNode }
39
+ ) => {
40
+ const mediaPlayerContext = useContext(MediaPlayerContext)
41
+ const mediaActor = MediaMachineContext.useActorRef()
42
+ const status = MediaMachineContext.useSelector((state) => state.value)
43
+ const volume = MediaMachineContext.useSelector((state) => state.context.media.volume)
44
+ const muted = MediaMachineContext.useSelector((state) => state.context.media.muted)
45
+ const isReady = MediaMachineContext.useSelector((state) => state.context.isReady)
46
+ const [mediaVolume, setMediaVolume] = useLocalStorage('mediaVolume', '1') as [string, (newValue: string) => void]
47
+ const [mediaMute, setMediaMute] = useLocalStorage('mediaMute', 'false') as [mediaMutedType, (newValue: mediaMutedType) => void]
48
+ const [firstRender, setFirstRender] = useState(true)
49
+
50
+ useEffect(
51
+ () => mediaActor.send({
52
+ type: 'MEDIA_SOURCE_OPTIONS',
53
+ mediaSourceOptions: {}
54
+ }),
55
+ []
56
+ )
109
57
 
110
- const fetchRef = useRef(fetch)
58
+ useEffect(
59
+ () => {
60
+ if (!mediaActor || !isReady || !mediaPlayerContext.downloadedRanges) return
61
+ mediaActor.send({
62
+ type: 'DOWNLOADED_RANGES_UPDATED',
63
+ downloadedRanges: mediaPlayerContext.downloadedRanges
64
+ })
65
+ },
66
+ [
67
+ mediaActor,
68
+ isReady,
69
+ (
70
+ mediaPlayerContext
71
+ .downloadedRanges
72
+ ?.map(range => `${range.startByteOffset}-${range.endByteOffset}`)
73
+ ?? []
74
+ ).join(',')
75
+ ]
76
+ )
111
77
 
112
78
  useEffect(() => {
113
- fetchRef.current = fetch
114
- }, [fetch])
79
+ const { size, read, publicPath, libavWorkerUrl, jassubWasmUrl } = options
80
+ if (!read || !size || !publicPath || !libavWorkerUrl || !jassubWasmUrl) return
115
81
 
116
- useEffect(() => {
117
- if (!contentLength || !videoElement) return
118
- let _remuxer: ReturnType<typeof makeTransmuxer>
119
- let rangeUpdateInterval: number
120
- ;(async () => {
121
- _remuxer = makeTransmuxer({
82
+ mediaActor.send({
83
+ type: 'REMUXER_OPTIONS',
84
+ remuxerOptions: {
122
85
  publicPath,
123
86
  workerUrl: libavWorkerUrl,
124
- workerOptions: libavWorkerOptions,
125
- bufferSize: baseBufferSize,
126
- length: contentLength,
127
- getStream: (offset, size) =>
128
- fetchRef
129
- .current(offset, size ? Math.min(offset + size, contentLength) - 1 : undefined)
130
- .then(res =>
131
- size
132
- ? res.body!
133
- : (
134
- toBufferedStream(3)(
135
- toStreamChunkSize(baseBufferSize)(
136
- res.body!
137
- )
138
- )
139
- )
140
- ),
141
- subtitle: (title, language, subtitle) => {
142
- setTracks(tracks =>
143
- tracks.find(({ title: _title }) => _title === title)
144
- ? tracks.map((track) => track.title === title ? { title, language, data: subtitle } : track)
145
- : [...tracks, { title, language, data: subtitle }]
146
- )
147
- },
148
- attachment: (filename: string, mimetype: string, buffer: ArrayBuffer) => {
149
- if (attachments?.find(({ filename: _filename }) => filename === _filename)) return
150
- setAttachments(attachments => [
151
- ...attachments ?? [],
152
- { filename, mimetype, data: new Uint8Array(buffer) }
153
- ])
154
- }
155
- })
156
-
157
- const remuxer = await _remuxer
158
-
159
- const headerChunk = await remuxer.init()
160
-
161
- if (!headerChunk) throw new Error('No header chunk found after remuxer init')
162
-
163
- const mediaInfo = await remuxer.getInfo()
164
- const duration = mediaInfo.input.duration / 1_000_000
165
-
166
- setDuration(duration)
167
-
168
- videoElement.addEventListener('error', ev => {
169
- // @ts-expect-error
170
- console.error(ev.target?.error)
171
- })
172
-
173
- const mediaSource = new MediaSource()
174
- videoElement.src = URL.createObjectURL(mediaSource)
175
-
176
- const sourceBuffer: SourceBuffer =
177
- await new Promise(resolve =>
178
- mediaSource.addEventListener(
179
- 'sourceopen',
180
- () => {
181
- const sourceBuffer = mediaSource.addSourceBuffer(`video/mp4; codecs="${mediaInfo.input.video_mime_type},${mediaInfo.input.audio_mime_type}"`)
182
- mediaSource.duration = duration
183
- sourceBuffer.mode = 'segments'
184
- resolve(sourceBuffer)
185
- },
186
- { once: true }
187
- )
188
- )
189
-
190
- const queue = new PQueue({ concurrency: 1 })
191
-
192
- const setupListeners = (resolve: (value: Event) => void, reject: (reason: Event) => void) => {
193
- const updateEndListener = (ev: Event) => {
194
- resolve(ev)
195
- unregisterListeners()
196
- }
197
- const abortListener = (ev: Event) => {
198
- resolve(ev)
199
- unregisterListeners()
200
- }
201
- const errorListener = (ev: Event) => {
202
- console.error(ev)
203
- reject(ev)
204
- unregisterListeners()
205
- }
206
- const unregisterListeners = () => {
207
- sourceBuffer.removeEventListener('updateend', updateEndListener)
208
- sourceBuffer.removeEventListener('abort', abortListener)
209
- sourceBuffer.removeEventListener('error', errorListener)
210
- }
211
- sourceBuffer.addEventListener('updateend', updateEndListener, { once: true })
212
- sourceBuffer.addEventListener('abort', abortListener, { once: true })
213
- sourceBuffer.addEventListener('error', errorListener, { once: true })
87
+ bufferSize: options.bufferSize ?? BUFFER_SIZE,
88
+ length: size,
89
+ read
214
90
  }
215
-
216
- const appendBuffer = (buffer: ArrayBuffer) =>
217
- queue.add(() =>
218
- new Promise<Event>((resolve, reject) => {
219
- setupListeners(resolve, reject)
220
- sourceBuffer.appendBuffer(buffer)
221
- })
222
- )
223
-
224
- const unbufferRange = async (start: number, end: number) =>
225
- queue.add(() =>
226
- new Promise((resolve, reject) => {
227
- setupListeners(resolve, reject)
228
- sourceBuffer.remove(start, end)
229
- })
230
- )
231
-
232
- const getTimeRanges = () =>
233
- Array(sourceBuffer.buffered.length)
234
- .fill(undefined)
235
- .map((_, index) => ({
236
- index,
237
- start: sourceBuffer.buffered.start(index),
238
- end: sourceBuffer.buffered.end(index)
239
- }))
240
-
241
- videoElement.addEventListener('canplaythrough', () => {
242
- videoElement.playbackRate = 1
243
- videoElement.play()
244
- }, { once: true })
245
-
246
- let chunks: Chunk[] = []
247
-
248
- const PREVIOUS_BUFFER_COUNT = 1
249
- const NEEDED_TIME_IN_SECONDS = 15
250
-
251
- await appendBuffer(headerChunk.buffer)
252
-
253
- let reachedEnd = false
91
+ })
92
+ }, [options.read, options.size, options.publicPath, options.libavWorkerUrl, options.bufferSize])
254
93
 
255
- const pull = async () => {
256
- if (reachedEnd) throw new Error('end')
257
- const chunk = await remuxer.read()
258
- if (chunk.isTrailer) reachedEnd = true
259
- chunks = [...chunks, chunk]
260
- return chunk
94
+ useEffect(() => {
95
+ const { jassubWorkerUrl, jassubWasmUrl, jassubModernWasmUrl } = options
96
+ if (!jassubWorkerUrl || !jassubWasmUrl || !jassubModernWasmUrl) return
97
+
98
+ mediaActor.send({
99
+ type: 'SUBTITLES_RENDERER_OPTIONS',
100
+ subtitlesRendererOptions: {
101
+ workerUrl: jassubWorkerUrl,
102
+ wasmUrl: jassubWasmUrl,
103
+ modernWasmUrl: jassubModernWasmUrl
261
104
  }
262
-
263
- let seeking = false
264
-
265
- const updateBuffers = queuedDebounceWithLastCall(250, async () => {
266
- if (seeking) return
267
- const { currentTime } = videoElement
268
- const currentChunkIndex = chunks.findIndex(({ pts, duration }) => pts <= currentTime && pts + duration >= currentTime)
269
- const sliceIndex = Math.max(0, currentChunkIndex - PREVIOUS_BUFFER_COUNT)
270
-
271
- const getLastChunkEndTime = () => {
272
- const lastChunk = chunks.at(-1)
273
- if (!lastChunk) return 0
274
- return lastChunk.pts + lastChunk.duration
275
- }
276
-
277
- while (getLastChunkEndTime() < currentTime + NEEDED_TIME_IN_SECONDS){
278
- const chunk = await pull()
279
- await appendBuffer(chunk.buffer)
280
- }
281
-
282
- if (sliceIndex) chunks = chunks.slice(sliceIndex)
283
-
284
- const bufferedRanges = getTimeRanges()
285
-
286
- const firstChunk = chunks.at(0)
287
- const lastChunk = chunks.at(-1)
288
- if (!firstChunk || !lastChunk || firstChunk === lastChunk) return
289
- const minTime = firstChunk.pts
290
-
291
- for (const { start, end } of bufferedRanges) {
292
- const chunkIndex = chunks.findIndex(({ pts, duration }) => start <= (pts + (duration / 2)) && (pts + (duration / 2)) <= end)
293
- if (chunkIndex === -1) {
294
- await unbufferRange(start, end)
295
- } else {
296
- if (start < minTime) {
297
- await unbufferRange(
298
- start,
299
- minTime
300
- )
301
- }
302
- }
303
- }
304
- })
105
+ })
106
+ }, [options.publicPath, options.jassubWorkerUrl, options.jassubWasmUrl])
305
107
 
306
- let firstSeekPaused: boolean | undefined
307
- const seek = debounceImmediateAndLatest(250, async (seekTime: number) => {
308
- try {
309
- reachedEnd = false
310
- if (firstSeekPaused === undefined) firstSeekPaused = videoElement.paused
311
- seeking = true
312
- chunks = []
313
- await remuxer.seek(seekTime)
314
- const chunk1 = await pull()
315
- // firefox sometimes throws "Uncaught (in promise) DOMException: An attempt was made to use an object that is not, or is no longer, usable"
316
- sourceBuffer.timestampOffset = chunk1.pts
317
- await appendBuffer(chunk1.buffer)
318
- if (firstSeekPaused === false) {
319
- await videoElement.play()
320
- }
321
- seeking = false
322
- await updateBuffers()
323
- if (firstSeekPaused === false) {
324
- await videoElement.play()
325
- }
326
- firstSeekPaused = undefined
327
- } catch (err: any) {
328
- if (err.message !== 'exit') throw err
329
- }
330
- })
108
+ useEffect(() => {
109
+ if (!videoElement) return
110
+ mediaActor.send({
111
+ type: 'SET_VIDEO_ELEMENT',
112
+ videoElement,
113
+ })
114
+ }, [videoElement])
331
115
 
332
- const firstChunk = await pull()
333
- appendBuffer(firstChunk.buffer)
334
-
335
- videoElement.addEventListener('timeupdate', () => {
336
- updateBuffers()
337
- })
338
-
339
- videoElement.addEventListener('waiting', () => {
340
- updateBuffers()
341
- })
342
-
343
- videoElement.addEventListener('seeking', (ev) => {
344
- seek(videoElement.currentTime)
116
+ useEffect(() => {
117
+ if (!isReady) return
118
+ if (firstRender) {
119
+ mediaActor.send({
120
+ type: 'SET_VOLUME',
121
+ muted: mediaMute === 'true',
122
+ volume:
123
+ isNaN(Number(mediaVolume))
124
+ ? 1
125
+ : Number(mediaVolume)
345
126
  })
346
-
347
- updateBuffers()
348
-
349
- rangeUpdateInterval = window.setInterval(() => {
350
- const ranges = getTimeRanges()
351
- const firstRange = ranges.sort(({ start }, { start: start2 }) => start - start2).at(0)
352
- const lastRange = ranges.sort(({ end }, { end: end2 }) => end - end2).at(-1)
353
- if (!firstRange || !lastRange) return
354
- let firstPts = chunks.filter(({ pts, duration }) => pts + (duration / 2) > firstRange.start).sort(({ pts }, { pts: pts2 }) => pts - pts2).at(0)?.pts
355
- let lastPts = chunks.filter(({ pts, duration }) => pts + (duration / 2) < lastRange.end).sort(({ pts }, { pts: pts2 }) => pts - pts2).at(-1)?.pts
356
- if (firstPts === undefined || lastPts === undefined) return
357
- setCurrentLoadedRange([firstPts, lastPts])
358
- }, 200)
359
- })()
127
+ setFirstRender(false)
128
+ } else {
129
+ setMediaVolume(volume.toString())
130
+ setMediaMute(muted ? 'true' : 'false')
131
+ }
132
+ }, [isReady, volume, muted, firstRender])
360
133
 
134
+ useEffect(() => {
135
+ if (status !== 'OK') return
361
136
  return () => {
362
- _remuxer.then(transmuxer => transmuxer.destroy(true))
363
- window.clearInterval(rangeUpdateInterval)
137
+ mediaActor.send({ type: 'DESTROY' })
364
138
  }
365
- }, [contentLength, videoElement])
366
-
367
- const waiting: React.DOMAttributes<HTMLVideoElement>['onWaiting'] = (ev) => {
368
- setLoading(true)
369
- }
370
-
371
- const [isSeeking, setIsSeeking] = useState(false)
372
-
373
- const seeked: React.DOMAttributes<HTMLVideoElement>['onSeeked'] = (ev) => {
374
- setIsSeeking(false)
375
- }
376
-
377
- const seeking: React.DOMAttributes<HTMLVideoElement>['onSeeking'] = (ev) => {
378
- if (!videoRef.current) return
379
- setIsSeeking(true)
380
- setCurrentTime(videoRef.current?.currentTime ?? 0)
381
- }
382
-
383
- const seek = (time: number) => {
384
- if (!videoElement) return
385
- videoElement.currentTime = time
386
- setIsSeeking(true)
387
- }
388
-
389
- const timeUpdate: React.DOMAttributes<HTMLVideoElement>['onTimeUpdate'] = (ev) => {
390
- if (isSeeking) return
391
- setCurrentTime(videoRef.current?.currentTime ?? 0)
392
- setLoading(false)
393
- }
394
-
395
- const playbackUpdate = (playing: boolean) => (ev: SyntheticEvent<HTMLVideoElement, Event>) => {
396
- setIsPlaying(playing)
397
- }
398
-
399
- // todo: implement subtitles in PiP using https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream
400
- const pictureInPicture = () => {
401
- if (document.pictureInPictureElement) return document.exitPictureInPicture()
402
- videoRef.current?.requestPictureInPicture()
403
- }
139
+ }, [status])
404
140
 
405
- const fullscreen = () => {
406
- if (document.fullscreenElement) return document.exitFullscreen()
407
- // @ts-ignore
408
- containerRef.current?.requestFullscreen()
409
- }
410
-
411
- const play = async () => {
412
- setNeedsInitialInteraction(false)
413
- const isPaused = videoRef.current?.paused
414
- if (isPaused) await videoRef.current?.play()
415
- else await videoRef.current?.pause()
416
- }
141
+ return (
142
+ <div css={FKNVideoRootStyle}>
143
+ <Chrome>
144
+ {children}
145
+ </Chrome>
146
+ </div>
147
+ )
148
+ }
417
149
 
418
- const setVolume = (volume: number) => {
419
- if (!videoRef.current) return
420
- videoRef.current.volume = volume
421
- }
150
+ const FKNVideo = (
151
+ { ref, ...options }:
152
+ FKNVideoOptions & { ref?: RefCallback<HTMLVideoElement> | MutableRefObject<HTMLVideoElement | null> }
153
+ ) => {
154
+ const updateContextFunction = (context: Parameters<MediaPlayerContextType['update']>[0]) => setMediaPlayerContext({ ...context, update: updateContextFunction })
155
+ const [chromeContext, setMediaPlayerContext] = useState<MediaPlayerContextType>({ update: updateContextFunction } as MediaPlayerContextType)
422
156
 
423
- const getVolume = () => {
424
- if (!videoRef.current) return
425
- return videoRef.current.volume
426
- }
157
+ const [videoElement, setVideoElement] = useState<HTMLVideoElement | undefined>()
427
158
 
428
- const refFunction: ClassAttributes<HTMLVideoElement>['ref'] = (element) => {
159
+ const refFunction: ClassAttributes<HTMLVideoElement>['ref'] = useCallback((element: HTMLVideoElement | null) => {
429
160
  if (typeof ref === 'function') ref(element)
430
- if (ref && 'current' in ref) ref.current = element
431
- videoRef.current = element ?? undefined
432
- setVideoElement(videoRef.current)
433
- }
161
+ else if (ref && 'current' in ref) ref.current = element
162
+ setVideoElement(element ?? undefined)
163
+ }, [])
164
+
165
+ useEffect(() => {
166
+ setMediaPlayerContext((previousContext) => ({
167
+ ...previousContext,
168
+ videoElement,
169
+ title: options?.title,
170
+ size: options?.size,
171
+ downloadedRanges: options?.downloadedRanges
172
+ }))
173
+ }, [videoElement, options?.title, options?.size, options?.downloadedRanges])
434
174
 
435
175
  return (
436
- <div css={style} ref={containerRef}>
437
- <video
438
- ref={refFunction}
439
- onWaiting={waiting}
440
- onSeeking={seeking}
441
- onSeeked={seeked}
442
- onTimeUpdate={timeUpdate}
443
- onPlay={playbackUpdate(true)}
444
- onPause={playbackUpdate(false)}
445
- // autoPlay={true}
446
- />
447
- <Chrome
448
- className="chrome"
449
- customOverlay={customOverlay}
450
- publicPath={publicPath}
451
- isPlaying={isPlaying}
452
- video={videoRef}
453
- needsInitialInteraction={needsInitialInteraction}
454
- loading={loading}
455
- duration={duration}
456
- currentTime={currentTime}
457
- loadedTime={currentLoadedRange}
458
- pictureInPicture={pictureInPicture}
459
- fullscreen={fullscreen}
460
- play={play}
461
- seek={seek}
462
- getVolume={getVolume}
463
- setVolume={setVolume}
464
- attachments={attachments}
465
- tracks={tracks}
466
- errors={errors}
467
- customControls={customControls}
468
- libassWorkerUrl={libassWorkerUrl}
469
- wasmUrl={wasmUrl}
470
- />
471
- </div>
176
+ <MediaPlayerContext.Provider value={chromeContext}>
177
+ <MediaMachineContext.Provider>
178
+ <FKNVideoRoot options={options} videoElement={videoElement}>
179
+ <video ref={refFunction} controls={false}/>
180
+ </FKNVideoRoot>
181
+ </MediaMachineContext.Provider>
182
+ </MediaPlayerContext.Provider>
472
183
  )
473
- })
184
+ }
474
185
 
475
186
  export default FKNVideo