feat(player): the OS media controls, the basic keys and resume for the library and shared-link players
Plex, library and /watch tell the operating system what plays (title, line, picture) and take its
play / pause / skip / seek through one hook (lib/mediaSession); the Plex player drives it with its own
seek and clock, so the lock screen shows the film's position, not the stream's. The YouTube player is
left alone: its iframe already publishes all of it, and the OS routes to the frame that plays.
The library and /watch players get the basic keys (lib/mediaKeys, the keymap's mediaPlayer rows) and
open where the viewer stopped (lib/resume): the library keeps the point on the server, per user and
library item (library_states, POST /api/downloads/{id}/progress, listed back on both listings), the
shared link in this browser. A started library row continues, its menu can start it over, and its
picture carries the cards' progress bar. The resume rule (too early / near the end = from the start)
is one function shared with the YouTube route.
The library dialog opens focused on its panel (Modal initialFocus "player"): on the ✕, Space closed
the film. While the media element itself has the focus, Space and the arrows stay its own controls'
— the browser acts on them whatever a listener does, and one Space played and paused.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// The keys of a player built on the browser's own media element — the library's and a shared link's:
|
||||
// the keymap's `mediaPlayer` rows (play/pause, a 5 s step, fullscreen, mute), bound to the element.
|
||||
// Its own controls do the rest; Escape stays with the dialog around it.
|
||||
//
|
||||
// While the element ITSELF has the focus (a click on its controls puts it there), Space and the
|
||||
// arrows are its own: the browser's controls act on them whatever a listener does with the keydown,
|
||||
// so ours as well played and paused on one Space, and stepped twice on one arrow (measured in Chrome).
|
||||
import { useEffect, useEffectEvent } from "react";
|
||||
|
||||
import { dispatch, isActivationTarget, useHelpConditions, useKeyScope } from "./keymap";
|
||||
import { exitFullscreen, fullscreenElement, requestFullscreen } from "./fullscreen";
|
||||
|
||||
export const MEDIA_STEP_S = 5;
|
||||
|
||||
/** Bind the `mediaPlayer` keys to `media` while it is mounted. */
|
||||
export function useMediaKeys(media: HTMLMediaElement | null): void {
|
||||
useKeyScope("mediaPlayer", media != null);
|
||||
useHelpConditions({ video: media instanceof HTMLVideoElement });
|
||||
const onKey = useEffectEvent((e: KeyboardEvent) => {
|
||||
if (!media) return;
|
||||
const step = (ev: KeyboardEvent, by: number) => {
|
||||
if (ev.target === media) return;
|
||||
ev.preventDefault();
|
||||
const end = Number.isFinite(media.duration) ? media.duration : Infinity;
|
||||
media.currentTime = Math.min(end, Math.max(0, media.currentTime + by));
|
||||
};
|
||||
const ownKey = (ev: KeyboardEvent) => ev.target === media && ev.key !== "k";
|
||||
dispatch(e, "mediaPlayer", {
|
||||
playPause: (ev) => {
|
||||
// Space on a focused button presses it (the dialog's ✕, a link's Download), as in every
|
||||
// player here.
|
||||
if (isActivationTarget(ev) || ownKey(ev)) return;
|
||||
ev.preventDefault();
|
||||
if (media.paused) void media.play().catch(() => {});
|
||||
else media.pause();
|
||||
},
|
||||
seekBack: (ev) => step(ev, -MEDIA_STEP_S),
|
||||
seekFwd: (ev) => step(ev, MEDIA_STEP_S),
|
||||
fullscreen: (ev) => {
|
||||
// An audio file has no picture to fill the screen with.
|
||||
if (!(media instanceof HTMLVideoElement)) return;
|
||||
ev.preventDefault();
|
||||
if (fullscreenElement()) exitFullscreen();
|
||||
else requestFullscreen(media);
|
||||
},
|
||||
mute: (ev) => {
|
||||
ev.preventDefault();
|
||||
media.muted = !media.muted;
|
||||
},
|
||||
});
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!media) return;
|
||||
const handler = (e: KeyboardEvent) => onKey(e);
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [media]);
|
||||
}
|
||||
Reference in New Issue
Block a user