mxivideo/src/components/VideoPlayer.tsx

315 lines
10 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react'
import { Play, Pause, Volume2, VolumeX, Maximize2, X, AlertCircle } from 'lucide-react'
import { convertFileSrc, invoke } 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>('')
const [fileExists, setFileExists] = useState<boolean>(true)
const [errorMessage, setErrorMessage] = useState<string>('')
const [loadingMethod, setLoadingMethod] = useState<'convertFileSrc' | 'dataUrl'>('convertFileSrc')
useEffect(() => {
const checkFileAndSetSrc = async () => {
if (isOpen && videoPath) {
try {
// 首先检查文件是否存在
const exists = await invoke<boolean>('check_file_exists', { filePath: videoPath })
console.log('File exists check:', { videoPath, exists })
if (!exists) {
setFileExists(false)
setErrorMessage(`文件不存在: ${videoPath}`)
return
}
setFileExists(true)
setErrorMessage('')
// 尝试多种方法加载视频
await tryLoadVideo(videoPath)
} catch (error) {
console.error('Error checking file:', error)
setFileExists(false)
setErrorMessage(`文件检查失败: ${error}`)
}
}
}
checkFileAndSetSrc()
}, [isOpen, videoPath])
// 尝试多种方法加载视频
const tryLoadVideo = async (path: string) => {
// 根据路径类型选择不同的加载策略
const videoPath = path.split('\\').filter(it => it.length > 0).join('/')
console.log(`try load video videoPath`, {videoPath, path})
const src = convertFileSrc(videoPath)
console.log(`try load video src`, src)
setVideoSrc(src)
setLoadingMethod('convertFileSrc')
return
}
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">
{!fileExists || errorMessage ? (
/* 错误显示 */
<div className="flex items-center justify-center h-96 text-white">
<div className="text-center">
<AlertCircle size={64} className="mx-auto mb-4 text-red-400" />
<h3 className="text-xl font-medium mb-2"></h3>
<p className="text-gray-300 mb-4">{errorMessage}</p>
<p className="text-sm text-gray-400 mb-4">: {videoPath}</p>
<p className="text-xs text-gray-500 mb-4">: {loadingMethod}</p>
{loadingMethod === 'convertFileSrc' && (
<button
onClick={() => {
if (videoPath) {
tryLoadVideo(videoPath).catch(err => {
console.error('Manual reload failed:', err)
setErrorMessage('备用加载方法也失败了')
})
}
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
</button>
)}
</div>
</div>
) : (
<video
ref={videoRef}
src={videoSrc}
className="w-full h-auto max-h-[70vh]"
onClick={handlePlayPause}
onError={async (e) => {
console.error('Video loading error:', {
error: e,
videoSrc,
originalPath: videoPath,
currentTarget: e.currentTarget,
networkState: e.currentTarget.networkState,
readyState: e.currentTarget.readyState,
currentMethod: loadingMethod
})
// 如果当前视频源失败,尝试重新加载
if (videoPath) {
console.log('Video error detected, attempting reload...')
tryLoadVideo(videoPath).catch(err => {
console.error('Reload failed:', err)
setErrorMessage('视频加载失败:文件可能损坏或格式不支持')
})
}
}}
onLoadStart={() => {
console.log('Video load started:', videoSrc)
}}
onCanPlay={() => {
console.log('Video can play:', videoSrc)
}}
onLoadedData={() => {
console.log('Video data loaded:', videoSrc)
}}
/>
)}
{/* 控制栏 */}
<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>
</div>
)
}
export default VideoPlayer