@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.
- package/README.md +0 -10
- package/build/components/chrome.d.ts +4 -0
- package/build/components/control-bar.d.ts +5 -0
- package/build/components/overlay.d.ts +1 -0
- package/build/components/progress-bar.d.ts +1 -0
- package/build/components/settings.d.ts +2 -0
- package/build/components/sound.d.ts +4 -0
- package/build/components/tooltip-display.d.ts +20 -0
- package/build/components/volume-slider.d.ts +6 -0
- package/build/index.d.ts +23 -48
- package/build/index.js +2576 -2250
- package/build/main.d.ts +1 -1
- package/build/state-machines/data-source.d.ts +27 -0
- package/build/state-machines/index.d.ts +33185 -0
- package/build/state-machines/media-properties.d.ts +45 -0
- package/build/state-machines/media-source.d.ts +34 -0
- package/build/state-machines/media.d.ts +8344 -0
- package/build/state-machines/subtitles.d.ts +53 -0
- package/build/state-machines/thumbnails.d.ts +24 -0
- package/build/state-machines/utils.d.ts +26 -0
- package/build/utils/actor-utils.d.ts +2 -0
- package/build/utils/colors.d.ts +8 -0
- package/build/utils/context.d.ts +14 -0
- package/build/utils/fonts.d.ts +28 -0
- package/build/{utils.d.ts → utils/index.d.ts} +4 -12
- package/build/utils/languages.d.ts +260 -0
- package/build/{mp4box.d.ts → utils/mp4box.d.ts} +61 -61
- package/build/utils/time.d.ts +2 -0
- package/build/utils/use-local-storage.d.ts +3 -0
- package/build/{use-scrub.d.ts → utils/use-scrub.d.ts} +11 -11
- package/build/utils/volume-utils.d.ts +17 -0
- package/build/utils/window-height.d.ts +2 -0
- package/package.json +15 -12
- package/src/assets/picture-in-picture.svg +5 -0
- package/src/components/chrome.tsx +89 -0
- package/src/components/control-bar.tsx +330 -0
- package/src/components/overlay.tsx +28 -0
- package/src/components/progress-bar.tsx +252 -0
- package/src/components/settings.tsx +269 -0
- package/src/components/sound.tsx +101 -0
- package/src/components/tooltip-display.tsx +83 -0
- package/src/components/volume-slider.tsx +156 -0
- package/src/index.tsx +145 -435
- package/src/main.tsx +66 -39
- package/src/state-machines/data-source.ts +83 -0
- package/src/state-machines/index.ts +5 -0
- package/src/state-machines/media-properties.ts +78 -0
- package/src/state-machines/media-source.ts +129 -0
- package/src/state-machines/media.ts +245 -0
- package/src/state-machines/subtitles.ts +247 -0
- package/src/state-machines/thumbnails.ts +123 -0
- package/src/state-machines/utils.ts +94 -0
- package/src/utils/actor-utils.ts +19 -0
- package/src/utils/colors.ts +9 -0
- package/src/utils/context.ts +23 -0
- package/src/utils/fonts.ts +156 -0
- package/src/{utils.ts → utils/index.ts} +2 -26
- package/src/utils/time.ts +16 -0
- package/src/utils/use-local-storage.ts +30 -0
- package/src/utils/volume-utils.ts +39 -0
- package/src/utils/window-height.ts +19 -0
- package/tsconfig.json +4 -2
- package/build/chrome/bottom.d.ts +0 -22
- package/build/chrome/index.d.ts +0 -28
- package/build/chrome/overlay.d.ts +0 -9
- package/build/languages.d.ts +0 -260
- package/build/use-local-storage.d.ts +0 -6
- package/src/chrome/bottom.tsx +0 -697
- package/src/chrome/index.tsx +0 -203
- package/src/chrome/overlay.tsx +0 -104
- package/src/use-local-storage.ts +0 -32
- /package/src/{languages.ts → utils/languages.ts} +0 -0
- /package/src/{mp4box.ts → utils/mp4box.ts} +0 -0
- /package/src/{use-scrub.ts → utils/use-scrub.ts} +0 -0
package/src/main.tsx
CHANGED
|
@@ -1,48 +1,46 @@
|
|
|
1
1
|
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
3
3
|
import { createRoot } from 'react-dom/client'
|
|
4
4
|
import { css, Global } from '@emotion/react'
|
|
5
5
|
|
|
6
6
|
import MediaPlayer from './index'
|
|
7
|
+
import { DownloadedRange } from './utils/context'
|
|
7
8
|
|
|
8
9
|
const mountStyle = css`
|
|
9
10
|
display: grid;
|
|
10
|
-
height:
|
|
11
|
-
width:
|
|
11
|
+
height: 100vh;
|
|
12
|
+
width: 100vw;
|
|
12
13
|
`
|
|
13
14
|
|
|
14
|
-
const BASE_BUFFER_SIZE =
|
|
15
|
-
const
|
|
15
|
+
const BASE_BUFFER_SIZE = 2_500_000
|
|
16
|
+
const url = '/video2.mkv'
|
|
16
17
|
|
|
17
18
|
const Mount = () => {
|
|
18
|
-
const [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Range: `bytes=${offset}-${end ?? (!BACKPRESSURE_STREAM_ENABLED ? Math.min(offset + BASE_BUFFER_SIZE, size!) : '')}`
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
)
|
|
19
|
+
const [contentLength, setContentLength] = useState<number>()
|
|
20
|
+
|
|
21
|
+
const read = useCallback(
|
|
22
|
+
(offset: number, size: number) => {
|
|
23
|
+
if (contentLength === undefined) return Promise.resolve(new Uint8Array(0).buffer)
|
|
24
|
+
if (offset >= contentLength) return Promise.resolve(new Uint8Array(0).buffer)
|
|
25
|
+
return (
|
|
26
|
+
fetch(url, { headers: { Range: `bytes=${offset}-${Math.min(offset + size, contentLength) - 1}` } })
|
|
27
|
+
.then(res => res.arrayBuffer())
|
|
28
|
+
)
|
|
29
|
+
},
|
|
30
|
+
[contentLength]
|
|
31
|
+
)
|
|
35
32
|
|
|
36
33
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
fetch(url, { headers: { Range: `bytes=${0}-${1}` } })
|
|
35
|
+
.then(async ({ headers, body }) => {
|
|
36
|
+
if (!body) throw new Error('no body')
|
|
37
|
+
const contentRangeContentLength = headers.get('Content-Range')?.split('/').at(1)
|
|
38
|
+
const contentLength =
|
|
39
|
+
contentRangeContentLength
|
|
40
|
+
? Number(contentRangeContentLength)
|
|
41
|
+
: Number(headers.get('Content-Length'))
|
|
42
|
+
setContentLength(contentLength)
|
|
43
|
+
})
|
|
46
44
|
}, [])
|
|
47
45
|
|
|
48
46
|
const jassubWorkerUrl = useMemo(() => {
|
|
@@ -57,17 +55,47 @@ const Mount = () => {
|
|
|
57
55
|
return URL.createObjectURL(blob)
|
|
58
56
|
}, [])
|
|
59
57
|
|
|
58
|
+
const jassubWasmUrl = useMemo(() => {
|
|
59
|
+
return new URL('/build/jassub-worker.wasm', new URL(window.location.toString()).origin).toString()
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
const jassubModernWasmUrl = useMemo(() => {
|
|
63
|
+
return new URL('/build/jassub-modern-worker.wasm', new URL(window.location.toString()).origin).toString()
|
|
64
|
+
}, [])
|
|
65
|
+
|
|
66
|
+
const [downloadedRanges, setDownloadedRanges] = useState<DownloadedRange[]>([])
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!contentLength) return
|
|
70
|
+
let i = 0
|
|
71
|
+
const increaseDownloadedRanges = () => {
|
|
72
|
+
setDownloadedRanges(() => [
|
|
73
|
+
{
|
|
74
|
+
startByteOffset: 0,
|
|
75
|
+
endByteOffset: contentLength * i
|
|
76
|
+
}
|
|
77
|
+
])
|
|
78
|
+
i += 0.1
|
|
79
|
+
if (i < 1) {
|
|
80
|
+
setTimeout(increaseDownloadedRanges, 1000)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
increaseDownloadedRanges()
|
|
84
|
+
}, [contentLength])
|
|
85
|
+
|
|
60
86
|
return (
|
|
61
87
|
<div css={mountStyle}>
|
|
62
88
|
<MediaPlayer
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
title={'video.mkv'}
|
|
90
|
+
downloadedRanges={contentLength ? downloadedRanges : undefined}
|
|
91
|
+
bufferSize={BASE_BUFFER_SIZE}
|
|
92
|
+
read={read}
|
|
93
|
+
size={contentLength}
|
|
67
94
|
publicPath={new URL('/build/', new URL(import.meta.url).origin).toString()}
|
|
68
|
-
|
|
95
|
+
jassubModernWasmUrl={jassubModernWasmUrl}
|
|
96
|
+
jassubWorkerUrl={jassubWorkerUrl}
|
|
97
|
+
jassubWasmUrl={jassubWasmUrl}
|
|
69
98
|
libavWorkerUrl={libavWorkerUrl}
|
|
70
|
-
libassWorkerUrl={jassubWorkerUrl}
|
|
71
99
|
/>
|
|
72
100
|
</div>
|
|
73
101
|
)
|
|
@@ -97,7 +125,6 @@ const globalStyle = css`
|
|
|
97
125
|
color: #fff;
|
|
98
126
|
|
|
99
127
|
font-family: Montserrat;
|
|
100
|
-
// font-family: "Segoe UI", Roboto, "Fira Sans", "Helvetica Neue", Arial, sans-serif;
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
body > div {
|
|
@@ -134,7 +161,7 @@ root.render(
|
|
|
134
161
|
)
|
|
135
162
|
|
|
136
163
|
if (import.meta.hot) {
|
|
137
|
-
import.meta.hot.dispose((
|
|
164
|
+
import.meta.hot.dispose(() => {
|
|
138
165
|
root.unmount()
|
|
139
166
|
mountElement.remove()
|
|
140
167
|
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { makeRemuxer } from 'libav-wasm'
|
|
2
|
+
|
|
3
|
+
import { fromAsyncCallback } from './utils'
|
|
4
|
+
import { queuedThrottleWithLastCall, toStreamChunkSize } from '../utils'
|
|
5
|
+
import { Attachment, SubtitleFragment } from 'libav-wasm/build/worker'
|
|
6
|
+
|
|
7
|
+
type DataSourceEvents =
|
|
8
|
+
| { type: 'METADATA', mimeType: string, duration: number }
|
|
9
|
+
| { type: 'SEEKING', currentTime: number }
|
|
10
|
+
| { type: 'NEED_DATA' }
|
|
11
|
+
|
|
12
|
+
type DataSourceEmittedEvents =
|
|
13
|
+
| { type: 'DATA', data: Uint8Array }
|
|
14
|
+
| { type: 'NEW_SUBTITLE_FRAGMENTS', subtitles: SubtitleFragment[] }
|
|
15
|
+
| { type: 'NEW_ATTACHMENTS', attachments: Attachment[] }
|
|
16
|
+
|
|
17
|
+
type DataSourceInput = {
|
|
18
|
+
remuxerOptions: Parameters<typeof makeRemuxer>[0]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default fromAsyncCallback<DataSourceEvents, DataSourceInput, DataSourceEmittedEvents>(async ({ sendBack, receive, input, self, emit }) => {
|
|
22
|
+
const { remuxerOptions } = input
|
|
23
|
+
const { publicPath, workerUrl, bufferSize, length, read } = remuxerOptions
|
|
24
|
+
|
|
25
|
+
const remuxer = await makeRemuxer({
|
|
26
|
+
publicPath,
|
|
27
|
+
workerUrl,
|
|
28
|
+
bufferSize,
|
|
29
|
+
length,
|
|
30
|
+
read
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const metadata = await remuxer.init()
|
|
34
|
+
if (metadata.attachments?.length) sendBack({ type: 'NEW_ATTACHMENTS', attachments: metadata.attachments })
|
|
35
|
+
if (metadata.subtitles?.length) sendBack({ type: 'NEW_SUBTITLE_FRAGMENTS', subtitles: metadata.subtitles })
|
|
36
|
+
sendBack({ type: 'METADATA', ...metadata })
|
|
37
|
+
|
|
38
|
+
let isFinished = false
|
|
39
|
+
let currentSeeks: { currentTime: number }[] = []
|
|
40
|
+
const loadMore = queuedThrottleWithLastCall(100, async () => {
|
|
41
|
+
if (currentSeeks.length || isFinished) return
|
|
42
|
+
try {
|
|
43
|
+
const { data, subtitles, finished } = await remuxer.read()
|
|
44
|
+
if (finished) {
|
|
45
|
+
isFinished = true
|
|
46
|
+
}
|
|
47
|
+
if (subtitles.length) {
|
|
48
|
+
sendBack({ type: 'NEW_SUBTITLE_FRAGMENTS', subtitles })
|
|
49
|
+
}
|
|
50
|
+
sendBack({ type: 'DATA', data })
|
|
51
|
+
} catch (err: any) {
|
|
52
|
+
if (err.message === 'Cancelled') return
|
|
53
|
+
console.error(err)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
receive(async (event) => {
|
|
58
|
+
if (event.type === 'NEED_DATA') {
|
|
59
|
+
loadMore()
|
|
60
|
+
} else if (event.type === 'SEEKING') {
|
|
61
|
+
isFinished = false
|
|
62
|
+
const { currentTime } = event
|
|
63
|
+
const seekObject = { currentTime }
|
|
64
|
+
currentSeeks = [...currentSeeks, seekObject]
|
|
65
|
+
try {
|
|
66
|
+
const { data, pts } = await remuxer
|
|
67
|
+
.seek(currentTime)
|
|
68
|
+
.finally(() => {
|
|
69
|
+
currentSeeks = currentSeeks.filter(seekObj => seekObj !== seekObject)
|
|
70
|
+
})
|
|
71
|
+
sendBack({ type: 'TIMESTAMP_OFFSET', timestampOffset: pts })
|
|
72
|
+
sendBack({ type: 'DATA', data })
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
if (err.message === 'Cancelled') return
|
|
75
|
+
console.error(err)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
remuxer.destroy()
|
|
82
|
+
}
|
|
83
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { fromCallback } from 'xstate'
|
|
2
|
+
|
|
3
|
+
type MediaPropertiesEvents =
|
|
4
|
+
| { type: 'PLAY' }
|
|
5
|
+
| { type: 'PAUSE' }
|
|
6
|
+
| { type: 'SET_TIME', value: number }
|
|
7
|
+
| { type: 'SET_VOLUME', muted: boolean, volume: number }
|
|
8
|
+
| { type: 'SET_PLAYBACK_RATE', playbackRate: number }
|
|
9
|
+
| { type: 'DESTROY' }
|
|
10
|
+
|
|
11
|
+
type MediaPropertiesEmittedEvents =
|
|
12
|
+
| { type: 'PLAYING' }
|
|
13
|
+
| { type: 'PAUSED' }
|
|
14
|
+
| { type: 'TIME_UPDATE', currentTime: number }
|
|
15
|
+
| { type: 'VOLUME_UPDATE', muted: boolean, volume: number }
|
|
16
|
+
| { type: 'PLAYBACK_RATE_UPDATE', playbackRate: number }
|
|
17
|
+
| { type: 'DURATION_UPDATE', duration: number }
|
|
18
|
+
| { type: 'SEEKING', currentTime: number }
|
|
19
|
+
| { type: 'ENDED' }
|
|
20
|
+
|
|
21
|
+
type MediaPropertiesInput = { videoElement: HTMLVideoElement }
|
|
22
|
+
|
|
23
|
+
export default fromCallback<MediaPropertiesEvents, MediaPropertiesInput, MediaPropertiesEmittedEvents>(({ sendBack, receive, input }) => {
|
|
24
|
+
const { videoElement } = input
|
|
25
|
+
|
|
26
|
+
receive((event) => {
|
|
27
|
+
if (event.type === 'PLAY') {
|
|
28
|
+
videoElement.play()
|
|
29
|
+
handlePlay()
|
|
30
|
+
}
|
|
31
|
+
if (event.type === 'PAUSE') {
|
|
32
|
+
videoElement.pause()
|
|
33
|
+
handlePlay()
|
|
34
|
+
}
|
|
35
|
+
if (event.type === 'SET_TIME') {
|
|
36
|
+
videoElement.currentTime = event.value
|
|
37
|
+
handleTimeUpdate()
|
|
38
|
+
}
|
|
39
|
+
if (event.type === 'SET_VOLUME') {
|
|
40
|
+
videoElement.volume = event.volume
|
|
41
|
+
videoElement.muted = event.muted
|
|
42
|
+
handleVolumeUpdate()
|
|
43
|
+
}
|
|
44
|
+
if (event.type === 'SET_PLAYBACK_RATE') {
|
|
45
|
+
videoElement.playbackRate = event.playbackRate
|
|
46
|
+
handlePlaybackRateUpdate()
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const handlePlay = () => sendBack({ type: 'PLAYING' })
|
|
51
|
+
const handlePause = () => sendBack({ type: 'PAUSED' })
|
|
52
|
+
const handleEnded = () => sendBack({ type: 'ENDED' })
|
|
53
|
+
const handleTimeUpdate = () => sendBack({ type: 'TIME_UPDATE', currentTime: videoElement.currentTime })
|
|
54
|
+
const handleVolumeUpdate = () => sendBack({ type: 'VOLUME_UPDATE', muted: videoElement.muted, volume: videoElement.volume })
|
|
55
|
+
const handlePlaybackRateUpdate = () => sendBack({ type: 'PLAYBACK_RATE_UPDATE', playbackRate: videoElement.playbackRate })
|
|
56
|
+
const handleSeeking = () => sendBack({ type: 'SEEKING', currentTime: videoElement.currentTime })
|
|
57
|
+
const handleDurationChange = () => sendBack({ type: 'DURATION_UPDATE', duration: videoElement.duration })
|
|
58
|
+
|
|
59
|
+
videoElement.addEventListener('play', handlePlay)
|
|
60
|
+
videoElement.addEventListener('pause', handlePause)
|
|
61
|
+
videoElement.addEventListener('ended', handleEnded)
|
|
62
|
+
videoElement.addEventListener('timeupdate', handleTimeUpdate)
|
|
63
|
+
videoElement.addEventListener('volumechange', handleVolumeUpdate)
|
|
64
|
+
videoElement.addEventListener('ratechange', handlePlaybackRateUpdate)
|
|
65
|
+
videoElement.addEventListener('seeking', handleSeeking)
|
|
66
|
+
videoElement.addEventListener('durationchange', handleDurationChange)
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
videoElement.removeEventListener('play', handlePlay)
|
|
70
|
+
videoElement.removeEventListener('pause', handlePause)
|
|
71
|
+
videoElement.removeEventListener('ended', handleEnded)
|
|
72
|
+
videoElement.removeEventListener('timeupdate', handleTimeUpdate)
|
|
73
|
+
videoElement.removeEventListener('volumechange', handleVolumeUpdate)
|
|
74
|
+
videoElement.removeEventListener('ratechange', handlePlaybackRateUpdate)
|
|
75
|
+
videoElement.removeEventListener('seeking', handleSeeking)
|
|
76
|
+
videoElement.removeEventListener('durationchange', handleDurationChange)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { makeRemuxer } from 'libav-wasm'
|
|
2
|
+
|
|
3
|
+
import { fromAsyncCallback, getTimeRanges, updateSourceBuffer } from './utils'
|
|
4
|
+
|
|
5
|
+
type MediaSourceEvents =
|
|
6
|
+
| ({ type: 'METADATA' } & Awaited<ReturnType<Awaited<ReturnType<typeof makeRemuxer>>['init']>>)
|
|
7
|
+
| { type: 'TIMESTAMP_OFFSET', timestampOffset: number }
|
|
8
|
+
| { type: 'DATA', data: ArrayBuffer }
|
|
9
|
+
|
|
10
|
+
type MediaSourceEmittedEvents =
|
|
11
|
+
| { type: 'SOURCE_BUFFER_READY' }
|
|
12
|
+
| { type: 'SOURCE_OPEN' }
|
|
13
|
+
| { type: 'SOURCE_ENDED' }
|
|
14
|
+
| { type: 'SOURCE_ERROR' }
|
|
15
|
+
| { type: 'NEED_DATA' }
|
|
16
|
+
|
|
17
|
+
export type MediaSourceOptions = {
|
|
18
|
+
preEvictionTime?: number
|
|
19
|
+
postEvictionTime?: number
|
|
20
|
+
bufferTargetTime?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type MediaSourceInput = {
|
|
24
|
+
videoElement: HTMLVideoElement
|
|
25
|
+
preEvictionTime?: number
|
|
26
|
+
postEvictionTime?: number
|
|
27
|
+
bufferTargetTime?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultPreEvictionTime = -20
|
|
31
|
+
const defaultPostEvictionTime = 60
|
|
32
|
+
const defaultBufferTargetTime = 30
|
|
33
|
+
|
|
34
|
+
export default fromAsyncCallback<MediaSourceEvents, MediaSourceInput, MediaSourceEmittedEvents>(async ({ sendBack, receive, input, self, emit }) => {
|
|
35
|
+
const { videoElement, preEvictionTime, postEvictionTime, bufferTargetTime } = input
|
|
36
|
+
|
|
37
|
+
const handlError = () => console.error(videoElement.error)
|
|
38
|
+
videoElement.addEventListener('error', handlError)
|
|
39
|
+
|
|
40
|
+
const mediaSource = new MediaSource()
|
|
41
|
+
const mediaSourceUrl = URL.createObjectURL(mediaSource)
|
|
42
|
+
videoElement.src = mediaSourceUrl
|
|
43
|
+
|
|
44
|
+
const getPreEvictionTime = () => videoElement.currentTime + (preEvictionTime ?? defaultPreEvictionTime)
|
|
45
|
+
const getPostEvictionTime = () => videoElement.currentTime + (postEvictionTime ?? defaultPostEvictionTime)
|
|
46
|
+
const getBufferTargetTime = () => videoElement.currentTime + (bufferTargetTime ?? defaultBufferTargetTime)
|
|
47
|
+
|
|
48
|
+
let headerBuffer: ArrayBuffer | undefined
|
|
49
|
+
const sourceBuffer =
|
|
50
|
+
await new Promise<SourceBuffer>(resolve =>
|
|
51
|
+
mediaSource.addEventListener(
|
|
52
|
+
'sourceopen',
|
|
53
|
+
() => {
|
|
54
|
+
let resolved = false
|
|
55
|
+
receive((event) => {
|
|
56
|
+
if (resolved) return
|
|
57
|
+
if (event.type === 'METADATA') {
|
|
58
|
+
const sourceBuffer = mediaSource.addSourceBuffer(`video/mp4; codecs="${event.info.input.videoMimeType},${event.info.input.audioMimeType}"`)
|
|
59
|
+
sourceBuffer.mode = 'segments'
|
|
60
|
+
mediaSource.duration = event.info.input.duration
|
|
61
|
+
headerBuffer = event.data
|
|
62
|
+
resolved = true
|
|
63
|
+
resolve(sourceBuffer)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
{ once: true }
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (!headerBuffer) throw new Error('No header buffer')
|
|
72
|
+
|
|
73
|
+
const { appendBuffer, unbufferRange, updateTimestampOffset } = updateSourceBuffer(sourceBuffer)
|
|
74
|
+
appendBuffer(headerBuffer)
|
|
75
|
+
|
|
76
|
+
receive(async (event) => {
|
|
77
|
+
if (event.type === 'DATA') {
|
|
78
|
+
await appendBuffer(event.data)
|
|
79
|
+
} else if (event.type === 'TIMESTAMP_OFFSET') {
|
|
80
|
+
await updateTimestampOffset(event.timestampOffset)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const unbuffer = async () => {
|
|
85
|
+
const preEvictionTime = getPreEvictionTime()
|
|
86
|
+
const postEvictionTime = getPostEvictionTime()
|
|
87
|
+
const bufferedRanges = getTimeRanges(sourceBuffer)
|
|
88
|
+
for (const { start, end } of bufferedRanges) {
|
|
89
|
+
if (start < preEvictionTime) {
|
|
90
|
+
await unbufferRange(
|
|
91
|
+
start,
|
|
92
|
+
preEvictionTime
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
if (end > postEvictionTime) {
|
|
96
|
+
await unbufferRange(
|
|
97
|
+
postEvictionTime,
|
|
98
|
+
end
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const interval = setInterval(async () => {
|
|
105
|
+
await unbuffer()
|
|
106
|
+
const timeRanges = getTimeRanges(sourceBuffer)
|
|
107
|
+
const maxBufferedTime = Math.max(...timeRanges.map(({ end }) => end))
|
|
108
|
+
if (maxBufferedTime < getBufferTargetTime()) {
|
|
109
|
+
sendBack({ type: 'NEED_DATA' })
|
|
110
|
+
}
|
|
111
|
+
}, 100)
|
|
112
|
+
|
|
113
|
+
const handleSourceOpen = () => sendBack({ type: 'SOURCE_OPEN' })
|
|
114
|
+
const handleSourceEnded = () => sendBack({ type: 'SOURCE_ENDED' })
|
|
115
|
+
const handleSourceError = () => sendBack({ type: 'SOURCE_ERROR' })
|
|
116
|
+
|
|
117
|
+
mediaSource.addEventListener('sourceopen', handleSourceOpen)
|
|
118
|
+
mediaSource.addEventListener('sourceended', handleSourceEnded)
|
|
119
|
+
mediaSource.addEventListener('error', handleSourceError)
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
URL.revokeObjectURL(mediaSourceUrl)
|
|
123
|
+
clearInterval(interval)
|
|
124
|
+
videoElement.removeEventListener('error', handlError)
|
|
125
|
+
mediaSource.removeEventListener('sourceopen', handleSourceOpen)
|
|
126
|
+
mediaSource.removeEventListener('sourceended', handleSourceEnded)
|
|
127
|
+
mediaSource.removeEventListener('error', handleSourceError)
|
|
128
|
+
}
|
|
129
|
+
})
|