@banou/media-player 0.0.3 → 0.2.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/build/chrome/bottom.d.ts +1 -1
- package/build/chrome/index.d.ts +1 -1
- package/build/chrome/overlay.d.ts +1 -1
- package/build/index.d.ts +3 -3
- package/build/index.js +1789 -1711
- package/build/utils.d.ts +3 -4
- package/package.json +5 -6
- package/src/index.tsx +137 -271
- package/src/main.tsx +10 -56
- package/src/utils.ts +135 -280
package/build/utils.d.ts
CHANGED
|
@@ -6,8 +6,7 @@ export type Chunk = {
|
|
|
6
6
|
pos: number;
|
|
7
7
|
buffered: boolean;
|
|
8
8
|
};
|
|
9
|
+
export declare function debounceImmediateAndLatest<T extends (...args: any[]) => any>(wait: number, func: T): T;
|
|
9
10
|
export declare const queuedDebounceWithLastCall: <T2 extends any[], T extends (...args: T2) => any>(time: number, func: T) => (...args: Parameters<T>) => Promise<any> | undefined;
|
|
10
|
-
export declare const
|
|
11
|
-
|
|
12
|
-
size: number;
|
|
13
|
-
}) => ReadableStream<Uint8Array>;
|
|
11
|
+
export declare const toStreamChunkSize: (SIZE: number) => (stream: ReadableStream) => ReadableStream<Uint8Array>;
|
|
12
|
+
export declare const toBufferedStream: (SIZE: number) => (stream: ReadableStream) => ReadableStream<Uint8Array>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banou/media-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
"dev": "vite --port 4560",
|
|
15
15
|
"build": "vite build && npm run types",
|
|
16
16
|
"build-for-dev": "vite build && npm run copy-dependencies && npm run types",
|
|
17
|
-
"copy-dependencies": "npm run copy-libass && npm run copy-worker && npm run copy-worker-
|
|
18
|
-
"copy-worker": "shx cp node_modules
|
|
19
|
-
"copy-worker-
|
|
20
|
-
"copy-worker-wasm": "shx cp node_modules/@banou26/libav-wasm/build/libav.wasm build/libav.wasm",
|
|
17
|
+
"copy-dependencies": "npm run copy-libass && npm run copy-worker && npm run copy-worker-wasm",
|
|
18
|
+
"copy-worker": "shx cp node_modules/libav-wasm/build/worker.js build/libav.js",
|
|
19
|
+
"copy-worker-wasm": "shx cp node_modules/libav-wasm/build/libav.wasm build/libav.wasm",
|
|
21
20
|
"copy-libass": "copyfiles -u 3 ./node_modules/jassub/dist/* build",
|
|
22
21
|
"types": "tsc"
|
|
23
22
|
},
|
|
@@ -38,7 +37,7 @@
|
|
|
38
37
|
"dependencies": {
|
|
39
38
|
"@emotion/react": "^11.10.5",
|
|
40
39
|
"jassub": "^1.7.1",
|
|
41
|
-
"libav-wasm": "^0.
|
|
40
|
+
"libav-wasm": "^0.3.2",
|
|
42
41
|
"mp4box": "^0.5.2",
|
|
43
42
|
"osra": "^0.0.11",
|
|
44
43
|
"p-queue": "^7.3.4",
|
package/src/index.tsx
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
2
|
import type { ClassAttributes, ReactNode, SyntheticEvent, VideoHTMLAttributes } from 'react'
|
|
3
|
-
import type { MP4Info } from 'mp4box'
|
|
4
3
|
|
|
5
4
|
import { forwardRef, useEffect, useRef, useState } from 'react'
|
|
6
5
|
import { css } from '@emotion/react'
|
|
7
|
-
import {
|
|
8
|
-
import { makeTransmuxer as libavMakeTransmuxer, SEEK_WHENCE_FLAG } from 'libav-wasm'
|
|
6
|
+
import { makeRemuxer as libavMakeRemuxer } from 'libav-wasm'
|
|
9
7
|
|
|
10
|
-
import { queuedDebounceWithLastCall } from './utils'
|
|
8
|
+
import { debounceImmediateAndLatest, queuedDebounceWithLastCall, toBufferedStream, toStreamChunkSize } from './utils'
|
|
11
9
|
import Chrome from './chrome'
|
|
12
10
|
import PQueue from 'p-queue'
|
|
13
11
|
|
|
@@ -37,10 +35,7 @@ type Chunk = {
|
|
|
37
35
|
pos: number
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
const BASE_BUFFER_SIZE =
|
|
41
|
-
const PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS = 10
|
|
42
|
-
const POST_SEEK_NEEDED_BUFFERS_IN_SECONDS = 20
|
|
43
|
-
const POST_SEEK_REMOVE_BUFFERS_IN_SECONDS = 60
|
|
38
|
+
const BASE_BUFFER_SIZE = 2_500_000
|
|
44
39
|
|
|
45
40
|
const style = css`
|
|
46
41
|
display: grid;
|
|
@@ -74,14 +69,14 @@ export type FKNVideoOptions = {
|
|
|
74
69
|
customOverlay?: ReactNode
|
|
75
70
|
baseBufferSize?: number
|
|
76
71
|
size?: number
|
|
77
|
-
fetch: (offset: number, size: number) => Promise<Response>
|
|
72
|
+
fetch: (offset: number, size: number | undefined) => Promise<Response>
|
|
78
73
|
customControls?: FKNVideoControl[]
|
|
79
74
|
publicPath: string
|
|
80
75
|
wasmUrl: string
|
|
81
76
|
libavWorkerUrl: string
|
|
82
77
|
libavWorkerOptions?: WorkerOptions
|
|
83
78
|
libassWorkerUrl: string
|
|
84
|
-
makeTransmuxer?: typeof
|
|
79
|
+
makeTransmuxer?: typeof libavMakeRemuxer
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
export type HeaderChunk = Chunk & { buffer: { buffer: { fileStart: number } } }
|
|
@@ -97,7 +92,7 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
97
92
|
libavWorkerUrl,
|
|
98
93
|
libavWorkerOptions,
|
|
99
94
|
libassWorkerUrl,
|
|
100
|
-
makeTransmuxer =
|
|
95
|
+
makeTransmuxer = libavMakeRemuxer
|
|
101
96
|
}, ref) => {
|
|
102
97
|
const [loading, setLoading] = useState(true)
|
|
103
98
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -110,7 +105,6 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
110
105
|
const [errors, setErrors] = useState<TransmuxError[]>([])
|
|
111
106
|
const [duration, setDuration] = useState<number>()
|
|
112
107
|
const [currentLoadedRange, setCurrentLoadedRange] = useState<[number, number]>([0, 0])
|
|
113
|
-
const seekRef = useRef<(time: number) => any>()
|
|
114
108
|
const [needsInitialInteraction, setNeedsInitialInteraction] = useState(false)
|
|
115
109
|
|
|
116
110
|
const fetchRef = useRef(fetch)
|
|
@@ -121,57 +115,29 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
121
115
|
|
|
122
116
|
useEffect(() => {
|
|
123
117
|
if (!contentLength || !videoElement) return
|
|
124
|
-
let
|
|
118
|
+
let _remuxer: ReturnType<typeof makeTransmuxer>
|
|
125
119
|
let rangeUpdateInterval: number
|
|
126
120
|
;(async () => {
|
|
127
|
-
|
|
128
|
-
mp4boxfile.onError = (error) => console.error('mp4box error', error)
|
|
129
|
-
|
|
130
|
-
let _resolveInfo: (value: unknown) => void
|
|
131
|
-
const infoPromise = new Promise((resolve) => { _resolveInfo = resolve })
|
|
132
|
-
|
|
133
|
-
let mime = 'video/mp4; codecs=\"'
|
|
134
|
-
let info: any | undefined
|
|
135
|
-
mp4boxfile.onReady = (_info: MP4Info) => {
|
|
136
|
-
info = _info
|
|
137
|
-
for (let i = 0; i < info.tracks.length; i++) {
|
|
138
|
-
if (i !== 0) mime += ','
|
|
139
|
-
mime += info.tracks[i].codec
|
|
140
|
-
}
|
|
141
|
-
mime += '\"'
|
|
142
|
-
_resolveInfo(info)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
let headerChunk: HeaderChunk | undefined
|
|
146
|
-
let chunks: Chunk[] = []
|
|
147
|
-
|
|
148
|
-
_transmuxer = makeTransmuxer({
|
|
121
|
+
_remuxer = makeTransmuxer({
|
|
149
122
|
publicPath,
|
|
150
123
|
workerUrl: libavWorkerUrl,
|
|
151
124
|
workerOptions: libavWorkerOptions,
|
|
152
125
|
bufferSize: baseBufferSize,
|
|
153
126
|
length: contentLength,
|
|
154
|
-
|
|
127
|
+
getStream: (offset, size) =>
|
|
155
128
|
fetchRef
|
|
156
|
-
.current(offset, Math.min(offset + size, contentLength) - 1)
|
|
157
|
-
.then(res =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return offset
|
|
169
|
-
}
|
|
170
|
-
if (whence === SEEK_WHENCE_FLAG.AVSEEK_SIZE) {
|
|
171
|
-
return contentLength
|
|
172
|
-
}
|
|
173
|
-
return -1
|
|
174
|
-
},
|
|
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
|
+
),
|
|
175
141
|
subtitle: (title, language, subtitle) => {
|
|
176
142
|
setTracks(tracks =>
|
|
177
143
|
tracks.find(({ title: _title }) => _title === title)
|
|
@@ -185,87 +151,44 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
185
151
|
...attachments ?? [],
|
|
186
152
|
{ filename, mimetype, data: new Uint8Array(buffer) }
|
|
187
153
|
])
|
|
188
|
-
},
|
|
189
|
-
write: ({ isHeader, offset, buffer, pts, duration, pos }) => {
|
|
190
|
-
if (isHeader) {
|
|
191
|
-
if (!headerChunk) {
|
|
192
|
-
headerChunk = {
|
|
193
|
-
offset,
|
|
194
|
-
buffer: new Uint8Array(buffer) as HeaderChunk['buffer'],
|
|
195
|
-
pts,
|
|
196
|
-
duration,
|
|
197
|
-
pos
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
chunks = [
|
|
203
|
-
...chunks,
|
|
204
|
-
{
|
|
205
|
-
offset,
|
|
206
|
-
buffer: new Uint8Array(buffer),
|
|
207
|
-
pts,
|
|
208
|
-
duration,
|
|
209
|
-
pos
|
|
210
|
-
}
|
|
211
|
-
]
|
|
212
154
|
}
|
|
213
155
|
})
|
|
214
156
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const transmuxer = await _transmuxer
|
|
224
|
-
|
|
225
|
-
await transmuxer.init()
|
|
226
|
-
|
|
227
|
-
if (!headerChunk) throw new Error('No header chunk found after transmuxer init')
|
|
228
|
-
|
|
229
|
-
headerChunk.buffer.buffer.fileStart = 0
|
|
230
|
-
mp4boxfile.appendBuffer(headerChunk.buffer.buffer)
|
|
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
|
|
231
165
|
|
|
232
|
-
const duration = (await transmuxer.getInfo()).input.duration / 1_000_000
|
|
233
166
|
setDuration(duration)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const video = videoElement
|
|
238
|
-
video.addEventListener('error', ev => {
|
|
239
|
-
// @ts-ignore
|
|
167
|
+
|
|
168
|
+
videoElement.addEventListener('error', ev => {
|
|
169
|
+
// @ts-expect-error
|
|
240
170
|
console.error(ev.target?.error)
|
|
241
171
|
})
|
|
242
|
-
|
|
172
|
+
|
|
243
173
|
const mediaSource = new MediaSource()
|
|
244
174
|
videoElement.src = URL.createObjectURL(mediaSource)
|
|
245
|
-
|
|
175
|
+
|
|
246
176
|
const sourceBuffer: SourceBuffer =
|
|
247
177
|
await new Promise(resolve =>
|
|
248
178
|
mediaSource.addEventListener(
|
|
249
179
|
'sourceopen',
|
|
250
|
-
() =>
|
|
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
|
+
},
|
|
251
186
|
{ once: true }
|
|
252
187
|
)
|
|
253
188
|
)
|
|
254
|
-
|
|
255
|
-
mediaSource.duration = duration
|
|
256
|
-
sourceBuffer.mode = 'segments'
|
|
257
|
-
|
|
189
|
+
|
|
258
190
|
const queue = new PQueue({ concurrency: 1 })
|
|
259
|
-
|
|
260
|
-
const getTimeRanges = () =>
|
|
261
|
-
Array(sourceBuffer.buffered.length)
|
|
262
|
-
.fill(undefined)
|
|
263
|
-
.map((_, index) => ({
|
|
264
|
-
index,
|
|
265
|
-
start: sourceBuffer.buffered.start(index),
|
|
266
|
-
end: sourceBuffer.buffered.end(index)
|
|
267
|
-
}))
|
|
268
|
-
|
|
191
|
+
|
|
269
192
|
const setupListeners = (resolve: (value: Event) => void, reject: (reason: Event) => void) => {
|
|
270
193
|
const updateEndListener = (ev: Event) => {
|
|
271
194
|
resolve(ev)
|
|
@@ -289,7 +212,7 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
289
212
|
sourceBuffer.addEventListener('abort', abortListener, { once: true })
|
|
290
213
|
sourceBuffer.addEventListener('error', errorListener, { once: true })
|
|
291
214
|
}
|
|
292
|
-
|
|
215
|
+
|
|
293
216
|
const appendBuffer = (buffer: ArrayBuffer) =>
|
|
294
217
|
queue.add(() =>
|
|
295
218
|
new Promise<Event>((resolve, reject) => {
|
|
@@ -297,9 +220,7 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
297
220
|
sourceBuffer.appendBuffer(buffer)
|
|
298
221
|
})
|
|
299
222
|
)
|
|
300
|
-
|
|
301
|
-
const bufferChunk = (chunk: Chunk) => appendBuffer(chunk.buffer.buffer)
|
|
302
|
-
|
|
223
|
+
|
|
303
224
|
const unbufferRange = async (start: number, end: number) =>
|
|
304
225
|
queue.add(() =>
|
|
305
226
|
new Promise((resolve, reject) => {
|
|
@@ -307,168 +228,112 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
307
228
|
sourceBuffer.remove(start, end)
|
|
308
229
|
})
|
|
309
230
|
)
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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 BUFFER_COUNT = 5
|
|
250
|
+
|
|
251
|
+
await appendBuffer(headerChunk.buffer)
|
|
252
|
+
|
|
253
|
+
const pull = async () => {
|
|
254
|
+
const chunk = await remuxer.read()
|
|
255
|
+
chunks = [...chunks, chunk]
|
|
256
|
+
return chunk
|
|
319
257
|
}
|
|
320
|
-
|
|
321
|
-
let isSeeking = false
|
|
322
|
-
|
|
323
|
-
// todo: add error checker & retry to seek a bit earlier
|
|
324
|
-
const seek = queuedDebounceWithLastCall(500, async (time: number) => {
|
|
325
|
-
const isPlaying = !video.paused
|
|
326
|
-
videoElement?.pause()
|
|
327
|
-
isSeeking = true
|
|
328
|
-
setCurrentTime(time)
|
|
329
|
-
const ranges = getTimeRanges()
|
|
330
|
-
if (ranges.some(({ start, end }) => time >= start && time <= end)) {
|
|
331
|
-
video.currentTime = time
|
|
332
|
-
isSeeking = false
|
|
333
|
-
if (isPlaying) video.play()
|
|
334
|
-
return
|
|
335
|
-
}
|
|
336
|
-
const allTasksDone = new Promise(resolve => {
|
|
337
|
-
processingQueue.size && processingQueue.pending
|
|
338
|
-
? (
|
|
339
|
-
processingQueue.on(
|
|
340
|
-
'next',
|
|
341
|
-
() =>
|
|
342
|
-
processingQueue.pending === 0
|
|
343
|
-
? resolve(undefined)
|
|
344
|
-
: undefined
|
|
345
|
-
)
|
|
346
|
-
)
|
|
347
|
-
: resolve(undefined)
|
|
348
|
-
})
|
|
349
|
-
processingQueue.pause()
|
|
350
|
-
processingQueue.clear()
|
|
351
|
-
await allTasksDone
|
|
352
|
-
processingQueue.start()
|
|
353
258
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
259
|
+
let seeking = false
|
|
260
|
+
|
|
261
|
+
const updateBuffers = queuedDebounceWithLastCall(250, async () => {
|
|
262
|
+
if (seeking) return
|
|
263
|
+
const { currentTime } = videoElement
|
|
264
|
+
const currentChunkIndex = chunks.findIndex(({ pts, duration }) => pts <= currentTime && pts + duration >= currentTime)
|
|
265
|
+
const sliceIndex = Math.max(0, currentChunkIndex - PREVIOUS_BUFFER_COUNT)
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < sliceIndex + BUFFER_COUNT; i++) {
|
|
268
|
+
if (chunks[i]) continue
|
|
269
|
+
const chunk = await pull()
|
|
270
|
+
await appendBuffer(chunk.buffer)
|
|
363
271
|
}
|
|
364
|
-
// updateBufferedRanges(time)
|
|
365
|
-
video.currentTime = time
|
|
366
|
-
isSeeking = false
|
|
367
|
-
if (isPlaying) video.play()
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
seekRef.current = seek
|
|
371
|
-
|
|
372
|
-
const updateBufferedRanges = async (time: number) => {
|
|
373
|
-
const ranges1 = getTimeRanges()
|
|
374
|
-
const neededChunks =
|
|
375
|
-
chunks
|
|
376
|
-
.filter(({ pts, duration }) =>
|
|
377
|
-
((time - PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS) < pts)
|
|
378
|
-
&& ((time + POST_SEEK_REMOVE_BUFFERS_IN_SECONDS) > (pts + duration))
|
|
379
|
-
)
|
|
380
272
|
|
|
381
|
-
|
|
382
|
-
neededChunks
|
|
383
|
-
.filter(({ pts, duration }) =>
|
|
384
|
-
((time - PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS) < pts)
|
|
385
|
-
&& ((time + POST_SEEK_NEEDED_BUFFERS_IN_SECONDS) > (pts + duration))
|
|
386
|
-
)
|
|
273
|
+
if (sliceIndex) chunks = chunks.slice(sliceIndex)
|
|
387
274
|
|
|
388
|
-
const
|
|
389
|
-
chunks
|
|
390
|
-
.filter(({ pts, duration }) => ranges1.some(({ start, end }) => start < (pts + (duration / 2)) && (pts + (duration / 2)) < end))
|
|
391
|
-
.filter((chunk) => !shouldBeBufferedChunks.includes(chunk))
|
|
275
|
+
const bufferedRanges = getTimeRanges()
|
|
392
276
|
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
277
|
+
const firstChunk = chunks.at(0)
|
|
278
|
+
const lastChunk = chunks.at(-1)
|
|
279
|
+
if (!firstChunk || !lastChunk || firstChunk === lastChunk) return
|
|
280
|
+
const minTime = firstChunk.pts
|
|
396
281
|
|
|
397
|
-
for (const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
await bufferChunk(chunk)
|
|
409
|
-
} catch (err) {
|
|
410
|
-
console.error(err)
|
|
411
|
-
if (!(err instanceof Event)) throw err
|
|
412
|
-
break
|
|
282
|
+
for (const { start, end } of bufferedRanges) {
|
|
283
|
+
const chunkIndex = chunks.findIndex(({ pts, duration }) => start <= (pts + (duration / 2)) && (pts + (duration / 2)) <= end)
|
|
284
|
+
if (chunkIndex === -1) {
|
|
285
|
+
await unbufferRange(start, end)
|
|
286
|
+
} else {
|
|
287
|
+
if (start < minTime) {
|
|
288
|
+
await unbufferRange(
|
|
289
|
+
start,
|
|
290
|
+
minTime
|
|
291
|
+
)
|
|
292
|
+
}
|
|
413
293
|
}
|
|
414
294
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
await
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
let firstSeekPaused: boolean | undefined
|
|
298
|
+
const seek = debounceImmediateAndLatest(250, async (seekTime: number) => {
|
|
299
|
+
try {
|
|
300
|
+
if (firstSeekPaused === undefined) firstSeekPaused = videoElement.paused
|
|
301
|
+
seeking = true
|
|
302
|
+
chunks = []
|
|
303
|
+
await remuxer.seek(seekTime)
|
|
304
|
+
const chunk1 = await pull()
|
|
305
|
+
sourceBuffer.timestampOffset = chunk1.pts
|
|
306
|
+
await appendBuffer(chunk1.buffer)
|
|
307
|
+
if (firstSeekPaused === false) {
|
|
308
|
+
await videoElement.play()
|
|
429
309
|
}
|
|
430
|
-
|
|
431
|
-
|
|
310
|
+
seeking = false
|
|
311
|
+
await updateBuffers()
|
|
312
|
+
if (firstSeekPaused === false) {
|
|
313
|
+
await videoElement.play()
|
|
432
314
|
}
|
|
315
|
+
firstSeekPaused = undefined
|
|
316
|
+
} catch (err: any) {
|
|
317
|
+
if (err.message !== 'exit') throw err
|
|
433
318
|
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const loadedMetadataPromise = new Promise(resolve => {
|
|
437
|
-
video.addEventListener('loadedmetadata', () => resolve(undefined), { once: true })
|
|
438
319
|
})
|
|
439
320
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
// Catch error if user denied autoplay
|
|
446
|
-
.catch(err => {
|
|
447
|
-
if (!(err instanceof DOMException) || err.name !== 'NotAllowedError') return
|
|
448
|
-
setNeedsInitialInteraction(true)
|
|
449
|
-
setLoading(false)
|
|
450
|
-
}),
|
|
451
|
-
{ once: true }
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
await appendBuffer(headerChunk.buffer)
|
|
455
|
-
await loadedMetadataPromise
|
|
456
|
-
|
|
457
|
-
await process(20)
|
|
458
|
-
await updateBufferedRanges(0)
|
|
459
|
-
|
|
460
|
-
const timeUpdateWork = queuedDebounceWithLastCall(500, async (time: number) => {
|
|
461
|
-
const lastChunk = chunks.sort(({ pts }, { pts: pts2 }) => pts - pts2).at(-1)
|
|
462
|
-
if (lastChunk && lastChunk.pts < time + POST_SEEK_NEEDED_BUFFERS_IN_SECONDS) {
|
|
463
|
-
await process()
|
|
464
|
-
}
|
|
465
|
-
await updateBufferedRanges(time)
|
|
321
|
+
const firstChunk = await pull()
|
|
322
|
+
appendBuffer(firstChunk.buffer)
|
|
323
|
+
|
|
324
|
+
videoElement.addEventListener('timeupdate', () => {
|
|
325
|
+
updateBuffers()
|
|
466
326
|
})
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
timeUpdateWork(video.currentTime)
|
|
327
|
+
|
|
328
|
+
videoElement.addEventListener('waiting', () => {
|
|
329
|
+
updateBuffers()
|
|
471
330
|
})
|
|
331
|
+
|
|
332
|
+
videoElement.addEventListener('seeking', (ev) => {
|
|
333
|
+
seek(videoElement.currentTime)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
updateBuffers()
|
|
472
337
|
|
|
473
338
|
rangeUpdateInterval = window.setInterval(() => {
|
|
474
339
|
const ranges = getTimeRanges()
|
|
@@ -483,7 +348,7 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
483
348
|
})()
|
|
484
349
|
|
|
485
350
|
return () => {
|
|
486
|
-
|
|
351
|
+
_remuxer.then(transmuxer => transmuxer.destroy(true))
|
|
487
352
|
window.clearInterval(rangeUpdateInterval)
|
|
488
353
|
}
|
|
489
354
|
}, [contentLength, videoElement])
|
|
@@ -505,8 +370,9 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
505
370
|
}
|
|
506
371
|
|
|
507
372
|
const seek = (time: number) => {
|
|
373
|
+
if (!videoElement) return
|
|
374
|
+
videoElement.currentTime = time
|
|
508
375
|
setIsSeeking(true)
|
|
509
|
-
seekRef.current?.(time)
|
|
510
376
|
}
|
|
511
377
|
|
|
512
378
|
const timeUpdate: React.DOMAttributes<HTMLVideoElement>['onTimeUpdate'] = (ev) => {
|
package/src/main.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import { createRoot } from 'react-dom/client'
|
|
|
4
4
|
import { css, Global } from '@emotion/react'
|
|
5
5
|
|
|
6
6
|
import MediaPlayer from './index'
|
|
7
|
-
import { bufferStream } from './utils'
|
|
8
7
|
|
|
9
8
|
const mountStyle = css`
|
|
10
9
|
display: grid;
|
|
@@ -24,69 +23,24 @@ const Mount = () => {
|
|
|
24
23
|
videoElemRef.addEventListener('error', err => console.log('err', err))
|
|
25
24
|
}, [videoElemRef])
|
|
26
25
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}, [streamReader])
|
|
36
|
-
|
|
37
|
-
const setupStream = async (offset: number) => {
|
|
38
|
-
if (streamReader) {
|
|
39
|
-
streamReader.cancel()
|
|
40
|
-
}
|
|
41
|
-
const streamResponse = await onFetch(offset, undefined, true)
|
|
42
|
-
if (!streamResponse.body) throw new Error('no body')
|
|
43
|
-
const stream = bufferStream({ stream: streamResponse.body, size: BASE_BUFFER_SIZE })
|
|
44
|
-
const reader = stream.getReader()
|
|
45
|
-
setStreamReader(reader)
|
|
46
|
-
setCurrentStreamOffset(offset)
|
|
47
|
-
return reader
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const onFetch = async (offset: number, end?: number, force?: boolean) => {
|
|
51
|
-
if (force || end !== undefined && ((end - offset) + 1) !== BASE_BUFFER_SIZE) {
|
|
52
|
-
return (
|
|
53
|
-
fetch(
|
|
54
|
-
'/video3.mkv',
|
|
55
|
-
{
|
|
56
|
-
headers: {
|
|
57
|
-
Range: `bytes=${offset}-${end ?? ''}`
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
)
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
const _streamReader =
|
|
64
|
-
currentStreamOffset !== offset
|
|
65
|
-
? await setupStream(offset)
|
|
66
|
-
: streamReader
|
|
67
|
-
|
|
68
|
-
if (!_streamReader) throw new Error('Stream reader not ready')
|
|
69
|
-
return new Response(
|
|
70
|
-
await _streamReader
|
|
71
|
-
.read()
|
|
72
|
-
.then(({ value }) => {
|
|
73
|
-
if (value) {
|
|
74
|
-
setCurrentStreamOffset(offset => offset + value.byteLength)
|
|
75
|
-
}
|
|
76
|
-
return value
|
|
77
|
-
})
|
|
26
|
+
const onFetch = async (offset: number, end?: number) =>
|
|
27
|
+
fetch(
|
|
28
|
+
'/video6.mkv',
|
|
29
|
+
{
|
|
30
|
+
headers: {
|
|
31
|
+
Range: `bytes=${offset}-${end ?? ''}`
|
|
32
|
+
}
|
|
33
|
+
}
|
|
78
34
|
)
|
|
79
|
-
}
|
|
80
35
|
|
|
81
36
|
useEffect(() => {
|
|
82
|
-
onFetch(0, 1
|
|
37
|
+
onFetch(0, 1).then(async ({ headers, body }) => {
|
|
83
38
|
if (!body) throw new Error('no body')
|
|
84
39
|
const contentRangeContentLength = headers.get('Content-Range')?.split('/').at(1)
|
|
85
40
|
const contentLength =
|
|
86
41
|
contentRangeContentLength
|
|
87
42
|
? Number(contentRangeContentLength)
|
|
88
43
|
: Number(headers.get('Content-Length'))
|
|
89
|
-
if (STREAM_RESPONSES) await setupStream(0)
|
|
90
44
|
setSize(contentLength)
|
|
91
45
|
})
|
|
92
46
|
}, [])
|
|
@@ -109,7 +63,7 @@ const Mount = () => {
|
|
|
109
63
|
baseBufferSize={BASE_BUFFER_SIZE}
|
|
110
64
|
ref={setVideoElemRef}
|
|
111
65
|
size={size}
|
|
112
|
-
fetch={(offset, end) => onFetch(offset, end
|
|
66
|
+
fetch={(offset, end) => onFetch(offset, end)}
|
|
113
67
|
publicPath={new URL('/build/', new URL(import.meta.url).origin).toString()}
|
|
114
68
|
wasmUrl={new URL('/build/jassub-worker-modern.wasm', new URL(import.meta.url).origin).toString()}
|
|
115
69
|
libavWorkerUrl={libavWorkerUrl}
|