@banou/media-player 0.10.12 → 0.10.14
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-DKjUYAmJ.js → engine-DdbOiEX3.js} +66 -69
- package/dist/index.js +50 -85
- package/package.json +2 -5
- package/src/lib/engine/playback.ts +59 -32
- package/src/lib/react/components/burn-in-hint.tsx +3 -20
- package/src/lib/react/components/control-bar.tsx +8 -38
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";
|
|
@@ -2123,15 +2123,6 @@ ${gt}
|
|
|
2123
2123
|
})]
|
|
2124
2124
|
});
|
|
2125
2125
|
}, It = .05, Lt = 5, Rt = 50, zt = 1e3, Bt = g`
|
|
2126
|
-
display: flex;
|
|
2127
|
-
flex-direction: column;
|
|
2128
|
-
gap: calc(0.4 * var(--mp-unit));
|
|
2129
|
-
|
|
2130
|
-
.hint {
|
|
2131
|
-
opacity: 0.72;
|
|
2132
|
-
font-size: 0.9em;
|
|
2133
|
-
}
|
|
2134
|
-
`, Vt = g`
|
|
2135
2126
|
position: absolute;
|
|
2136
2127
|
bottom: 0;
|
|
2137
2128
|
width: 100%;
|
|
@@ -2261,7 +2252,7 @@ ${gt}
|
|
|
2261
2252
|
}
|
|
2262
2253
|
}
|
|
2263
2254
|
}
|
|
2264
|
-
`,
|
|
2255
|
+
`, Vt = () => {
|
|
2265
2256
|
let e = G(), t = G((e) => e.paused), n = G((e) => e.currentTime), r = G((e) => e.duration), i = G((e) => e.seekingTo), a = G((e) => e.fullscreen), o = G((e) => e.hideUI), s = G((e) => e.togglePictureInPicture), c = G((e) => e.pictureInPictureMode), l = G((e) => e.burnedInSubtitles), f = G((e) => e.passThroughPictureInPicture), h = G((e) => e.subtitleTracks), g = G((e) => e.requestSeek), [_, v] = m(null), y = p(null), b = p(void 0), x = c === "burn-in", S = h.length > 0, C = r > 0 && n === r, w = u(({ direction: t, stepSize: n }) => {
|
|
2266
2257
|
let r = Ne(e.volume), i = t === "up" ? n : -n, a = Math.max(0, Math.min(1, r + i)), o = e.muted;
|
|
2267
2258
|
e.setVolume(Me(a)), o && !e.muted && e.toggleMuted();
|
|
@@ -2332,7 +2323,7 @@ ${gt}
|
|
|
2332
2323
|
e = Date.now() + zt;
|
|
2333
2324
|
};
|
|
2334
2325
|
}, [k]), d(() => () => clearInterval(b.current), []), /* @__PURE__ */ E("div", {
|
|
2335
|
-
css:
|
|
2326
|
+
css: Bt,
|
|
2336
2327
|
style: { ...o ? {
|
|
2337
2328
|
opacity: "0",
|
|
2338
2329
|
pointerEvents: "none"
|
|
@@ -2399,16 +2390,7 @@ ${gt}
|
|
|
2399
2390
|
alt: ""
|
|
2400
2391
|
})
|
|
2401
2392
|
}),
|
|
2402
|
-
toolTipText: /* @__PURE__ */
|
|
2403
|
-
css: Bt,
|
|
2404
|
-
children: [/* @__PURE__ */ T("span", {
|
|
2405
|
-
className: "lead",
|
|
2406
|
-
children: x ? S ? l ? "Subtitles are in the picture" : "Put the subtitles in the picture" : "This file has no subtitles" : "Picture in picture"
|
|
2407
|
-
}), x && S ? /* @__PURE__ */ T("span", {
|
|
2408
|
-
className: "hint",
|
|
2409
|
-
children: "Then hover the video and click your browser's own pop out button"
|
|
2410
|
-
}) : null]
|
|
2411
|
-
})
|
|
2393
|
+
toolTipText: /* @__PURE__ */ T("span", { children: x ? S ? l ? "Subtitles are in the picture" : "Put the subtitles in the picture" : "This file has no subtitles" : "Picture in picture" })
|
|
2412
2394
|
}) : null,
|
|
2413
2395
|
/* @__PURE__ */ T($, {
|
|
2414
2396
|
id: "full-screen",
|
|
@@ -2427,7 +2409,7 @@ ${gt}
|
|
|
2427
2409
|
})]
|
|
2428
2410
|
})]
|
|
2429
2411
|
});
|
|
2430
|
-
},
|
|
2412
|
+
}, Ht = 9e3, Ut = g`
|
|
2431
2413
|
position: absolute;
|
|
2432
2414
|
inset: 0;
|
|
2433
2415
|
z-index: 2;
|
|
@@ -2458,22 +2440,11 @@ ${gt}
|
|
|
2458
2440
|
opacity: 1;
|
|
2459
2441
|
}
|
|
2460
2442
|
|
|
2461
|
-
.
|
|
2462
|
-
|
|
2463
|
-
flex-direction: column;
|
|
2464
|
-
gap: calc(0.3 * var(--mp-unit));
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
.title {
|
|
2468
|
-
${Z.bMedium.bold}
|
|
2443
|
+
.text {
|
|
2444
|
+
${Z.bMedium.medium}
|
|
2469
2445
|
color: #fff;
|
|
2470
2446
|
}
|
|
2471
2447
|
|
|
2472
|
-
.body {
|
|
2473
|
-
${Z.bSmall.regular}
|
|
2474
|
-
color: rgba(255, 255, 255, 0.75);
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
2448
|
/* Points at the edge the browser's control lives on, which is the whole message. */
|
|
2478
2449
|
.arrow {
|
|
2479
2450
|
flex: none;
|
|
@@ -2481,7 +2452,7 @@ ${gt}
|
|
|
2481
2452
|
line-height: 1;
|
|
2482
2453
|
color: #fff;
|
|
2483
2454
|
}
|
|
2484
|
-
`,
|
|
2455
|
+
`, Wt = () => {
|
|
2485
2456
|
let e = G((e) => e.burnedInSubtitles), t = G((e) => e.pictureInPictureMode), [n, r] = m(!1);
|
|
2486
2457
|
return d(() => {
|
|
2487
2458
|
if (t !== "burn-in" || !e) {
|
|
@@ -2489,23 +2460,17 @@ ${gt}
|
|
|
2489
2460
|
return;
|
|
2490
2461
|
}
|
|
2491
2462
|
r(!0);
|
|
2492
|
-
let n = setTimeout(() => r(!1),
|
|
2463
|
+
let n = setTimeout(() => r(!1), Ht);
|
|
2493
2464
|
return () => clearTimeout(n);
|
|
2494
2465
|
}, [t, e]), t !== "burn-in" || !e ? null : /* @__PURE__ */ T("div", {
|
|
2495
|
-
css:
|
|
2466
|
+
css: Ut,
|
|
2496
2467
|
className: n ? "show" : "",
|
|
2497
2468
|
role: "status",
|
|
2498
2469
|
children: /* @__PURE__ */ E("div", {
|
|
2499
2470
|
className: "card",
|
|
2500
|
-
children: [/* @__PURE__ */
|
|
2501
|
-
className: "
|
|
2502
|
-
children:
|
|
2503
|
-
className: "title",
|
|
2504
|
-
children: "Subtitles are in the picture"
|
|
2505
|
-
}), /* @__PURE__ */ T("div", {
|
|
2506
|
-
className: "body",
|
|
2507
|
-
children: "Now hover the video and click your browser's own pop out button"
|
|
2508
|
-
})]
|
|
2471
|
+
children: [/* @__PURE__ */ T("div", {
|
|
2472
|
+
className: "text",
|
|
2473
|
+
children: "Now click your browser's pop out button"
|
|
2509
2474
|
}), /* @__PURE__ */ T("div", {
|
|
2510
2475
|
className: "arrow",
|
|
2511
2476
|
"aria-hidden": "true",
|
|
@@ -2513,10 +2478,10 @@ ${gt}
|
|
|
2513
2478
|
})]
|
|
2514
2479
|
})
|
|
2515
2480
|
});
|
|
2516
|
-
},
|
|
2481
|
+
}, Gt = 1, Kt = 6e3, qt = 1, Jt = {
|
|
2517
2482
|
opening: "Skip Opening",
|
|
2518
2483
|
ending: "Skip Ending"
|
|
2519
|
-
},
|
|
2484
|
+
}, Yt = g`
|
|
2520
2485
|
position: absolute;
|
|
2521
2486
|
inset: 0;
|
|
2522
2487
|
z-index: 2;
|
|
@@ -2567,10 +2532,10 @@ ${gt}
|
|
|
2567
2532
|
bottom: calc(9 * var(--mp-unit));
|
|
2568
2533
|
}
|
|
2569
2534
|
}
|
|
2570
|
-
`,
|
|
2535
|
+
`, Xt = () => {
|
|
2571
2536
|
let e = G((e) => e.chapters), t = G((e) => e.currentTime), n = G((e) => e.requestSeek), r = G((e) => e.seekPreview), i = G(), a = f(() => ct(e), [e]), o = f(() => {
|
|
2572
2537
|
if (typeof t != "number") return;
|
|
2573
|
-
let n = e.findIndex((e, n) => a[n] !== void 0 && e.start -
|
|
2538
|
+
let n = e.findIndex((e, n) => a[n] !== void 0 && e.start - Gt <= t && t < e.end), r = n >= 0 ? a[n] : void 0;
|
|
2574
2539
|
return r ? {
|
|
2575
2540
|
kind: r,
|
|
2576
2541
|
end: e[n].end
|
|
@@ -2582,7 +2547,7 @@ ${gt}
|
|
|
2582
2547
|
]), s = p(0), [c, l] = m(0);
|
|
2583
2548
|
d(() => {
|
|
2584
2549
|
let e = typeof t == "number" ? t : 0, n = s.current;
|
|
2585
|
-
s.current = e, e < n -
|
|
2550
|
+
s.current = e, e < n - qt && l((e) => e + 1);
|
|
2586
2551
|
}, [t]);
|
|
2587
2552
|
let [u, h] = m(!1), g = o ? `${o.kind}@${o.end}` : "";
|
|
2588
2553
|
return d(() => {
|
|
@@ -2591,12 +2556,12 @@ ${gt}
|
|
|
2591
2556
|
return;
|
|
2592
2557
|
}
|
|
2593
2558
|
h(!0);
|
|
2594
|
-
let e = setTimeout(() => h(!1),
|
|
2559
|
+
let e = setTimeout(() => h(!1), Kt);
|
|
2595
2560
|
return () => clearTimeout(e);
|
|
2596
2561
|
}, [g, c]), d(() => {
|
|
2597
2562
|
o || h(!1);
|
|
2598
2563
|
}, [o]), o ? /* @__PURE__ */ T("div", {
|
|
2599
|
-
css:
|
|
2564
|
+
css: Yt,
|
|
2600
2565
|
className: u && !r ? "show" : "",
|
|
2601
2566
|
children: /* @__PURE__ */ T("button", {
|
|
2602
2567
|
type: "button",
|
|
@@ -2604,10 +2569,10 @@ ${gt}
|
|
|
2604
2569
|
onClick: () => {
|
|
2605
2570
|
h(!1), n ? n(o.end) : i.seek(o.end);
|
|
2606
2571
|
},
|
|
2607
|
-
children:
|
|
2572
|
+
children: Jt[o.kind]
|
|
2608
2573
|
})
|
|
2609
2574
|
}) : null;
|
|
2610
|
-
},
|
|
2575
|
+
}, Zt = 3e3, Qt = g`
|
|
2611
2576
|
position: absolute;
|
|
2612
2577
|
inset: 0;
|
|
2613
2578
|
z-index: 2;
|
|
@@ -2618,10 +2583,10 @@ ${gt}
|
|
|
2618
2583
|
hoverable while faded out, popping a tooltip over nothing. It cascades where pointer-events does
|
|
2619
2584
|
not, and transitioning it holds the item on screen for the length of the fade. */
|
|
2620
2585
|
transition: opacity 0.1s cubic-bezier(.4,0,1,1), visibility 0.1s;
|
|
2621
|
-
`,
|
|
2586
|
+
`, $t = (e, t) => {
|
|
2622
2587
|
let n = e.left - t.left, r = e.top - t.top;
|
|
2623
2588
|
return `path(evenodd, 'M0 0H${t.width}V${t.height}H0Z M${n} ${r}H${n + e.width}V${r + e.height}H${n}Z')`;
|
|
2624
|
-
},
|
|
2589
|
+
}, en = g`
|
|
2625
2590
|
position: relative;
|
|
2626
2591
|
background-color: black;
|
|
2627
2592
|
display: flex;
|
|
@@ -2675,13 +2640,13 @@ ${gt}
|
|
|
2675
2640
|
&.hide {
|
|
2676
2641
|
cursor: none;
|
|
2677
2642
|
}
|
|
2678
|
-
`,
|
|
2643
|
+
`, tn = (e, t = "") => s.toArray(e).flatMap((e, n) => {
|
|
2679
2644
|
let r = `${t}${l(e) && e.key != null ? e.key : n}`;
|
|
2680
|
-
return l(e) && e.type === c ?
|
|
2645
|
+
return l(e) && e.type === c ? tn(e.props.children, `${r}/`) : [{
|
|
2681
2646
|
key: r,
|
|
2682
2647
|
item: e
|
|
2683
2648
|
}];
|
|
2684
|
-
}),
|
|
2649
|
+
}), nn = ({ ref: e, onVideoRef: t, onSubtitleRef: n, overlay: r, controls: i, children: a }) => {
|
|
2685
2650
|
let o = G(), s = G((e) => e.hideUI), c = G((e) => e.setHideUI), l = G((e) => e.hasMedia), u = G((e) => e.passThroughPictureInPicture), f = p(void 0), m = p("mouse"), h = p({
|
|
2686
2651
|
x: -1,
|
|
2687
2652
|
y: -1
|
|
@@ -2702,15 +2667,15 @@ ${gt}
|
|
|
2702
2667
|
return !!n && g.current.contains(n) && !n.closest(".video");
|
|
2703
2668
|
}, b = () => {
|
|
2704
2669
|
if (y()) {
|
|
2705
|
-
f.current = setTimeout(b,
|
|
2670
|
+
f.current = setTimeout(b, Zt);
|
|
2706
2671
|
return;
|
|
2707
2672
|
}
|
|
2708
2673
|
_();
|
|
2709
2674
|
}, x = () => {
|
|
2710
|
-
c(!1), clearTimeout(f.current), f.current = setTimeout(b,
|
|
2675
|
+
c(!1), clearTimeout(f.current), f.current = setTimeout(b, Zt);
|
|
2711
2676
|
};
|
|
2712
2677
|
return /* @__PURE__ */ E("div", {
|
|
2713
|
-
css:
|
|
2678
|
+
css: en,
|
|
2714
2679
|
ref: v,
|
|
2715
2680
|
onPointerMove: (e) => {
|
|
2716
2681
|
m.current = e.pointerType, e.pointerType === "mouse" && (h.current = {
|
|
@@ -2730,8 +2695,8 @@ ${gt}
|
|
|
2730
2695
|
className: s ? "hide" : "",
|
|
2731
2696
|
children: [
|
|
2732
2697
|
/* @__PURE__ */ T(ke, { onSubtitleRef: n }),
|
|
2733
|
-
|
|
2734
|
-
css:
|
|
2698
|
+
tn(r).map(({ key: e, item: t }) => /* @__PURE__ */ T("div", {
|
|
2699
|
+
css: Qt,
|
|
2735
2700
|
style: { ...s ? {
|
|
2736
2701
|
opacity: "0",
|
|
2737
2702
|
visibility: "hidden",
|
|
@@ -2739,12 +2704,12 @@ ${gt}
|
|
|
2739
2704
|
} : {} },
|
|
2740
2705
|
children: t
|
|
2741
2706
|
}, e)),
|
|
2742
|
-
i === !1 ? null : /* @__PURE__ */ T(
|
|
2743
|
-
/* @__PURE__ */ T(
|
|
2744
|
-
/* @__PURE__ */ T(
|
|
2707
|
+
i === !1 ? null : /* @__PURE__ */ T(Vt, {}),
|
|
2708
|
+
/* @__PURE__ */ T(Wt, {}),
|
|
2709
|
+
/* @__PURE__ */ T(Xt, {}),
|
|
2745
2710
|
u?.armed && g.current ? /* @__PURE__ */ T("div", {
|
|
2746
2711
|
className: "pass-through",
|
|
2747
|
-
style: { clipPath:
|
|
2712
|
+
style: { clipPath: $t(u.armed, g.current.getBoundingClientRect()) },
|
|
2748
2713
|
onPointerOver: u.disarm
|
|
2749
2714
|
}) : null,
|
|
2750
2715
|
/* @__PURE__ */ E("div", {
|
|
@@ -2763,7 +2728,7 @@ ${gt}
|
|
|
2763
2728
|
})
|
|
2764
2729
|
]
|
|
2765
2730
|
});
|
|
2766
|
-
},
|
|
2731
|
+
}, rn = ({ options: e, children: t }) => {
|
|
2767
2732
|
let n = "media" in e ? e : null, r = n ? null : e, { title: i, chapters: a } = e, { size: o, downloadedRanges: s, publicPath: c, libavWorkerUrl: l, read: u, thumbnailRead: h, thumbnailsEnabled: g } = r ?? {}, _ = b(), v = y(), [x, S] = m(null), [C, w] = m(null), E = n?.media ?? x;
|
|
2768
2733
|
d(() => (_?.(E), () => _?.(null)), [E, _]);
|
|
2769
2734
|
let { expose: D } = e, O = D === !0 ? {} : D || void 0, k = p(O);
|
|
@@ -2868,7 +2833,7 @@ ${gt}
|
|
|
2868
2833
|
});
|
|
2869
2834
|
}, [K, X]), d(() => {
|
|
2870
2835
|
a && K({ chapters: a });
|
|
2871
|
-
}, [K, a]), /* @__PURE__ */ T(
|
|
2836
|
+
}, [K, a]), /* @__PURE__ */ T(nn, {
|
|
2872
2837
|
ref: v,
|
|
2873
2838
|
onVideoRef: n ? void 0 : S,
|
|
2874
2839
|
onSubtitleRef: w,
|
|
@@ -2876,7 +2841,7 @@ ${gt}
|
|
|
2876
2841
|
controls: e.controls,
|
|
2877
2842
|
children: t
|
|
2878
2843
|
});
|
|
2879
|
-
},
|
|
2844
|
+
}, an = g`
|
|
2880
2845
|
/**
|
|
2881
2846
|
* The chrome's whole scale, in one place.
|
|
2882
2847
|
*
|
|
@@ -2946,17 +2911,17 @@ ${gt}
|
|
|
2946
2911
|
height: 100%;
|
|
2947
2912
|
width: 100%;
|
|
2948
2913
|
overflow: hidden;
|
|
2949
|
-
`,
|
|
2950
|
-
css:
|
|
2951
|
-
children: /* @__PURE__ */ T(W.Provider, { children: /* @__PURE__ */ T(
|
|
2914
|
+
`, on = (e) => /* @__PURE__ */ T("div", {
|
|
2915
|
+
css: an,
|
|
2916
|
+
children: /* @__PURE__ */ T(W.Provider, { children: /* @__PURE__ */ T(rn, {
|
|
2952
2917
|
options: e,
|
|
2953
2918
|
children: e.children
|
|
2954
2919
|
}) })
|
|
2955
|
-
}),
|
|
2920
|
+
}), sn = (e) => !!e && typeof e == "object" && "at" in e, cn = (e) => !!e && typeof e == "object" && "selection" in e, ln = ({ blob: e, name: t }) => ({
|
|
2956
2921
|
length: e.size,
|
|
2957
2922
|
name: t ?? (e instanceof File ? e.name : void 0),
|
|
2958
2923
|
read: (t, n) => e.slice(t, t + n).arrayBuffer()
|
|
2959
|
-
}),
|
|
2924
|
+
}), un = async (e, t) => {
|
|
2960
2925
|
let n = await fetch(e, {
|
|
2961
2926
|
headers: { Range: "bytes=0-1" },
|
|
2962
2927
|
credentials: t
|
|
@@ -2966,8 +2931,8 @@ ${gt}
|
|
|
2966
2931
|
if (!Number.isFinite(i) || i <= 0) throw Error("The source did not report its length");
|
|
2967
2932
|
if (!r && n.status !== 206) throw Error("The source does not support range requests");
|
|
2968
2933
|
return i;
|
|
2969
|
-
},
|
|
2970
|
-
let i = t ?? await
|
|
2934
|
+
}, dn = async ({ url: e, length: t, name: n, credentials: r }) => {
|
|
2935
|
+
let i = t ?? await un(e, r);
|
|
2971
2936
|
return {
|
|
2972
2937
|
length: i,
|
|
2973
2938
|
name: n,
|
|
@@ -2982,9 +2947,9 @@ ${gt}
|
|
|
2982
2947
|
return o.arrayBuffer();
|
|
2983
2948
|
}
|
|
2984
2949
|
};
|
|
2985
|
-
},
|
|
2986
|
-
if ("blob" in e) return
|
|
2987
|
-
if ("url" in e) return
|
|
2950
|
+
}, fn = async (e) => {
|
|
2951
|
+
if ("blob" in e) return ln(e);
|
|
2952
|
+
if ("url" in e) return dn(e);
|
|
2988
2953
|
if ("read" in e) return {
|
|
2989
2954
|
length: e.length,
|
|
2990
2955
|
name: e.name,
|
|
@@ -2993,4 +2958,4 @@ ${gt}
|
|
|
2993
2958
|
throw Error(`Unknown source type: ${e}`);
|
|
2994
2959
|
};
|
|
2995
2960
|
//#endregion
|
|
2996
|
-
export {
|
|
2961
|
+
export { on as MediaPlayer, on as default, W as Player, e as SUBTITLES_OFF, a as createPictureInPicture, o as createSubtitleRenderer, r as createThumbnailGenerator, fn as inputToRemuxerInput, cn as isDelegatedTracks, sn as isExternalThumbnails, U as sourceFeature, i as startPlayback, de as usePictureInPicture, G as usePlayer, ue as useSeekThumbnails };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banou/media-player",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.14",
|
|
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) {
|
|
@@ -4,7 +4,6 @@ import { css } from '@emotion/react'
|
|
|
4
4
|
import { fonts } from '../../utils/fonts'
|
|
5
5
|
import { usePlayer } from '../player'
|
|
6
6
|
|
|
7
|
-
/** Long enough to read twice, short enough not to sit over the picture. */
|
|
8
7
|
const VISIBLE_MS = 9000
|
|
9
8
|
|
|
10
9
|
const style = css`
|
|
@@ -38,22 +37,11 @@ const style = css`
|
|
|
38
37
|
opacity: 1;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
flex-direction: column;
|
|
44
|
-
gap: calc(0.3 * var(--mp-unit));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.title {
|
|
48
|
-
${fonts.bMedium.bold}
|
|
40
|
+
.text {
|
|
41
|
+
${fonts.bMedium.medium}
|
|
49
42
|
color: #fff;
|
|
50
43
|
}
|
|
51
44
|
|
|
52
|
-
.body {
|
|
53
|
-
${fonts.bSmall.regular}
|
|
54
|
-
color: rgba(255, 255, 255, 0.75);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
45
|
/* Points at the edge the browser's control lives on, which is the whole message. */
|
|
58
46
|
.arrow {
|
|
59
47
|
flex: none;
|
|
@@ -90,12 +78,7 @@ export const BurnInHint = () => {
|
|
|
90
78
|
return (
|
|
91
79
|
<div css={style} className={show ? 'show' : ''} role='status'>
|
|
92
80
|
<div className='card'>
|
|
93
|
-
<div className='
|
|
94
|
-
<div className='title'>Subtitles are in the picture</div>
|
|
95
|
-
<div className='body'>
|
|
96
|
-
Now hover the video and click your browser's own pop out button
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
81
|
+
<div className='text'>Now click your browser's pop out button</div>
|
|
99
82
|
<div className='arrow' aria-hidden='true'>→</div>
|
|
100
83
|
</div>
|
|
101
84
|
</div>
|
|
@@ -25,23 +25,6 @@ const SEEK_STEP = 5
|
|
|
25
25
|
const REFOCUS_POLL = 50
|
|
26
26
|
const REFOCUS_AFTER = 1_000
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Two stacked lines inside a tooltip.
|
|
30
|
-
*
|
|
31
|
-
* The width to wrap against is no longer here: TooltipDisplay bounds every chip it draws, at the
|
|
32
|
-
* same 26 units this had imposed by hand, so a call site only says what its content is.
|
|
33
|
-
*/
|
|
34
|
-
const tooltipLinesStyle = css`
|
|
35
|
-
display: flex;
|
|
36
|
-
flex-direction: column;
|
|
37
|
-
gap: calc(0.4 * var(--mp-unit));
|
|
38
|
-
|
|
39
|
-
.hint {
|
|
40
|
-
opacity: 0.72;
|
|
41
|
-
font-size: 0.9em;
|
|
42
|
-
}
|
|
43
|
-
`
|
|
44
|
-
|
|
45
28
|
const style = css`
|
|
46
29
|
position: absolute;
|
|
47
30
|
bottom: 0;
|
|
@@ -384,7 +367,6 @@ export const ControlBar = () => {
|
|
|
384
367
|
? (
|
|
385
368
|
<TooltipDisplay
|
|
386
369
|
id='picture-in-picture'
|
|
387
|
-
// anchored to its end, or the two-line burn-in copy runs off the right of the player
|
|
388
370
|
tooltipPlace='top-end'
|
|
389
371
|
text={
|
|
390
372
|
<button
|
|
@@ -409,26 +391,14 @@ export const ControlBar = () => {
|
|
|
409
391
|
</button>
|
|
410
392
|
}
|
|
411
393
|
toolTipText={
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
? 'This file has no subtitles'
|
|
421
|
-
: burnedInSubtitles
|
|
422
|
-
? 'Subtitles are in the picture'
|
|
423
|
-
: 'Put the subtitles in the picture'}
|
|
424
|
-
</span>
|
|
425
|
-
{burnIn && hasSubtitles
|
|
426
|
-
? (
|
|
427
|
-
<span className='hint'>
|
|
428
|
-
Then hover the video and click your browser's own pop out button
|
|
429
|
-
</span>
|
|
430
|
-
)
|
|
431
|
-
: null}
|
|
394
|
+
<span>
|
|
395
|
+
{!burnIn
|
|
396
|
+
? 'Picture in picture'
|
|
397
|
+
: !hasSubtitles
|
|
398
|
+
? 'This file has no subtitles'
|
|
399
|
+
: burnedInSubtitles
|
|
400
|
+
? 'Subtitles are in the picture'
|
|
401
|
+
: 'Put the subtitles in the picture'}
|
|
432
402
|
</span>
|
|
433
403
|
}
|
|
434
404
|
/>
|