fix: video player
This commit is contained in:
388
docs/video-player-usage.md
Normal file
388
docs/video-player-usage.md
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
# VideoPlayer 组件使用指南
|
||||||
|
|
||||||
|
基于 demo.md 重写的 VideoPlayer 组件使用说明
|
||||||
|
|
||||||
|
## 🎯 设计特点
|
||||||
|
|
||||||
|
### 参考 demo.md 的优秀设计模式
|
||||||
|
1. **useImperativeHandle**: 暴露组件方法供外部调用
|
||||||
|
2. **清晰的接口定义**: TypeScript 接口规范
|
||||||
|
3. **完善的事件处理**: 加载、播放、错误等事件回调
|
||||||
|
4. **加载状态管理**: 统一的加载状态显示
|
||||||
|
5. **资源清理机制**: 防止内存泄漏
|
||||||
|
6. **错误处理回调**: 可自定义错误处理逻辑
|
||||||
|
|
||||||
|
## 📚 接口定义
|
||||||
|
|
||||||
|
### VideoPlayerRef
|
||||||
|
```typescript
|
||||||
|
export interface VideoPlayerRef {
|
||||||
|
seek(time: number): void; // 跳转到指定时间
|
||||||
|
pause(): void; // 暂停播放
|
||||||
|
play(): void; // 开始播放
|
||||||
|
getCurrentTime(): number; // 获取当前播放时间
|
||||||
|
isPaused(): boolean; // 是否暂停状态
|
||||||
|
getDuration(): number; // 获取视频总时长
|
||||||
|
captureFrame(): string | undefined; // 截取当前帧
|
||||||
|
fullScreen(): void; // 全屏播放
|
||||||
|
reload(): void; // 重新加载视频
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 事件回调接口
|
||||||
|
```typescript
|
||||||
|
export interface OnSourceLoaded {
|
||||||
|
(player: HTMLVideoElement | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnPlayerError {
|
||||||
|
(err: Error, player: HTMLVideoElement | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnPlayerTimeChange {
|
||||||
|
(player: HTMLVideoElement | null): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件Props
|
||||||
|
```typescript
|
||||||
|
interface VideoPlayerProps {
|
||||||
|
videoPath: string // 视频文件路径
|
||||||
|
isOpen: boolean // 是否打开播放器
|
||||||
|
onClose: () => void // 关闭回调
|
||||||
|
title?: string // 播放器标题
|
||||||
|
onSourceLoaded?: OnSourceLoaded // 视频加载完成回调
|
||||||
|
onError?: OnPlayerError // 错误处理回调
|
||||||
|
onTimeChange?: OnPlayerTimeChange // 时间变化回调
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 基本使用
|
||||||
|
|
||||||
|
### 1. 简单使用
|
||||||
|
```tsx
|
||||||
|
import VideoPlayer from '@/components/VideoPlayer'
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const [isPlayerOpen, setIsPlayerOpen] = useState(false)
|
||||||
|
const [videoPath, setVideoPath] = useState('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setIsPlayerOpen(true)}>
|
||||||
|
播放视频
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<VideoPlayer
|
||||||
|
videoPath={videoPath}
|
||||||
|
isOpen={isPlayerOpen}
|
||||||
|
onClose={() => setIsPlayerOpen(false)}
|
||||||
|
title="我的视频"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 带事件处理的使用
|
||||||
|
```tsx
|
||||||
|
import VideoPlayer, { VideoPlayerRef } from '@/components/VideoPlayer'
|
||||||
|
|
||||||
|
function AdvancedVideoPlayer() {
|
||||||
|
const playerRef = useRef<VideoPlayerRef>(null)
|
||||||
|
const [isPlayerOpen, setIsPlayerOpen] = useState(false)
|
||||||
|
const [videoPath, setVideoPath] = useState('')
|
||||||
|
|
||||||
|
const handleSourceLoaded = (player: HTMLVideoElement | null) => {
|
||||||
|
console.log('视频加载完成:', {
|
||||||
|
duration: player?.duration,
|
||||||
|
videoWidth: player?.videoWidth,
|
||||||
|
videoHeight: player?.videoHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = (err: Error, player: HTMLVideoElement | null) => {
|
||||||
|
console.error('视频播放错误:', err.message)
|
||||||
|
// 可以显示自定义错误提示
|
||||||
|
alert(`播放失败: ${err.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTimeChange = (player: HTMLVideoElement | null) => {
|
||||||
|
if (player) {
|
||||||
|
console.log(`播放进度: ${player.currentTime}/${player.duration}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCaptureFrame = () => {
|
||||||
|
const frameData = playerRef.current?.captureFrame()
|
||||||
|
if (frameData) {
|
||||||
|
console.log('截取帧数据:', frameData)
|
||||||
|
// 可以保存或显示截图
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<button onClick={() => setIsPlayerOpen(true)}>
|
||||||
|
播放视频
|
||||||
|
</button>
|
||||||
|
<button onClick={() => playerRef.current?.play()}>
|
||||||
|
播放
|
||||||
|
</button>
|
||||||
|
<button onClick={() => playerRef.current?.pause()}>
|
||||||
|
暂停
|
||||||
|
</button>
|
||||||
|
<button onClick={() => playerRef.current?.seek(30)}>
|
||||||
|
跳转到30秒
|
||||||
|
</button>
|
||||||
|
<button onClick={handleCaptureFrame}>
|
||||||
|
截取当前帧
|
||||||
|
</button>
|
||||||
|
<button onClick={() => playerRef.current?.fullScreen()}>
|
||||||
|
全屏
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VideoPlayer
|
||||||
|
ref={playerRef}
|
||||||
|
videoPath={videoPath}
|
||||||
|
isOpen={isPlayerOpen}
|
||||||
|
onClose={() => setIsPlayerOpen(false)}
|
||||||
|
title="高级视频播放器"
|
||||||
|
onSourceLoaded={handleSourceLoaded}
|
||||||
|
onError={handleError}
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 控制方法
|
||||||
|
|
||||||
|
### 播放控制
|
||||||
|
```typescript
|
||||||
|
// 播放视频
|
||||||
|
playerRef.current?.play()
|
||||||
|
|
||||||
|
// 暂停视频
|
||||||
|
playerRef.current?.pause()
|
||||||
|
|
||||||
|
// 检查播放状态
|
||||||
|
const isPaused = playerRef.current?.isPaused()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 时间控制
|
||||||
|
```typescript
|
||||||
|
// 跳转到指定时间(秒)
|
||||||
|
playerRef.current?.seek(120) // 跳转到2分钟
|
||||||
|
|
||||||
|
// 获取当前播放时间
|
||||||
|
const currentTime = playerRef.current?.getCurrentTime()
|
||||||
|
|
||||||
|
// 获取视频总时长
|
||||||
|
const duration = playerRef.current?.getDuration()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级功能
|
||||||
|
```typescript
|
||||||
|
// 截取当前帧
|
||||||
|
const frameDataUrl = playerRef.current?.captureFrame()
|
||||||
|
if (frameDataUrl) {
|
||||||
|
// 可以用于显示缩略图或保存截图
|
||||||
|
const img = new Image()
|
||||||
|
img.src = frameDataUrl
|
||||||
|
document.body.appendChild(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏播放
|
||||||
|
playerRef.current?.fullScreen()
|
||||||
|
|
||||||
|
// 重新加载视频
|
||||||
|
playerRef.current?.reload()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 事件处理
|
||||||
|
|
||||||
|
### 视频加载完成
|
||||||
|
```typescript
|
||||||
|
const handleSourceLoaded = (player: HTMLVideoElement | null) => {
|
||||||
|
if (player) {
|
||||||
|
console.log('视频信息:', {
|
||||||
|
duration: player.duration,
|
||||||
|
width: player.videoWidth,
|
||||||
|
height: player.videoHeight,
|
||||||
|
readyState: player.readyState
|
||||||
|
})
|
||||||
|
|
||||||
|
// 可以在这里设置默认音量、播放速度等
|
||||||
|
player.volume = 0.8
|
||||||
|
player.playbackRate = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
```typescript
|
||||||
|
const handleError = (err: Error, player: HTMLVideoElement | null) => {
|
||||||
|
console.error('播放错误:', err)
|
||||||
|
|
||||||
|
// 根据错误类型进行不同处理
|
||||||
|
if (err.message.includes('文件不存在')) {
|
||||||
|
// 处理文件不存在的情况
|
||||||
|
showNotification('视频文件不存在,请检查文件路径')
|
||||||
|
} else if (err.message.includes('格式不支持')) {
|
||||||
|
// 处理格式不支持的情况
|
||||||
|
showNotification('视频格式不支持,请转换为MP4格式')
|
||||||
|
} else {
|
||||||
|
// 通用错误处理
|
||||||
|
showNotification('视频播放失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 播放进度监听
|
||||||
|
```typescript
|
||||||
|
const handleTimeChange = (player: HTMLVideoElement | null) => {
|
||||||
|
if (player) {
|
||||||
|
const progress = (player.currentTime / player.duration) * 100
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
setPlayProgress(progress)
|
||||||
|
|
||||||
|
// 记录观看进度
|
||||||
|
saveWatchProgress(videoId, player.currentTime)
|
||||||
|
|
||||||
|
// 在特定时间点触发事件
|
||||||
|
if (Math.floor(player.currentTime) === 60) {
|
||||||
|
console.log('播放到1分钟了!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 样式定制
|
||||||
|
|
||||||
|
### 加载状态自定义
|
||||||
|
组件内置了加载状态显示,使用 Loader2 图标和半透明背景。如需自定义,可以修改组件内的样式:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-white" />
|
||||||
|
<span className="text-white text-sm">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 视频容器样式
|
||||||
|
```tsx
|
||||||
|
<video
|
||||||
|
className="w-full h-auto max-h-[70vh] bg-black"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
preload="auto"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 最佳实践
|
||||||
|
|
||||||
|
### 1. 错误处理
|
||||||
|
```typescript
|
||||||
|
// 总是提供错误处理回调
|
||||||
|
<VideoPlayer
|
||||||
|
onError={(err, player) => {
|
||||||
|
// 记录错误日志
|
||||||
|
console.error('Video error:', err)
|
||||||
|
|
||||||
|
// 显示用户友好的错误信息
|
||||||
|
toast.error('视频播放失败,请重试')
|
||||||
|
|
||||||
|
// 可选:自动重试
|
||||||
|
setTimeout(() => {
|
||||||
|
playerRef.current?.reload()
|
||||||
|
}, 2000)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 性能优化
|
||||||
|
```typescript
|
||||||
|
// 使用 useMemo 缓存回调函数
|
||||||
|
const handleSourceLoaded = useMemo(() => (player: HTMLVideoElement | null) => {
|
||||||
|
// 处理逻辑
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleError = useMemo(() => (err: Error, player: HTMLVideoElement | null) => {
|
||||||
|
// 错误处理逻辑
|
||||||
|
}, [])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 资源管理
|
||||||
|
```typescript
|
||||||
|
// 组件卸载时清理资源
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// VideoPlayer 内部已经处理了资源清理
|
||||||
|
// 但如果有外部资源需要清理,可以在这里处理
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 高级用法
|
||||||
|
|
||||||
|
### 视频分析和处理
|
||||||
|
```typescript
|
||||||
|
const VideoAnalyzer = () => {
|
||||||
|
const playerRef = useRef<VideoPlayerRef>(null)
|
||||||
|
const [frames, setFrames] = useState<string[]>([])
|
||||||
|
|
||||||
|
const captureFramesAtInterval = () => {
|
||||||
|
const duration = playerRef.current?.getDuration()
|
||||||
|
if (!duration) return
|
||||||
|
|
||||||
|
const interval = duration / 10 // 每10%截取一帧
|
||||||
|
const framePromises = []
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const time = i * interval
|
||||||
|
framePromises.push(
|
||||||
|
new Promise<string>((resolve) => {
|
||||||
|
playerRef.current?.seek(time)
|
||||||
|
setTimeout(() => {
|
||||||
|
const frame = playerRef.current?.captureFrame()
|
||||||
|
resolve(frame || '')
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(framePromises).then(setFrames)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={captureFramesAtInterval}>
|
||||||
|
生成视频缩略图
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-5 gap-2 mt-4">
|
||||||
|
{frames.map((frame, index) => (
|
||||||
|
<img key={index} src={frame} alt={`Frame ${index}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VideoPlayer
|
||||||
|
ref={playerRef}
|
||||||
|
// ... 其他props
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*重写后的 VideoPlayer 组件提供了更强大的功能和更好的开发体验!*
|
||||||
@@ -1,19 +1,51 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react'
|
import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef } from 'react'
|
||||||
import { Play, Pause, Volume2, VolumeX, Maximize2, X, AlertCircle } from 'lucide-react'
|
import { Play, Pause, Volume2, VolumeX, Maximize2, X, AlertCircle, Loader2 } from 'lucide-react'
|
||||||
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
|
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
// 接口定义
|
||||||
|
export interface OnSourceLoaded {
|
||||||
|
(player: HTMLVideoElement | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnPlayerError {
|
||||||
|
(err: Error, player: HTMLVideoElement | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnPlayerTimeChange {
|
||||||
|
(player: HTMLVideoElement | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoPlayerRef {
|
||||||
|
seek(time: number): void;
|
||||||
|
pause(): void;
|
||||||
|
play(): void;
|
||||||
|
getCurrentTime(): number;
|
||||||
|
isPaused(): boolean;
|
||||||
|
getDuration(): number;
|
||||||
|
captureFrame(): string | undefined;
|
||||||
|
fullScreen(): void;
|
||||||
|
reload(): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface VideoPlayerProps {
|
interface VideoPlayerProps {
|
||||||
videoPath: string
|
videoPath: string
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
title?: string
|
title?: string
|
||||||
|
onSourceLoaded?: OnSourceLoaded
|
||||||
|
onError?: OnPlayerError
|
||||||
|
onTimeChange?: OnPlayerTimeChange
|
||||||
}
|
}
|
||||||
|
|
||||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
||||||
videoPath,
|
videoPath,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
title = '视频播放'
|
title = '视频播放',
|
||||||
}) => {
|
onSourceLoaded,
|
||||||
|
onError,
|
||||||
|
onTimeChange
|
||||||
|
}, ref) => {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
const [isMuted, setIsMuted] = useState(false)
|
const [isMuted, setIsMuted] = useState(false)
|
||||||
@@ -25,7 +57,98 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
const [fileExists, setFileExists] = useState<boolean>(true)
|
const [fileExists, setFileExists] = useState<boolean>(true)
|
||||||
const [errorMessage, setErrorMessage] = useState<string>('')
|
const [errorMessage, setErrorMessage] = useState<string>('')
|
||||||
const [loadingMethod, setLoadingMethod] = useState<'convertFileSrc' | 'dataUrl'>('convertFileSrc')
|
const [loadingMethod, setLoadingMethod] = useState<'convertFileSrc' | 'dataUrl'>('convertFileSrc')
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
// 暴露组件方法
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
seek: (time: number) => {
|
||||||
|
const video = videoRef.current
|
||||||
|
if (video) {
|
||||||
|
video.currentTime = time
|
||||||
|
setCurrentTime(time)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pause: () => {
|
||||||
|
videoRef.current?.pause()
|
||||||
|
},
|
||||||
|
play: () => {
|
||||||
|
videoRef.current?.play().catch(err => {
|
||||||
|
console.error('Play failed:', err)
|
||||||
|
onError?.(new Error('播放失败'), videoRef.current)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isPaused: () => {
|
||||||
|
return !!videoRef.current?.paused
|
||||||
|
},
|
||||||
|
getCurrentTime: () => {
|
||||||
|
return videoRef.current?.currentTime || 0
|
||||||
|
},
|
||||||
|
getDuration: () => {
|
||||||
|
return videoRef.current?.duration || 0
|
||||||
|
},
|
||||||
|
captureFrame: () => {
|
||||||
|
const video = videoRef.current
|
||||||
|
if (!video) return undefined
|
||||||
|
|
||||||
|
// 创建canvas元素
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return undefined
|
||||||
|
|
||||||
|
// 设置canvas尺寸
|
||||||
|
canvas.width = video.videoWidth
|
||||||
|
canvas.height = video.videoHeight
|
||||||
|
|
||||||
|
// 绘制视频帧到canvas
|
||||||
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||||
|
const dataURL = canvas.toDataURL('image/jpeg', 0.8)
|
||||||
|
return dataURL
|
||||||
|
},
|
||||||
|
fullScreen: () => {
|
||||||
|
const video = videoRef.current
|
||||||
|
if (video) {
|
||||||
|
video.requestFullscreen().catch(err => {
|
||||||
|
console.error('Fullscreen failed:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reload: () => {
|
||||||
|
if (videoPath) {
|
||||||
|
tryLoadVideo(videoPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 加载状态管理
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
|
const showLoading = () => setIsLoading(true)
|
||||||
|
const hideLoading = () => setIsLoading(false)
|
||||||
|
|
||||||
|
// 加载状态事件
|
||||||
|
video.addEventListener('waiting', showLoading)
|
||||||
|
video.addEventListener('seeking', showLoading)
|
||||||
|
video.addEventListener('loadstart', showLoading)
|
||||||
|
|
||||||
|
video.addEventListener('playing', hideLoading)
|
||||||
|
video.addEventListener('seeked', hideLoading)
|
||||||
|
video.addEventListener('canplay', hideLoading)
|
||||||
|
video.addEventListener('loadeddata', hideLoading)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('waiting', showLoading)
|
||||||
|
video.removeEventListener('seeking', showLoading)
|
||||||
|
video.removeEventListener('loadstart', showLoading)
|
||||||
|
video.removeEventListener('playing', hideLoading)
|
||||||
|
video.removeEventListener('seeked', hideLoading)
|
||||||
|
video.removeEventListener('canplay', hideLoading)
|
||||||
|
video.removeEventListener('loadeddata', hideLoading)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 视频源加载和事件处理
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkFileAndSetSrc = async () => {
|
const checkFileAndSetSrc = async () => {
|
||||||
if (isOpen && videoPath) {
|
if (isOpen && videoPath) {
|
||||||
@@ -37,6 +160,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
if (!exists) {
|
if (!exists) {
|
||||||
setFileExists(false)
|
setFileExists(false)
|
||||||
setErrorMessage(`文件不存在: ${videoPath}`)
|
setErrorMessage(`文件不存在: ${videoPath}`)
|
||||||
|
onError?.(new Error(`文件不存在: ${videoPath}`), videoRef.current)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +172,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking file:', error)
|
console.error('Error checking file:', error)
|
||||||
setFileExists(false)
|
setFileExists(false)
|
||||||
setErrorMessage(`文件检查失败: ${error}`)
|
const errorMsg = `文件检查失败: ${error}`
|
||||||
|
setErrorMessage(errorMsg)
|
||||||
|
onError?.(new Error(errorMsg), videoRef.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkFileAndSetSrc()
|
checkFileAndSetSrc()
|
||||||
}, [isOpen, videoPath])
|
}, [isOpen, videoPath, onError])
|
||||||
|
|
||||||
// 尝试多种方法加载视频
|
// 尝试多种方法加载视频
|
||||||
const tryLoadVideo = async (path: string) => {
|
const tryLoadVideo = async (path: string) => {
|
||||||
@@ -83,21 +209,56 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视频播放事件处理
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = videoRef.current
|
const video = videoRef.current
|
||||||
if (!video) return
|
if (!video) return
|
||||||
|
|
||||||
const handleTimeUpdate = () => setCurrentTime(video.currentTime)
|
const handleTimeUpdate = () => {
|
||||||
const handleDurationChange = () => setDuration(video.duration)
|
setCurrentTime(video.currentTime)
|
||||||
const handleEnded = () => setIsPlaying(false)
|
onTimeChange?.(video)
|
||||||
const handlePlay = () => setIsPlaying(true)
|
}
|
||||||
const handlePause = () => setIsPlaying(false)
|
|
||||||
|
|
||||||
|
const handleDurationChange = () => {
|
||||||
|
setDuration(video.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEnded = () => {
|
||||||
|
setIsPlaying(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
setIsPlaying(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePause = () => {
|
||||||
|
setIsPlaying(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
console.log('Video metadata loaded:', {
|
||||||
|
duration: video.duration,
|
||||||
|
videoWidth: video.videoWidth,
|
||||||
|
videoHeight: video.videoHeight
|
||||||
|
})
|
||||||
|
onSourceLoaded?.(video)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = (e: Event) => {
|
||||||
|
console.error('Video error:', e)
|
||||||
|
const error = new Error('视频播放错误')
|
||||||
|
setErrorMessage('视频播放错误')
|
||||||
|
onError?.(error, video)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加事件监听器
|
||||||
video.addEventListener('timeupdate', handleTimeUpdate)
|
video.addEventListener('timeupdate', handleTimeUpdate)
|
||||||
video.addEventListener('durationchange', handleDurationChange)
|
video.addEventListener('durationchange', handleDurationChange)
|
||||||
video.addEventListener('ended', handleEnded)
|
video.addEventListener('ended', handleEnded)
|
||||||
video.addEventListener('play', handlePlay)
|
video.addEventListener('play', handlePlay)
|
||||||
video.addEventListener('pause', handlePause)
|
video.addEventListener('pause', handlePause)
|
||||||
|
video.addEventListener('loadedmetadata', handleLoadedMetadata)
|
||||||
|
video.addEventListener('error', handleError)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
video.removeEventListener('timeupdate', handleTimeUpdate)
|
video.removeEventListener('timeupdate', handleTimeUpdate)
|
||||||
@@ -105,24 +266,29 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
video.removeEventListener('ended', handleEnded)
|
video.removeEventListener('ended', handleEnded)
|
||||||
video.removeEventListener('play', handlePlay)
|
video.removeEventListener('play', handlePlay)
|
||||||
video.removeEventListener('pause', handlePause)
|
video.removeEventListener('pause', handlePause)
|
||||||
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata)
|
||||||
|
video.removeEventListener('error', handleError)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [onTimeChange, onSourceLoaded, onError])
|
||||||
|
|
||||||
// 单独处理视频源的设置
|
// 视频源设置和清理
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = videoRef.current
|
const video = videoRef.current
|
||||||
if (!video || !videoSrc) return
|
if (!video || !videoSrc) return
|
||||||
|
|
||||||
// 清理旧的source元素
|
// 设置视频源
|
||||||
while (video.firstChild) {
|
|
||||||
video.removeChild(video.firstChild)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接设置src而不是添加source元素
|
|
||||||
video.src = videoSrc
|
video.src = videoSrc
|
||||||
video.load() // 重新加载视频
|
video.load()
|
||||||
|
|
||||||
console.log('Video src set to:', videoSrc)
|
console.log('Video src set to:', videoSrc)
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
if (video) {
|
||||||
|
video.src = ''
|
||||||
|
video.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [videoSrc])
|
}, [videoSrc])
|
||||||
|
|
||||||
const handlePlayPause = () => {
|
const handlePlayPause = () => {
|
||||||
@@ -240,39 +406,25 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="relative w-full">
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className="w-full h-auto max-h-[70vh]"
|
className="w-full h-auto max-h-[70vh] bg-black"
|
||||||
onClick={handlePlayPause}
|
onClick={handlePlayPause}
|
||||||
onError={async (e) => {
|
crossOrigin="anonymous"
|
||||||
console.error('Video loading error:', {
|
preload="auto"
|
||||||
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)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 加载状态显示 */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-white" />
|
||||||
|
<span className="text-white text-sm">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 控制栏 */}
|
{/* 控制栏 */}
|
||||||
@@ -340,6 +492,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
VideoPlayer.displayName = 'VideoPlayer'
|
||||||
|
|
||||||
export default VideoPlayer
|
export default VideoPlayer
|
||||||
|
|||||||
Reference in New Issue
Block a user