@banou/media-player 0.10.16 → 0.10.17
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/README.md +4 -0
- package/dist/index.js +118 -102
- package/dist/react/components/chrome.d.ts +2 -1
- package/dist/react/components/control-bar.d.ts +5 -1
- package/dist/react/video-player.d.ts +7 -0
- package/package.json +1 -1
- package/src/lib/react/components/chrome.tsx +8 -2
- package/src/lib/react/components/control-bar.tsx +24 -5
- package/src/lib/react/video-player.tsx +9 -0
package/README.md
CHANGED
|
@@ -158,6 +158,10 @@ an offer to skip an opening or ending, volume on a log curve, mute, playback spe
|
|
|
158
158
|
selection, subtitle track selection, picture in picture, fullscreen, and keyboard shortcuts. Nothing
|
|
159
159
|
is persisted: volume, speed and track choices start at their defaults every load.
|
|
160
160
|
|
|
161
|
+
The shortcuts (Space or `k`, `f`, `m`, the arrow keys) listen on the whole page. Pass `shortcuts="player"` for a
|
|
162
|
+
player among other content: a key then acts only from inside the player (a click on the picture puts the focus
|
|
163
|
+
there) or while it is fullscreen, and Space and the arrows keep scrolling the page everywhere else.
|
|
164
|
+
|
|
161
165
|
### Chapters
|
|
162
166
|
|
|
163
167
|
Chapters come from the container, and the seekbar draws them as segments with a break at each
|
package/dist/index.js
CHANGED
|
@@ -2252,82 +2252,90 @@ ${gt}
|
|
|
2252
2252
|
}
|
|
2253
2253
|
}
|
|
2254
2254
|
}
|
|
2255
|
-
`, Vt = () => {
|
|
2256
|
-
let
|
|
2257
|
-
let r = Ne(
|
|
2258
|
-
|
|
2259
|
-
}, [
|
|
2255
|
+
`, Vt = ({ shortcuts: e = "window", root: t }) => {
|
|
2256
|
+
let n = G(), r = G((e) => e.paused), i = G((e) => e.currentTime), a = G((e) => e.duration), o = G((e) => e.seekingTo), s = G((e) => e.fullscreen), c = G((e) => e.hideUI), l = G((e) => e.togglePictureInPicture), f = G((e) => e.pictureInPictureMode), h = G((e) => e.burnedInSubtitles), g = G((e) => e.passThroughPictureInPicture), _ = G((e) => e.subtitleTracks), v = G((e) => e.requestSeek), [y, b] = m(null), x = p(null), S = p(void 0), C = f === "burn-in", w = _.length > 0, D = a > 0 && i === a, O = u(({ direction: e, stepSize: t }) => {
|
|
2257
|
+
let r = Ne(n.volume), i = e === "up" ? t : -t, a = Math.max(0, Math.min(1, r + i)), o = n.muted;
|
|
2258
|
+
n.setVolume(Me(a)), o && !n.muted && n.toggleMuted();
|
|
2259
|
+
}, [n]), k = p(!1);
|
|
2260
2260
|
d(() => {
|
|
2261
|
-
let
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
let
|
|
2265
|
-
if (
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2261
|
+
let r = (e) => v ? v(e) : n.seek(e), i = (e) => e.composedPath()[0], a = (e) => {
|
|
2262
|
+
k.current = !!t.current?.contains(i(e) ?? null);
|
|
2263
|
+
}, o = (a) => {
|
|
2264
|
+
let o = i(a);
|
|
2265
|
+
if (o?.isContentEditable || /^(input|textarea|select)$/i.test(o?.tagName ?? "")) return;
|
|
2266
|
+
let s = t.current?.contains(o ?? null) || o === document.body && k.current;
|
|
2267
|
+
if (e === "player" && !n.fullscreen && !s) return;
|
|
2268
|
+
let c = !0;
|
|
2269
|
+
if (a.key === "f") n.toggleFullscreen();
|
|
2270
|
+
else if (a.key === "k") n.togglePaused();
|
|
2271
|
+
else if (a.key === " ") n.togglePaused();
|
|
2272
|
+
else if (a.key === "m") n.toggleMuted();
|
|
2273
|
+
else if (a.key === "ArrowUp") O({
|
|
2270
2274
|
direction: "up",
|
|
2271
2275
|
stepSize: It
|
|
2272
2276
|
});
|
|
2273
|
-
else if (
|
|
2277
|
+
else if (a.key === "ArrowDown") O({
|
|
2274
2278
|
direction: "down",
|
|
2275
2279
|
stepSize: It
|
|
2276
2280
|
});
|
|
2277
|
-
else if (
|
|
2278
|
-
if (!
|
|
2279
|
-
|
|
2280
|
-
} else
|
|
2281
|
-
|
|
2281
|
+
else if (a.key === "ArrowRight") {
|
|
2282
|
+
if (!n.duration) return;
|
|
2283
|
+
r(Math.min(n.currentTime + Lt, n.duration));
|
|
2284
|
+
} else a.key === "ArrowLeft" ? r(Math.max(n.currentTime - Lt, 0)) : c = !1;
|
|
2285
|
+
c && a.preventDefault();
|
|
2286
|
+
};
|
|
2287
|
+
return window.addEventListener("keydown", o), window.addEventListener("pointerdown", a, !0), window.addEventListener("focusin", a, !0), () => {
|
|
2288
|
+
window.removeEventListener("keydown", o), window.removeEventListener("pointerdown", a, !0), window.removeEventListener("focusin", a, !0);
|
|
2282
2289
|
};
|
|
2283
|
-
return window.addEventListener("keydown", n), () => window.removeEventListener("keydown", n);
|
|
2284
2290
|
}, [
|
|
2291
|
+
n,
|
|
2292
|
+
O,
|
|
2293
|
+
v,
|
|
2285
2294
|
e,
|
|
2286
|
-
|
|
2287
|
-
g
|
|
2295
|
+
t
|
|
2288
2296
|
]);
|
|
2289
|
-
let
|
|
2290
|
-
e.preventDefault(), e.deltaY < 0 ?
|
|
2297
|
+
let A = u((e) => {
|
|
2298
|
+
e.preventDefault(), e.deltaY < 0 ? O({
|
|
2291
2299
|
direction: "up",
|
|
2292
2300
|
stepSize: It
|
|
2293
|
-
}) :
|
|
2301
|
+
}) : O({
|
|
2294
2302
|
direction: "down",
|
|
2295
2303
|
stepSize: It
|
|
2296
2304
|
});
|
|
2297
|
-
}, [
|
|
2305
|
+
}, [O]);
|
|
2298
2306
|
d(() => {
|
|
2299
|
-
if (
|
|
2300
|
-
}, [
|
|
2301
|
-
let
|
|
2302
|
-
if (!
|
|
2307
|
+
if (y) return y.addEventListener("wheel", A, { passive: !1 }), () => y.removeEventListener("wheel", A);
|
|
2308
|
+
}, [y, A]);
|
|
2309
|
+
let j = (e) => {
|
|
2310
|
+
if (!g || g.active || g.armed || e.pointerType === "touch") return;
|
|
2303
2311
|
let { left: t, top: n, width: r, height: i } = e.currentTarget.getBoundingClientRect();
|
|
2304
|
-
|
|
2312
|
+
g.arm({
|
|
2305
2313
|
left: t,
|
|
2306
2314
|
top: n,
|
|
2307
2315
|
width: r,
|
|
2308
2316
|
height: i
|
|
2309
2317
|
});
|
|
2310
|
-
},
|
|
2318
|
+
}, M = !!g?.armed;
|
|
2311
2319
|
return d(() => {
|
|
2312
|
-
if (!
|
|
2313
|
-
clearInterval(
|
|
2320
|
+
if (!M) return;
|
|
2321
|
+
clearInterval(S.current);
|
|
2314
2322
|
let e = Infinity, t = setInterval(() => {
|
|
2315
2323
|
if (Date.now() > e) {
|
|
2316
2324
|
clearInterval(t);
|
|
2317
2325
|
return;
|
|
2318
2326
|
}
|
|
2319
|
-
let n =
|
|
2327
|
+
let n = x.current, r = document.activeElement;
|
|
2320
2328
|
n && r?.tagName === "IFRAME" && r.closest(".video")?.parentElement?.contains(n) && n.focus({ preventScroll: !0 });
|
|
2321
2329
|
}, Rt);
|
|
2322
|
-
return
|
|
2330
|
+
return S.current = t, () => {
|
|
2323
2331
|
e = Date.now() + zt;
|
|
2324
2332
|
};
|
|
2325
|
-
}, [
|
|
2333
|
+
}, [M]), d(() => () => clearInterval(S.current), []), /* @__PURE__ */ E("div", {
|
|
2326
2334
|
css: Bt,
|
|
2327
|
-
style: { ...
|
|
2335
|
+
style: { ...c ? {
|
|
2328
2336
|
opacity: "0",
|
|
2329
2337
|
pointerEvents: "none"
|
|
2330
|
-
} :
|
|
2338
|
+
} : M ? { pointerEvents: "none" } : {} },
|
|
2331
2339
|
children: [/* @__PURE__ */ T(dt, {}), /* @__PURE__ */ E("div", {
|
|
2332
2340
|
className: "actions",
|
|
2333
2341
|
children: [/* @__PURE__ */ E("div", {
|
|
@@ -2339,16 +2347,16 @@ ${gt}
|
|
|
2339
2347
|
text: /* @__PURE__ */ T("button", {
|
|
2340
2348
|
className: "play",
|
|
2341
2349
|
type: "button",
|
|
2342
|
-
onClick: () =>
|
|
2343
|
-
"aria-label":
|
|
2344
|
-
children: T(
|
|
2350
|
+
onClick: () => n.togglePaused(),
|
|
2351
|
+
"aria-label": D ? "Replay" : r ? "Play" : "Pause",
|
|
2352
|
+
children: T(D ? L : r ? I : F, {})
|
|
2345
2353
|
}),
|
|
2346
|
-
toolTipText: /* @__PURE__ */ T("span", { children:
|
|
2354
|
+
toolTipText: /* @__PURE__ */ T("span", { children: D ? "Replay (k)" : r ? "Play (k)" : "Pause (k)" })
|
|
2347
2355
|
}),
|
|
2348
|
-
/* @__PURE__ */ T(Ft, { ref:
|
|
2356
|
+
/* @__PURE__ */ T(Ft, { ref: b }),
|
|
2349
2357
|
/* @__PURE__ */ T("div", {
|
|
2350
2358
|
className: "time",
|
|
2351
|
-
children: Fe(
|
|
2359
|
+
children: Fe(o ?? i, a)
|
|
2352
2360
|
})
|
|
2353
2361
|
]
|
|
2354
2362
|
}), /* @__PURE__ */ E("div", {
|
|
@@ -2357,40 +2365,40 @@ ${gt}
|
|
|
2357
2365
|
/* @__PURE__ */ T(wt, {}),
|
|
2358
2366
|
/* @__PURE__ */ T(At, {}),
|
|
2359
2367
|
/* @__PURE__ */ T(Ot, {}),
|
|
2360
|
-
|
|
2368
|
+
g ? /* @__PURE__ */ T($, {
|
|
2361
2369
|
id: "picture-in-picture",
|
|
2362
2370
|
tooltipPlace: "top-end",
|
|
2363
2371
|
text: /* @__PURE__ */ T("button", {
|
|
2364
|
-
ref:
|
|
2365
|
-
className:
|
|
2372
|
+
ref: x,
|
|
2373
|
+
className: g.armed ? "picture-in-picture armed" : "picture-in-picture",
|
|
2366
2374
|
type: "button",
|
|
2367
|
-
onPointerEnter:
|
|
2368
|
-
onPointerMove:
|
|
2369
|
-
onClick:
|
|
2375
|
+
onPointerEnter: j,
|
|
2376
|
+
onPointerMove: j,
|
|
2377
|
+
onClick: g.active ? g.exit : void 0,
|
|
2370
2378
|
"aria-label": "Picture in picture",
|
|
2371
|
-
"aria-pressed":
|
|
2379
|
+
"aria-pressed": g.active,
|
|
2372
2380
|
children: /* @__PURE__ */ T("img", {
|
|
2373
2381
|
src: ft,
|
|
2374
2382
|
alt: ""
|
|
2375
2383
|
})
|
|
2376
2384
|
}),
|
|
2377
|
-
toolTipText: /* @__PURE__ */ T("span", { children:
|
|
2378
|
-
}) :
|
|
2385
|
+
toolTipText: /* @__PURE__ */ T("span", { children: g.active ? "Exit picture in picture" : "Picture in picture" })
|
|
2386
|
+
}) : l ? /* @__PURE__ */ T($, {
|
|
2379
2387
|
id: "picture-in-picture",
|
|
2380
2388
|
tooltipPlace: "top-end",
|
|
2381
2389
|
text: /* @__PURE__ */ T("button", {
|
|
2382
2390
|
className: "picture-in-picture",
|
|
2383
2391
|
type: "button",
|
|
2384
|
-
onClick:
|
|
2385
|
-
disabled:
|
|
2386
|
-
"aria-label":
|
|
2387
|
-
"aria-pressed":
|
|
2388
|
-
children:
|
|
2392
|
+
onClick: l,
|
|
2393
|
+
disabled: C && !w,
|
|
2394
|
+
"aria-label": C ? "Put the subtitles in the video" : "Picture in picture",
|
|
2395
|
+
"aria-pressed": C ? h : void 0,
|
|
2396
|
+
children: C ? T(h ? mt : ht, {}) : /* @__PURE__ */ T("img", {
|
|
2389
2397
|
src: ft,
|
|
2390
2398
|
alt: ""
|
|
2391
2399
|
})
|
|
2392
2400
|
}),
|
|
2393
|
-
toolTipText: /* @__PURE__ */ T("span", { children:
|
|
2401
|
+
toolTipText: /* @__PURE__ */ T("span", { children: C ? w ? h ? "Subtitles are in the picture" : "Put the subtitles in the picture" : "This file has no subtitles" : "Picture in picture" })
|
|
2394
2402
|
}) : null,
|
|
2395
2403
|
/* @__PURE__ */ T($, {
|
|
2396
2404
|
id: "full-screen",
|
|
@@ -2398,12 +2406,12 @@ ${gt}
|
|
|
2398
2406
|
text: /* @__PURE__ */ T("button", {
|
|
2399
2407
|
className: "full-screen",
|
|
2400
2408
|
type: "button",
|
|
2401
|
-
onClick: () =>
|
|
2409
|
+
onClick: () => n.toggleFullscreen(),
|
|
2402
2410
|
"aria-label": "Full screen",
|
|
2403
|
-
"aria-pressed":
|
|
2404
|
-
children: T(
|
|
2411
|
+
"aria-pressed": s,
|
|
2412
|
+
children: T(s ? P : N, {})
|
|
2405
2413
|
}),
|
|
2406
|
-
toolTipText: /* @__PURE__ */ T("span", { children:
|
|
2414
|
+
toolTipText: /* @__PURE__ */ T("span", { children: s ? "Exit full screen (f)" : "Full screen (f)" })
|
|
2407
2415
|
})
|
|
2408
2416
|
]
|
|
2409
2417
|
})]
|
|
@@ -2594,6 +2602,9 @@ ${gt}
|
|
|
2594
2602
|
align-items: center;
|
|
2595
2603
|
height: 100%;
|
|
2596
2604
|
width: 100%;
|
|
2605
|
+
/* focusable with shortcuts='player', and the first key after a click on the picture would otherwise
|
|
2606
|
+
ring the whole player as :focus-visible */
|
|
2607
|
+
outline: none;
|
|
2597
2608
|
|
|
2598
2609
|
& > div:not(:last-of-type) {
|
|
2599
2610
|
position: absolute;
|
|
@@ -2646,85 +2657,89 @@ ${gt}
|
|
|
2646
2657
|
key: r,
|
|
2647
2658
|
item: e
|
|
2648
2659
|
}];
|
|
2649
|
-
}), nn = ({ ref: e, onVideoRef: t, onSubtitleRef: n, overlay: r, controls: i,
|
|
2650
|
-
let
|
|
2660
|
+
}), nn = ({ ref: e, onVideoRef: t, onSubtitleRef: n, overlay: r, controls: i, shortcuts: a, children: o }) => {
|
|
2661
|
+
let s = G(), c = G((e) => e.hideUI), l = G((e) => e.setHideUI), u = G((e) => e.hasMedia), f = G((e) => e.passThroughPictureInPicture), m = p(void 0), h = p("mouse"), g = p({
|
|
2651
2662
|
x: -1,
|
|
2652
2663
|
y: -1
|
|
2653
|
-
}),
|
|
2654
|
-
d(() => () => clearTimeout(
|
|
2655
|
-
|
|
2656
|
-
}, [
|
|
2657
|
-
let
|
|
2658
|
-
|
|
2659
|
-
},
|
|
2660
|
-
|
|
2661
|
-
}, y = () => {
|
|
2662
|
-
if (o.passThroughPictureInPicture?.armed) return !0;
|
|
2663
|
-
if (m.current !== "mouse") return !1;
|
|
2664
|
-
let { x: e, y: t } = h.current;
|
|
2665
|
-
if (!g.current || e < 0) return !1;
|
|
2666
|
-
let n = document.elementFromPoint(e, t);
|
|
2667
|
-
return !!n && g.current.contains(n) && !n.closest(".video");
|
|
2664
|
+
}), _ = p(null);
|
|
2665
|
+
d(() => () => clearTimeout(m.current), []), d(() => {
|
|
2666
|
+
u || (clearTimeout(m.current), l(!1));
|
|
2667
|
+
}, [u, l]);
|
|
2668
|
+
let v = () => {
|
|
2669
|
+
u && l(!0);
|
|
2670
|
+
}, y = (t) => {
|
|
2671
|
+
_.current = t, typeof e == "function" ? e(t) : e && (e.current = t);
|
|
2668
2672
|
}, b = () => {
|
|
2669
|
-
if (
|
|
2670
|
-
|
|
2673
|
+
if (s.passThroughPictureInPicture?.armed) return !0;
|
|
2674
|
+
if (h.current !== "mouse") return !1;
|
|
2675
|
+
let { x: e, y: t } = g.current;
|
|
2676
|
+
if (!_.current || e < 0) return !1;
|
|
2677
|
+
let n = document.elementFromPoint(e, t);
|
|
2678
|
+
return !!n && _.current.contains(n) && !n.closest(".video");
|
|
2679
|
+
}, x = () => {
|
|
2680
|
+
if (b()) {
|
|
2681
|
+
m.current = setTimeout(x, Zt);
|
|
2671
2682
|
return;
|
|
2672
2683
|
}
|
|
2673
|
-
|
|
2674
|
-
},
|
|
2675
|
-
|
|
2684
|
+
v();
|
|
2685
|
+
}, S = () => {
|
|
2686
|
+
l(!1), clearTimeout(m.current), m.current = setTimeout(x, Zt);
|
|
2676
2687
|
};
|
|
2677
2688
|
return /* @__PURE__ */ E("div", {
|
|
2678
2689
|
css: en,
|
|
2679
|
-
ref:
|
|
2690
|
+
ref: y,
|
|
2680
2691
|
onPointerMove: (e) => {
|
|
2681
|
-
|
|
2692
|
+
h.current = e.pointerType, e.pointerType === "mouse" && (g.current = {
|
|
2682
2693
|
x: e.clientX,
|
|
2683
2694
|
y: e.clientY
|
|
2684
|
-
},
|
|
2695
|
+
}, S());
|
|
2685
2696
|
},
|
|
2686
2697
|
onPointerDown: (e) => {
|
|
2687
|
-
|
|
2698
|
+
h.current = e.pointerType;
|
|
2688
2699
|
},
|
|
2689
2700
|
onMouseOut: (e) => {
|
|
2690
2701
|
let t = e.relatedTarget;
|
|
2691
2702
|
if (t && e.currentTarget.contains(t)) return;
|
|
2692
2703
|
let { left: n, right: r, top: i, bottom: a } = e.currentTarget.getBoundingClientRect();
|
|
2693
|
-
e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(
|
|
2704
|
+
e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(m.current), v());
|
|
2694
2705
|
},
|
|
2695
|
-
className:
|
|
2706
|
+
className: c ? "hide" : "",
|
|
2707
|
+
tabIndex: a === "player" ? -1 : void 0,
|
|
2696
2708
|
children: [
|
|
2697
2709
|
/* @__PURE__ */ T(ke, { onSubtitleRef: n }),
|
|
2698
2710
|
tn(r).map(({ key: e, item: t }) => /* @__PURE__ */ T("div", {
|
|
2699
2711
|
css: Qt,
|
|
2700
|
-
style: { ...
|
|
2712
|
+
style: { ...c ? {
|
|
2701
2713
|
opacity: "0",
|
|
2702
2714
|
visibility: "hidden",
|
|
2703
2715
|
pointerEvents: "none"
|
|
2704
2716
|
} : {} },
|
|
2705
2717
|
children: t
|
|
2706
2718
|
}, e)),
|
|
2707
|
-
i === !1 ? null : /* @__PURE__ */ T(Vt, {
|
|
2719
|
+
i === !1 ? null : /* @__PURE__ */ T(Vt, {
|
|
2720
|
+
shortcuts: a,
|
|
2721
|
+
root: _
|
|
2722
|
+
}),
|
|
2708
2723
|
/* @__PURE__ */ T(Wt, {}),
|
|
2709
2724
|
/* @__PURE__ */ T(Xt, {}),
|
|
2710
|
-
|
|
2725
|
+
f?.armed && _.current ? /* @__PURE__ */ T("div", {
|
|
2711
2726
|
className: "pass-through",
|
|
2712
|
-
style: { clipPath: $t(
|
|
2713
|
-
onPointerOver:
|
|
2727
|
+
style: { clipPath: $t(f.armed, _.current.getBoundingClientRect()) },
|
|
2728
|
+
onPointerOver: f.disarm
|
|
2714
2729
|
}) : null,
|
|
2715
2730
|
/* @__PURE__ */ E("div", {
|
|
2716
2731
|
className: "video",
|
|
2717
2732
|
onClick: () => {
|
|
2718
|
-
if (
|
|
2719
|
-
|
|
2733
|
+
if (h.current === "mouse") {
|
|
2734
|
+
s.togglePaused();
|
|
2720
2735
|
return;
|
|
2721
2736
|
}
|
|
2722
|
-
|
|
2737
|
+
c ? S() : (clearTimeout(m.current), v());
|
|
2723
2738
|
},
|
|
2724
2739
|
children: [t ? /* @__PURE__ */ T("video", {
|
|
2725
2740
|
ref: t,
|
|
2726
2741
|
playsInline: !0
|
|
2727
|
-
}) : null,
|
|
2742
|
+
}) : null, o]
|
|
2728
2743
|
})
|
|
2729
2744
|
]
|
|
2730
2745
|
});
|
|
@@ -2839,6 +2854,7 @@ ${gt}
|
|
|
2839
2854
|
onSubtitleRef: w,
|
|
2840
2855
|
overlay: e.overlay,
|
|
2841
2856
|
controls: e.controls,
|
|
2857
|
+
shortcuts: e.shortcuts,
|
|
2842
2858
|
children: t
|
|
2843
2859
|
});
|
|
2844
2860
|
}, an = g`
|
|
@@ -8,7 +8,8 @@ export type ChromeProps = {
|
|
|
8
8
|
overlay?: ReactNode;
|
|
9
9
|
/** False draws no control bar at all, leaving the picture, the title and the overlay. */
|
|
10
10
|
controls?: boolean;
|
|
11
|
+
shortcuts?: 'window' | 'player';
|
|
11
12
|
children?: ReactNode;
|
|
12
13
|
};
|
|
13
|
-
export declare const Chrome: ({ ref, onVideoRef, onSubtitleRef, overlay, controls, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare const Chrome: ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shortcuts, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
14
15
|
export default Chrome;
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
export declare const ControlBar: ({ shortcuts, root }: {
|
|
3
|
+
shortcuts?: 'window' | 'player';
|
|
4
|
+
root: RefObject<HTMLDivElement | null>;
|
|
5
|
+
}) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
2
6
|
export default ControlBar;
|
|
@@ -59,6 +59,13 @@ type CommonOptions = {
|
|
|
59
59
|
* store's `attach` and does nothing at all until a media arrives.
|
|
60
60
|
*/
|
|
61
61
|
controls?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Where the keyboard shortcuts listen. `'window'`, the default, takes keys from the whole page, which suits a
|
|
64
|
+
* page that is only the player. `'player'` takes a key only when the focus is inside the player (a click on the
|
|
65
|
+
* picture puts it there), or nothing has the focus and the last click or focus was in the player, or the player
|
|
66
|
+
* is fullscreen, so Space and the arrow keys keep scrolling the page and pressing its buttons everywhere else.
|
|
67
|
+
*/
|
|
68
|
+
shortcuts?: 'window' | 'player';
|
|
62
69
|
/**
|
|
63
70
|
* The app's own content over the video: a download readout, a badge, a logo, anything the player
|
|
64
71
|
* itself has no opinion about.
|
package/package.json
CHANGED
|
@@ -52,6 +52,9 @@ const style = css`
|
|
|
52
52
|
align-items: center;
|
|
53
53
|
height: 100%;
|
|
54
54
|
width: 100%;
|
|
55
|
+
/* focusable with shortcuts='player', and the first key after a click on the picture would otherwise
|
|
56
|
+
ring the whole player as :focus-visible */
|
|
57
|
+
outline: none;
|
|
55
58
|
|
|
56
59
|
& > div:not(:last-of-type) {
|
|
57
60
|
position: absolute;
|
|
@@ -129,10 +132,11 @@ export type ChromeProps = {
|
|
|
129
132
|
overlay?: ReactNode
|
|
130
133
|
/** False draws no control bar at all, leaving the picture, the title and the overlay. */
|
|
131
134
|
controls?: boolean
|
|
135
|
+
shortcuts?: 'window' | 'player'
|
|
132
136
|
children?: ReactNode
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, children }: ChromeProps) => {
|
|
139
|
+
export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shortcuts, children }: ChromeProps) => {
|
|
136
140
|
const player = usePlayer()
|
|
137
141
|
const hideUI = usePlayer((state) => state.hideUI)
|
|
138
142
|
const setHideUI = usePlayer((state) => state.setHideUI)
|
|
@@ -264,6 +268,8 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, chil
|
|
|
264
268
|
onPointerDown={onPointerDown}
|
|
265
269
|
onMouseOut={onMouseOut}
|
|
266
270
|
className={hideUI ? 'hide' : ''}
|
|
271
|
+
// focusable, so a click on the picture puts the focus, and with it the shortcuts, inside the player
|
|
272
|
+
tabIndex={shortcuts === 'player' ? -1 : undefined}
|
|
267
273
|
>
|
|
268
274
|
<Overlay onSubtitleRef={onSubtitleRef} />
|
|
269
275
|
{overlayItems(overlay).map(({ key, item }) => (
|
|
@@ -275,7 +281,7 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, chil
|
|
|
275
281
|
{item}
|
|
276
282
|
</div>
|
|
277
283
|
))}
|
|
278
|
-
{controls === false ? null : <ControlBar />}
|
|
284
|
+
{controls === false ? null : <ControlBar shortcuts={shortcuts} root={root} />}
|
|
279
285
|
{/* Not tied to `hideUI`: it says what to do next, and it is on screen for nine seconds. */}
|
|
280
286
|
<BurnInHint />
|
|
281
287
|
<SkipChapter />
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
+
import type { RefObject } from 'react'
|
|
3
|
+
|
|
2
4
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
5
|
import { css } from '@emotion/react'
|
|
4
6
|
import { Maximize, Minimize, Pause, Play, RotateCcw } from 'lucide-react'
|
|
@@ -157,7 +159,9 @@ const style = css`
|
|
|
157
159
|
}
|
|
158
160
|
`
|
|
159
161
|
|
|
160
|
-
export const ControlBar = (
|
|
162
|
+
export const ControlBar = (
|
|
163
|
+
{ shortcuts = 'window', root }: { shortcuts?: 'window' | 'player', root: RefObject<HTMLDivElement | null> },
|
|
164
|
+
) => {
|
|
161
165
|
const player = usePlayer()
|
|
162
166
|
const paused = usePlayer((state) => state.paused)
|
|
163
167
|
const currentTime = usePlayer((state) => state.currentTime)
|
|
@@ -198,12 +202,21 @@ export const ControlBar = () => {
|
|
|
198
202
|
if (wasMuted && !player.muted) player.toggleMuted()
|
|
199
203
|
}, [player])
|
|
200
204
|
|
|
205
|
+
// whether the last press or focus on the page landed in the player: a focused control that goes away (Skip
|
|
206
|
+
// Opening, once pressed) leaves the focus on the body, and the shortcuts stay with the player through that
|
|
207
|
+
const engaged = useRef(false)
|
|
208
|
+
|
|
201
209
|
useEffect(() => {
|
|
202
210
|
const seek = (time: number) => requestSeek ? requestSeek(time) : player.seek(time)
|
|
211
|
+
// the path, not ev.target, which a shadow root around the player retargets to its host
|
|
212
|
+
const targetOf = (ev: Event) => ev.composedPath()[0] as HTMLElement | undefined
|
|
213
|
+
const engage = (ev: Event) => { engaged.current = !!root.current?.contains(targetOf(ev) ?? null) }
|
|
203
214
|
const eventListener = (ev: KeyboardEvent) => {
|
|
204
|
-
// on the window, so typing into a consumer's own input
|
|
205
|
-
const target = ev
|
|
215
|
+
// on the window, so typing into a consumer's own input opts out
|
|
216
|
+
const target = targetOf(ev)
|
|
206
217
|
if (target?.isContentEditable || /^(input|textarea|select)$/i.test(target?.tagName ?? '')) return
|
|
218
|
+
const fromPlayer = root.current?.contains(target ?? null) || (target === document.body && engaged.current)
|
|
219
|
+
if (shortcuts === 'player' && !player.fullscreen && !fromPlayer) return
|
|
207
220
|
|
|
208
221
|
let shouldPreventDefault = true
|
|
209
222
|
// space would otherwise reach the browser's own shortcut
|
|
@@ -233,8 +246,14 @@ export const ControlBar = () => {
|
|
|
233
246
|
}
|
|
234
247
|
}
|
|
235
248
|
window.addEventListener('keydown', eventListener)
|
|
236
|
-
|
|
237
|
-
|
|
249
|
+
window.addEventListener('pointerdown', engage, true)
|
|
250
|
+
window.addEventListener('focusin', engage, true)
|
|
251
|
+
return () => {
|
|
252
|
+
window.removeEventListener('keydown', eventListener)
|
|
253
|
+
window.removeEventListener('pointerdown', engage, true)
|
|
254
|
+
window.removeEventListener('focusin', engage, true)
|
|
255
|
+
}
|
|
256
|
+
}, [player, modifyVolume, requestSeek, shortcuts, root])
|
|
238
257
|
|
|
239
258
|
const handleWheel = useCallback((e: WheelEvent) => {
|
|
240
259
|
e.preventDefault()
|
|
@@ -73,6 +73,14 @@ type CommonOptions = {
|
|
|
73
73
|
*/
|
|
74
74
|
controls?: boolean
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Where the keyboard shortcuts listen. `'window'`, the default, takes keys from the whole page, which suits a
|
|
78
|
+
* page that is only the player. `'player'` takes a key only when the focus is inside the player (a click on the
|
|
79
|
+
* picture puts it there), or nothing has the focus and the last click or focus was in the player, or the player
|
|
80
|
+
* is fullscreen, so Space and the arrow keys keep scrolling the page and pressing its buttons everywhere else.
|
|
81
|
+
*/
|
|
82
|
+
shortcuts?: 'window' | 'player'
|
|
83
|
+
|
|
76
84
|
/**
|
|
77
85
|
* The app's own content over the video: a download readout, a badge, a logo, anything the player
|
|
78
86
|
* itself has no opinion about.
|
|
@@ -411,6 +419,7 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
|
|
|
411
419
|
onSubtitleRef={setSubtitleLayer}
|
|
412
420
|
overlay={options.overlay}
|
|
413
421
|
controls={options.controls}
|
|
422
|
+
shortcuts={options.shortcuts}
|
|
414
423
|
>
|
|
415
424
|
{children}
|
|
416
425
|
</Chrome>
|