@banou/media-player 0.0.2 → 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/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 bufferStream: ({ stream, size: SIZE }: {
11
- stream: ReadableStream;
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.2",
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-deps && npm run copy-worker-wasm",
18
- "copy-worker": "shx cp node_modules/@banou26/libav-wasm/build/worker.js build/libav.js",
19
- "copy-worker-deps": "copyfiles -u 4 node_modules/@banou26/libav-wasm/build/shared-memory-api_pb.*.js build",
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.9",
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 { createFile } from 'mp4box'
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 = 5_000_000
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 libavMakeTransmuxer
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 = libavMakeTransmuxer
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 _transmuxer: ReturnType<typeof makeTransmuxer>
118
+ let _remuxer: ReturnType<typeof makeTransmuxer>
125
119
  let rangeUpdateInterval: number
126
120
  ;(async () => {
127
- let mp4boxfile = createFile()
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
- read: (offset, size) =>
127
+ getStream: (offset, size) =>
155
128
  fetchRef
156
- .current(offset, Math.min(offset + size, contentLength) - 1)
157
- .then(res => res.arrayBuffer()),
158
- seek: async (currentOffset, offset, whence) => {
159
- if (whence === SEEK_WHENCE_FLAG.SEEK_CUR) {
160
- return currentOffset + offset
161
- }
162
- if (whence === SEEK_WHENCE_FLAG.SEEK_END) {
163
- return -1
164
- }
165
- if (whence === SEEK_WHENCE_FLAG.SEEK_SET) {
166
- // little trick to prevent libav from requesting end of file data on init that might take a while to fetch
167
- // if (!initDone && offset > (contentLength - 1_000_000)) return -1
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 processingQueue = new PQueue({ concurrency: 1 })
216
-
217
- const process = (timeToProcess = POST_SEEK_NEEDED_BUFFERS_IN_SECONDS) =>
218
- processingQueue.add(
219
- () => transmuxer.process(timeToProcess),
220
- { throwOnTimeout: true }
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
- await infoPromise
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
- () => resolve(mediaSource.addSourceBuffer(mime)),
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 unbufferChunk = (chunk: Chunk) =>
312
- unbufferRange(chunk.pts, chunk.pts + chunk.duration)
313
-
314
- const removeChunk = async (chunk: Chunk) => {
315
- const chunkIndex = chunks.indexOf(chunk)
316
- if (chunkIndex === -1) throw new RangeError('No chunk found')
317
- await unbufferChunk(chunk)
318
- chunks = chunks.filter(_chunk => _chunk !== chunk)
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
- const seekTime = Math.max(0, time - PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS)
355
- await transmuxer.seek(seekTime)
356
- await process(POST_SEEK_NEEDED_BUFFERS_IN_SECONDS + POST_SEEK_NEEDED_BUFFERS_IN_SECONDS)
357
- for (const range of ranges) {
358
- await unbufferRange(range.start, range.end)
359
- }
360
- for (const chunk of chunks) {
361
- if (chunk.pts <= seekTime) continue
362
- await bufferChunk(chunk)
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
- const shouldBeBufferedChunks =
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 shouldBeUnbufferedChunks =
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 nonNeededChunks =
394
- chunks
395
- .filter((chunk) => !neededChunks.includes(chunk))
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 shouldBeUnbufferedChunk of shouldBeUnbufferedChunks) {
398
- await unbufferChunk(shouldBeUnbufferedChunk)
399
- }
400
- for (const nonNeededChunk of nonNeededChunks) {
401
- await removeChunk(nonNeededChunk)
402
- }
403
- const firstChunk = neededChunks.sort(({ pts }, { pts: pts2 }) => pts - pts2).at(0)
404
- const lastChunk = neededChunks.sort(({ pts, duration }, { pts: pts2, duration: duration2 }) => (pts + duration) - (pts2 + duration2)).at(-1)
405
-
406
- for (const chunk of shouldBeBufferedChunks) {
407
- try {
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
- const lowestAllowedStart =
417
- firstChunk
418
- ? Math.max(firstChunk?.pts - PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS, 0)
419
- : undefined
420
- const highestAllowedEnd =
421
- lastChunk
422
- ? Math.min(lastChunk.pts + lastChunk.duration + POST_SEEK_NEEDED_BUFFERS_IN_SECONDS, duration)
423
- : undefined
424
- const ranges = getTimeRanges()
425
- for (const { start, end } of ranges) {
426
- if (!lowestAllowedStart || !highestAllowedEnd) continue
427
- if (lowestAllowedStart !== undefined && start < lowestAllowedStart) {
428
- await unbufferRange(start, lowestAllowedStart)
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
- if (highestAllowedEnd !== undefined && end > highestAllowedEnd) {
431
- await unbufferRange(highestAllowedEnd, end)
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
- video.addEventListener(
441
- 'canplay',
442
- () =>
443
- video
444
- .play()
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)
325
+ const firstChunk = await pull()
326
+ // @ts-expect-error
327
+ appendBuffer(firstChunk.buffer)
328
+
329
+ videoElement.addEventListener('timeupdate', () => {
330
+ updateBuffers()
466
331
  })
467
-
468
- video.addEventListener('timeupdate', () => {
469
- if (isSeeking) return
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
- _transmuxer.then(transmuxer => transmuxer.destroy(true))
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) => {