"use client"; import { MouseEvent as ReactMouseEvent, useEffect, useRef, useState, } from "react"; import { Play, Pause, Volume2, VolumeX, Maximize2 } from "lucide-react"; import { cn } from "@/lib/cn"; interface PerformanceVideoProps { src?: string; poster?: string; className?: string; } function fmtTime(s: number): string { if (!isFinite(s) || s < 0) return "00:00"; const m = Math.floor(s / 60); const sec = Math.floor(s % 60); return `${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`; } export default function PerformanceVideo({ src, poster, className, }: PerformanceVideoProps) { const videoRef = useRef(null); const progressRef = useRef(null); const [playing, setPlaying] = useState(false); const [muted, setMuted] = useState(false); const [current, setCurrent] = useState(0); const [duration, setDuration] = useState(0); const [seeking, setSeeking] = useState(false); // 视频时间事件 + 首帧封面(loadedmetadata 后 seek 0.001s 让浏览器渲染首帧) useEffect(() => { const v = videoRef.current; if (!v) return; const onTime = () => !seeking && setCurrent(v.currentTime); const onMeta = () => { setDuration(v.duration); // 没有 poster 时强制渲染首帧 if (!poster && v.currentTime === 0) { try { v.currentTime = 0.001; } catch { /* noop */ } } }; const onEnd = () => setPlaying(false); v.addEventListener("timeupdate", onTime); v.addEventListener("loadedmetadata", onMeta); v.addEventListener("ended", onEnd); return () => { v.removeEventListener("timeupdate", onTime); v.removeEventListener("loadedmetadata", onMeta); v.removeEventListener("ended", onEnd); }; }, [seeking, poster]); const togglePlay = () => { const v = videoRef.current; if (!v) return; if (v.paused) { v.play(); setPlaying(true); } else { v.pause(); setPlaying(false); } }; const toggleMute = () => { const v = videoRef.current; if (!v) return; v.muted = !v.muted; setMuted(v.muted); }; const goFullscreen = () => { videoRef.current?.requestFullscreen?.(); }; // 进度条:点击 / 拖拽 seek const seekTo = (clientX: number) => { const bar = progressRef.current; const v = videoRef.current; if (!bar || !v || !duration) return; const rect = bar.getBoundingClientRect(); const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); const t = ratio * duration; v.currentTime = t; setCurrent(t); }; const handleBarMouseDown = (e: ReactMouseEvent) => { e.preventDefault(); e.stopPropagation(); setSeeking(true); seekTo(e.clientX); const onMove = (ev: MouseEvent) => seekTo(ev.clientX); const onUp = () => { setSeeking(false); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); }; const handleBarTouchStart = (e: React.TouchEvent) => { e.stopPropagation(); setSeeking(true); const t = e.touches[0]; if (t) seekTo(t.clientX); const onMove = (ev: TouchEvent) => { const tt = ev.touches[0]; if (tt) seekTo(tt.clientX); }; const onEnd = () => { setSeeking(false); window.removeEventListener("touchmove", onMove); window.removeEventListener("touchend", onEnd); }; window.addEventListener("touchmove", onMove, { passive: true }); window.addEventListener("touchend", onEnd); }; const progress = duration > 0 ? (current / duration) * 100 : 0; return (
{src ? (