training_data/apps/web/src/components/presentation/video-player/VideoControls.tsx

277 lines
7.3 KiB
TypeScript
Raw Normal View History

2025-01-08 00:56:15 +08:00
import React, { useEffect, useRef, useContext, useState } from "react";
import Hls from "hls.js";
import { motion, AnimatePresence } from "framer-motion";
import {
PlayIcon,
PauseIcon,
SpeakerWaveIcon,
SpeakerXMarkIcon,
Cog6ToothIcon,
ArrowsPointingOutIcon,
ArrowsPointingInIcon,
} from "@heroicons/react/24/solid";
import "plyr/dist/plyr.css";
import { VideoPlayerContext } from "./VideoPlayer";
import { formatTime } from "./utlis";
import { PlaybackSpeed } from "./type";
export const Controls = () => {
const {
showControls,
setShowControls,
isSettingsOpen,
setIsSettingsOpen,
playbackSpeed,
setPlaybackSpeed,
videoRef,
isReady,
setIsReady,
isPlaying,
setIsPlaying,
bufferingState,
setBufferingState,
volume,
setVolume,
isMuted,
setIsMuted,
loadingProgress,
setLoadingProgress,
currentTime,
setCurrentTime,
duration,
setDuration,
brightness,
setBrightness,
isDragging,
setIsDragging,
isHovering,
setIsHovering,
progressRef,
} = useContext(VideoPlayerContext);
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (!videoRef.current || !progressRef.current) return;
const rect = progressRef.current.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
videoRef.current.currentTime = percent * videoRef.current.duration;
};
// 控制栏显示逻辑
useEffect(() => {
let timer: number;
if (!isHovering && !isDragging) {
timer = window.setTimeout(() => {
setShowControls(false);
}, 2000);
}
return () => {
if (timer) window.clearTimeout(timer);
};
}, [isHovering, isDragging]);
return (
<motion.div
layoutId="video-controls"
initial={false}
animate={{
opacity: showControls ? 1 : 0,
y: showControls ? 0 : 20,
}}
transition={{
duration: 0.2,
ease: "easeInOut",
}}
className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4">
{/* 进度条 */}
<div
ref={progressRef}
className="relative h-1 mb-4 cursor-pointer group"
onClick={handleProgressClick}
onMouseDown={(e) => {
setIsDragging(true);
handleProgressClick(e);
}}>
{/* 背景条 */}
<div className="absolute w-full h-full bg-black/80 rounded-full" />
{/* 播放进度 */}
<motion.div
className="absolute h-full bg-primary-500 rounded-full"
style={{
width: `${(currentTime / duration) * 100}%`,
}}
transition={{ type: "tween" }}
/>
{/* 进度球 */}
<motion.div
className={`absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-3 h-3 rounded-full bg-primary shadow-lg
${isHovering || isDragging ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`}
style={{
left: `${(currentTime / duration) * 100}%`,
}}
transition={{ duration: 0.1 }}
/>
{/* 预览进度 */}
<motion.div
className="absolute h-full bg-white/30 rounded-full opacity-0 group-hover:opacity-100 pointer-events-none"
style={{
width: `${(currentTime / duration) * 100}%`,
transform: "scaleY(2.5)",
transformOrigin: "center",
}}
transition={{ duration: 0.1 }}
/>
</div>
{/* 控制按钮区域 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{/* 播放/暂停按钮 */}
<button
onClick={() =>
videoRef.current?.paused
? videoRef.current.play()
: videoRef.current?.pause()
}
className="text-white hover:text-primary-400">
{isPlaying ? (
<PauseIcon className="w-6 h-6" />
) : (
<PlayIcon className="w-6 h-6" />
)}
</button>
{/* 音量控制 */}
<div className="group relative flex items-center">
<button
onClick={() => setIsMuted(!isMuted)}
className="text-white hover:text-primary-400">
{isMuted ? (
<SpeakerXMarkIcon className="w-6 h-6" />
) : (
<SpeakerWaveIcon className="w-6 h-6" />
)}
</button>
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div className="bg-black/80 rounded-lg p-2">
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={(e) => {
const newVolume = parseFloat(
e.target.value
);
setVolume(newVolume);
if (videoRef.current) {
videoRef.current.volume = newVolume;
}
}}
className="h-24 w-2 accent-primary-500 [-webkit-appearance:slider-vertical]"
/>
</div>
</div>
</div>
{/* 时间显示 */}
{duration && (
<span className="text-white text-sm">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
)}
</div>
{/* 右侧控制按钮 */}
<div className="flex items-center space-x-4">
{/* 设置按钮 */}
<div className="relative">
<button
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
className="text-white hover:text-primary-400">
<Cog6ToothIcon className="w-6 h-6" />
</button>
{/* 设置菜单 */}
<AnimatePresence>
{isSettingsOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute right-0 bottom-full mb-2 bg-black/90 rounded-lg p-4 min-w-[200px]">
{/* 倍速选择 */}
<div className="mb-4">
<h3 className="text-white text-sm mb-2">
</h3>
{[0.5, 0.75, 1.0, 1.25, 1.5, 2.0].map(
(speed) => (
<button
key={speed}
onClick={() => {
setPlaybackSpeed(
speed as PlaybackSpeed
);
if (videoRef.current) {
videoRef.current.playbackRate =
speed;
}
}}
className={`block w-full text-left px-2 py-1 text-sm ${
playbackSpeed === speed
? "text-primaryHover"
: "text-white"
}`}>
{speed}x
</button>
)
)}
</div>
{/* 亮度调节 */}
<div className="mb-4">
<h3 className="text-white text-sm mb-2">
</h3>
<input
type="range"
min="0.1"
max="2"
step="0.1"
value={brightness}
onChange={(e) =>
setBrightness(
parseFloat(e.target.value)
)
}
className="w-full accent-primary-500"
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* 全屏按钮 */}
<button
onClick={() => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
videoRef.current?.parentElement?.requestFullscreen();
}
}}
className="text-white hover:text-primary-400">
{document.fullscreenElement ? (
<ArrowsPointingInIcon className="w-6 h-6" />
) : (
<ArrowsPointingOutIcon className="w-6 h-6" />
)}
</button>
</div>
</div>
</motion.div>
);
};
export default Controls;