@banou/media-player 0.8.12 → 0.8.13
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/dist/engine/index.js +1 -1
- package/dist/engine/playback.d.ts +7 -0
- package/dist/{engine-Cownv8gD.js → engine-BUcS6UrE.js} +127 -116
- package/dist/index.js +282 -258
- package/dist/react/player.d.ts +1 -0
- package/dist/react/source-feature.d.ts +24 -0
- package/dist/react/video-player.d.ts +8 -0
- package/package.json +1 -1
- package/src/lib/engine/playback.ts +63 -0
- package/src/lib/react/components/control-bar.tsx +7 -3
- package/src/lib/react/components/progress-bar.tsx +7 -2
- package/src/lib/react/hooks/use-playback.ts +68 -2
- package/src/lib/react/source-feature.ts +13 -0
- package/src/lib/react/video-player.tsx +8 -0
package/dist/react/player.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare const Player: import("@videojs/react").CreatePlayerResult<import(
|
|
|
17
17
|
togglePictureInPicture: (() => void) | null;
|
|
18
18
|
pictureInPictureMode: import("../engine").PictureInPictureMode | null;
|
|
19
19
|
burnedInSubtitles: boolean;
|
|
20
|
+
requestSeek?: (time: number) => void;
|
|
20
21
|
playbackError: unknown;
|
|
21
22
|
playbackErrors: import("./source-feature").PlaybackErrorEntry[];
|
|
22
23
|
ready: boolean;
|
|
@@ -115,6 +115,18 @@ export type SourceState = {
|
|
|
115
115
|
pictureInPictureMode: PictureInPictureMode | null;
|
|
116
116
|
/** Burn-in only. True while the composite is the picture on screen. */
|
|
117
117
|
burnedInSubtitles: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Move the playhead, letting the pipeline get the data there first.
|
|
120
|
+
*
|
|
121
|
+
* The chrome calls this instead of the player's own `seek`, because an element that demuxes into a
|
|
122
|
+
* hole is what wedges firefox's decoder: the underrun drains it and nothing ever flushes it again.
|
|
123
|
+
* Waiting is bounded by a deadline, so a source that cannot answer in time costs that deadline and
|
|
124
|
+
* not the whole read.
|
|
125
|
+
*
|
|
126
|
+
* Undefined for a media this player does not own, where there is no pipeline to prepare and the
|
|
127
|
+
* caller should seek directly.
|
|
128
|
+
*/
|
|
129
|
+
requestSeek?: (time: number) => void;
|
|
118
130
|
/** Set when the pipeline fails. Cleared when it recovers. */
|
|
119
131
|
playbackError: unknown;
|
|
120
132
|
/**
|
|
@@ -190,6 +202,18 @@ export declare const sourceFeature: import("@videojs/react").PlayerFeature<{
|
|
|
190
202
|
pictureInPictureMode: PictureInPictureMode | null;
|
|
191
203
|
/** Burn-in only. True while the composite is the picture on screen. */
|
|
192
204
|
burnedInSubtitles: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Move the playhead, letting the pipeline get the data there first.
|
|
207
|
+
*
|
|
208
|
+
* The chrome calls this instead of the player's own `seek`, because an element that demuxes into a
|
|
209
|
+
* hole is what wedges firefox's decoder: the underrun drains it and nothing ever flushes it again.
|
|
210
|
+
* Waiting is bounded by a deadline, so a source that cannot answer in time costs that deadline and
|
|
211
|
+
* not the whole read.
|
|
212
|
+
*
|
|
213
|
+
* Undefined for a media this player does not own, where there is no pipeline to prepare and the
|
|
214
|
+
* caller should seek directly.
|
|
215
|
+
*/
|
|
216
|
+
requestSeek?: (time: number) => void;
|
|
193
217
|
/** Set when the pipeline fails. Cleared when it recovers. */
|
|
194
218
|
playbackError: unknown;
|
|
195
219
|
/**
|
|
@@ -93,6 +93,14 @@ export type MediaPlayerLocalOptions = CommonOptions & MediaPlayerSource & {
|
|
|
93
93
|
/** Fallback face for `liberation sans`, used when a subtitle track names a font the file does not carry. */
|
|
94
94
|
defaultFontUrl?: string;
|
|
95
95
|
bufferSize?: number;
|
|
96
|
+
/**
|
|
97
|
+
* How long a seek waits for its own data before the playhead moves anyway, in ms. Default 500.
|
|
98
|
+
*
|
|
99
|
+
* Seeking into a hole is what wedges firefox's decoder, so the pipeline is asked for the target
|
|
100
|
+
* first. This is the ceiling on that wait, since a read over a torrent has none of its own. Set
|
|
101
|
+
* it to 0 to seek immediately and rely on the recovery instead.
|
|
102
|
+
*/
|
|
103
|
+
seekPrepareBudgetMs?: number;
|
|
96
104
|
/** Byte spans available, painted on the seekbar and informing the thumbnail generator. */
|
|
97
105
|
downloadedRanges?: DownloadedRange[];
|
|
98
106
|
/**
|
package/package.json
CHANGED
|
@@ -36,6 +36,13 @@ export type PlaybackOptions = {
|
|
|
36
36
|
|
|
37
37
|
export type PlaybackController = {
|
|
38
38
|
destroy: () => void
|
|
39
|
+
/**
|
|
40
|
+
* Get the data for a position in place BEFORE the playhead moves there.
|
|
41
|
+
*
|
|
42
|
+
* Resolves at once when that position already has data. The caller decides how long to wait,
|
|
43
|
+
* because this can take as long as a read, which over a torrent has no ceiling.
|
|
44
|
+
*/
|
|
45
|
+
prepareSeek: (time: number) => Promise<void>
|
|
39
46
|
selectSubtitleStream: (streamIndex: number | undefined) => void
|
|
40
47
|
/** Keyframe index of the input, which is what maps a downloaded byte range onto the timeline. */
|
|
41
48
|
indexes: MediaIndex[]
|
|
@@ -210,6 +217,8 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
210
217
|
|
|
211
218
|
let reading = false
|
|
212
219
|
let seeking = false
|
|
220
|
+
// how many prepares are in flight, so eviction knows not to delete what they are fetching
|
|
221
|
+
let preparing = 0
|
|
213
222
|
let finished = false
|
|
214
223
|
// libav aborts the running task when a new one starts, so both flags need a generation token
|
|
215
224
|
let readGeneration = 0
|
|
@@ -237,6 +246,18 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
237
246
|
}
|
|
238
247
|
|
|
239
248
|
const evict = async (tight = false) => {
|
|
249
|
+
/*
|
|
250
|
+
* Hold off while data is being put in place for a position the playhead has not reached.
|
|
251
|
+
*
|
|
252
|
+
* Eviction is anchored on the playhead, and `prepareSeek` deliberately appends far from it, so
|
|
253
|
+
* running now deletes exactly what was just fetched. The 100ms interval below made that a
|
|
254
|
+
* certainty. The benchmark caught it as prepared seeks being the SLOWEST arm, which is the
|
|
255
|
+
* opposite of the point.
|
|
256
|
+
*
|
|
257
|
+
* `tight` still runs, because that one is the answer to a quota refusal and has to be able to
|
|
258
|
+
* free space no matter what else is happening.
|
|
259
|
+
*/
|
|
260
|
+
if (preparing > 0 && !tight) return
|
|
240
261
|
const ct = videoElement.currentTime
|
|
241
262
|
const pre = tight ? PRE_EVICT_TIGHT : PRE_EVICT
|
|
242
263
|
const post = tight ? POST_EVICT_TIGHT : POST_EVICT
|
|
@@ -375,6 +396,38 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
375
396
|
*/
|
|
376
397
|
const alreadyPlayable = (time: number) => time >= lastSeekPosition && !!playheadRange()
|
|
377
398
|
|
|
399
|
+
/** Whether a GIVEN time has data behind it, as opposed to whether the playhead does. */
|
|
400
|
+
const playableAt = (time: number) =>
|
|
401
|
+
getTimeRanges(sourceBuffer).some((r) => r.start <= time + BOUNDARY_SLACK && time < r.end)
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Put the data in place for a position the playhead has NOT moved to yet.
|
|
405
|
+
*
|
|
406
|
+
* Firefox wedges its own decoder when the element demuxes into a hole: the underrun requests a
|
|
407
|
+
* drain, the drain completes, the re-prime that would flush the decoder never runs, and every
|
|
408
|
+
* packet after that comes back `avcodec_send_packet error: End of file`. Seeking only after the
|
|
409
|
+
* target has data removes the hole, and with it the drain and the whole failure.
|
|
410
|
+
*
|
|
411
|
+
* Measured on a standalone rig against a real 1080p stream: 7 wedges in 7 runs seeking the
|
|
412
|
+
* ordinary way, 0 in 4 seeking this way. The control matters, because appending first also
|
|
413
|
+
* delays the seek: delaying the seek by the same amount while appending nothing still wedged 4
|
|
414
|
+
* out of 4, so it is the data and not the delay.
|
|
415
|
+
*
|
|
416
|
+
* Returns as soon as there is nothing to wait for, so a seek into buffered ground costs nothing.
|
|
417
|
+
* The CALLER owns the deadline: this can take as long as a read takes, and over a torrent that
|
|
418
|
+
* is unbounded.
|
|
419
|
+
*/
|
|
420
|
+
const prepareSeek = async (time: number) => {
|
|
421
|
+
if (destroyed || playableAt(time)) return
|
|
422
|
+
finished = false
|
|
423
|
+
preparing++
|
|
424
|
+
try {
|
|
425
|
+
await seekTo(time)
|
|
426
|
+
} finally {
|
|
427
|
+
preparing--
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
378
431
|
/**
|
|
379
432
|
* Where a drag reaches the consumer, at a rate a consumer can act on.
|
|
380
433
|
*
|
|
@@ -437,6 +490,15 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
437
490
|
const dragging = now - lastSeekingAt < DRAG_SETTLE_MS
|
|
438
491
|
lastSeekingAt = now
|
|
439
492
|
if (alreadyPlayable(time)) return
|
|
493
|
+
/*
|
|
494
|
+
* A seek for exactly this position is already running, so let it finish.
|
|
495
|
+
*
|
|
496
|
+
* `prepareSeek` starts one BEFORE the playhead moves, and the caller moves the playhead anyway
|
|
497
|
+
* once its budget runs out. Without this, the move fires `seeking`, which starts the identical
|
|
498
|
+
* read again and bumps the generation, throwing away everything the first one had done. The
|
|
499
|
+
* benchmark caught it as a seek that took twice as long as seeking the old way.
|
|
500
|
+
*/
|
|
501
|
+
if (seeking && Math.abs(time - lastSeekPosition) < 0.001) return
|
|
440
502
|
finished = false
|
|
441
503
|
if (dragTimer) clearTimeout(dragTimer)
|
|
442
504
|
if (!dragging) {
|
|
@@ -501,6 +563,7 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
501
563
|
|
|
502
564
|
return {
|
|
503
565
|
destroy: runTeardown,
|
|
566
|
+
prepareSeek,
|
|
504
567
|
selectSubtitleStream: (streamIndex: number | undefined) => subtitles.selectStream(streamIndex),
|
|
505
568
|
indexes: metadata.indexes ?? [],
|
|
506
569
|
duration: metadata.info.input.duration,
|
|
@@ -190,6 +190,9 @@ export const ControlBar = () => {
|
|
|
190
190
|
const pictureInPictureMode = usePlayer((state) => state.pictureInPictureMode)
|
|
191
191
|
const burnedInSubtitles = usePlayer((state) => state.burnedInSubtitles)
|
|
192
192
|
const subtitleTracks = usePlayer((state) => state.subtitleTracks)
|
|
193
|
+
// Keyboard seeks go through the same door as the seek bar: data first, then the playhead. See
|
|
194
|
+
// `requestSeek` on the source state for why an element that seeks into a hole is the problem.
|
|
195
|
+
const requestSeek = usePlayer((state) => state.requestSeek)
|
|
193
196
|
const [volumeElement, setVolumeElement] = useState<HTMLButtonElement | null>(null)
|
|
194
197
|
|
|
195
198
|
const burnIn = pictureInPictureMode === 'burn-in'
|
|
@@ -213,6 +216,7 @@ export const ControlBar = () => {
|
|
|
213
216
|
}, [player])
|
|
214
217
|
|
|
215
218
|
useEffect(() => {
|
|
219
|
+
const seek = (time: number) => requestSeek ? requestSeek(time) : player.seek(time)
|
|
216
220
|
const eventListener = (ev: KeyboardEvent) => {
|
|
217
221
|
// on the window, so typing into a consumer's own input is the one case that opts out
|
|
218
222
|
const target = ev.target as HTMLElement | null
|
|
@@ -232,10 +236,10 @@ export const ControlBar = () => {
|
|
|
232
236
|
}
|
|
233
237
|
else if (ev.key === 'ArrowRight') {
|
|
234
238
|
if (!player.duration) return
|
|
235
|
-
|
|
239
|
+
seek(Math.min(player.currentTime + SEEK_STEP, player.duration))
|
|
236
240
|
}
|
|
237
241
|
else if (ev.key === 'ArrowLeft') {
|
|
238
|
-
|
|
242
|
+
seek(Math.max(player.currentTime - SEEK_STEP, 0))
|
|
239
243
|
}
|
|
240
244
|
else {
|
|
241
245
|
shouldPreventDefault = false
|
|
@@ -247,7 +251,7 @@ export const ControlBar = () => {
|
|
|
247
251
|
}
|
|
248
252
|
window.addEventListener('keydown', eventListener)
|
|
249
253
|
return () => window.removeEventListener('keydown', eventListener)
|
|
250
|
-
}, [player, modifyVolume])
|
|
254
|
+
}, [player, modifyVolume, requestSeek])
|
|
251
255
|
|
|
252
256
|
const handleWheel = useCallback((e: WheelEvent) => {
|
|
253
257
|
e.preventDefault()
|
|
@@ -184,6 +184,7 @@ const style = css`
|
|
|
184
184
|
export const ProgressBar = () => {
|
|
185
185
|
const player = usePlayer()
|
|
186
186
|
const currentTime = usePlayer((state) => state.currentTime)
|
|
187
|
+
const requestSeek = usePlayer((state) => state.requestSeek)
|
|
187
188
|
const duration = usePlayer((state) => state.duration)
|
|
188
189
|
const size = usePlayer((state) => state.size)
|
|
189
190
|
const downloadedRanges = usePlayer((state) => state.downloadedRanges)
|
|
@@ -275,8 +276,12 @@ export const ProgressBar = () => {
|
|
|
275
276
|
useEffect(() => {
|
|
276
277
|
if (seekFraction === undefined || !duration) return
|
|
277
278
|
const timestamp = seekFraction * duration
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
// `requestSeek` gets the data in place before the playhead moves, which is what stops firefox
|
|
280
|
+
// wedging its decoder on a seek into a hole. Absent for a media this player does not own, where
|
|
281
|
+
// there is no pipeline to ask and the element's own seek is all there is.
|
|
282
|
+
if (requestSeek) requestSeek(timestamp)
|
|
283
|
+
else player.seek(timestamp)
|
|
284
|
+
}, [player, requestSeek, seekFraction, duration])
|
|
280
285
|
|
|
281
286
|
const scaleX = useMemo(() => {
|
|
282
287
|
return !duration || typeof currentTime !== 'number'
|
|
@@ -25,6 +25,21 @@ const RESTART_BACKOFF_MS = [0, 250, 1_000, 3_000, 10_000]
|
|
|
25
25
|
// far enough back that an unrelated hiccup later in an episode starts from no delay again
|
|
26
26
|
const RESTART_SETTLED_MS = 60_000
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* How long a seek may wait for its data before the playhead moves anyway.
|
|
30
|
+
*
|
|
31
|
+
* The prevention only works while the wait is honoured, and a read over a torrent has no ceiling, so
|
|
32
|
+
* this is the ceiling. Half a second is the point where a seek stops feeling like a seek.
|
|
33
|
+
*/
|
|
34
|
+
const SEEK_PREPARE_BUDGET_MS = 500
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Under this gap between seek requests, it is a drag rather than a series of decisions.
|
|
38
|
+
*
|
|
39
|
+
* Kept equal to the engine's own settle window, so both layers agree on what a drag is.
|
|
40
|
+
*/
|
|
41
|
+
const DRAG_SETTLE_MS = 250
|
|
42
|
+
|
|
28
43
|
const messageOf = (error: unknown) =>
|
|
29
44
|
error instanceof Error ? error.message : String(error)
|
|
30
45
|
|
|
@@ -66,6 +81,7 @@ export const usePlayback = (
|
|
|
66
81
|
const {
|
|
67
82
|
read, size, publicPath = '', libavWorkerUrl = '', jassubWorkerUrl = '', jassubWasmUrl = '',
|
|
68
83
|
jassubLegacyWasmUrl, defaultFontUrl, bufferSize, autoplay = false,
|
|
84
|
+
seekPrepareBudgetMs = SEEK_PREPARE_BUDGET_MS,
|
|
69
85
|
} = options ?? ({} as Partial<MediaPlayerLocalOptions>)
|
|
70
86
|
|
|
71
87
|
// The track the viewer picked, which is what a restart is keyed on. Distinct from the store's
|
|
@@ -111,6 +127,8 @@ export const usePlayback = (
|
|
|
111
127
|
* deliberately not, since a streaming consumer passes a fresh closure several times a second.
|
|
112
128
|
*/
|
|
113
129
|
const resumeRef = useRef<{ time: number, size: number } | null>(null)
|
|
130
|
+
// when the seek bar last asked for a position, which is how a drag is told from a click
|
|
131
|
+
const lastSeekRequestAt = useRef(0)
|
|
114
132
|
// The renderer turns the first track on by itself, so the menu has to mirror that or it shows
|
|
115
133
|
// "Disable" ticked over subtitles that are visibly on screen.
|
|
116
134
|
const subtitleChoiceMade = useRef(false)
|
|
@@ -131,6 +149,54 @@ export const usePlayback = (
|
|
|
131
149
|
controllerRef.current?.selectSubtitleStream(streamIndex)
|
|
132
150
|
}, [player])
|
|
133
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Seek with the data already there, or after the deadline, whichever comes first.
|
|
154
|
+
*
|
|
155
|
+
* The prevention for the firefox decoder wedge. Seeking into a hole makes the reader drain the
|
|
156
|
+
* decoder and never flush it again, so the pipeline is asked for the target first. The deadline is
|
|
157
|
+
* what keeps that honest: a read over a torrent has no ceiling, and a seek bar that waits on one
|
|
158
|
+
* is worse than the fault it avoids. So the playhead moves either when the data lands or when the
|
|
159
|
+
* budget runs out, once, whichever happens first.
|
|
160
|
+
*
|
|
161
|
+
* A seek into buffered ground resolves immediately, so scrubbing inside the buffer is unaffected.
|
|
162
|
+
*/
|
|
163
|
+
const requestSeek = useCallback((time: number) => {
|
|
164
|
+
const controller = controllerRef.current
|
|
165
|
+
// no budget means the feature is off: seek at once and do not spend a read preparing something
|
|
166
|
+
// nobody is going to wait for
|
|
167
|
+
if (!controller || seekPrepareBudgetMs <= 0) { player.seek(time); return }
|
|
168
|
+
|
|
169
|
+
/*
|
|
170
|
+
* A drag is left exactly as it was.
|
|
171
|
+
*
|
|
172
|
+
* The seek bar reports a fraction on every pointermove, so preparing each one would remux per
|
|
173
|
+
* move, and waiting on each would make a scrub feel like treacle. Neither is worth paying:
|
|
174
|
+
* a drag never reproduced this fault (its seeks land ~28ms apart, far too fast for a drain to
|
|
175
|
+
* complete), and the engine already coalesces the remux to wherever the drag stops.
|
|
176
|
+
*
|
|
177
|
+
* Discrete seeks are the ones that wedge it, at a few hundred ms apart, and those get the data
|
|
178
|
+
* first.
|
|
179
|
+
*/
|
|
180
|
+
const now = performance.now()
|
|
181
|
+
const dragging = now - lastSeekRequestAt.current < DRAG_SETTLE_MS
|
|
182
|
+
lastSeekRequestAt.current = now
|
|
183
|
+
if (dragging) { player.seek(time); return }
|
|
184
|
+
|
|
185
|
+
let moved = false
|
|
186
|
+
const move = () => {
|
|
187
|
+
if (moved) return
|
|
188
|
+
moved = true
|
|
189
|
+
player.seek(time)
|
|
190
|
+
}
|
|
191
|
+
const deadline = setTimeout(move, seekPrepareBudgetMs)
|
|
192
|
+
void controller
|
|
193
|
+
.prepareSeek(time)
|
|
194
|
+
// a failed prepare is not a reason to refuse the seek: the pump and the existing recovery
|
|
195
|
+
// both still apply, and refusing would strand the viewer on a bar that does nothing
|
|
196
|
+
.catch(() => {})
|
|
197
|
+
.finally(() => { clearTimeout(deadline); move() })
|
|
198
|
+
}, [player, seekPrepareBudgetMs])
|
|
199
|
+
|
|
134
200
|
const selectAudioTrack = useCallback((id: string | number) => {
|
|
135
201
|
if (typeof id !== 'number') return
|
|
136
202
|
player.setSourceState({ selectedAudioTrack: id })
|
|
@@ -143,8 +209,8 @@ export const usePlayback = (
|
|
|
143
209
|
const setSourceState = usePlayer((state) => state.setSourceState)
|
|
144
210
|
|
|
145
211
|
useEffect(() => {
|
|
146
|
-
setSourceState({ selectSubtitleTrack, selectAudioTrack })
|
|
147
|
-
}, [setSourceState, selectSubtitleTrack, selectAudioTrack])
|
|
212
|
+
setSourceState({ selectSubtitleTrack, selectAudioTrack, requestSeek })
|
|
213
|
+
}, [setSourceState, selectSubtitleTrack, selectAudioTrack, requestSeek])
|
|
148
214
|
|
|
149
215
|
useEffect(() => {
|
|
150
216
|
if (!video || !canvas || !size || !read) return
|
|
@@ -128,6 +128,19 @@ export type SourceState = {
|
|
|
128
128
|
/** Burn-in only. True while the composite is the picture on screen. */
|
|
129
129
|
burnedInSubtitles: boolean
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Move the playhead, letting the pipeline get the data there first.
|
|
133
|
+
*
|
|
134
|
+
* The chrome calls this instead of the player's own `seek`, because an element that demuxes into a
|
|
135
|
+
* hole is what wedges firefox's decoder: the underrun drains it and nothing ever flushes it again.
|
|
136
|
+
* Waiting is bounded by a deadline, so a source that cannot answer in time costs that deadline and
|
|
137
|
+
* not the whole read.
|
|
138
|
+
*
|
|
139
|
+
* Undefined for a media this player does not own, where there is no pipeline to prepare and the
|
|
140
|
+
* caller should seek directly.
|
|
141
|
+
*/
|
|
142
|
+
requestSeek?: (time: number) => void
|
|
143
|
+
|
|
131
144
|
/** Set when the pipeline fails. Cleared when it recovers. */
|
|
132
145
|
playbackError: unknown
|
|
133
146
|
/**
|
|
@@ -109,6 +109,14 @@ export type MediaPlayerLocalOptions =
|
|
|
109
109
|
/** Fallback face for `liberation sans`, used when a subtitle track names a font the file does not carry. */
|
|
110
110
|
defaultFontUrl?: string
|
|
111
111
|
bufferSize?: number
|
|
112
|
+
/**
|
|
113
|
+
* How long a seek waits for its own data before the playhead moves anyway, in ms. Default 500.
|
|
114
|
+
*
|
|
115
|
+
* Seeking into a hole is what wedges firefox's decoder, so the pipeline is asked for the target
|
|
116
|
+
* first. This is the ceiling on that wait, since a read over a torrent has none of its own. Set
|
|
117
|
+
* it to 0 to seek immediately and rely on the recovery instead.
|
|
118
|
+
*/
|
|
119
|
+
seekPrepareBudgetMs?: number
|
|
112
120
|
|
|
113
121
|
/** Byte spans available, painted on the seekbar and informing the thumbnail generator. */
|
|
114
122
|
downloadedRanges?: DownloadedRange[]
|