import { useEffect, useRef, useState } from "react"; function fmt(s: number): string { if (!isFinite(s)) return "0:00"; const m = Math.floor(s / 60); const sec = Math.floor(s % 60); return `${m}:${sec.toString().padStart(2, "0")}`; } const IconPlay = () => ( ); const IconPause = () => ( ); const IconVolume = ({ muted }: { muted: boolean }) => ( {muted ? : } ); const IconFullscreen = () => ( ); const HIDE_DELAY = 2500; interface MediaPlayerProps { src: string; kind: "audio" | "video"; mime?: string; } export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) { const mediaRef = useRef(null); const [playing, setPlaying] = useState(false); const [current, setCurrent] = useState(0); const [duration, setDuration] = useState(0); const [dragging, setDragging] = useState(false); const [volume, setVolume] = useState(1); const [muted, setMuted] = useState(false); const [controlsVisible, setControlsVisible] = useState(true); const hideTimer = useRef | null>(null); useEffect(() => { const a = mediaRef.current!; const onTime = () => { if (!dragging) setCurrent(a.currentTime); }; const onDuration = () => setDuration(a.duration); const onEnded = () => setPlaying(false); a.addEventListener("timeupdate", onTime); a.addEventListener("durationchange", onDuration); a.addEventListener("ended", onEnded); return () => { a.removeEventListener("timeupdate", onTime); a.removeEventListener("durationchange", onDuration); a.removeEventListener("ended", onEnded); }; }, [dragging]); // Show controls when paused; schedule hide when playing useEffect(() => { if (kind !== "video") return; if (hideTimer.current) clearTimeout(hideTimer.current); if (playing) { hideTimer.current = setTimeout(() => setControlsVisible(false), HIDE_DELAY); } else { setControlsVisible(true); } return () => { if (hideTimer.current) clearTimeout(hideTimer.current); }; }, [playing, kind]); const showControlsTemporarily = () => { if (kind !== "video") return; setControlsVisible(true); if (hideTimer.current) clearTimeout(hideTimer.current); if (playing) { hideTimer.current = setTimeout(() => setControlsVisible(false), HIDE_DELAY); } }; const toggle = () => { const a = mediaRef.current!; if (playing) { a.pause(); setPlaying(false); } else { a.play(); setPlaying(true); } }; const seek = (e: React.ChangeEvent) => { const v = Number(e.target.value); setCurrent(v); mediaRef.current!.currentTime = v; }; const changeVolume = (e: React.ChangeEvent) => { const v = Number(e.target.value); setVolume(v); mediaRef.current!.volume = v; if (v > 0 && muted) { setMuted(false); mediaRef.current!.muted = false; } }; const toggleMute = () => { const next = !muted; setMuted(next); mediaRef.current!.muted = next; }; const goFullscreen = () => { (mediaRef.current as HTMLVideoElement).requestFullscreen?.(); }; const progress = duration > 0 ? current / duration : 0; const controls = ( <> {playing ? : } {fmt(current)} setDragging(true)} onMouseUp={() => setDragging(false)} onChange={seek} aria-label="Seek" /> {fmt(duration)} {kind === "video" && ( )} > ); if (kind === "video") { return ( playing && setControlsVisible(false)} > {/* eslint-disable-next-line jsx-a11y/media-has-caption */} } src={src} preload="metadata" className="video-player-video" onClick={toggle} > {mime && } {controls} ); } return ( } src={src} preload="metadata" /> {controls} ); }