315 lines
10 KiB
TypeScript
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
|