fix: video player
This commit is contained in:
parent
c75985188e
commit
18b3905c2b
|
|
@ -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 { Play, Pause, Volume2, VolumeX, Maximize2, X, AlertCircle } from 'lucide-react'
|
||||
import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef } from 'react'
|
||||
import { Play, Pause, Volume2, VolumeX, Maximize2, X, AlertCircle, Loader2 } from 'lucide-react'
|
||||
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 {
|
||||
videoPath: string
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
title?: string
|
||||
onSourceLoaded?: OnSourceLoaded
|
||||
onError?: OnPlayerError
|
||||
onTimeChange?: OnPlayerTimeChange
|
||||
}
|
||||
|
||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
||||
videoPath,
|
||||
isOpen,
|
||||
onClose,
|
||||
title = '视频播放'
|
||||
}) => {
|
||||
title = '视频播放',
|
||||
onSourceLoaded,
|
||||
onError,
|
||||
onTimeChange
|
||||
}, ref) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [isMuted, setIsMuted] = useState(false)
|
||||
|
|
@ -25,7 +57,98 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||
const [fileExists, setFileExists] = useState<boolean>(true)
|
||||
const [errorMessage, setErrorMessage] = useState<string>('')
|
||||
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(() => {
|
||||
const checkFileAndSetSrc = async () => {
|
||||
if (isOpen && videoPath) {
|
||||
|
|
@ -37,6 +160,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||
if (!exists) {
|
||||
setFileExists(false)
|
||||
setErrorMessage(`文件不存在: ${videoPath}`)
|
||||
onError?.(new Error(`文件不存在: ${videoPath}`), videoRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -48,13 +172,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||
} catch (error) {
|
||||
console.error('Error checking file:', error)
|
||||
setFileExists(false)
|
||||
setErrorMessage(`文件检查失败: ${error}`)
|
||||
const errorMsg = `文件检查失败: ${error}`
|
||||
setErrorMessage(errorMsg)
|
||||
onError?.(new Error(errorMsg), videoRef.current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkFileAndSetSrc()
|
||||
}, [isOpen, videoPath])
|
||||
}, [isOpen, videoPath, onError])
|
||||
|
||||
// 尝试多种方法加载视频
|
||||
const tryLoadVideo = async (path: string) => {
|
||||
|
|
@ -83,21 +209,56 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
// 视频播放事件处理
|
||||
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)
|
||||
const handleTimeUpdate = () => {
|
||||
setCurrentTime(video.currentTime)
|
||||
onTimeChange?.(video)
|
||||
}
|
||||
|
||||
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('durationchange', handleDurationChange)
|
||||
video.addEventListener('ended', handleEnded)
|
||||
video.addEventListener('play', handlePlay)
|
||||
video.addEventListener('pause', handlePause)
|
||||
video.addEventListener('loadedmetadata', handleLoadedMetadata)
|
||||
video.addEventListener('error', handleError)
|
||||
|
||||
return () => {
|
||||
video.removeEventListener('timeupdate', handleTimeUpdate)
|
||||
|
|
@ -105,24 +266,29 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||
video.removeEventListener('ended', handleEnded)
|
||||
video.removeEventListener('play', handlePlay)
|
||||
video.removeEventListener('pause', handlePause)
|
||||
video.removeEventListener('loadedmetadata', handleLoadedMetadata)
|
||||
video.removeEventListener('error', handleError)
|
||||
}
|
||||
}, [])
|
||||
}, [onTimeChange, onSourceLoaded, onError])
|
||||
|
||||
// 单独处理视频源的设置
|
||||
// 视频源设置和清理
|
||||
useEffect(() => {
|
||||
const video = videoRef.current
|
||||
if (!video || !videoSrc) return
|
||||
|
||||
// 清理旧的source元素
|
||||
while (video.firstChild) {
|
||||
video.removeChild(video.firstChild)
|
||||
}
|
||||
|
||||
// 直接设置src而不是添加source元素
|
||||
// 设置视频源
|
||||
video.src = videoSrc
|
||||
video.load() // 重新加载视频
|
||||
video.load()
|
||||
|
||||
console.log('Video src set to:', videoSrc)
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (video) {
|
||||
video.src = ''
|
||||
video.load()
|
||||
}
|
||||
}
|
||||
}, [videoSrc])
|
||||
|
||||
const handlePlayPause = () => {
|
||||
|
|
@ -240,39 +406,25 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<video
|
||||
ref={videoRef}
|
||||
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="relative w-full">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full h-auto max-h-[70vh] bg-black"
|
||||
onClick={handlePlayPause}
|
||||
crossOrigin="anonymous"
|
||||
preload="auto"
|
||||
/>
|
||||
|
||||
{/* 加载状态显示 */}
|
||||
{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>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
VideoPlayer.displayName = 'VideoPlayer'
|
||||
|
||||
export default VideoPlayer
|
||||
|
|
|
|||
Loading…
Reference in New Issue