// 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]); }