fix: 播放器播放问题

This commit is contained in:
root 2025-07-11 11:18:14 +08:00
parent 074a6b7834
commit 766d59cdf0
4 changed files with 319 additions and 44 deletions

View File

@ -40,3 +40,57 @@ pub async fn read_image_as_data_url(file_path: String) -> Result<String, String>
Err(e) => Err(format!("Failed to read file: {}", e)),
}
}
/// 读取视频文件并返回base64编码的数据URL
#[command]
pub async fn read_video_as_data_url(file_path: String) -> Result<String, String> {
let path = Path::new(&file_path);
// 检查文件是否存在
if !path.exists() {
return Err("File does not exist".to_string());
}
// 检查是否是视频文件
let extension = path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase())
.unwrap_or_default();
let mime_type = match extension.as_str() {
"mp4" => "video/mp4",
"avi" => "video/x-msvideo",
"mov" => "video/quicktime",
"mkv" => "video/x-matroska",
"wmv" => "video/x-ms-wmv",
"flv" => "video/x-flv",
"webm" => "video/webm",
"m4v" => "video/x-m4v",
_ => return Err("Unsupported video format".to_string()),
};
// 读取文件内容
match fs::read(&file_path) {
Ok(file_data) => {
// 编码为base64
let base64_data = general_purpose::STANDARD.encode(&file_data);
// 返回data URL格式
Ok(format!("data:{};base64,{}", mime_type, base64_data))
}
Err(e) => Err(format!("Failed to read file: {}", e)),
}
}
/// 获取视频文件的blob URL用于大文件
#[command]
pub async fn get_video_blob_url(file_path: String) -> Result<String, String> {
let path = Path::new(&file_path);
// 检查文件是否存在
if !path.exists() {
return Err("File does not exist".to_string());
}
// 对于视频文件我们返回文件路径让前端使用convertFileSrc
Ok(file_path)
}

View File

@ -82,7 +82,9 @@ pub fn run() {
commands::media::search_segments,
commands::media::delete_segment,
commands::media::delete_original_video,
commands::file_utils::read_image_as_data_url
commands::file_utils::read_image_as_data_url,
commands::file_utils::read_video_as_data_url,
commands::file_utils::get_video_blob_url
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -0,0 +1,245 @@
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

View File

@ -1,7 +1,8 @@
import React, { useState } from 'react'
import { Play, Pause, Video, Trash2, Tag, Eye, Clock, HardDrive, Hash, TrendingUp, X } from 'lucide-react'
import { Play, Video, Trash2, Tag, Eye, Clock, HardDrive, Hash, TrendingUp, X } from 'lucide-react'
import { VideoSegment, MediaService } from '../services/mediaService'
import ProjectImage from './ProjectImage'
import VideoPlayer from './VideoPlayer'
interface VideoSegmentCardProps {
segment: VideoSegment
@ -16,42 +17,12 @@ const VideoSegmentCard: React.FC<VideoSegmentCardProps> = ({
onTagsUpdate,
onUse
}) => {
const [isPlaying, setIsPlaying] = useState(false)
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null)
const [showVideoPlayer, setShowVideoPlayer] = useState(false)
const [showTagEditor, setShowTagEditor] = useState(false)
const [newTag, setNewTag] = useState('')
const handlePlayPause = () => {
if (!videoElement) {
// 创建视频元素
const video_el = document.createElement('video')
video_el.src = `file://${segment.file_path}`
video_el.addEventListener('ended', () => setIsPlaying(false))
video_el.addEventListener('error', (e) => {
console.error('Video playback error:', e)
setIsPlaying(false)
})
setVideoElement(video_el)
video_el.play()
.then(() => setIsPlaying(true))
.catch(err => {
console.error('Failed to play video:', err)
setIsPlaying(false)
})
} else {
if (isPlaying) {
videoElement.pause()
setIsPlaying(false)
} else {
videoElement.play()
.then(() => setIsPlaying(true))
.catch(err => {
console.error('Failed to play video:', err)
setIsPlaying(false)
})
}
}
const handlePlayVideo = () => {
setShowVideoPlayer(true)
}
const handleUse = () => {
@ -61,11 +32,6 @@ const VideoSegmentCard: React.FC<VideoSegmentCardProps> = ({
}
const handleDelete = () => {
if (videoElement) {
videoElement.pause()
setVideoElement(null)
setIsPlaying(false)
}
onDelete(segment.id)
}
@ -101,11 +67,11 @@ const VideoSegmentCard: React.FC<VideoSegmentCardProps> = ({
{/* 播放按钮覆盖层 */}
<div className="absolute inset-0 bg-black bg-opacity-0 hover:bg-opacity-30 transition-all flex items-center justify-center">
<button
onClick={handlePlayPause}
onClick={handlePlayVideo}
className="p-3 bg-white/80 rounded-full hover:bg-white transition-colors opacity-0 hover:opacity-100"
title={isPlaying ? '暂停' : '播放'}
title="播放视频"
>
{isPlaying ? <Pause size={24} className="text-gray-800" /> : <Play size={24} className="text-gray-800" />}
<Play size={24} className="text-gray-800" />
</button>
</div>
@ -196,7 +162,7 @@ const VideoSegmentCard: React.FC<VideoSegmentCardProps> = ({
onChange={(e) => setNewTag(e.target.value)}
placeholder="添加标签"
className="flex-1 px-2 py-1 text-xs border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
onKeyPress={(e) => e.key === 'Enter' && handleAddTag()}
onKeyDown={(e) => e.key === 'Enter' && handleAddTag()}
/>
<button
onClick={handleAddTag}
@ -244,6 +210,14 @@ const VideoSegmentCard: React.FC<VideoSegmentCardProps> = ({
</div>
</div>
</div>
{/* 视频播放器 */}
<VideoPlayer
videoPath={segment.file_path}
isOpen={showVideoPlayer}
onClose={() => setShowVideoPlayer(false)}
title={`片段 ${segment.segment_index + 1} - ${segment.filename}`}
/>
</div>
)
}