@banou/media-player 0.6.2 → 0.7.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.
- package/README.md +148 -3
- package/dist/engine/index.d.ts +10 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/picture-in-picture.d.ts +27 -0
- package/dist/engine/playback.d.ts +45 -0
- package/dist/engine/source-buffer.d.ts +12 -0
- package/dist/engine/subtitles.d.ts +36 -0
- package/dist/engine/thumbnails.d.ts +22 -0
- package/dist/engine-Dmc7JSd9.js +470 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1566 -0
- package/dist/react/components/chrome.d.ts +9 -0
- package/dist/react/components/control-bar.d.ts +2 -0
- package/dist/react/components/overlay.d.ts +4 -0
- package/dist/react/components/playback-slider.d.ts +2 -0
- package/dist/react/components/progress-bar.d.ts +2 -0
- package/dist/react/components/settings.d.ts +2 -0
- package/dist/react/components/sound.d.ts +7 -0
- package/{build → dist/react}/components/tooltip-display.d.ts +8 -8
- package/dist/react/components/volume-slider.d.ts +6 -0
- package/dist/react/hooks/use-drag-value.d.ts +23 -0
- package/dist/react/hooks/use-picture-in-picture.d.ts +2 -0
- package/dist/react/hooks/use-playback.d.ts +6 -0
- package/dist/react/hooks/use-thumbnails.d.ts +11 -0
- package/dist/react/player.d.ts +25 -0
- package/dist/react/source-feature.d.ts +84 -0
- package/dist/react/video-player.d.ts +43 -0
- package/dist/utils/source.d.ts +30 -0
- package/dist/utils/track-label.d.ts +19 -0
- package/dist/utils/volume-utils.d.ts +6 -0
- package/package.json +49 -37
- package/src/lib/engine/index.ts +14 -0
- package/src/lib/engine/picture-in-picture.ts +250 -0
- package/src/lib/engine/playback.ts +352 -0
- package/src/lib/engine/source-buffer.ts +61 -0
- package/src/lib/engine/subtitles.ts +210 -0
- package/src/lib/engine/thumbnails.ts +137 -0
- package/src/lib/globals.d.ts +8 -0
- package/src/lib/index.tsx +36 -0
- package/src/lib/react/components/chrome.tsx +126 -0
- package/src/{components → lib/react/components}/control-bar.tsx +74 -96
- package/src/lib/react/components/overlay.tsx +121 -0
- package/src/{components → lib/react/components}/playback-slider.tsx +30 -52
- package/src/{components → lib/react/components}/progress-bar.tsx +128 -41
- package/src/lib/react/components/settings.tsx +391 -0
- package/src/lib/react/components/sound.tsx +133 -0
- package/src/{components → lib/react/components}/tooltip-display.tsx +11 -8
- package/src/{components → lib/react/components}/volume-slider.tsx +32 -46
- package/src/lib/react/hooks/use-drag-value.ts +73 -0
- package/src/lib/react/hooks/use-picture-in-picture.ts +36 -0
- package/src/lib/react/hooks/use-playback.ts +134 -0
- package/src/lib/react/hooks/use-thumbnails.ts +77 -0
- package/src/lib/react/player.ts +20 -0
- package/src/lib/react/source-feature.ts +90 -0
- package/src/lib/react/video-player.tsx +119 -0
- package/src/lib/utils/source.ts +60 -0
- package/src/lib/utils/track-label.ts +36 -0
- package/src/lib/utils/volume-utils.ts +13 -0
- package/build/components/chrome.d.ts +0 -7
- package/build/components/control-bar.d.ts +0 -6
- package/build/components/overlay.d.ts +0 -4
- package/build/components/playback-slider.d.ts +0 -2
- package/build/components/progress-bar.d.ts +0 -1
- package/build/components/settings.d.ts +0 -2
- package/build/components/sound.d.ts +0 -4
- package/build/components/volume-slider.d.ts +0 -6
- package/build/index.d.ts +0 -26
- package/build/index.js +0 -3416
- package/build/main.d.ts +0 -1
- package/build/state-machines/data-source.d.ts +0 -27
- package/build/state-machines/index.d.ts +0 -34038
- package/build/state-machines/media-properties.d.ts +0 -45
- package/build/state-machines/media-source.d.ts +0 -34
- package/build/state-machines/media.d.ts +0 -8558
- package/build/state-machines/subtitles.d.ts +0 -56
- package/build/state-machines/thumbnails.d.ts +0 -24
- package/build/state-machines/utils.d.ts +0 -26
- package/build/utils/actor-utils.d.ts +0 -2
- package/build/utils/context.d.ts +0 -14
- package/build/utils/index.d.ts +0 -4
- package/build/utils/languages.d.ts +0 -260
- package/build/utils/mp4box.d.ts +0 -61
- package/build/utils/use-local-storage.d.ts +0 -3
- package/build/utils/use-scrub.d.ts +0 -11
- package/build/utils/volume-utils.d.ts +0 -17
- package/build/utils/window-height.d.ts +0 -2
- package/src/components/chrome.tsx +0 -95
- package/src/components/overlay.tsx +0 -91
- package/src/components/settings.tsx +0 -322
- package/src/components/sound.tsx +0 -101
- package/src/index.tsx +0 -195
- package/src/main.tsx +0 -169
- package/src/state-machines/data-source.ts +0 -84
- package/src/state-machines/index.ts +0 -5
- package/src/state-machines/media-properties.ts +0 -82
- package/src/state-machines/media-source.ts +0 -129
- package/src/state-machines/media.ts +0 -255
- package/src/state-machines/subtitles.ts +0 -285
- package/src/state-machines/thumbnails.ts +0 -123
- package/src/state-machines/utils.ts +0 -94
- package/src/utils/actor-utils.ts +0 -19
- package/src/utils/context.ts +0 -23
- package/src/utils/index.ts +0 -229
- package/src/utils/languages.ts +0 -262
- package/src/utils/mp4box.ts +0 -74
- package/src/utils/use-local-storage.ts +0 -30
- package/src/utils/use-scrub.ts +0 -40
- package/src/utils/volume-utils.ts +0 -39
- package/src/utils/window-height.ts +0 -19
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.json +0 -28
- package/tsconfig.node.json +0 -9
- /package/{build → dist}/utils/colors.d.ts +0 -0
- /package/{build → dist}/utils/fonts.d.ts +0 -0
- /package/{build → dist}/utils/time.d.ts +0 -0
- /package/src/{assets → lib/assets}/picture-in-picture.svg +0 -0
- /package/src/{utils → lib/utils}/colors.ts +0 -0
- /package/src/{utils → lib/utils}/fonts.ts +0 -0
- /package/src/{utils → lib/utils}/time.ts +0 -0
package/src/main.tsx
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
3
|
-
import { createRoot } from 'react-dom/client'
|
|
4
|
-
import { css, Global } from '@emotion/react'
|
|
5
|
-
|
|
6
|
-
import MediaPlayer from './index'
|
|
7
|
-
import { DownloadedRange } from './utils/context'
|
|
8
|
-
|
|
9
|
-
const mountStyle = css`
|
|
10
|
-
display: grid;
|
|
11
|
-
height: 100vh;
|
|
12
|
-
width: 100vw;
|
|
13
|
-
`
|
|
14
|
-
|
|
15
|
-
const BASE_BUFFER_SIZE = 2_500_000
|
|
16
|
-
const url = '/video.mkv'
|
|
17
|
-
|
|
18
|
-
const Mount = () => {
|
|
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
|
-
)
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
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
|
-
})
|
|
44
|
-
}, [])
|
|
45
|
-
|
|
46
|
-
const jassubWorkerUrl = useMemo(() => {
|
|
47
|
-
const workerUrl = new URL('/build/jassub-worker.js', import.meta.url).toString()
|
|
48
|
-
const blob = new Blob([`importScripts(${JSON.stringify(workerUrl)})`], { type: 'application/javascript' })
|
|
49
|
-
return URL.createObjectURL(blob)
|
|
50
|
-
}, [])
|
|
51
|
-
|
|
52
|
-
const libavWorkerUrl = useMemo(() => {
|
|
53
|
-
const workerUrl = new URL('/build/libav.js', new URL(window.location.toString()).origin).toString()
|
|
54
|
-
const blob = new Blob([`importScripts(${JSON.stringify(workerUrl)})`], { type: 'application/javascript' })
|
|
55
|
-
return URL.createObjectURL(blob)
|
|
56
|
-
}, [])
|
|
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
|
-
|
|
86
|
-
return (
|
|
87
|
-
<div css={mountStyle}>
|
|
88
|
-
<MediaPlayer
|
|
89
|
-
title={'video.mkv'}
|
|
90
|
-
downloadedRanges={contentLength ? downloadedRanges : undefined}
|
|
91
|
-
bufferSize={BASE_BUFFER_SIZE}
|
|
92
|
-
read={read}
|
|
93
|
-
size={contentLength}
|
|
94
|
-
autoplay={true}
|
|
95
|
-
publicPath={new URL('/build/', new URL(import.meta.url).origin).toString()}
|
|
96
|
-
jassubModernWasmUrl={jassubModernWasmUrl}
|
|
97
|
-
jassubWorkerUrl={jassubWorkerUrl}
|
|
98
|
-
jassubWasmUrl={jassubWasmUrl}
|
|
99
|
-
libavWorkerUrl={libavWorkerUrl}
|
|
100
|
-
/>
|
|
101
|
-
</div>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const globalStyle = css`
|
|
106
|
-
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,500;1,600;1,700;1,800;1,900&family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
|
|
107
|
-
|
|
108
|
-
*, *::before, *::after {
|
|
109
|
-
box-sizing: border-box;
|
|
110
|
-
margin: 0;
|
|
111
|
-
padding: 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
html {
|
|
115
|
-
font-size: 62.5%;
|
|
116
|
-
height: 100%;
|
|
117
|
-
width: 100%;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
body {
|
|
121
|
-
margin: 0;
|
|
122
|
-
height: 100%;
|
|
123
|
-
width: 100%;
|
|
124
|
-
font-size: 1.6rem;
|
|
125
|
-
font-family: Fira Sans;
|
|
126
|
-
color: #fff;
|
|
127
|
-
|
|
128
|
-
font-family: Montserrat;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
body > div {
|
|
132
|
-
height: 100%;
|
|
133
|
-
width: 100%;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
a {
|
|
137
|
-
color: #777777;
|
|
138
|
-
text-decoration: none;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
a:hover {
|
|
142
|
-
color: #fff;
|
|
143
|
-
text-decoration: underline;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
ul {
|
|
147
|
-
list-style: none;
|
|
148
|
-
}
|
|
149
|
-
`
|
|
150
|
-
|
|
151
|
-
const mountElement = document.createElement('div')
|
|
152
|
-
|
|
153
|
-
const root = createRoot(
|
|
154
|
-
document.body.appendChild(mountElement)
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
root.render(
|
|
158
|
-
<>
|
|
159
|
-
<Global styles={globalStyle}/>
|
|
160
|
-
<Mount/>
|
|
161
|
-
</>
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
if (import.meta.hot) {
|
|
165
|
-
import.meta.hot.dispose(() => {
|
|
166
|
-
root.unmount()
|
|
167
|
-
mountElement.remove()
|
|
168
|
-
})
|
|
169
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
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.indexes) sendBack({ type: 'INDEXES', indexes: metadata.indexes })
|
|
35
|
-
if (metadata.attachments?.length) sendBack({ type: 'NEW_ATTACHMENTS', attachments: metadata.attachments })
|
|
36
|
-
if (metadata.subtitles?.length) sendBack({ type: 'NEW_SUBTITLE_FRAGMENTS', subtitles: metadata.subtitles })
|
|
37
|
-
sendBack({ type: 'METADATA', ...metadata })
|
|
38
|
-
|
|
39
|
-
let isFinished = false
|
|
40
|
-
let currentSeeks: { currentTime: number }[] = []
|
|
41
|
-
const loadMore = queuedThrottleWithLastCall(100, async () => {
|
|
42
|
-
if (currentSeeks.length || isFinished) return
|
|
43
|
-
try {
|
|
44
|
-
const { data, subtitles, finished } = await remuxer.read()
|
|
45
|
-
if (finished) {
|
|
46
|
-
isFinished = true
|
|
47
|
-
}
|
|
48
|
-
if (subtitles.length) {
|
|
49
|
-
sendBack({ type: 'NEW_SUBTITLE_FRAGMENTS', subtitles })
|
|
50
|
-
}
|
|
51
|
-
sendBack({ type: 'DATA', data })
|
|
52
|
-
} catch (err: any) {
|
|
53
|
-
if (err.message === 'Cancelled') return
|
|
54
|
-
console.error(err)
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
receive(async (event) => {
|
|
59
|
-
if (event.type === 'NEED_DATA') {
|
|
60
|
-
loadMore()
|
|
61
|
-
} else if (event.type === 'SEEKING') {
|
|
62
|
-
isFinished = false
|
|
63
|
-
const { currentTime } = event
|
|
64
|
-
const seekObject = { currentTime }
|
|
65
|
-
currentSeeks = [...currentSeeks, seekObject]
|
|
66
|
-
try {
|
|
67
|
-
const { data, pts } = await remuxer
|
|
68
|
-
.seek(currentTime)
|
|
69
|
-
.finally(() => {
|
|
70
|
-
currentSeeks = currentSeeks.filter(seekObj => seekObj !== seekObject)
|
|
71
|
-
})
|
|
72
|
-
sendBack({ type: 'TIMESTAMP_OFFSET', timestampOffset: pts })
|
|
73
|
-
sendBack({ type: 'DATA', data })
|
|
74
|
-
} catch (err: any) {
|
|
75
|
-
if (err.message === 'Cancelled') return
|
|
76
|
-
console.error(err)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
return () => {
|
|
82
|
-
remuxer.destroy()
|
|
83
|
-
}
|
|
84
|
-
})
|
|
@@ -1,82 +0,0 @@
|
|
|
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
|
|
29
|
-
.play()
|
|
30
|
-
.then(() => handlePlay())
|
|
31
|
-
.catch((error) => {
|
|
32
|
-
console.error(error)
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
if (event.type === 'PAUSE') {
|
|
36
|
-
videoElement.pause()
|
|
37
|
-
handlePause()
|
|
38
|
-
}
|
|
39
|
-
if (event.type === 'SET_TIME') {
|
|
40
|
-
videoElement.currentTime = event.value
|
|
41
|
-
handleTimeUpdate()
|
|
42
|
-
}
|
|
43
|
-
if (event.type === 'SET_VOLUME') {
|
|
44
|
-
videoElement.volume = event.volume
|
|
45
|
-
videoElement.muted = event.muted
|
|
46
|
-
handleVolumeUpdate()
|
|
47
|
-
}
|
|
48
|
-
if (event.type === 'SET_PLAYBACK_RATE') {
|
|
49
|
-
videoElement.playbackRate = event.playbackRate
|
|
50
|
-
handlePlaybackRateUpdate()
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
const handlePlay = () => sendBack({ type: 'PLAYING' })
|
|
55
|
-
const handlePause = () => sendBack({ type: 'PAUSED' })
|
|
56
|
-
const handleEnded = () => sendBack({ type: 'ENDED' })
|
|
57
|
-
const handleTimeUpdate = () => sendBack({ type: 'TIME_UPDATE', currentTime: videoElement.currentTime })
|
|
58
|
-
const handleVolumeUpdate = () => sendBack({ type: 'VOLUME_UPDATE', muted: videoElement.muted, volume: videoElement.volume })
|
|
59
|
-
const handlePlaybackRateUpdate = () => sendBack({ type: 'PLAYBACK_RATE_UPDATE', playbackRate: videoElement.playbackRate })
|
|
60
|
-
const handleSeeking = () => sendBack({ type: 'SEEKING', currentTime: videoElement.currentTime })
|
|
61
|
-
const handleDurationChange = () => sendBack({ type: 'DURATION_UPDATE', duration: videoElement.duration })
|
|
62
|
-
|
|
63
|
-
videoElement.addEventListener('play', handlePlay)
|
|
64
|
-
videoElement.addEventListener('pause', handlePause)
|
|
65
|
-
videoElement.addEventListener('ended', handleEnded)
|
|
66
|
-
videoElement.addEventListener('timeupdate', handleTimeUpdate)
|
|
67
|
-
videoElement.addEventListener('volumechange', handleVolumeUpdate)
|
|
68
|
-
videoElement.addEventListener('ratechange', handlePlaybackRateUpdate)
|
|
69
|
-
videoElement.addEventListener('seeking', handleSeeking)
|
|
70
|
-
videoElement.addEventListener('durationchange', handleDurationChange)
|
|
71
|
-
|
|
72
|
-
return () => {
|
|
73
|
-
videoElement.removeEventListener('play', handlePlay)
|
|
74
|
-
videoElement.removeEventListener('pause', handlePause)
|
|
75
|
-
videoElement.removeEventListener('ended', handleEnded)
|
|
76
|
-
videoElement.removeEventListener('timeupdate', handleTimeUpdate)
|
|
77
|
-
videoElement.removeEventListener('volumechange', handleVolumeUpdate)
|
|
78
|
-
videoElement.removeEventListener('ratechange', handlePlaybackRateUpdate)
|
|
79
|
-
videoElement.removeEventListener('seeking', handleSeeking)
|
|
80
|
-
videoElement.removeEventListener('durationchange', handleDurationChange)
|
|
81
|
-
}
|
|
82
|
-
})
|
|
@@ -1,129 +0,0 @@
|
|
|
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.output.videoMimeType, event.info.output.audioMimeType].filter(Boolean).join(',')}"`)
|
|
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
|
-
})
|