mxivideo/docs/video-player-usage.md

9.6 KiB
Raw Permalink Blame History

VideoPlayer 组件使用指南

基于 demo.md 重写的 VideoPlayer 组件使用说明

🎯 设计特点

参考 demo.md 的优秀设计模式

  1. useImperativeHandle: 暴露组件方法供外部调用
  2. 清晰的接口定义: TypeScript 接口规范
  3. 完善的事件处理: 加载、播放、错误等事件回调
  4. 加载状态管理: 统一的加载状态显示
  5. 资源清理机制: 防止内存泄漏
  6. 错误处理回调: 可自定义错误处理逻辑

📚 接口定义

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 组件提供了更强大的功能和更好的开发体验!