@banou/media-player 0.10.11 → 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.
package/dist/engine/index.js
CHANGED
|
@@ -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-
|
|
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,
|
|
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), (!
|
|
245
|
-
},
|
|
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
|
|
249
|
-
for (let { end: e } of o) e > t + a && await
|
|
250
|
-
},
|
|
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 =
|
|
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
|
-
},
|
|
256
|
-
let
|
|
257
|
-
return
|
|
258
|
-
},
|
|
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 && (
|
|
260
|
+
e.length && (W = Math.max(W, ...e));
|
|
264
261
|
}, J = async (e) => {
|
|
265
|
-
L = !0,
|
|
266
|
-
let t = ++
|
|
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 !==
|
|
270
|
-
n.byteLength && (await F(n),
|
|
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 !==
|
|
269
|
+
t === V && (K = !1), q(e, N || t !== V);
|
|
273
270
|
} finally {
|
|
274
|
-
t ===
|
|
271
|
+
t === V && (L = !1);
|
|
275
272
|
}
|
|
276
|
-
},
|
|
277
|
-
let e =
|
|
273
|
+
}, ye = async () => {
|
|
274
|
+
let e = H;
|
|
278
275
|
if (e) try {
|
|
279
|
-
await F(e),
|
|
276
|
+
await F(e), ve(), H === e && (H = null, U = 0);
|
|
280
277
|
} catch (t) {
|
|
281
|
-
if (
|
|
278
|
+
if (H !== e) return;
|
|
282
279
|
if (t?.name === "QuotaExceededError") {
|
|
283
|
-
let t = await
|
|
284
|
-
if (
|
|
280
|
+
let t = await pe(!0).then(() => !0, () => !1);
|
|
281
|
+
if (H !== e) return;
|
|
285
282
|
if (t) {
|
|
286
|
-
|
|
283
|
+
B = !1, await J(ge()?.end ?? n.currentTime);
|
|
287
284
|
return;
|
|
288
285
|
}
|
|
289
286
|
}
|
|
290
|
-
if (
|
|
291
|
-
throw
|
|
287
|
+
if (U += 1, U < C) return;
|
|
288
|
+
throw H = null, t;
|
|
292
289
|
}
|
|
293
|
-
},
|
|
294
|
-
if (I || L || Q !== void 0 || N || !
|
|
295
|
-
let e = ++
|
|
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 (!
|
|
295
|
+
if (!H) {
|
|
299
296
|
let { data: e, subtitles: t, finished: r } = await j.read();
|
|
300
|
-
if (n() || (r && (
|
|
301
|
-
|
|
297
|
+
if (n() || (r && (B = !0), t?.length && s.pushFragments(t), !e.byteLength)) return;
|
|
298
|
+
H = e, U = 0;
|
|
302
299
|
}
|
|
303
|
-
await
|
|
300
|
+
await ye(), !n() && G && (G = !1, re?.());
|
|
304
301
|
} catch (e) {
|
|
305
302
|
q(e, n());
|
|
306
303
|
} finally {
|
|
307
|
-
e ===
|
|
304
|
+
e === de && (I = !1);
|
|
308
305
|
}
|
|
309
|
-
},
|
|
310
|
-
let t =
|
|
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
|
-
},
|
|
313
|
-
if (!N && !(
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
let t =
|
|
318
|
-
for (let n = 0; n < ee && !(N || t !==
|
|
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
|
|
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
|
-
},
|
|
327
|
-
let t = performance.now(), n = t -
|
|
323
|
+
}, Te = 0, X = null, Z, Ee = (e) => {
|
|
324
|
+
let t = performance.now(), n = t - Te;
|
|
328
325
|
if (n >= D) {
|
|
329
|
-
|
|
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) && (
|
|
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
|
|
338
|
+
let De = 0, Q;
|
|
342
339
|
M.push(() => {
|
|
343
340
|
Q && clearTimeout(Q);
|
|
344
341
|
});
|
|
345
|
-
let
|
|
342
|
+
let Oe = () => {
|
|
346
343
|
let t = n.currentTime, r = e.info.input.duration || n.duration;
|
|
347
|
-
r > 0 &&
|
|
348
|
-
let i = performance.now(), a = i -
|
|
349
|
-
if (
|
|
350
|
-
if (
|
|
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
|
-
|
|
354
|
+
xe(e) || (B = !1, J(e));
|
|
358
355
|
}, O);
|
|
359
356
|
}
|
|
360
357
|
};
|
|
361
|
-
n.addEventListener("seeking",
|
|
362
|
-
let $ = !1,
|
|
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",
|
|
369
|
-
let
|
|
370
|
-
$ || n.error || (
|
|
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(
|
|
369
|
+
return M.push(() => clearInterval(Ae)), {
|
|
373
370
|
destroy: P,
|
|
374
|
-
prepareSeek:
|
|
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,
|
|
516
|
-
let { video: t, subtitles: n } = e, r = e.mode ??
|
|
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,
|
|
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-
|
|
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.
|
|
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",
|
|
@@ -102,7 +102,15 @@ const PRE_EVICT_TIGHT = -5
|
|
|
102
102
|
const POST_EVICT_TIGHT = 20
|
|
103
103
|
const MAX_APPEND_ATTEMPTS = 5
|
|
104
104
|
const SOURCE_OPEN_TIMEOUT = 15_000
|
|
105
|
-
|
|
105
|
+
/*
|
|
106
|
+
* How far past the playhead a range may start and still count as the one holding it, which decides how
|
|
107
|
+
* much to read and never whether to fetch.
|
|
108
|
+
*
|
|
109
|
+
* Firefox 151 reports a range from its first presented frame, the reorder delay past its keyframe: 82 ms
|
|
110
|
+
* on most fixtures and 166 ms on anime-chapters, at load and after every remuxer seek, where Chrome 153
|
|
111
|
+
* reports the keyframe itself. Without the slack a playhead paused at 0 reads the whole file, and a
|
|
112
|
+
* prepared seek to a keyframe builds no runway (measured 2026-10-11).
|
|
113
|
+
*/
|
|
106
114
|
const BOUNDARY_SLACK = 1
|
|
107
115
|
/*
|
|
108
116
|
* Seconds of data a seek target needs behind it before the playhead is allowed to move there.
|
|
@@ -272,6 +280,8 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
272
280
|
let outstandingError = false
|
|
273
281
|
// where the remuxer was last positioned, which is the floor of what it can still read forward from
|
|
274
282
|
let lastSeekPosition = 0
|
|
283
|
+
// whether nothing was removed since, so everything read forward from there is still buffered
|
|
284
|
+
let positioned = true
|
|
275
285
|
|
|
276
286
|
// 'Cancelled' covers both an aborted task and a read that gave up, so it is only noise mid-seek
|
|
277
287
|
const reportError = (error: unknown, aborted: boolean, terminal = false) => {
|
|
@@ -305,12 +315,16 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
305
315
|
const pre = tight ? PRE_EVICT_TIGHT : PRE_EVICT
|
|
306
316
|
const post = tight ? POST_EVICT_TIGHT : POST_EVICT
|
|
307
317
|
const ranges = getTimeRanges(sourceBuffer)
|
|
318
|
+
const remove = (start: number, end: number) => {
|
|
319
|
+
positioned = false
|
|
320
|
+
return unbufferRange(start, end)
|
|
321
|
+
}
|
|
308
322
|
// From 0, never from the reported start: once a group is removed, Firefox 151 reports the start
|
|
309
323
|
// 1 ms after the keyframe it still holds, so a remove from there keeps that frame for good
|
|
310
324
|
// (measured 2026-10-11).
|
|
311
|
-
if (ranges.length && ranges[0].start < ct + pre) await
|
|
325
|
+
if (ranges.length && ranges[0].start < ct + pre) await remove(0, ct + pre)
|
|
312
326
|
for (const { end } of ranges) {
|
|
313
|
-
if (end > ct + post) await
|
|
327
|
+
if (end > ct + post) await remove(ct + post, end)
|
|
314
328
|
}
|
|
315
329
|
}
|
|
316
330
|
|
|
@@ -336,15 +350,16 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
336
350
|
return !range || range.end < ct + BUFFER_TARGET
|
|
337
351
|
}
|
|
338
352
|
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
353
|
+
const rangeAt = (time: number) =>
|
|
354
|
+
getTimeRanges(sourceBuffer).find((r) => r.start <= time + BOUNDARY_SLACK && time < r.end)
|
|
355
|
+
const playheadRange = () => rangeAt(videoElement.currentTime)
|
|
343
356
|
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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)
|
|
348
363
|
}
|
|
349
364
|
const trackAppendedEnd = () => {
|
|
350
365
|
const ends = getTimeRanges(sourceBuffer).map((r) => r.end)
|
|
@@ -356,6 +371,7 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
356
371
|
pending = null
|
|
357
372
|
pendingAttempts = 0
|
|
358
373
|
lastSeekPosition = time
|
|
374
|
+
positioned = true
|
|
359
375
|
const generation = ++seekGeneration
|
|
360
376
|
try {
|
|
361
377
|
// `seek` ends by reading, and that read can hit the end of the file, which finalizes the
|
|
@@ -373,6 +389,7 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
373
389
|
trackAppendedEnd()
|
|
374
390
|
}
|
|
375
391
|
} catch (error) {
|
|
392
|
+
if (generation === seekGeneration) positioned = false
|
|
376
393
|
reportError(error, destroyed || generation !== seekGeneration)
|
|
377
394
|
} finally {
|
|
378
395
|
if (generation === seekGeneration) seeking = false
|
|
@@ -446,16 +463,41 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
446
463
|
* BOTH the input and the output context and re-runs write_header, because the fragmented mp4 muxer
|
|
447
464
|
* cannot rewind. Skipping it when the target is already playable is most of what a seek costs.
|
|
448
465
|
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
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.
|
|
453
472
|
*/
|
|
454
|
-
const alreadyPlayable = (time: number) => time >= lastSeekPosition &&
|
|
473
|
+
const alreadyPlayable = (time: number) => time >= lastSeekPosition && playableAt(time) && hasRunway(time)
|
|
455
474
|
|
|
456
|
-
/**
|
|
475
|
+
/**
|
|
476
|
+
* Whether the element can play from a GIVEN time without a fetch.
|
|
477
|
+
*
|
|
478
|
+
* Exact. A seek that lands before a range waits there for good once the gap passes 60 to 90 ms in
|
|
479
|
+
* Chrome 153, or 250 to 300 ms in Firefox 151, so Firefox's reorder delay cannot be allowed for here
|
|
480
|
+
* and a target inside it costs a remuxer seek (24 fps fixtures, measured 2026-10-11).
|
|
481
|
+
*/
|
|
457
482
|
const playableAt = (time: number) =>
|
|
458
|
-
getTimeRanges(sourceBuffer).some((r) => r.start <= time
|
|
483
|
+
getTimeRanges(sourceBuffer).some((r) => r.start <= time && time < r.end)
|
|
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))
|
|
459
501
|
|
|
460
502
|
/**
|
|
461
503
|
* Put the data in place for a position the playhead has NOT moved to yet.
|
|
@@ -470,24 +512,19 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
470
512
|
* delays the seek: delaying the seek by the same amount while appending nothing still wedged 4
|
|
471
513
|
* out of 4, so it is the data and not the delay.
|
|
472
514
|
*
|
|
473
|
-
* Returns
|
|
474
|
-
*
|
|
475
|
-
*
|
|
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.
|
|
476
519
|
*/
|
|
477
|
-
/** Seconds of contiguous data past a position that make it safe to play from. */
|
|
478
|
-
const runwayFrom = (time: number) => {
|
|
479
|
-
const range = getTimeRanges(sourceBuffer).find((r) => r.start <= time + BOUNDARY_SLACK && time < r.end)
|
|
480
|
-
return range ? range.end - time : 0
|
|
481
|
-
}
|
|
482
|
-
|
|
483
520
|
const prepareSeek = async (time: number) => {
|
|
484
521
|
if (destroyed) return
|
|
485
|
-
if (
|
|
522
|
+
if (playableAt(time) && hasRunway(time)) return
|
|
486
523
|
finished = false
|
|
487
524
|
preparing++
|
|
488
525
|
prepareTarget = time
|
|
489
526
|
try {
|
|
490
|
-
|
|
527
|
+
await seekTo(time)
|
|
491
528
|
/*
|
|
492
529
|
* Captured AFTER the seek, never before.
|
|
493
530
|
*
|
|
@@ -585,14 +622,18 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
585
622
|
lastSeekingAt = now
|
|
586
623
|
if (alreadyPlayable(time)) return
|
|
587
624
|
/*
|
|
588
|
-
*
|
|
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.
|
|
589
628
|
*
|
|
590
|
-
* `prepareSeek`
|
|
591
|
-
* once its budget runs out
|
|
592
|
-
* read again and bumps the generation, throwing
|
|
593
|
-
* benchmark caught it as a seek that took twice as
|
|
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.
|
|
594
635
|
*/
|
|
595
|
-
if (seeking && Math.abs(time - lastSeekPosition) < 0.001) return
|
|
636
|
+
if (positioned && (seeking || runwayFrom(time) > 0) && Math.abs(time - lastSeekPosition) < 0.001) return
|
|
596
637
|
finished = false
|
|
597
638
|
if (dragTimer) clearTimeout(dragTimer)
|
|
598
639
|
if (!dragging) {
|