@banou/media-player 0.6.0 → 0.6.2
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 +6 -6
- package/package.json +51 -51
- package/src/assets/picture-in-picture.svg +5 -5
- package/src/components/chrome.tsx +95 -95
- package/src/components/control-bar.tsx +333 -333
- package/src/components/overlay.tsx +91 -91
- package/src/components/playback-slider.tsx +174 -174
- package/src/components/progress-bar.tsx +251 -251
- package/src/components/settings.tsx +321 -321
- package/src/components/sound.tsx +100 -100
- package/src/components/tooltip-display.tsx +83 -83
- package/src/components/volume-slider.tsx +156 -156
- package/src/index.tsx +195 -195
- package/src/main.tsx +169 -169
- package/src/state-machines/data-source.ts +84 -84
- package/src/state-machines/index.ts +5 -5
- package/src/state-machines/media-properties.ts +82 -82
- package/src/state-machines/media-source.ts +129 -129
- package/src/state-machines/media.ts +255 -255
- package/src/state-machines/subtitles.ts +285 -285
- package/src/state-machines/thumbnails.ts +123 -123
- package/src/state-machines/utils.ts +94 -94
- package/src/utils/actor-utils.ts +19 -19
- package/src/utils/colors.ts +8 -8
- package/src/utils/context.ts +23 -23
- package/src/utils/fonts.ts +159 -159
- package/src/utils/index.ts +229 -229
- package/src/utils/languages.ts +262 -262
- package/src/utils/mp4box.ts +74 -74
- package/src/utils/time.ts +15 -15
- package/src/utils/use-local-storage.ts +30 -30
- package/src/utils/use-scrub.ts +40 -40
- package/src/utils/volume-utils.ts +39 -39
- package/src/utils/window-height.ts +19 -19
- package/src/vite-env.d.ts +1 -1
- package/tsconfig.json +28 -28
- package/tsconfig.node.json +9 -9
package/src/main.tsx
CHANGED
|
@@ -1,169 +1,169 @@
|
|
|
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 = '/
|
|
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
|
+
/// <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 +1,84 @@
|
|
|
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
|
+
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,5 +1,5 @@
|
|
|
1
|
-
import { createActorContext } from '@xstate/react'
|
|
2
|
-
|
|
3
|
-
import mediaMachine from './media'
|
|
4
|
-
|
|
5
|
-
export const MediaMachineContext = createActorContext(mediaMachine)
|
|
1
|
+
import { createActorContext } from '@xstate/react'
|
|
2
|
+
|
|
3
|
+
import mediaMachine from './media'
|
|
4
|
+
|
|
5
|
+
export const MediaMachineContext = createActorContext(mediaMachine)
|