9.6 KiB
9.6 KiB
VideoPlayer 组件使用指南
基于 demo.md 重写的 VideoPlayer 组件使用说明
🎯 设计特点
参考 demo.md 的优秀设计模式
- useImperativeHandle: 暴露组件方法供外部调用
- 清晰的接口定义: TypeScript 接口规范
- 完善的事件处理: 加载、播放、错误等事件回调
- 加载状态管理: 统一的加载状态显示
- 资源清理机制: 防止内存泄漏
- 错误处理回调: 可自定义错误处理逻辑
📚 接口定义
VideoPlayerRef
export interface VideoPlayerRef {
seek(time: number): void; // 跳转到指定时间
pause(): void; // 暂停播放
play(): void; // 开始播放
getCurrentTime(): number; // 获取当前播放时间
isPaused(): boolean; // 是否暂停状态
getDuration(): number; // 获取视频总时长
captureFrame(): string | undefined; // 截取当前帧
fullScreen(): void; // 全屏播放
reload(): void; // 重新加载视频
}
事件回调接口
export interface OnSourceLoaded {
(player: HTMLVideoElement | null): void;
}
export interface OnPlayerError {
(err: Error, player: HTMLVideoElement | null): void;
}
export interface OnPlayerTimeChange {
(player: HTMLVideoElement | null): void;
}
组件Props
interface VideoPlayerProps {
videoPath: string // 视频文件路径
isOpen: boolean // 是否打开播放器
onClose: () => void // 关闭回调
title?: string // 播放器标题
onSourceLoaded?: OnSourceLoaded // 视频加载完成回调
onError?: OnPlayerError // 错误处理回调
onTimeChange?: OnPlayerTimeChange // 时间变化回调
}
🔧 基本使用
1. 简单使用
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. 带事件处理的使用
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}
/>
</>
)
}
🎮 控制方法
播放控制
// 播放视频
playerRef.current?.play()
// 暂停视频
playerRef.current?.pause()
// 检查播放状态
const isPaused = playerRef.current?.isPaused()
时间控制
// 跳转到指定时间(秒)
playerRef.current?.seek(120) // 跳转到2分钟
// 获取当前播放时间
const currentTime = playerRef.current?.getCurrentTime()
// 获取视频总时长
const duration = playerRef.current?.getDuration()
高级功能
// 截取当前帧
const frameDataUrl = playerRef.current?.captureFrame()
if (frameDataUrl) {
// 可以用于显示缩略图或保存截图
const img = new Image()
img.src = frameDataUrl
document.body.appendChild(img)
}
// 全屏播放
playerRef.current?.fullScreen()
// 重新加载视频
playerRef.current?.reload()
🔄 事件处理
视频加载完成
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
}
}
错误处理
const handleError = (err: Error, player: HTMLVideoElement | null) => {
console.error('播放错误:', err)
// 根据错误类型进行不同处理
if (err.message.includes('文件不存在')) {
// 处理文件不存在的情况
showNotification('视频文件不存在,请检查文件路径')
} else if (err.message.includes('格式不支持')) {
// 处理格式不支持的情况
showNotification('视频格式不支持,请转换为MP4格式')
} else {
// 通用错误处理
showNotification('视频播放失败,请重试')
}
}
播放进度监听
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 图标和半透明背景。如需自定义,可以修改组件内的样式:
{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>
)}
视频容器样式
<video
className="w-full h-auto max-h-[70vh] bg-black"
crossOrigin="anonymous"
preload="auto"
/>
🔧 最佳实践
1. 错误处理
// 总是提供错误处理回调
<VideoPlayer
onError={(err, player) => {
// 记录错误日志
console.error('Video error:', err)
// 显示用户友好的错误信息
toast.error('视频播放失败,请重试')
// 可选:自动重试
setTimeout(() => {
playerRef.current?.reload()
}, 2000)
}}
/>
2. 性能优化
// 使用 useMemo 缓存回调函数
const handleSourceLoaded = useMemo(() => (player: HTMLVideoElement | null) => {
// 处理逻辑
}, [])
const handleError = useMemo(() => (err: Error, player: HTMLVideoElement | null) => {
// 错误处理逻辑
}, [])
3. 资源管理
// 组件卸载时清理资源
useEffect(() => {
return () => {
// VideoPlayer 内部已经处理了资源清理
// 但如果有外部资源需要清理,可以在这里处理
}
}, [])
🚀 高级用法
视频分析和处理
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 组件提供了更强大的功能和更好的开发体验!