| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- import { useEffect, useRef, useState } from 'react'
- import { PlayCircleFilled } from '@ant-design/icons'
- interface VideoPlayerProps {
- src: string
- poster?: string
- /** 容器最大宽度,默认 360 */
- maxWidth?: number
- }
- /**
- * 原生 <video> 封装(替代 video.js)
- *
- * 实现细节:
- * - src 始终绑定到 video 元素 (不做 React state 切换),避免 setState 后
- * 还没 rerender 就 play() 拿到空 src 的 race condition
- * - preload="none" → 浏览器在用户点击前不会发任何网络请求
- * - 用蒙层 + 大播放按钮覆盖,点击触发 play()
- * - controls 在点击后才显示
- * - 自适应视频原生宽高比 (不强制 9:16 / 16:9)
- */
- export default function VideoPlayer({ src, poster, maxWidth = 360 }: VideoPlayerProps) {
- const videoRef = useRef<HTMLVideoElement | null>(null)
- const [started, setStarted] = useState(false)
- const [error, setError] = useState<string | null>(null)
- // src 变更(查新视频)时重置状态
- useEffect(() => {
- setStarted(false)
- setError(null)
- }, [src])
- const onPlayClick = () => {
- const v = videoRef.current
- if (!v) return
- setStarted(true)
- setError(null)
- const p = v.play()
- if (p && typeof p.catch === 'function') {
- p.catch((err) => setError(err?.message ?? String(err)))
- }
- }
- return (
- <div
- style={{
- width: '100%',
- maxWidth,
- position: 'relative',
- background: '#000',
- borderRadius: 6,
- overflow: 'hidden',
- minHeight: started ? 'auto' : 200,
- }}
- >
- <video
- ref={videoRef}
- src={src}
- poster={poster}
- controls={started}
- preload="none"
- playsInline
- onError={(e) => {
- const v = e.currentTarget as HTMLVideoElement
- const err = v.error
- const codeMap: Record<number, string> = {
- 1: 'MEDIA_ERR_ABORTED 播放被中止',
- 2: 'MEDIA_ERR_NETWORK 网络错误',
- 3: 'MEDIA_ERR_DECODE 解码失败',
- 4: 'MEDIA_ERR_SRC_NOT_SUPPORTED 源不支持(URL 不可达/格式不被识别)',
- }
- setError(`${err ? codeMap[err.code] ?? `code=${err.code}` : ''} ${err?.message ?? ''}`)
- }}
- style={{
- width: '100%',
- height: 'auto',
- maxHeight: 540,
- display: 'block',
- }}
- >
- 您的浏览器不支持 video 标签。
- </video>
- {!started && (
- <button
- onClick={onPlayClick}
- aria-label="播放视频"
- style={{
- position: 'absolute',
- inset: 0,
- background: 'rgba(0,0,0,0.25)',
- border: 'none',
- color: '#fff',
- fontSize: 64,
- cursor: 'pointer',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- transition: 'background .15s',
- }}
- onMouseEnter={(e) => {
- e.currentTarget.style.background = 'rgba(0,0,0,0.4)'
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.background = 'rgba(0,0,0,0.25)'
- }}
- >
- <PlayCircleFilled style={{ filter: 'drop-shadow(0 2px 6px rgba(0,0,0,0.5))' }} />
- </button>
- )}
- {error && (
- <div
- style={{
- position: 'absolute',
- bottom: 0,
- left: 0,
- right: 0,
- padding: '6px 8px',
- background: 'rgba(255,77,79,0.9)',
- color: '#fff',
- fontSize: 12,
- }}
- >
- {error}
- </div>
- )}
- </div>
- )
- }
|