389 lines
9.6 KiB
Markdown
389 lines
9.6 KiB
Markdown
# 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 组件提供了更强大的功能和更好的开发体验!*
|