@banou/media-player 0.0.3 → 0.2.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/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 +142 -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.0
|
|
3
|
+
"version": "0.2.0",
|
|
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.1
|
|
40
|
+
"libav-wasm": "^0.3.1",
|
|
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,117 @@ 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
|
+
// @ts-expect-error
|
|
256
|
+
chunks = [...chunks, chunk]
|
|
257
|
+
return chunk
|
|
319
258
|
}
|
|
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
259
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
260
|
+
let seeking = false
|
|
261
|
+
|
|
262
|
+
const updateBuffers = queuedDebounceWithLastCall(250, async () => {
|
|
263
|
+
if (seeking) return
|
|
264
|
+
const { currentTime } = videoElement
|
|
265
|
+
const currentChunkIndex = chunks.findIndex(({ pts, duration }) => pts <= currentTime && pts + duration >= currentTime)
|
|
266
|
+
const sliceIndex = Math.max(0, currentChunkIndex - PREVIOUS_BUFFER_COUNT)
|
|
267
|
+
|
|
268
|
+
for (let i = 0; i < sliceIndex + BUFFER_COUNT; i++) {
|
|
269
|
+
if (chunks[i]) continue
|
|
270
|
+
const chunk = await pull()
|
|
271
|
+
// @ts-expect-error
|
|
272
|
+
await appendBuffer(chunk.buffer)
|
|
363
273
|
}
|
|
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
274
|
|
|
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
|
-
)
|
|
275
|
+
if (sliceIndex) chunks = chunks.slice(sliceIndex)
|
|
387
276
|
|
|
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))
|
|
277
|
+
const bufferedRanges = getTimeRanges()
|
|
392
278
|
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
279
|
+
const firstChunk = chunks.at(0)
|
|
280
|
+
const lastChunk = chunks.at(-1)
|
|
281
|
+
if (!firstChunk || !lastChunk || firstChunk === lastChunk) return
|
|
282
|
+
const minTime = firstChunk.pts
|
|
396
283
|
|
|
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
|
|
284
|
+
for (const { start, end } of bufferedRanges) {
|
|
285
|
+
const chunkIndex = chunks.findIndex(({ pts, duration }) => start <= (pts + (duration / 2)) && (pts + (duration / 2)) <= end)
|
|
286
|
+
if (chunkIndex === -1) {
|
|
287
|
+
await unbufferRange(start, end)
|
|
288
|
+
} else {
|
|
289
|
+
if (start < minTime) {
|
|
290
|
+
await unbufferRange(
|
|
291
|
+
start,
|
|
292
|
+
minTime
|
|
293
|
+
)
|
|
294
|
+
}
|
|
413
295
|
}
|
|
414
296
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
let firstSeekPaused: boolean | undefined
|
|
300
|
+
const seek = debounceImmediateAndLatest(250, async (seekTime: number) => {
|
|
301
|
+
try {
|
|
302
|
+
if (firstSeekPaused === undefined) firstSeekPaused = videoElement.paused
|
|
303
|
+
seeking = true
|
|
304
|
+
chunks = []
|
|
305
|
+
await remuxer.seek(seekTime)
|
|
306
|
+
const chunk1 = await pull()
|
|
307
|
+
// @ts-expect-error
|
|
308
|
+
sourceBuffer.timestampOffset = chunk1.pts
|
|
309
|
+
// @ts-expect-error
|
|
310
|
+
await appendBuffer(chunk1.buffer)
|
|
311
|
+
if (firstSeekPaused === false) {
|
|
312
|
+
await videoElement.play()
|
|
429
313
|
}
|
|
430
|
-
|
|
431
|
-
|
|
314
|
+
seeking = false
|
|
315
|
+
await updateBuffers()
|
|
316
|
+
if (firstSeekPaused === false) {
|
|
317
|
+
await videoElement.play()
|
|
432
318
|
}
|
|
319
|
+
firstSeekPaused = undefined
|
|
320
|
+
} catch (err: any) {
|
|
321
|
+
if (err.message !== 'exit') throw err
|
|
433
322
|
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const loadedMetadataPromise = new Promise(resolve => {
|
|
437
|
-
video.addEventListener('loadedmetadata', () => resolve(undefined), { once: true })
|
|
438
323
|
})
|
|
439
324
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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)
|
|
325
|
+
const firstChunk = await pull()
|
|
326
|
+
// @ts-expect-error
|
|
327
|
+
appendBuffer(firstChunk.buffer)
|
|
328
|
+
|
|
329
|
+
videoElement.addEventListener('timeupdate', () => {
|
|
330
|
+
updateBuffers()
|
|
466
331
|
})
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
timeUpdateWork(video.currentTime)
|
|
332
|
+
|
|
333
|
+
videoElement.addEventListener('waiting', () => {
|
|
334
|
+
updateBuffers()
|
|
471
335
|
})
|
|
336
|
+
|
|
337
|
+
videoElement.addEventListener('seeking', (ev) => {
|
|
338
|
+
seek(videoElement.currentTime)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
updateBuffers()
|
|
472
342
|
|
|
473
343
|
rangeUpdateInterval = window.setInterval(() => {
|
|
474
344
|
const ranges = getTimeRanges()
|
|
@@ -483,7 +353,7 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
483
353
|
})()
|
|
484
354
|
|
|
485
355
|
return () => {
|
|
486
|
-
|
|
356
|
+
_remuxer.then(transmuxer => transmuxer.destroy(true))
|
|
487
357
|
window.clearInterval(rangeUpdateInterval)
|
|
488
358
|
}
|
|
489
359
|
}, [contentLength, videoElement])
|
|
@@ -505,8 +375,9 @@ const FKNVideo = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLInputEleme
|
|
|
505
375
|
}
|
|
506
376
|
|
|
507
377
|
const seek = (time: number) => {
|
|
378
|
+
if (!videoElement) return
|
|
379
|
+
videoElement.currentTime = time
|
|
508
380
|
setIsSeeking(true)
|
|
509
|
-
seekRef.current?.(time)
|
|
510
381
|
}
|
|
511
382
|
|
|
512
383
|
const timeUpdate: React.DOMAttributes<HTMLVideoElement>['onTimeUpdate'] = (ev) => {
|