p-stream/src/components/player/internals/KeyboardEvents.tsx
mrapple 02bace8a47 We have short attention spans
now instead of watching your entire movie at one speed, you can do it in VARYING SPEEDS!
2025-05-10 22:19:12 +01:00

322 lines
10 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { useCaptions } from "@/components/player/hooks/useCaptions";
import { useVolume } from "@/components/player/hooks/useVolume";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { useOverlayStack } from "@/stores/interface/overlayStack";
import { usePlayerStore } from "@/stores/player/store";
import { useSubtitleStore } from "@/stores/subtitles";
import { useEmpheralVolumeStore } from "@/stores/volume";
export function KeyboardEvents() {
const router = useOverlayRouter("");
const display = usePlayerStore((s) => s.display);
const mediaProgress = usePlayerStore((s) => s.progress);
const { isSeeking } = usePlayerStore((s) => s.interface);
const mediaPlaying = usePlayerStore((s) => s.mediaPlaying);
const time = usePlayerStore((s) => s.progress.time);
const { setVolume, toggleMute } = useVolume();
const setSpeedBoosted = usePlayerStore((s) => s.setSpeedBoosted);
const setShowSpeedIndicator = usePlayerStore((s) => s.setShowSpeedIndicator);
const { toggleLastUsed } = useCaptions();
const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume);
const setDelay = useSubtitleStore((s) => s.setDelay);
const delay = useSubtitleStore((s) => s.delay);
const setShowDelayIndicator = useSubtitleStore(
(s) => s.setShowDelayIndicator,
);
const [isRolling, setIsRolling] = useState(false);
const volumeDebounce = useRef<ReturnType<typeof setTimeout> | undefined>();
const subtitleDebounce = useRef<ReturnType<typeof setTimeout> | undefined>();
const speedIndicatorTimeoutRef = useRef<
ReturnType<typeof setTimeout> | undefined
>();
const boostTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>();
const isPendingBoostRef = useRef<boolean>(false);
// Tracking previous playback rate when space is held down
const previousRateRef = useRef<number>(1);
const isSpaceHeldRef = useRef<boolean>(false);
const setCurrentOverlay = useOverlayStack((s) => s.setCurrentOverlay);
const dataRef = useRef({
setShowVolume,
setVolume,
toggleMute,
setIsRolling,
toggleLastUsed,
display,
mediaPlaying,
mediaProgress,
isSeeking,
isRolling,
time,
router,
setDelay,
delay,
setShowDelayIndicator,
setCurrentOverlay,
previousRateRef,
isSpaceHeldRef,
setSpeedBoosted,
setShowSpeedIndicator,
speedIndicatorTimeoutRef,
boostTimeoutRef,
isPendingBoostRef,
});
useEffect(() => {
dataRef.current = {
setShowVolume,
setVolume,
toggleMute,
setIsRolling,
toggleLastUsed,
display,
mediaPlaying,
mediaProgress,
isSeeking,
isRolling,
time,
router,
setDelay,
delay,
setShowDelayIndicator,
setCurrentOverlay,
previousRateRef,
isSpaceHeldRef,
setSpeedBoosted,
setShowSpeedIndicator,
speedIndicatorTimeoutRef,
boostTimeoutRef,
isPendingBoostRef,
};
}, [
setShowVolume,
setVolume,
toggleMute,
setIsRolling,
toggleLastUsed,
display,
mediaPlaying,
mediaProgress,
isSeeking,
isRolling,
time,
router,
setDelay,
delay,
setShowDelayIndicator,
setCurrentOverlay,
setSpeedBoosted,
setShowSpeedIndicator,
]);
useEffect(() => {
const keydownEventHandler = (evt: KeyboardEvent) => {
if (evt.target && (evt.target as HTMLInputElement).nodeName === "INPUT")
return;
const k = evt.key;
const keyL = evt.key.toLowerCase();
// Volume
if (["ArrowUp", "ArrowDown", "m", "M"].includes(k)) {
dataRef.current.setShowVolume(true);
dataRef.current.setCurrentOverlay("volume");
if (volumeDebounce.current) clearTimeout(volumeDebounce.current);
volumeDebounce.current = setTimeout(() => {
dataRef.current.setShowVolume(false);
dataRef.current.setCurrentOverlay(null);
}, 3e3);
}
if (k === "ArrowUp")
dataRef.current.setVolume(
(dataRef.current.mediaPlaying?.volume || 0) + 0.15,
);
if (k === "ArrowDown")
dataRef.current.setVolume(
(dataRef.current.mediaPlaying?.volume || 0) - 0.15,
);
if (keyL === "m") dataRef.current.toggleMute();
// Video playback speed
if (k === ">" || k === "<") {
const options = [0.25, 0.5, 1, 1.5, 2];
let idx = options.indexOf(dataRef.current.mediaPlaying?.playbackRate);
if (idx === -1) idx = options.indexOf(1);
const nextIdx = idx + (k === ">" ? 1 : -1);
const next = options[nextIdx];
if (next) dataRef.current.display?.setPlaybackRate(next);
}
// Handle spacebar hold for 2x speed
if (
k === " " &&
!dataRef.current.isSpaceHeldRef.current &&
!dataRef.current.mediaPlaying.isPaused &&
!dataRef.current.isPendingBoostRef.current
) {
// Skip if a button is targeted
if (
evt.target &&
(evt.target as HTMLInputElement).nodeName === "BUTTON"
) {
return;
}
// Prevent the default spacebar behavior (play/pause)
evt.preventDefault();
// Save current rate
dataRef.current.previousRateRef.current =
dataRef.current.mediaPlaying.playbackRate;
// Set pending boost flag
dataRef.current.isPendingBoostRef.current = true;
// Add delay before boosting speed
if (dataRef.current.boostTimeoutRef.current) {
clearTimeout(dataRef.current.boostTimeoutRef.current);
}
dataRef.current.boostTimeoutRef.current = setTimeout(() => {
// Only apply boost if the key is still held down
if (dataRef.current.isPendingBoostRef.current) {
dataRef.current.isSpaceHeldRef.current = true;
dataRef.current.isPendingBoostRef.current = false;
// Show speed indicator
dataRef.current.setSpeedBoosted(true);
dataRef.current.setShowSpeedIndicator(true);
dataRef.current.setCurrentOverlay("speed");
// Clear any existing timeout
if (dataRef.current.speedIndicatorTimeoutRef.current) {
clearTimeout(dataRef.current.speedIndicatorTimeoutRef.current);
}
dataRef.current.display?.setPlaybackRate(2);
}
}, 300); // 300ms delay before boost takes effect
}
// Video progress
if (k === "ArrowRight")
dataRef.current.display?.setTime(dataRef.current.time + 5);
if (k === "ArrowLeft")
dataRef.current.display?.setTime(dataRef.current.time - 5);
if (keyL === "j")
dataRef.current.display?.setTime(dataRef.current.time - 10);
if (keyL === "l")
dataRef.current.display?.setTime(dataRef.current.time + 10);
if (k === "." && dataRef.current.mediaPlaying?.isPaused)
dataRef.current.display?.setTime(dataRef.current.time + 1);
if (k === "," && dataRef.current.mediaPlaying?.isPaused)
dataRef.current.display?.setTime(dataRef.current.time - 1);
// Utils
if (keyL === "f") dataRef.current.display?.toggleFullscreen();
if (
(k === " " || keyL === "k") &&
!dataRef.current.isSpaceHeldRef.current
) {
if (
evt.target &&
(evt.target as HTMLInputElement).nodeName === "BUTTON"
) {
return;
}
const action = dataRef.current.mediaPlaying.isPaused ? "play" : "pause";
dataRef.current.display?.[action]();
}
if (k === "Escape") dataRef.current.router.close();
// captions
if (keyL === "c") dataRef.current.toggleLastUsed().catch(() => {}); // ignore errors
// Do a barrell roll!
if (keyL === "r") {
if (dataRef.current.isRolling || evt.ctrlKey || evt.metaKey) return;
dataRef.current.setIsRolling(true);
document.querySelector(".popout-location")?.classList.add("roll");
document.body.setAttribute("data-no-scroll", "true");
setTimeout(() => {
document.querySelector(".popout-location")?.classList.remove("roll");
document.body.removeAttribute("data-no-scroll");
dataRef.current.setIsRolling(false);
}, 1e3);
}
// Subtitle sync
if (k === "[" || k === "]") {
const change = k === "[" ? -0.5 : 0.5;
dataRef.current.setDelay(dataRef.current.delay + change);
dataRef.current.setShowDelayIndicator(true);
dataRef.current.setCurrentOverlay("subtitle");
if (subtitleDebounce.current) clearTimeout(subtitleDebounce.current);
subtitleDebounce.current = setTimeout(() => {
dataRef.current.setShowDelayIndicator(false);
dataRef.current.setCurrentOverlay(null);
}, 3000);
}
};
const keyupEventHandler = (evt: KeyboardEvent) => {
const k = evt.key;
// Handle spacebar release
if (k === " ") {
// If we haven't applied the boost yet but were about to, cancel it
if (dataRef.current.isPendingBoostRef.current) {
dataRef.current.isPendingBoostRef.current = false;
if (dataRef.current.boostTimeoutRef.current) {
clearTimeout(dataRef.current.boostTimeoutRef.current);
}
// The space key was released quickly, so trigger play/pause
const action = dataRef.current.mediaPlaying.isPaused
? "play"
: "pause";
dataRef.current.display?.[action]();
} else if (dataRef.current.isSpaceHeldRef.current) {
// We were in boost mode, restore previous rate
dataRef.current.display?.setPlaybackRate(
dataRef.current.previousRateRef.current,
);
dataRef.current.isSpaceHeldRef.current = false;
// Update UI state
dataRef.current.setSpeedBoosted(false);
// Set a timeout to hide the speed indicator
if (dataRef.current.speedIndicatorTimeoutRef.current) {
clearTimeout(dataRef.current.speedIndicatorTimeoutRef.current);
}
dataRef.current.speedIndicatorTimeoutRef.current = setTimeout(() => {
dataRef.current.setShowSpeedIndicator(false);
dataRef.current.setCurrentOverlay(null);
}, 1500);
}
}
};
window.addEventListener("keydown", keydownEventHandler);
window.addEventListener("keyup", keyupEventHandler);
return () => {
window.removeEventListener("keydown", keydownEventHandler);
window.removeEventListener("keyup", keyupEventHandler);
};
}, []);
return null;
}