@banou/media-player 0.10.12 → 0.10.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.
@@ -1,2 +1,2 @@
1
- import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../engine-DKjUYAmJ.js";
1
+ import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../engine-DdbOiEX3.js";
2
2
  export { i as DEFAULT_BUFFER_SIZE, e as MediaElementError, a as SUBTITLES_OFF, u as createPictureInPicture, d as createSubtitleRenderer, c as createThumbnailGenerator, n as getTimeRanges, s as isMediaElementError, o as pictureInPictureMode, l as startPlayback, t as terminateRemuxer, r as updateSourceBuffer };
@@ -239,139 +239,136 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
239
239
  m.addEventListener("sourceopen", a, { once: !0 }), m.addEventListener("sourceclose", o, { once: !0 }), m.addEventListener("error", o, { once: !0 });
240
240
  }), { appendBuffer: F, unbufferRange: se, updateTimestampOffset: ce, endOfStream: le } = o(A, m);
241
241
  await F(e.data), e.subtitles?.length && s.pushFragments(e.subtitles), te?.();
242
- let I = !1, L = !1, R = 0, z, B = () => z ?? n.currentTime, V = !1, H = 0, U = 0, W = null, G = 0, ue = 0, K = !1, de = 0, q = (e, t, n = !1) => {
242
+ let I = !1, L = !1, R = 0, z, ue = () => z ?? n.currentTime, B = !1, de = 0, V = 0, H = null, U = 0, W = 0, G = !1, fe = 0, K = !0, q = (e, t, n = !1) => {
243
243
  let r = e?.message === "Cancelled";
244
- t && r || (console.error(e), (!K || n) && (K = !0, ne?.(r ? Error("Reading the video file failed", { cause: e }) : e)));
245
- }, fe = async (e = !1) => {
244
+ t && r || (console.error(e), (!G || n) && (G = !0, ne?.(r ? Error("Reading the video file failed", { cause: e }) : e)));
245
+ }, pe = async (e = !1) => {
246
246
  if (R > 0 && !e) return;
247
- let t = n.currentTime, r = e ? x : v, a = e ? S : y, o = i(A);
248
- o.length && o[0].start < t + r && await se(0, t + r);
249
- for (let { end: e } of o) e > t + a && await se(t + a, e);
250
- }, pe = () => {
247
+ let t = n.currentTime, r = e ? x : v, a = e ? S : y, o = i(A), s = (e, t) => (K = !1, se(e, t));
248
+ o.length && o[0].start < t + r && await s(0, t + r);
249
+ for (let { end: e } of o) e > t + a && await s(t + a, e);
250
+ }, me = () => {
251
251
  let e = i(A);
252
252
  if (!e.length) return !0;
253
- let t = B(), n = e.find((e) => e.start <= t + T && t < e.end);
253
+ let t = ue(), n = e.find((e) => e.start <= t + T && t < e.end);
254
254
  return n && n.end >= t + y ? !1 : !n || n.end < t + b;
255
- }, me = () => {
256
- let e = n.currentTime;
257
- return i(A).find((t) => t.start <= e + T && e < t.end);
258
- }, he = () => {
259
- let e = me();
260
- return !!e && e.end >= ue - .1;
261
- }, ge = () => {
255
+ }, he = (e) => i(A).find((t) => t.start <= e + T && e < t.end), ge = () => he(n.currentTime), _e = (e = n.currentTime) => {
256
+ let t = he(e);
257
+ return e >= W - .1 || !!t && t.end >= W - .1;
258
+ }, ve = () => {
262
259
  let e = i(A).map((e) => e.end);
263
- e.length && (ue = Math.max(ue, ...e));
260
+ e.length && (W = Math.max(W, ...e));
264
261
  }, J = async (e) => {
265
- L = !0, W = null, G = 0, de = e;
266
- let t = ++U;
262
+ L = !0, H = null, U = 0, fe = e, K = !0;
263
+ let t = ++V;
267
264
  try {
268
265
  let { data: n, pts: r, subtitles: i, finished: a } = await j.seek(e);
269
- if (N || t !== U || (a && (V = !0), i?.length && s.pushFragments(i), await ce(r), N || t !== U)) return;
270
- n.byteLength && (await F(n), ge());
266
+ if (N || t !== V || (a && (B = !0), i?.length && s.pushFragments(i), await ce(r), N || t !== V)) return;
267
+ n.byteLength && (await F(n), ve());
271
268
  } catch (e) {
272
- q(e, N || t !== U);
269
+ t === V && (K = !1), q(e, N || t !== V);
273
270
  } finally {
274
- t === U && (L = !1);
271
+ t === V && (L = !1);
275
272
  }
276
- }, _e = async () => {
277
- let e = W;
273
+ }, ye = async () => {
274
+ let e = H;
278
275
  if (e) try {
279
- await F(e), ge(), W === e && (W = null, G = 0);
276
+ await F(e), ve(), H === e && (H = null, U = 0);
280
277
  } catch (t) {
281
- if (W !== e) return;
278
+ if (H !== e) return;
282
279
  if (t?.name === "QuotaExceededError") {
283
- let t = await fe(!0).then(() => !0, () => !1);
284
- if (W !== e) return;
280
+ let t = await pe(!0).then(() => !0, () => !1);
281
+ if (H !== e) return;
285
282
  if (t) {
286
- V = !1, await J(me()?.end ?? n.currentTime);
283
+ B = !1, await J(ge()?.end ?? n.currentTime);
287
284
  return;
288
285
  }
289
286
  }
290
- if (G += 1, G < C) return;
291
- throw W = null, t;
287
+ if (U += 1, U < C) return;
288
+ throw H = null, t;
292
289
  }
293
- }, ve = async () => {
294
- if (I || L || Q !== void 0 || N || !W && (V || !pe())) return;
295
- let e = ++H, t = U, n = () => N || e !== H || t !== U;
290
+ }, be = async () => {
291
+ if (I || L || Q !== void 0 || N || !H && (B || !me())) return;
292
+ let e = ++de, t = V, n = () => N || e !== de || t !== V;
296
293
  I = !0;
297
294
  try {
298
- if (!W) {
295
+ if (!H) {
299
296
  let { data: e, subtitles: t, finished: r } = await j.read();
300
- if (n() || (r && (V = !0), t?.length && s.pushFragments(t), !e.byteLength)) return;
301
- W = e, G = 0;
297
+ if (n() || (r && (B = !0), t?.length && s.pushFragments(t), !e.byteLength)) return;
298
+ H = e, U = 0;
302
299
  }
303
- await _e(), !n() && K && (K = !1, re?.());
300
+ await ye(), !n() && G && (G = !1, re?.());
304
301
  } catch (e) {
305
302
  q(e, n());
306
303
  } finally {
307
- e === H && (I = !1);
304
+ e === de && (I = !1);
308
305
  }
309
- }, ye = (e) => e >= de && be(e), be = (e) => i(A).some((t) => t.start <= e && e < t.end), Y = (e) => {
310
- let t = i(A).find((t) => t.start <= e + T && e < t.end);
306
+ }, xe = (e) => e >= fe && Se(e) && Ce(e), Se = (e) => i(A).some((t) => t.start <= e && e < t.end), Y = (e) => {
307
+ let t = he(e);
311
308
  return t ? t.end - e : 0;
312
- }, xe = async (e) => {
313
- if (!N && !(be(e) && Y(e) >= E)) {
314
- V = !1, R++, z = e;
309
+ }, Ce = (e) => Y(e) >= E || B && _e(e), we = async (e) => {
310
+ if (!N && !(Se(e) && Ce(e))) {
311
+ B = !1, R++, z = e;
315
312
  try {
316
- be(e) || await J(e);
317
- let t = U;
318
- for (let n = 0; n < ee && !(N || t !== U || V || Y(e) >= E); n++) {
313
+ await J(e);
314
+ let t = V;
315
+ for (let n = 0; n < ee && !(N || t !== V || B || Y(e) >= E); n++) {
319
316
  let t = Y(e);
320
- if (await ve(), Y(e) <= t) break;
317
+ if (await be(), Y(e) <= t) break;
321
318
  }
322
319
  } finally {
323
320
  R--, z === e && (z = void 0);
324
321
  }
325
322
  }
326
- }, Se = 0, X = null, Z, Ce = (e) => {
327
- let t = performance.now(), n = t - Se;
323
+ }, Te = 0, X = null, Z, Ee = (e) => {
324
+ let t = performance.now(), n = t - Te;
328
325
  if (n >= D) {
329
- Se = t, X = null, ie?.(e);
326
+ Te = t, X = null, ie?.(e);
330
327
  return;
331
328
  }
332
329
  X = e, !Z && (Z = setTimeout(() => {
333
330
  Z = void 0;
334
331
  let e = X;
335
- X = null, !(e === null || N) && (Se = performance.now(), ie?.(e));
332
+ X = null, !(e === null || N) && (Te = performance.now(), ie?.(e));
336
333
  }, D - n));
337
334
  };
338
335
  M.push(() => {
339
336
  Z && clearTimeout(Z);
340
337
  });
341
- let we = 0, Q;
338
+ let De = 0, Q;
342
339
  M.push(() => {
343
340
  Q && clearTimeout(Q);
344
341
  });
345
- let Te = () => {
342
+ let Oe = () => {
346
343
  let t = n.currentTime, r = e.info.input.duration || n.duration;
347
- r > 0 && Ce(Math.min(Math.max(t / r, 0), 1));
348
- let i = performance.now(), a = i - we < O;
349
- if (we = i, !ye(t) && !(L && Math.abs(t - de) < .001)) {
350
- if (V = !1, Q && clearTimeout(Q), !a) {
344
+ r > 0 && Ee(Math.min(Math.max(t / r, 0), 1));
345
+ let i = performance.now(), a = i - De < O;
346
+ if (De = i, !xe(t) && !(K && (L || Y(t) > 0) && Math.abs(t - fe) < .001)) {
347
+ if (B = !1, Q && clearTimeout(Q), !a) {
351
348
  Q = void 0, J(t);
352
349
  return;
353
350
  }
354
351
  Q = setTimeout(() => {
355
352
  if (Q = void 0, N) return;
356
353
  let e = n.currentTime;
357
- ye(e) || (V = !1, J(e));
354
+ xe(e) || (B = !1, J(e));
358
355
  }, O);
359
356
  }
360
357
  };
361
- n.addEventListener("seeking", Te), M.push(() => n.removeEventListener("seeking", Te));
362
- let $ = !1, Ee = () => {
358
+ n.addEventListener("seeking", Oe), M.push(() => n.removeEventListener("seeking", Oe));
359
+ let $ = !1, ke = () => {
363
360
  if ($) return;
364
361
  $ = !0;
365
362
  let e = n.error;
366
363
  q(new g(`the media element failed: ${e?.message ?? "unknown"}`, { cause: e }), !1, !0);
367
364
  };
368
- n.addEventListener("error", Ee), M.push(() => n.removeEventListener("error", Ee));
369
- let De = setInterval(() => {
370
- $ || n.error || (fe().catch(() => {}), ve().catch((e) => q(e, !1)), V && !W && he() && m.readyState === "open" && le().catch(() => {}));
365
+ n.addEventListener("error", ke), M.push(() => n.removeEventListener("error", ke));
366
+ let Ae = setInterval(() => {
367
+ $ || n.error || (pe().catch(() => {}), be().catch((e) => q(e, !1)), B && !H && _e() && m.readyState === "open" && le().catch(() => {}));
371
368
  }, 100);
372
- return M.push(() => clearInterval(De)), {
369
+ return M.push(() => clearInterval(Ae)), {
373
370
  destroy: P,
374
- prepareSeek: xe,
371
+ prepareSeek: we,
375
372
  videoElement: n,
376
373
  selectSubtitleStream: (e) => s.selectStream(e),
377
374
  indexes: e.indexes ?? [],
@@ -512,8 +509,8 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
512
509
  } catch (e) {
513
510
  throw A(c), e;
514
511
  }
515
- }, se = 1280, ce = 1920, le = 250, I = 100, L = () => typeof HTMLCanvasElement < "u" && "captureStream" in HTMLCanvasElement.prototype, R = () => typeof document < "u" && typeof HTMLVideoElement < "u" && typeof HTMLVideoElement.prototype.requestPictureInPicture == "function" && !!document.pictureInPictureEnabled, z = () => typeof HTMLVideoElement < "u" && "mozCaptureStream" in HTMLVideoElement.prototype, B = () => L() ? R() ? "window" : z() ? "burn-in" : null : null, V = (e) => {
516
- let { video: t, subtitles: n } = e, r = e.mode ?? B() ?? "window", i = e.maxWidth ?? (r === "burn-in" ? ce : se), a, o, s, c = !1, l = !1, u = !1, d = !1, f, p = () => r === "burn-in" ? !!a : !!a && document.pictureInPictureElement === a.mirror, m = (e, r) => {
512
+ }, se = 1280, ce = 1920, le = 250, I = 100, L = () => typeof HTMLCanvasElement < "u" && "captureStream" in HTMLCanvasElement.prototype, R = () => typeof document < "u" && typeof HTMLVideoElement < "u" && typeof HTMLVideoElement.prototype.requestPictureInPicture == "function" && !!document.pictureInPictureEnabled, z = () => typeof HTMLVideoElement < "u" && "mozCaptureStream" in HTMLVideoElement.prototype, ue = () => L() ? R() ? "window" : z() ? "burn-in" : null : null, B = (e) => {
513
+ let { video: t, subtitles: n } = e, r = e.mode ?? ue() ?? "window", i = e.maxWidth ?? (r === "burn-in" ? ce : se), a, o, s, c = !1, l = !1, u = !1, d = !1, f, p = () => r === "burn-in" ? !!a : !!a && document.pictureInPictureElement === a.mirror, m = (e, r) => {
517
514
  r.drawImage(t, 0, 0, e.width, e.height);
518
515
  let i = n.querySelector("canvas");
519
516
  i && i.width > 0 && i.height > 0 && r.drawImage(i, 0, 0, e.width, e.height);
@@ -629,4 +626,4 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
629
626
  };
630
627
  };
631
628
  //#endregion
632
- export { g as a, A as c, i as d, o as f, k as i, s as l, B as n, _ as o, F as r, te as s, V as t, h as u };
629
+ export { g as a, A as c, i as d, o as f, k as i, s as l, ue as n, _ as o, F as r, te as s, B as t, h as u };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { l as e, n as t, o as n, r, s as i, t as a, u as o } from "./engine-DKjUYAmJ.js";
1
+ import { l as e, n as t, o as n, r, s as i, t as a, u as o } from "./engine-DdbOiEX3.js";
2
2
  import { Children as s, Fragment as c, isValidElement as l, useCallback as u, useEffect as d, useMemo as f, useRef as p, useState as m, useSyncExternalStore as h } from "react";
3
3
  import { css as g, keyframes as _ } from "@emotion/react";
4
4
  import { createPlayer as v, useContainerAttach as y, useMediaAttach as b } from "@videojs/react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
4
4
  "description": "A video player for containers and codecs the browser cannot play natively, remuxed on the fly",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,10 +23,7 @@
23
23
  },
24
24
  "files": [
25
25
  "dist",
26
- "src/lib",
27
- "!src/lib/**/*.test.ts",
28
- "!src/lib/**/*.test.tsx",
29
- "!src/lib/**/*.fixture.ts"
26
+ "src/lib"
30
27
  ],
31
28
  "scripts": {
32
29
  "dev": "npm run copy-assets && vp dev --port 4560",
@@ -280,6 +280,8 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
280
280
  let outstandingError = false
281
281
  // where the remuxer was last positioned, which is the floor of what it can still read forward from
282
282
  let lastSeekPosition = 0
283
+ // whether nothing was removed since, so everything read forward from there is still buffered
284
+ let positioned = true
283
285
 
284
286
  // 'Cancelled' covers both an aborted task and a read that gave up, so it is only noise mid-seek
285
287
  const reportError = (error: unknown, aborted: boolean, terminal = false) => {
@@ -313,12 +315,16 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
313
315
  const pre = tight ? PRE_EVICT_TIGHT : PRE_EVICT
314
316
  const post = tight ? POST_EVICT_TIGHT : POST_EVICT
315
317
  const ranges = getTimeRanges(sourceBuffer)
318
+ const remove = (start: number, end: number) => {
319
+ positioned = false
320
+ return unbufferRange(start, end)
321
+ }
316
322
  // From 0, never from the reported start: once a group is removed, Firefox 151 reports the start
317
323
  // 1 ms after the keyframe it still holds, so a remove from there keeps that frame for good
318
324
  // (measured 2026-10-11).
319
- if (ranges.length && ranges[0].start < ct + pre) await unbufferRange(0, ct + pre)
325
+ if (ranges.length && ranges[0].start < ct + pre) await remove(0, ct + pre)
320
326
  for (const { end } of ranges) {
321
- if (end > ct + post) await unbufferRange(ct + post, end)
327
+ if (end > ct + post) await remove(ct + post, end)
322
328
  }
323
329
  }
324
330
 
@@ -344,15 +350,16 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
344
350
  return !range || range.end < ct + BUFFER_TARGET
345
351
  }
346
352
 
347
- const playheadRange = () => {
348
- const ct = videoElement.currentTime
349
- return getTimeRanges(sourceBuffer).find((r) => r.start <= ct + BOUNDARY_SLACK && ct < r.end)
350
- }
353
+ const rangeAt = (time: number) =>
354
+ getTimeRanges(sourceBuffer).find((r) => r.start <= time + BOUNDARY_SLACK && time < r.end)
355
+ const playheadRange = () => rangeAt(videoElement.currentTime)
351
356
 
352
- // the end may only be signalled once the playhead's own range covers it
353
- const atEnd = () => {
354
- const range = playheadRange()
355
- return !!range && range.end >= lastAppendedEnd - 0.1
357
+ // whether a position is at the furthest append, or the range holding it runs to it; the end may only
358
+ // be signalled then. Past the shorter track's end no range holds the playhead while the MediaSource is
359
+ // open, so a range alone never re-armed the end of a file whose tracks end 1 s apart (measured 2026-10-11)
360
+ const atEnd = (time = videoElement.currentTime) => {
361
+ const range = rangeAt(time)
362
+ return time >= lastAppendedEnd - 0.1 || (!!range && range.end >= lastAppendedEnd - 0.1)
356
363
  }
357
364
  const trackAppendedEnd = () => {
358
365
  const ends = getTimeRanges(sourceBuffer).map((r) => r.end)
@@ -364,6 +371,7 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
364
371
  pending = null
365
372
  pendingAttempts = 0
366
373
  lastSeekPosition = time
374
+ positioned = true
367
375
  const generation = ++seekGeneration
368
376
  try {
369
377
  // `seek` ends by reading, and that read can hit the end of the file, which finalizes the
@@ -381,6 +389,7 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
381
389
  trackAppendedEnd()
382
390
  }
383
391
  } catch (error) {
392
+ if (generation === seekGeneration) positioned = false
384
393
  reportError(error, destroyed || generation !== seekGeneration)
385
394
  } finally {
386
395
  if (generation === seekGeneration) seeking = false
@@ -454,12 +463,14 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
454
463
  * BOTH the input and the output context and re-runs write_header, because the fragmented mp4 muxer
455
464
  * cannot rewind. Skipping it when the target is already playable is most of what a seek costs.
456
465
  *
457
- * Both conditions are needed. Buffered alone is not enough: landing before the last remuxer position
458
- * means the reader is somewhere ahead, so the gap between here and it can never be filled without a
459
- * seek, and playback would stall at the end of this range instead. At or after it, the bytes from
460
- * here to the read cursor are contiguous and reading simply carries on.
466
+ * Buffered alone is not enough: landing before the last remuxer position means the reader is
467
+ * somewhere ahead, so the gap between here and it can never be filled without a seek, and playback
468
+ * would stall at the end of this range instead. Nor is landing after it: the range holding the
469
+ * target can be an island ahead of the reader, so the target also needs its runway, or the element
470
+ * plays to the island's end and waits there until the reader has read the whole gap, which over a
471
+ * torrent is as long as downloading it.
461
472
  */
462
- const alreadyPlayable = (time: number) => time >= lastSeekPosition && playableAt(time)
473
+ const alreadyPlayable = (time: number) => time >= lastSeekPosition && playableAt(time) && hasRunway(time)
463
474
 
464
475
  /**
465
476
  * Whether the element can play from a GIVEN time without a fetch.
@@ -471,6 +482,23 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
471
482
  const playableAt = (time: number) =>
472
483
  getTimeRanges(sourceBuffer).some((r) => r.start <= time && time < r.end)
473
484
 
485
+ /** Seconds of contiguous data past a position that make it safe to play from. */
486
+ const runwayFrom = (time: number) => {
487
+ const range = rangeAt(time)
488
+ return range ? range.end - time : 0
489
+ }
490
+
491
+ /**
492
+ * Whether a position has its runway, which stops at the end of the file once the remuxer has read to it.
493
+ *
494
+ * No target in the last 3 s can have the full runway, and re-positioning the remuxer there re-appends
495
+ * over the buffered end, after which Firefox 151 stays at the end and never fires `ended`. The end is
496
+ * the furthest append, the one `endOfStream` waits on, never the declared duration: that is the longer
497
+ * track, while an open MediaSource's `buffered` ends at the shorter one, and evict() reopens it on
498
+ * every trim (measured 2026-10-11).
499
+ */
500
+ const hasRunway = (time: number) => runwayFrom(time) >= SEEK_RUNWAY || (finished && atEnd(time))
501
+
474
502
  /**
475
503
  * Put the data in place for a position the playhead has NOT moved to yet.
476
504
  *
@@ -484,24 +512,19 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
484
512
  * delays the seek: delaying the seek by the same amount while appending nothing still wedged 4
485
513
  * out of 4, so it is the data and not the delay.
486
514
  *
487
- * Returns as soon as there is nothing to wait for, so a seek into buffered ground costs nothing.
488
- * The CALLER owns the deadline: this can take as long as a read takes, and over a torrent that
489
- * is unbounded.
515
+ * Returns at once when the target already has its runway, so a seek into buffered ground costs
516
+ * nothing. Otherwise the remuxer is positioned at the target, since the range holding it can be an
517
+ * island the reader never extends. The CALLER owns the deadline: this can take as long as a read
518
+ * takes, and over a torrent that is unbounded.
490
519
  */
491
- /** Seconds of contiguous data past a position that make it safe to play from. */
492
- const runwayFrom = (time: number) => {
493
- const range = getTimeRanges(sourceBuffer).find((r) => r.start <= time + BOUNDARY_SLACK && time < r.end)
494
- return range ? range.end - time : 0
495
- }
496
-
497
520
  const prepareSeek = async (time: number) => {
498
521
  if (destroyed) return
499
- if (playableAt(time) && runwayFrom(time) >= SEEK_RUNWAY) return
522
+ if (playableAt(time) && hasRunway(time)) return
500
523
  finished = false
501
524
  preparing++
502
525
  prepareTarget = time
503
526
  try {
504
- if (!playableAt(time)) await seekTo(time)
527
+ await seekTo(time)
505
528
  /*
506
529
  * Captured AFTER the seek, never before.
507
530
  *
@@ -599,14 +622,18 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
599
622
  lastSeekingAt = now
600
623
  if (alreadyPlayable(time)) return
601
624
  /*
602
- * A seek for exactly this position is already running, so let it finish.
625
+ * The remuxer is already positioned at exactly this time with nothing removed since, so let it
626
+ * read on. Its seek still running or data at the target is required too: the browser also removes
627
+ * data, by its own eviction when an append goes over its memory budget.
603
628
  *
604
- * `prepareSeek` starts one BEFORE the playhead moves, and the caller moves the playhead anyway
605
- * once its budget runs out. Without this, the move fires `seeking`, which starts the identical
606
- * read again and bumps the generation, throwing away everything the first one had done. The
607
- * benchmark caught it as a seek that took twice as long as seeking the old way.
629
+ * `prepareSeek` positions it BEFORE the playhead moves, and the caller moves the playhead anyway
630
+ * once its budget runs out, with the seek or the runway's reads still running. Without this, the
631
+ * move fires `seeking`, which starts the identical read again and bumps the generation, throwing
632
+ * away everything the first one had done. The benchmark caught it as a seek that took twice as
633
+ * long as seeking the old way. It also covers a finished prepare to a keyframe in Firefox, whose
634
+ * range starts the reorder delay past the target, so the target never counts as buffered.
608
635
  */
609
- if (seeking && Math.abs(time - lastSeekPosition) < 0.001) return
636
+ if (positioned && (seeking || runwayFrom(time) > 0) && Math.abs(time - lastSeekPosition) < 0.001) return
610
637
  finished = false
611
638
  if (dragTimer) clearTimeout(dragTimer)
612
639
  if (!dragging) {