/* eslint-disable jsx-a11y/click-events-have-key-events */ import React from 'react'; import cls from 'classnames'; import BaseComponent from '../_base/baseComponent'; import { cssClasses, DEFAULT_PLAYBACK_RATE, numbers, strings } from '@douyinfe/semi-foundation/videoPlayer/constants'; import VideoPlayerFoundation, { VideoPlayerAdapter } from '@douyinfe/semi-foundation/videoPlayer/foundation'; import '@douyinfe/semi-foundation/videoPlayer/videoPlayer.scss'; import { IconPlay, IconPause, IconVolume1, IconVolume2, IconRestart, IconFlipHorizontal, IconMinimize, IconMaximize, IconMute, IconPlayCircle, IconMiniPlayer } from '@douyinfe/semi-icons'; import Button from '../button'; import Popover from '../popover'; import AudioSlider from '../audioPlayer/audioSlider'; import Dropdown from '../dropdown'; import VideoProgress from './videoProgress'; import { formatTime } from './utils'; import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined'; import { isUndefined } from 'lodash'; import LocaleConsumer from '../locale/localeConsumer'; import { Locale } from '../locale/interface'; import ErrorSVG from './ErrorSvg'; import { Marker } from '@douyinfe/semi-foundation/videoPlayer/progressFoundation'; const prefixCls = cssClasses.PREFIX; export interface VideoPlayerProps { autoPlay: boolean; captionsSrc?: string; className?: string; clickToPlay: boolean; controlsList?: Array; crossOrigin?: React.MediaHTMLAttributes['crossOrigin']; defaultPlaybackRate: number; defaultQuality?: string; defaultRoute?: string; forwardRef?: React.Ref; height?: number | string; loop?: boolean; markers?: Marker[]; muted: boolean; onPause?: () => void; onPlay?: () => void; onQualityChange?: (quality: string) => void; onRateChange?: (rate: number) => void; onRouteChange?: (route: string) => void; onVolumeChange?: (volume: number) => void; playbackRateList: { label: string; value: number }[]; poster?: string; // todo: 预览缩略图 // previewThumbnails?: boolean | Record; qualityList?: Array<{ label: string; value: string }>; routeList?: Array<{ label: string; value: string }>; seekTime?: number; src?: string; style?: React.CSSProperties; theme: string; volume: number; width?: number | string } export interface VideoPlayerState { bufferedValue: number; currentQuality: string; currentRoute: string; currentTime: number; isError: boolean; isMirror: boolean; isPlaying: boolean; muted: boolean; notificationContent: string; playbackRate: number; playbackRateList: { label: string; value: number }[]; showNotification: boolean; showControls: boolean; src: string; totalTime: number; volume: number } class VideoPlayer extends BaseComponent { static defaultProps: VideoPlayerProps = { autoPlay: false, clickToPlay: true, defaultPlaybackRate: numbers.DEFAULT_PLAYBACK_RATE, controlsList: [strings.PLAY, strings.NEXT, strings.TIME, strings.VOLUME, strings.PLAYBACK_RATE, strings.QUALITY, strings.ROUTE, strings.MIRROR, strings.FULLSCREEN, strings.PICTURE_IN_PICTURE], loop: false, muted: false, playbackRateList: DEFAULT_PLAYBACK_RATE, seekTime: numbers.DEFAULT_SEEK_TIME, theme: strings.DARK, volume: numbers.DEFAULT_VOLUME, }; private videoRef: React.RefObject; private videoWrapperRef: React.RefObject; foundation: VideoPlayerFoundation; constructor(props: VideoPlayerProps) { super(props); this.state = { bufferedValue: 0, currentQuality: props.defaultQuality || '', currentRoute: props.defaultRoute || '', currentTime: 0, isError: false, isMirror: false, isPlaying: false, muted: props.muted, notificationContent: '', playbackRate: props.defaultPlaybackRate || 1, playbackRateList: props.playbackRateList, showNotification: false, showControls: true, src: props.src || '', totalTime: 0, volume: props.muted ? 0 : props.volume, }; this.videoRef = React.createRef(); this.videoWrapperRef = React.createRef(); this.foundation = new VideoPlayerFoundation(this.adapter); } get adapter(): VideoPlayerAdapter { return { ...super.adapter, getVideo: () => this.videoRef.current, getVideoWrapper: () => this.videoWrapperRef.current, notifyPause: () => this.props.onPause?.(), notifyPlay: () => this.props.onPlay?.(), notifyQualityChange: (quality: string) => this.props.onQualityChange?.(quality), notifyRateChange: (rate: number) => this.props.onRateChange?.(rate), notifyRouteChange: (route: string) => this.props.onRouteChange?.(route), notifyVolumeChange: (volume: number) => this.props.onVolumeChange?.(volume), setBufferedValue: (bufferedValue: number) => this.setState({ bufferedValue }), setCurrentTime: (currentTime: number) => this.setState({ currentTime }), setIsError: (isError: boolean) => this.setState({ isError }), setIsMirror: (isMirror: boolean) => this.setState({ isMirror }), setIsPlaying: (isPlaying: boolean) => this.setState({ isPlaying }), setMuted: (muted: boolean) => this.setState({ muted }), setNotificationContent: (content: string) => this.setState({ notificationContent: content }), setPlaybackRate: (rate: number) => this.setState({ playbackRate: rate }), setQuality: (quality: string) => this.setState({ currentQuality: quality }), setRoute: (route: string) => this.setState({ currentRoute: route }), setShowControls: (showControls: boolean) => this.setState({ showControls }), setShowNotification: (showNotification: boolean) => this.setState({ showNotification: showNotification }), setTotalTime: (totalTime: number) => this.setState({ totalTime }), setVolume: (volume: number) => this.setState({ volume }), }; } getVideoRef() { const { forwardRef } = this.props; if (!isUndefined(forwardRef)) { if (typeof forwardRef === 'function') { return (node: HTMLVideoElement) => { forwardRef(node); this.videoRef = { current: node }; }; } else if (Object.prototype.toString.call(forwardRef) === '[object Object]') { this.videoRef = forwardRef as React.RefObject; return forwardRef; } } return this.videoRef; } static getDerivedStateFromProps(props: VideoPlayerProps, state: VideoPlayerState): Partial { const states: Partial = {}; if (!isNullOrUndefined(props.src) && props.src !== state.src) { states.src = props.src; } return states; } componentDidMount() { this.foundation.init(); } componentWillUnmount() { this.foundation.destroy(); } handleMouseEnterWrapper = () => { this.foundation.handleMouseEnterWrapper(); } handleMouseLeaveWrapper = () => { this.foundation.handleMouseLeaveWrapper(); } handleTimeChange = (value: number) => { this.foundation.handleTimeChange(value); } handleTimeUpdate = () => { this.foundation.handleTimeUpdate(); } handleError = () => { this.foundation.handleError(); } handlePlay = () => { this.foundation.handlePlay(); } handlePause = () => { this.foundation.handlePause(); } handleVideoPlay = () => { this.foundation.handleVideoPlay(); } handleVideoPause = () => { this.foundation.handleVideoPause(); } handleCanPlay = () => { this.foundation.handleCanPlay(); } handleWaiting = (locale: Locale['VideoPlayer']) => { this.foundation.handleWaiting(locale); } handleStalled = (locale: Locale['VideoPlayer']) => { this.foundation.handleStalled(locale); } handleProgress = () => { this.foundation.handleProgress(); } handleEnded = () => { this.foundation.handleEnded(); } handleDurationChange = () => { this.foundation.handleDurationChange(); } handleVolumeChange = (value: number) => { this.foundation.handleVolumeChange(value); } handleVolumeSilent = () => { this.foundation.handleVolumeSilent(); } handleRateChange = (option: { label: string; value: number }, locale: Locale['VideoPlayer']) => { this.foundation.handleRateChange(option, locale); } handleQualityChange = (option: { label: string; value: string }, locale: Locale['VideoPlayer']) => { this.foundation.handleQualityChange(option, locale); } handleRouteChange = (option: { label: string; value: string }, locale: Locale['VideoPlayer']) => { this.foundation.handleRouteChange(option, locale); } handleMirror = (locale: Locale['VideoPlayer']) => { this.foundation.handleMirror(locale); } handleFullscreen = () => { this.foundation.handleFullscreen(); } handlePictureInPicture = () => { this.foundation.handlePictureInPicture(); } getVolumeIcon = () => { const { volume, muted } = this.state; if (muted) { return ; } if (volume < 50) { return ; } return ; } isResourceNotFound = () => { const { src } = this.props; return isNullOrUndefined(src); } renderTime = () => { const { currentTime, totalTime } = this.state; if (this.foundation.shouldShowControlItem(strings.TIME)) { return
{formatTime(currentTime)} / {formatTime(totalTime)}
; } return null; } renderResourceNotFound = (locale: Locale['VideoPlayer']) => { return (
{locale.noResource}
); } renderPauseIcon = () => { const { isPlaying, isError } = this.state; if (!isPlaying && !isError) { return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
); } return null; } renderError = (locale: Locale['VideoPlayer']) => { const { isError } = this.state; const { theme } = this.props; if (isError) { return (
{locale.videoError}
); } return null; } renderPoster = () => { const { poster } = this.props; const { isPlaying, currentTime, totalTime } = this.state; const isHide = currentTime > 0 && currentTime < totalTime; if (!isPlaying && poster) { return ( poster ); } return null; } renderNotification = () => { const { showNotification, notificationContent } = this.state; if (!showNotification || !notificationContent) { return null; } return (
{this.state.notificationContent}
); } renderVolume = () => { const { volume, muted } = this.state; if (this.foundation.shouldShowControlItem(strings.VOLUME)) { return (
{muted ? 0 : volume}%
} >