import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react'; import styled from 'styled-components'; import type { PlayerControllerRef } from '@labelu/components-react'; import { PlayerController, VIDEO_PLAYBACK_RATE_SPEED } from '@labelu/components-react'; import './video-js.css'; import invalidVideoIcon from './icons/video-error.svg'; /** 视频不支持的样式 */ const notSupportedErrorContent = () => { const errorImage = document.createElement('img'); errorImage.src = invalidVideoIcon; const description = document.createElement('div'); description.innerText = '暂不支持此编码的视频数据'; const container = document.createElement('div'); container.className = 'videojs-error-notSupported'; container.appendChild(errorImage); container.appendChild(description); return container; }; const defaultOptions = { playbackRates: VIDEO_PLAYBACK_RATE_SPEED, controls: false, controlBar: { children: ['progressControl'], }, }; const videoPlayerClassName = 'video-player'; const cachedRate = localStorage.getItem('labelu:video-rate') ?? '1'; const StyledVideoPlayer = styled.div.attrs((props) => ({ ...props, className: videoPlayerClassName, }))` position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; .${videoPlayerClassName}__wrap { position: relative; display: flex; flex-direction: column; width: 100%; text-align: center; height: var(--height); } .${videoPlayerClassName}__video { display: inline-block; flex-grow: 1; width: 100%; vertical-align: middle; outline: none; user-select: none; } `; interface IProps { src?: string; showControllers?: boolean; size?: { width: number; height: number; }; paused?: boolean; className?: string; onMetaDataLoad?: (videoRef: HTMLVideoElement) => void; children?: React.ReactNode; onPlaying?: (time: number) => void; onStatusChange?: (isPlaying: boolean) => void; wrapperRef: React.ForwardedRef; } /** * 视频播放器 * @param props * @returns */ export const VideoPlayer = memo( forwardRef>( ( { src, paused, className, showControllers = true, children, onMetaDataLoad, onPlaying, onStatusChange, wrapperRef: propsWrapperRef, }, ref, ) => { const playerRef = useRef(null); const wrapperRef = useRef(null); const controllerRef = useRef(null); const [playing, setPlaying] = useState(false); const [duration, setDuration] = useState(0); const videoRef = useRef(undefined); // 记录slider拖动前是否正在播放 const [parentHeight, setParentHeight] = useState(0); // rate 缓存在浏览器 const [rate, setRate] = useState(Number(cachedRate)); const handleRateChange = useCallback((value: number) => { localStorage.setItem('labelu:rate', value.toString()); setRate(value); playerRef.current?.playbackRate(value); }, []); useLayoutEffect(() => { if (!wrapperRef.current) { return; } setParentHeight(wrapperRef.current.clientHeight); }, []); // 暴露给 ref 的一些方法 useImperativeHandle( ref, () => { return playerRef.current; }, // eslint-disable-next-line react-hooks/exhaustive-deps [playerRef.current], ); useImperativeHandle( propsWrapperRef, () => { return wrapperRef.current as HTMLDivElement; }, // eslint-disable-next-line react-hooks/exhaustive-deps [wrapperRef.current], ); const handlePlayerReady = useCallback(() => { if (!playerRef.current) { return; } const onPause = () => { controllerRef.current?.pause(); }; const onPlay = () => { controllerRef.current?.play(); }; const onError = () => { console.log(playerRef.current?.error()); }; playerRef.current.off('pause', onPause); playerRef.current.off('play', onPlay); playerRef.current.off('error', onError); playerRef.current.on('pause', onPause); playerRef.current.on('play', onPlay); playerRef.current.on('error', onError); /** * 改写错误显示方法 * sourceLine: https://github.com/videojs/video.js/blob/master/src/js/error-display.js#L10 line:47 */ playerRef.current.errorDisplay.content = () => { let error; if (playerRef.current) { error = playerRef.current.error(); if (error?.code === 4) { return notSupportedErrorContent(); } return error ? playerRef.current.localize(error.message) : ''; } return 'Unknown Error'; }; }, []); /** 实例化videojs */ useEffect(() => { if (!videoRef.current || !document.contains(videoRef.current)) { return; } import('video.js').then((m) => { if (playerRef.current) { playerRef.current.src([ { src, type: 'video/mp4', }, ]); } else { playerRef.current = m.default( videoRef.current, { ...defaultOptions, sources: src ? [ { src, type: 'video/mp4', }, ] : [], }, handlePlayerReady, ); } }); }, [src, handlePlayerReady, videoRef, parentHeight]); useEffect(() => { if (paused) { setPlaying(false); } }, [paused]); useEffect(() => { const player = playerRef.current; return () => { if (player) { player.dispose(); playerRef.current = null; } }; }, [playerRef]); const initProcessPosition = () => { const processDom: HTMLDivElement | null = document.querySelector('.video-player-progressWrap'); if (!processDom) { return; } const { offsetTop, offsetHeight, offsetLeft, offsetWidth } = videoRef.current; processDom.style.top = `${offsetTop + offsetHeight - 52}px`; processDom.style.left = `${offsetLeft}px`; processDom.style.width = `${offsetWidth}px`; }; useEffect(() => { window.addEventListener('resize', initProcessPosition); return () => { window.removeEventListener('resize', initProcessPosition); }; }, []); const togglePlaying = useCallback( (isPlay: boolean) => { if (!playerRef.current) { return; } onStatusChange?.(isPlay); playerRef.current.playbackRate(rate); if (isPlay) { playerRef.current.play(); } else { playerRef.current.pause(); } setPlaying(!playing); }, [onStatusChange, playing, rate], ); const handleMetaDataOnLoad = useCallback(() => { setDuration(videoRef.current.duration); onMetaDataLoad?.(videoRef.current); }, [onMetaDataLoad]); const getCurrentTime = useCallback(() => { return playerRef.current?.currentTime() || 0; }, []); const handlePlaying = useCallback( (time: number) => { onPlaying?.(time); }, [onPlaying], ); console.log(playerRef.current); console.log(playerRef.current?.playbackRates); return ( // @ts-ignore {/* @ts-ignore */}
); }, ), );