@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.
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",
|
|
@@ -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
|
|
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
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
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
|
|
488
|
-
*
|
|
489
|
-
*
|
|
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) &&
|
|
522
|
+
if (playableAt(time) && hasRunway(time)) return
|
|
500
523
|
finished = false
|
|
501
524
|
preparing++
|
|
502
525
|
prepareTarget = time
|
|
503
526
|
try {
|
|
504
|
-
|
|
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
|
-
*
|
|
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`
|
|
605
|
-
* once its budget runs out
|
|
606
|
-
* read again and bumps the generation, throwing
|
|
607
|
-
* 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.
|
|
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) {
|