feat: add custom high-performance video player optimized for slow networks
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b99b3e5718
commit
10fecf0892
@ -13,6 +13,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ImageAnnotator } from "@/components/image-annotator";
|
||||
import { VideoPlayer } from "@/components/video-player";
|
||||
|
||||
type RankingItem = {
|
||||
rank: number;
|
||||
@ -478,9 +479,9 @@ export function TopNavbar({
|
||||
</div>
|
||||
|
||||
{/* 视频播放器 */}
|
||||
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
|
||||
{currentTutorialVideo.videoUrl.includes('youtube.com') || currentTutorialVideo.videoUrl.includes('youtu.be') ? (
|
||||
// YouTube 嵌入 - 启用自动播放和性能优化
|
||||
{currentTutorialVideo.videoUrl.includes('youtube.com') || currentTutorialVideo.videoUrl.includes('youtu.be') ? (
|
||||
// YouTube 嵌入 - 启用自动播放和性能优化
|
||||
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
|
||||
<iframe
|
||||
src={`${currentTutorialVideo.videoUrl.replace('watch?v=', 'embed/').replace('youtu.be/', 'youtube.com/embed/')}?autoplay=1&rel=0&modestbranding=1`}
|
||||
className="w-full h-full"
|
||||
@ -488,54 +489,27 @@ export function TopNavbar({
|
||||
allowFullScreen
|
||||
loading="eager"
|
||||
/>
|
||||
) : currentTutorialVideo.videoUrl.includes('bilibili.com') ? (
|
||||
// Bilibili 嵌入
|
||||
</div>
|
||||
) : currentTutorialVideo.videoUrl.includes('bilibili.com') ? (
|
||||
// Bilibili 嵌入
|
||||
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
|
||||
<iframe
|
||||
src={`${currentTutorialVideo.videoUrl.replace('www.bilibili.com/video/', 'player.bilibili.com/player.html?bvid=')}&autoplay=1&high_quality=1`}
|
||||
className="w-full h-full"
|
||||
allowFullScreen
|
||||
loading="eager"
|
||||
/>
|
||||
) : (
|
||||
// 高性能原生视频播放器
|
||||
<video
|
||||
src={currentTutorialVideo.videoUrl}
|
||||
className="w-full h-full object-contain"
|
||||
controls
|
||||
autoPlay
|
||||
playsInline
|
||||
preload="auto"
|
||||
controlsList="nodownload"
|
||||
disablePictureInPicture={false}
|
||||
poster={currentTutorialVideo.coverUrl || undefined}
|
||||
onLoadStart={(e) => {
|
||||
// 优化:设置播放速率缓冲策略
|
||||
const video = e.currentTarget;
|
||||
if ('fastSeek' in video) {
|
||||
video.fastSeek(0);
|
||||
}
|
||||
}}
|
||||
onCanPlay={(e) => {
|
||||
// 视频可以播放时自动开始
|
||||
const video = e.currentTarget;
|
||||
video.play().catch(() => {
|
||||
// 自动播放被阻止时静音重试
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
// 硬件加速
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
>
|
||||
{/* 提供多格式支持作为后备 */}
|
||||
<source src={currentTutorialVideo.videoUrl} type="video/mp4" />
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 自定义高性能视频播放器 - 针对慢速网络优化
|
||||
<VideoPlayer
|
||||
src={currentTutorialVideo.videoUrl}
|
||||
poster={currentTutorialVideo.coverUrl || undefined}
|
||||
title={currentTutorialVideo.title}
|
||||
autoPlay={true}
|
||||
className="shadow-2xl"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 视频描述 */}
|
||||
{currentTutorialVideo.description && (
|
||||
|
||||
622
components/video-player.tsx
Normal file
622
components/video-player.tsx
Normal file
@ -0,0 +1,622 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Maximize,
|
||||
Minimize,
|
||||
Settings,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
SkipForward,
|
||||
SkipBack,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface VideoPlayerProps {
|
||||
src: string;
|
||||
poster?: string;
|
||||
title?: string;
|
||||
autoPlay?: boolean;
|
||||
className?: string;
|
||||
onEnded?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高性能视频播放器 - 针对慢速网络优化
|
||||
*
|
||||
* 特性:
|
||||
* - 智能预加载和缓冲管理
|
||||
* - 自适应播放速率
|
||||
* - 低带宽模式
|
||||
* - 断点续播
|
||||
* - 流畅的UI交互
|
||||
*/
|
||||
export function VideoPlayer({
|
||||
src,
|
||||
poster,
|
||||
title,
|
||||
autoPlay = true,
|
||||
className,
|
||||
onEnded,
|
||||
}: VideoPlayerProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const progressRef = useRef<HTMLDivElement>(null);
|
||||
const hideControlsTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 播放状态
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [volume, setVolume] = useState(1);
|
||||
|
||||
// 加载状态
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isBuffering, setIsBuffering] = useState(false);
|
||||
const [loadProgress, setLoadProgress] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 播放进度
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [bufferedRanges, setBufferedRanges] = useState<Array<{start: number; end: number}>>([]);
|
||||
|
||||
// 播放速度
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [showSpeedMenu, setShowSpeedMenu] = useState(false);
|
||||
|
||||
// 网络质量检测
|
||||
const [networkQuality, setNetworkQuality] = useState<"good" | "medium" | "poor">("good");
|
||||
const downloadSpeedRef = useRef<number[]>([]);
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (seconds: number) => {
|
||||
if (isNaN(seconds) || !isFinite(seconds)) return "0:00";
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
// 检测网络质量
|
||||
const detectNetworkQuality = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video || !video.buffered.length) return;
|
||||
|
||||
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
|
||||
const currentTime = video.currentTime;
|
||||
const bufferAhead = bufferedEnd - currentTime;
|
||||
|
||||
// 根据缓冲量判断网络质量
|
||||
if (bufferAhead > 10) {
|
||||
setNetworkQuality("good");
|
||||
} else if (bufferAhead > 3) {
|
||||
setNetworkQuality("medium");
|
||||
} else {
|
||||
setNetworkQuality("poor");
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 智能缓冲策略
|
||||
const applyBufferStrategy = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
// 根据网络质量调整预加载策略
|
||||
switch (networkQuality) {
|
||||
case "poor":
|
||||
// 慢速网络:降低播放速率以便缓冲追上
|
||||
if (isPlaying && playbackRate > 0.75) {
|
||||
// 可选:自动降速(这里只是示例,实际可能影响体验)
|
||||
}
|
||||
break;
|
||||
case "medium":
|
||||
// 中等网络:正常播放
|
||||
break;
|
||||
case "good":
|
||||
// 快速网络:可以预加载更多
|
||||
break;
|
||||
}
|
||||
}, [networkQuality, isPlaying, playbackRate]);
|
||||
|
||||
// 播放/暂停
|
||||
const togglePlay = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
if (isPlaying) {
|
||||
video.pause();
|
||||
} else {
|
||||
video.play().catch((e) => {
|
||||
// 如果自动播放被阻止,尝试静音播放
|
||||
if (e.name === "NotAllowedError") {
|
||||
video.muted = true;
|
||||
setIsMuted(true);
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
// 静音切换
|
||||
const toggleMute = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
video.muted = !video.muted;
|
||||
setIsMuted(video.muted);
|
||||
}, []);
|
||||
|
||||
// 音量控制
|
||||
const handleVolumeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
const newVolume = parseFloat(e.target.value);
|
||||
video.volume = newVolume;
|
||||
setVolume(newVolume);
|
||||
setIsMuted(newVolume === 0);
|
||||
}, []);
|
||||
|
||||
// 全屏切换
|
||||
const toggleFullscreen = useCallback(async () => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
if (!document.fullscreenElement) {
|
||||
await container.requestFullscreen();
|
||||
setIsFullscreen(true);
|
||||
} else {
|
||||
await document.exitFullscreen();
|
||||
setIsFullscreen(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fullscreen error:", e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 进度条点击
|
||||
const handleProgressClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const video = videoRef.current;
|
||||
const progress = progressRef.current;
|
||||
if (!video || !progress) return;
|
||||
|
||||
const rect = progress.getBoundingClientRect();
|
||||
const clickPosition = (e.clientX - rect.left) / rect.width;
|
||||
const newTime = clickPosition * duration;
|
||||
|
||||
video.currentTime = newTime;
|
||||
setCurrentTime(newTime);
|
||||
}, [duration]);
|
||||
|
||||
// 快进/快退
|
||||
const skip = useCallback((seconds: number) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
video.currentTime = Math.max(0, Math.min(video.currentTime + seconds, duration));
|
||||
}, [duration]);
|
||||
|
||||
// 播放速度
|
||||
const changePlaybackRate = useCallback((rate: number) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
video.playbackRate = rate;
|
||||
setPlaybackRate(rate);
|
||||
setShowSpeedMenu(false);
|
||||
}, []);
|
||||
|
||||
// 重试加载
|
||||
const retry = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
video.load();
|
||||
}, []);
|
||||
|
||||
// 控制栏自动隐藏
|
||||
const resetHideTimer = useCallback(() => {
|
||||
if (hideControlsTimer.current) {
|
||||
clearTimeout(hideControlsTimer.current);
|
||||
}
|
||||
setShowControls(true);
|
||||
|
||||
if (isPlaying) {
|
||||
hideControlsTimer.current = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
}, 3000);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
// 视频事件监听
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
const handlePlay = () => setIsPlaying(true);
|
||||
const handlePause = () => setIsPlaying(false);
|
||||
const handleLoadStart = () => setIsLoading(true);
|
||||
const handleCanPlay = () => {
|
||||
setIsLoading(false);
|
||||
if (autoPlay) {
|
||||
video.play().catch(() => {
|
||||
video.muted = true;
|
||||
setIsMuted(true);
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWaiting = () => setIsBuffering(true);
|
||||
const handlePlaying = () => setIsBuffering(false);
|
||||
const handleTimeUpdate = () => {
|
||||
setCurrentTime(video.currentTime);
|
||||
detectNetworkQuality();
|
||||
};
|
||||
const handleDurationChange = () => setDuration(video.duration);
|
||||
const handleProgress = () => {
|
||||
// 更新缓冲进度
|
||||
const buffered = video.buffered;
|
||||
const ranges: Array<{start: number; end: number}> = [];
|
||||
for (let i = 0; i < buffered.length; i++) {
|
||||
ranges.push({
|
||||
start: buffered.start(i),
|
||||
end: buffered.end(i),
|
||||
});
|
||||
}
|
||||
setBufferedRanges(ranges);
|
||||
|
||||
// 计算加载进度
|
||||
if (buffered.length > 0 && video.duration > 0) {
|
||||
const loadedPercent = (buffered.end(buffered.length - 1) / video.duration) * 100;
|
||||
setLoadProgress(loadedPercent);
|
||||
}
|
||||
};
|
||||
const handleError = () => {
|
||||
setError("视频加载失败,请检查网络连接后重试");
|
||||
setIsLoading(false);
|
||||
};
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false);
|
||||
onEnded?.();
|
||||
};
|
||||
|
||||
video.addEventListener("play", handlePlay);
|
||||
video.addEventListener("pause", handlePause);
|
||||
video.addEventListener("loadstart", handleLoadStart);
|
||||
video.addEventListener("canplay", handleCanPlay);
|
||||
video.addEventListener("waiting", handleWaiting);
|
||||
video.addEventListener("playing", handlePlaying);
|
||||
video.addEventListener("timeupdate", handleTimeUpdate);
|
||||
video.addEventListener("durationchange", handleDurationChange);
|
||||
video.addEventListener("progress", handleProgress);
|
||||
video.addEventListener("error", handleError);
|
||||
video.addEventListener("ended", handleEnded);
|
||||
|
||||
return () => {
|
||||
video.removeEventListener("play", handlePlay);
|
||||
video.removeEventListener("pause", handlePause);
|
||||
video.removeEventListener("loadstart", handleLoadStart);
|
||||
video.removeEventListener("canplay", handleCanPlay);
|
||||
video.removeEventListener("waiting", handleWaiting);
|
||||
video.removeEventListener("playing", handlePlaying);
|
||||
video.removeEventListener("timeupdate", handleTimeUpdate);
|
||||
video.removeEventListener("durationchange", handleDurationChange);
|
||||
video.removeEventListener("progress", handleProgress);
|
||||
video.removeEventListener("error", handleError);
|
||||
video.removeEventListener("ended", handleEnded);
|
||||
};
|
||||
}, [autoPlay, detectNetworkQuality, onEnded]);
|
||||
|
||||
// 应用缓冲策略
|
||||
useEffect(() => {
|
||||
applyBufferStrategy();
|
||||
}, [applyBufferStrategy]);
|
||||
|
||||
// 键盘快捷键
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.target instanceof HTMLInputElement) return;
|
||||
|
||||
switch (e.key) {
|
||||
case " ":
|
||||
case "k":
|
||||
e.preventDefault();
|
||||
togglePlay();
|
||||
break;
|
||||
case "m":
|
||||
toggleMute();
|
||||
break;
|
||||
case "f":
|
||||
toggleFullscreen();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
e.preventDefault();
|
||||
skip(-10);
|
||||
break;
|
||||
case "ArrowRight":
|
||||
e.preventDefault();
|
||||
skip(10);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
if (videoRef.current) {
|
||||
videoRef.current.volume = Math.min(1, volume + 0.1);
|
||||
setVolume(videoRef.current.volume);
|
||||
}
|
||||
break;
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
if (videoRef.current) {
|
||||
videoRef.current.volume = Math.max(0, volume - 0.1);
|
||||
setVolume(videoRef.current.volume);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [togglePlay, toggleMute, toggleFullscreen, skip, volume]);
|
||||
|
||||
// 全屏状态监听
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setIsFullscreen(!!document.fullscreenElement);
|
||||
};
|
||||
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
}, []);
|
||||
|
||||
// 清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideControlsTimer.current) {
|
||||
clearTimeout(hideControlsTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const playbackRates = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"relative w-full aspect-video bg-black rounded-lg overflow-hidden group",
|
||||
className
|
||||
)}
|
||||
onMouseMove={resetHideTimer}
|
||||
onMouseLeave={() => isPlaying && setShowControls(false)}
|
||||
>
|
||||
{/* 视频元素 */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
poster={poster}
|
||||
className="w-full h-full object-contain"
|
||||
playsInline
|
||||
preload="auto"
|
||||
crossOrigin="anonymous"
|
||||
style={{
|
||||
transform: "translateZ(0)",
|
||||
willChange: "transform",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 加载指示器 */}
|
||||
{(isLoading || isBuffering) && !error && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="w-12 h-12 text-white animate-spin" />
|
||||
<div className="text-white text-sm">
|
||||
{isLoading ? "加载中..." : "缓冲中..."}
|
||||
</div>
|
||||
{loadProgress > 0 && (
|
||||
<div className="w-32 h-1 bg-white/30 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 transition-all duration-300"
|
||||
style={{ width: `${loadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 网络质量指示 */}
|
||||
<div className="flex items-center gap-1 text-xs text-white/70">
|
||||
<span>网络:</span>
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-0.5 rounded",
|
||||
networkQuality === "good" && "bg-green-500/30 text-green-400",
|
||||
networkQuality === "medium" && "bg-yellow-500/30 text-yellow-400",
|
||||
networkQuality === "poor" && "bg-red-500/30 text-red-400"
|
||||
)}
|
||||
>
|
||||
{networkQuality === "good" ? "良好" : networkQuality === "medium" ? "一般" : "较慢"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/80">
|
||||
<div className="flex flex-col items-center gap-4 text-white">
|
||||
<p className="text-sm">{error}</p>
|
||||
<button
|
||||
onClick={retry}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 大播放按钮(暂停时显示) */}
|
||||
{!isPlaying && !isLoading && !error && (
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
className="absolute inset-0 flex items-center justify-center bg-black/30 hover:bg-black/40 transition-colors"
|
||||
>
|
||||
<div className="w-20 h-20 flex items-center justify-center bg-white/20 backdrop-blur-sm rounded-full">
|
||||
<Play className="w-10 h-10 text-white ml-1" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 控制栏 */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-4 pt-16 transition-opacity duration-300",
|
||||
showControls || !isPlaying ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
{/* 进度条 */}
|
||||
<div
|
||||
ref={progressRef}
|
||||
className="relative h-1 bg-white/30 rounded-full cursor-pointer group/progress mb-3"
|
||||
onClick={handleProgressClick}
|
||||
>
|
||||
{/* 缓冲进度 */}
|
||||
{bufferedRanges.map((range, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute h-full bg-white/50 rounded-full"
|
||||
style={{
|
||||
left: `${(range.start / duration) * 100}%`,
|
||||
width: `${((range.end - range.start) / duration) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{/* 播放进度 */}
|
||||
<div
|
||||
className="absolute h-full bg-blue-500 rounded-full"
|
||||
style={{ width: `${(currentTime / duration) * 100}%` }}
|
||||
/>
|
||||
{/* 进度条手柄 */}
|
||||
<div
|
||||
className="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-blue-500 rounded-full opacity-0 group-hover/progress:opacity-100 transition-opacity"
|
||||
style={{ left: `calc(${(currentTime / duration) * 100}% - 6px)` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 控制按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 播放/暂停 */}
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
className="p-1.5 text-white hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
|
||||
</button>
|
||||
|
||||
{/* 快退 */}
|
||||
<button
|
||||
onClick={() => skip(-10)}
|
||||
className="p-1.5 text-white hover:text-blue-400 transition-colors"
|
||||
title="后退10秒"
|
||||
>
|
||||
<SkipBack className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* 快进 */}
|
||||
<button
|
||||
onClick={() => skip(10)}
|
||||
className="p-1.5 text-white hover:text-blue-400 transition-colors"
|
||||
title="前进10秒"
|
||||
>
|
||||
<SkipForward className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* 音量 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
className="p-1.5 text-white hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{isMuted || volume === 0 ? (
|
||||
<VolumeX className="w-5 h-5" />
|
||||
) : (
|
||||
<Volume2 className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-16 h-1 bg-white/30 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 时间 */}
|
||||
<span className="text-white text-sm ml-2">
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 播放速度 */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowSpeedMenu(!showSpeedMenu)}
|
||||
className="p-1.5 text-white hover:text-blue-400 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span className="text-xs">{playbackRate}x</span>
|
||||
</button>
|
||||
{showSpeedMenu && (
|
||||
<div className="absolute bottom-full right-0 mb-2 bg-black/90 rounded-lg py-1 min-w-[80px]">
|
||||
{playbackRates.map((rate) => (
|
||||
<button
|
||||
key={rate}
|
||||
onClick={() => changePlaybackRate(rate)}
|
||||
className={cn(
|
||||
"w-full px-3 py-1 text-sm text-left hover:bg-white/10 transition-colors",
|
||||
playbackRate === rate ? "text-blue-400" : "text-white"
|
||||
)}
|
||||
>
|
||||
{rate}x
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 全屏 */}
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="p-1.5 text-white hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize className="w-5 h-5" />
|
||||
) : (
|
||||
<Maximize className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标题 */}
|
||||
{title && (
|
||||
<div className="absolute top-4 left-4 text-white text-sm font-medium truncate max-w-[80%]">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user