@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/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 [currentStreamOffset, setCurrentStreamOffset] = useState<number>(0)
28
- const [streamReader, setStreamReader] = useState<ReadableStreamDefaultReader<Uint8Array>>()
29
-
30
- useEffect(() => {
31
- if (!streamReader) return
32
- return () => {
33
- streamReader.cancel()
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, true).then(async ({ headers, body }) => {
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, STREAM_RESPONSES ? false : true)}
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}
package/src/utils.ts CHANGED
@@ -7,6 +7,38 @@ export type Chunk = {
7
7
  buffered: boolean
8
8
  }
9
9
 
10
+ export function debounceImmediateAndLatest<T extends (...args: any[]) => any>(
11
+ wait: number,
12
+ func: T
13
+ ): T {
14
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
15
+ let lastArgs: any[] | null = null;
16
+
17
+ const debouncedFunction = function(...args: any[]) {
18
+ const context = this;
19
+
20
+ if (timeoutId === null) {
21
+ // Call immediately on the first call
22
+ func.apply(context, args);
23
+ } else {
24
+ // Store the latest arguments for the last call
25
+ lastArgs = args;
26
+ }
27
+
28
+ clearTimeout(timeoutId as ReturnType<typeof setTimeout>);
29
+
30
+ timeoutId = setTimeout(() => {
31
+ if (lastArgs) {
32
+ func.apply(context, lastArgs);
33
+ lastArgs = null;
34
+ }
35
+ timeoutId = null;
36
+ }, wait);
37
+ };
38
+
39
+ return debouncedFunction as T;
40
+ }
41
+
10
42
  export const queuedDebounceWithLastCall = <T2 extends any[], T extends (...args: T2) => any>(time: number, func: T) => {
11
43
  let runningFunction: Promise<ReturnType<T>> | undefined
12
44
  let lastCall: Promise<ReturnType<T>> | undefined
@@ -84,63 +116,6 @@ export const queuedDebounceWithLastCall = <T2 extends any[], T extends (...args:
84
116
  }
85
117
  }
86
118
 
87
- // todo: reimplement this into a ReadableByteStream https://web.dev/streams/ once FF gets support
88
- // todo: remove firefox workaround once https://bugzilla.mozilla.org/show_bug.cgi?id=1808868 ships
89
- export const bufferStream = ({ stream, size: SIZE }: { stream: ReadableStream, size: number }) =>
90
- new ReadableStream<Uint8Array>({
91
- start() {
92
- // @ts-expect-error
93
- this.reader = stream.getReader()
94
- },
95
- async pull(controller) {
96
- try {
97
- // @ts-expect-error
98
- const { leftOverData }: { leftOverData: Uint8Array | undefined } = this
99
-
100
- const accumulate = async ({ buffer = new Uint8Array(SIZE), currentSize = 0 } = {}): Promise<{ buffer?: Uint8Array, currentSize?: number, done: boolean }> => {
101
- // @ts-expect-error
102
- const { value: newBuffer, done } = await this.reader.read()
103
-
104
- if (currentSize === 0 && leftOverData) {
105
- // console.log('currentSize', currentSize)
106
- // console.log('buffer', buffer)
107
- // console.log('leftOverData', leftOverData)
108
- buffer.set(leftOverData)
109
- currentSize += leftOverData.byteLength
110
- // @ts-expect-error
111
- this.leftOverData = undefined
112
- }
113
-
114
- if (done) {
115
- return { buffer: buffer.slice(0, currentSize), currentSize, done }
116
- }
117
-
118
- let newSize
119
- const slicedBuffer = newBuffer.slice(0, SIZE - currentSize)
120
- newSize = currentSize + slicedBuffer.byteLength
121
- buffer.set(slicedBuffer, currentSize)
122
-
123
- if (newSize === SIZE) {
124
- // @ts-expect-error
125
- this.leftOverData = newBuffer.slice(SIZE - currentSize)
126
- return { buffer, currentSize: newSize, done: false }
127
- }
128
-
129
- return accumulate({ buffer, currentSize: newSize })
130
- }
131
- const { buffer, done } = await accumulate()
132
- if (buffer?.byteLength) controller.enqueue(buffer)
133
- if (done) controller.close()
134
- } catch (err) {
135
- console.error(err)
136
- }
137
- },
138
- cancel() {
139
- // @ts-expect-error
140
- this.reader.cancel()
141
- }
142
- })
143
-
144
119
  const getTimeRanges = (sourceBuffer: SourceBuffer) =>
145
120
  Array(sourceBuffer.buffered.length)
146
121
  .fill(undefined)
@@ -156,230 +131,110 @@ const getTimeRange =
156
131
  getTimeRanges(sourceBuffer)
157
132
  .find(({ start, end }) => time >= start && time <= end)
158
133
 
159
- // const setupListeners = (sourceBuffer: SourceBuffer, resolve: (value: Event) => void, reject: (reason: Event) => void) => {
160
- // const updateEndListener = (ev: Event) => {
161
- // resolve(ev)
162
- // unregisterListeners()
163
- // }
164
- // const abortListener = (ev: Event) => {
165
- // resolve(ev)
166
- // unregisterListeners()
167
- // }
168
- // const errorListener = (ev: Event) => {
169
- // reject(ev),
170
- // unregisterListeners()
171
- // }
172
- // const unregisterListeners = () => {
173
- // sourceBuffer.removeEventListener('updateend', updateEndListener)
174
- // sourceBuffer.removeEventListener('abort', abortListener)
175
- // sourceBuffer.removeEventListener('error', errorListener)
176
- // }
177
- // sourceBuffer.addEventListener('updateend', updateEndListener, { once: true })
178
- // sourceBuffer.addEventListener('abort', abortListener, { once: true })
179
- // sourceBuffer.addEventListener('error', errorListener, { once: true })
180
- // }
181
-
182
- // const appendBuffer = (queue: PQueue, sourceBuffer: SourceBuffer, buffer: ArrayBuffer) =>
183
- // queue.add(() =>
184
- // new Promise<Event>((resolve, reject) => {
185
- // setupListeners(sourceBuffer, resolve, reject)
186
- // sourceBuffer.appendBuffer(buffer)
187
- // })
188
- // )
189
-
190
- // const bufferChunk = async (queue: PQueue, sourceBuffer: SourceBuffer, chunk: Chunk) => {
191
- // await appendBuffer(queue, sourceBuffer, chunk.buffer.buffer)
192
- // chunk.buffered = true
193
- // }
194
-
195
- // const unbufferChunk = async (queue: PQueue, sourceBuffer: SourceBuffer, chunks: Chunk[], chunk: Chunk) =>
196
- // queue.add(() =>
197
- // new Promise((resolve, reject) => {
198
- // setupListeners(sourceBuffer, resolve, reject)
199
-
200
- // const chunkIndex = chunks.indexOf(chunk)
201
- // if (chunkIndex === -1) return reject('No chunk found')
202
- // sourceBuffer.remove(chunk.pts, chunk.pts + chunk.duration)
203
- // chunk.buffered = false
204
- // })
205
- // )
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
- // const listenForOperationResult =
218
- // (sourceBuffer: SourceBuffer) =>
219
- // (resolve: (value: Event) => void, reject: (reason: Event) => void) => {
220
- // const updateEndListener = (ev: Event) => {
221
- // resolve(ev)
222
- // unregisterListeners()
223
- // }
224
- // const abortListener = (ev: Event) => {
225
- // resolve(ev)
226
- // unregisterListeners()
227
- // }
228
- // const errorListener = (ev: Event) => {
229
- // reject(ev),
230
- // unregisterListeners()
231
- // }
232
- // const unregisterListeners = () => {
233
- // sourceBuffer.removeEventListener('updateend', updateEndListener)
234
- // sourceBuffer.removeEventListener('abort', abortListener)
235
- // sourceBuffer.removeEventListener('error', errorListener)
236
- // }
237
- // sourceBuffer.addEventListener('updateend', updateEndListener, { once: true })
238
- // sourceBuffer.addEventListener('abort', abortListener, { once: true })
239
- // sourceBuffer.addEventListener('error', errorListener, { once: true })
240
- // }
241
-
242
- // export const appendBuffer =
243
- // (sourceBuffer: SourceBuffer) =>
244
- // (buffer: ArrayBuffer) =>
245
- // new Promise<Event>((resolve, reject) => {
246
- // listenForOperationResult(sourceBuffer)(resolve, reject)
247
- // sourceBuffer.appendBuffer(buffer)
248
- // })
249
134
 
250
- // export const removeRange =
251
- // (sourceBuffer: SourceBuffer) =>
252
- // ({ start, end, index }: { start: number, end: number, index: number }) =>
253
- // listenForOperationResult(sourceBuffer)(() => {
254
- // sourceBuffer.remove(
255
- // Math.max(sourceBuffer.buffered.start(index), start),
256
- // Math.min(sourceBuffer.buffered.end(index), end)
257
- // )
258
- // })
259
-
260
- // export const abort =
261
- // (sourceBuffer: SourceBuffer) =>
262
- // listenForOperationResult(sourceBuffer)(() => {
263
- // sourceBuffer.abort()
264
- // })
265
-
266
- // export const appendChunk =
267
- // (sourceBuffer: SourceBuffer) =>
268
- // async (chunk: Chunk) => {
269
- // await appendBuffer(sourceBuffer)(chunk.arrayBuffer.buffer)
270
- // }
271
-
272
- // export const removeChunk =
273
- // (sourceBuffer: SourceBuffer) =>
274
- // async (chunk: Chunk) => {
275
- // if (chunk.keyframeIndex < 0) return console.log('skipped remove', chunk)
276
- // const range = getTimeRange(sourceBuffer)(chunk.startTime) ?? getTimeRange(sourceBuffer)(chunk.endTime)
277
- // if (!range) throw new RangeError('No TimeRange found with this chunk')
278
- // await removeRange(sourceBuffer)({ start: chunk.startTime, end: chunk.endTime, index: range.index })
279
- // }
280
-
281
- // const PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS = 15
282
- // const POST_SEEK_NEEDED_BUFFERS_IN_SECONDS = 30
283
-
284
- // // todo: fix issue with last chunk not properly loaded
285
- // export const updateSourceBuffer =
286
- // (sourceBuffer: SourceBuffer, getChunkArrayBuffer: (id: number) => Promise<Uint8Array | undefined>) => {
287
- // const cache: Map<number, Uint8Array | Promise<Uint8Array | undefined>> = new Map()
288
- // const bufferedChunksMap: Map<number, Uint8Array> = new Map()
289
- // let renderCount = 0
290
-
291
- // const update = async ({ currentTime, chunks, retry = false }: { currentTime: number, chunks: Chunk[], retry: boolean }) => {
292
- // const currentRenderCount = renderCount
293
- // renderCount++
294
-
295
- // const neededChunks =
296
- // [
297
- // // header chunk
298
- // { keyframeIndex: 0, startTime: 0, endTime: 0 },
299
- // ...chunks
300
- // .filter(({ startTime, endTime }) =>
301
- // currentTime - PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS < startTime
302
- // && currentTime + POST_SEEK_NEEDED_BUFFERS_IN_SECONDS > endTime
303
- // )
304
- // ]
135
+ // todo: reimplement this into a ReadableByteStream https://web.dev/streams/ once Safari gets support
136
+ export const toStreamChunkSize = (SIZE: number) => (stream: ReadableStream) =>
137
+ new ReadableStream<Uint8Array>({
138
+ reader: undefined,
139
+ leftOverData: undefined,
140
+ start() {
141
+ this.reader = stream.getReader()
142
+ },
143
+ async pull(controller) {
144
+ const { leftOverData }: { leftOverData: Uint8Array | undefined } = this
145
+
146
+ const accumulate = async ({ buffer = new Uint8Array(SIZE), currentSize = 0 } = {}): Promise<{ buffer?: Uint8Array, currentSize?: number, done: boolean }> => {
147
+ const { value: newBuffer, done } = await this.reader!.read()
148
+ if (currentSize === 0 && leftOverData) {
149
+ buffer.set(leftOverData)
150
+ currentSize += leftOverData.byteLength
151
+ this.leftOverData = undefined
152
+ }
153
+
154
+ if (done) {
155
+ const finalResult = { buffer: buffer.slice(0, currentSize), currentSize, done }
156
+ this.reader = undefined
157
+ this.leftOverData = undefined
158
+ return finalResult
159
+ }
160
+
161
+ let newSize
162
+ const slicedBuffer = newBuffer.slice(0, SIZE - currentSize)
163
+ newSize = currentSize + slicedBuffer.byteLength
164
+ buffer.set(slicedBuffer, currentSize)
165
+
166
+ if (newSize === SIZE) {
167
+ this.leftOverData = newBuffer.slice(SIZE - currentSize)
168
+ return { buffer, currentSize: newSize, done: false }
169
+ }
170
+
171
+ return accumulate({ buffer, currentSize: newSize })
172
+ }
173
+ const { buffer, done } = await accumulate()
174
+ if (buffer?.byteLength) controller.enqueue(buffer)
175
+ if (done) controller.close()
176
+ },
177
+ cancel() {
178
+ this.reader!.cancel()
179
+ this.leftOverData = undefined
180
+ }
181
+ } as UnderlyingDefaultSource<Uint8Array> & {
182
+ reader: ReadableStreamDefaultReader<Uint8Array> | undefined
183
+ leftOverData: Uint8Array | undefined
184
+ })
305
185
 
306
- // // todo: improve perf by making this loop inclusive of bufferedChunksMap instead of chunks
307
- // const shouldUnbufferChunks =
308
- // chunks
309
- // .filter(chunk => !neededChunks.includes(chunk))
186
+ export const toBufferedStream = (SIZE: number) => (stream: ReadableStream) =>
187
+ new ReadableStream<Uint8Array>({
188
+ buffers: [],
189
+ currentPullPromise: undefined,
190
+ reader: undefined,
191
+ leftOverData: undefined,
192
+ start() {
193
+ this.reader = stream.getReader()
194
+ },
195
+ async pull(controller) {
196
+ const pull = async () => {
197
+ if (this.buffers.length >= SIZE) return
198
+ this.currentPullPromise = this.reader!.read()
199
+ const { value: newBuffer, done } = await this.currentPullPromise
200
+ this.currentPullPromise = undefined
201
+ if (done) {
202
+ try {
203
+ controller.close()
204
+ } catch (err) {
205
+ // console.error(err)
206
+ }
207
+ return
208
+ }
209
+ this.buffers.push(newBuffer)
210
+ return newBuffer
211
+ }
310
212
 
311
- // for (const chunk of shouldUnbufferChunks) {
312
- // if (cache.has(chunk.keyframeIndex)) cache.delete(chunk.keyframeIndex)
313
- // }
213
+ const tryToBuffer = async (): Promise<void> => {
214
+ if (this.buffers.length >= SIZE) return
215
+
216
+ if (this.buffers.length === 0) {
217
+ const buffer = await pull()
218
+ if (!buffer) return
219
+ return tryToBuffer()
220
+ } else {
221
+ pull().then((buffer) => {
222
+ if (!buffer) return
223
+ tryToBuffer()
224
+ })
225
+ }
226
+ }
314
227
 
315
- // if (sourceBuffer.updating) {
316
- // await abort(sourceBuffer).catch((err) => {
317
- // if (!(err instanceof Event)) throw err
318
- // })
319
- // }
320
- // for (const chunk of shouldUnbufferChunks) {
321
- // if (!bufferedChunksMap.has(chunk.keyframeIndex)) continue
322
- // try {
323
- // await removeChunk(sourceBuffer)(chunk)
324
- // bufferedChunksMap.delete(chunk.keyframeIndex)
325
- // } catch (err) {
326
- // if (err.message === 'No TimeRange found with this chunk') {
327
- // bufferedChunksMap.delete(chunk.keyframeIndex)
328
- // }
329
- // if (err.message !== 'No TimeRange found with this chunk') throw err
330
- // }
331
- // }
332
- // const bufferedOutOfRangeRanges =
333
- // getTimeRanges(sourceBuffer)
334
- // .filter(({ start, end }) =>
335
- // currentTime - PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS > start && currentTime - PRE_SEEK_NEEDED_BUFFERS_IN_SECONDS > end ||
336
- // currentTime + POST_SEEK_NEEDED_BUFFERS_IN_SECONDS < start && currentTime + POST_SEEK_NEEDED_BUFFERS_IN_SECONDS < end
337
- // )
338
- // for (const range of bufferedOutOfRangeRanges) {
339
- // try {
340
- // await removeRange(sourceBuffer)(range)
341
- // } catch (err) {
342
- // throw err
343
- // }
344
- // }
345
- // for (const chunk of neededChunks) {
346
- // // console.log('chunk', chunk, neededChunks, bufferedChunksMap.has(chunk.keyframeIndex), chunk.keyframeIndex + 1 === chunks.length)
347
- // if (bufferedChunksMap.has(chunk.keyframeIndex) || (chunk.keyframeIndex !== 0 && chunk.keyframeIndex + 1 === chunks.length)) continue
348
- // // console.log('check PASSED', chunk, neededChunks)
349
- // try {
350
- // const cachedBuffer = cache.get(chunk.keyframeIndex)
351
- // if (!cachedBuffer) {
352
- // const fetchBuffer =
353
- // getChunkArrayBuffer(chunk.keyframeIndex)
354
- // .then(arrayBuffer => {
355
- // if (arrayBuffer) cache.set(chunk.keyframeIndex, arrayBuffer)
356
- // return arrayBuffer
357
- // })
358
- // cache.set(chunk.keyframeIndex, fetchBuffer)
359
- // }
360
- // const buffer = await cache.get(chunk.keyframeIndex)
361
- // if (renderCount !== currentRenderCount + 1) return
362
- // if (!buffer) continue
363
- // const _chunk = { ...chunk, arrayBuffer: buffer }
364
- // await appendChunk(sourceBuffer)(_chunk).catch(err => {
365
- // bufferedChunksMap.delete(chunk.keyframeIndex)
366
- // throw err
367
- // })
368
- // bufferedChunksMap.set(chunk.keyframeIndex, buffer)
369
- // } catch (err) {
370
- // if (!(err instanceof Event)) {
371
- // for (const range of getTimeRanges(sourceBuffer)) {
372
- // await removeRange(sourceBuffer)(range)
373
- // // todo: remove cached buffers from Maps
374
- // }
375
- // if (!retry) await update({ currentTime, chunks, retry: true })
376
- // else throw err
377
- // }
378
- // // if (err.message !== 'Failed to execute \'appendBuffer\' on \'SourceBuffer\': This SourceBuffer is still processing an \'appendBuffer\' or \'remove\' operation.') throw err
379
- // break
380
- // }
381
- // }
382
- // }
383
-
384
- // return update
385
- // }
228
+ await tryToBuffer()
229
+ controller.enqueue(this.buffers.shift())
230
+ tryToBuffer()
231
+ },
232
+ cancel() {
233
+ this.reader!.cancel()
234
+ }
235
+ } as UnderlyingDefaultSource<Uint8Array> & {
236
+ reader: ReadableStreamDefaultReader<Uint8Array> | undefined
237
+ leftOverData: Uint8Array | undefined
238
+ buffers: Uint8Array[]
239
+ currentPullPromise: Promise<ReadableStreamReadResult<Uint8Array>> | undefined
240
+ })