Files
mxivideo/src/components/VideoPlayer.tsx
2025-07-11 11:18:14 +08:00

246 lines
7.3 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react'
import { Play, Pause, Volume2, VolumeX, Maximize2, X } from 'lucide-react'
import { convertFileSrc } from '@tauri-apps/api/core'
interface VideoPlayerProps {
videoPath: string
isOpen: boolean
onClose: () => void
title?: string
}
const VideoPlayer: React.FC<VideoPlayerProps> = ({
videoPath,
isOpen,
onClose,
title = '视频播放'
}) => {
const videoRef = useRef<HTMLVideoElement>(null)
const [isPlaying, setIsPlaying] = useState(false)
const [isMuted, setIsMuted] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [volume, setVolume] = useState(1)
const [isFullscreen, setIsFullscreen] = useState(false)
const [videoSrc, setVideoSrc] = useState<string>('')
useEffect(() => {
if (isOpen && videoPath) {
// 使用Tauri的convertFileSrc来获取安全的文件URL
const src = convertFileSrc(videoPath)
setVideoSrc(src)
}
}, [isOpen, videoPath])
useEffect(() => {
const video = videoRef.current
if (!video) return
const handleTimeUpdate = () => setCurrentTime(video.currentTime)
const handleDurationChange = () => setDuration(video.duration)
const handleEnded = () => setIsPlaying(false)
const handlePlay = () => setIsPlaying(true)
const handlePause = () => setIsPlaying(false)
video.addEventListener('timeupdate', handleTimeUpdate)
video.addEventListener('durationchange', handleDurationChange)
video.addEventListener('ended', handleEnded)
video.addEventListener('play', handlePlay)
video.addEventListener('pause', handlePause)
return () => {
video.removeEventListener('timeupdate', handleTimeUpdate)
video.removeEventListener('durationchange', handleDurationChange)
video.removeEventListener('ended', handleEnded)
video.removeEventListener('play', handlePlay)
video.removeEventListener('pause', handlePause)
}
}, [videoSrc])
const handlePlayPause = () => {
const video = videoRef.current
if (!video) return
if (isPlaying) {
video.pause()
} else {
video.play().catch(err => {
console.error('Failed to play video:', err)
})
}
}
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
const video = videoRef.current
if (!video) return
const newTime = parseFloat(e.target.value)
video.currentTime = newTime
setCurrentTime(newTime)
}
const handleVolumeChange = (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 handleMuteToggle = () => {
const video = videoRef.current
if (!video) return
if (isMuted) {
video.volume = volume
setIsMuted(false)
} else {
video.volume = 0
setIsMuted(true)
}
}
const handleFullscreen = () => {
const video = videoRef.current
if (!video) return
if (!isFullscreen) {
if (video.requestFullscreen) {
video.requestFullscreen()
setIsFullscreen(true)
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
setIsFullscreen(false)
}
}
}
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50">
<div className="relative w-full max-w-4xl mx-4">
{/* 关闭按钮 */}
<button
onClick={onClose}
className="absolute top-4 right-4 z-10 p-2 bg-black/50 text-white rounded-full hover:bg-black/70 transition-colors"
>
<X size={24} />
</button>
{/* 标题 */}
<div className="absolute top-4 left-4 z-10 text-white">
<h3 className="text-lg font-medium">{title}</h3>
</div>
{/* 视频容器 */}
<div className="relative bg-black rounded-lg overflow-hidden">
<video
ref={videoRef}
src={videoSrc}
className="w-full h-auto max-h-[70vh]"
onClick={handlePlayPause}
onError={(e) => {
console.error('Video error:', e)
}}
/>
{/* 控制栏 */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4">
{/* 进度条 */}
<div className="mb-4">
<input
type="range"
min="0"
max={duration || 0}
value={currentTime}
onChange={handleSeek}
className="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer slider"
/>
</div>
{/* 控制按钮 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{/* 播放/暂停 */}
<button
onClick={handlePlayPause}
className="p-2 text-white hover:text-blue-400 transition-colors"
>
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
</button>
{/* 音量控制 */}
<div className="flex items-center space-x-2">
<button
onClick={handleMuteToggle}
className="text-white hover:text-blue-400 transition-colors"
>
{isMuted ? <VolumeX size={20} /> : <Volume2 size={20} />}
</button>
<input
type="range"
min="0"
max="1"
step="0.1"
value={isMuted ? 0 : volume}
onChange={handleVolumeChange}
className="w-20 h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer"
/>
</div>
{/* 时间显示 */}
<div className="text-white text-sm">
{formatTime(currentTime)} / {formatTime(duration)}
</div>
</div>
<div className="flex items-center space-x-2">
{/* 全屏按钮 */}
<button
onClick={handleFullscreen}
className="p-2 text-white hover:text-blue-400 transition-colors"
>
<Maximize2 size={20} />
</button>
</div>
</div>
</div>
</div>
</div>
<style jsx>{`
.slider::-webkit-slider-thumb {
appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
}
.slider::-moz-range-thumb {
height: 16px;
width: 16px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
border: none;
}
`}</style>
</div>
)
}
export default VideoPlayer