|
@@ -1,10 +1,14 @@
|
|
|
import { debounce } from 'lodash'
|
|
|
-import React, { useEffect, useRef, useState } from 'react'
|
|
|
-import { useCallback } from 'react'
|
|
|
+import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
+import { CSSTransition } from 'react-transition-group'
|
|
|
|
|
|
import { usePersonalDataStore } from '@/providers'
|
|
|
import {
|
|
|
SvgOutlineVideo,
|
|
|
+ SvgPlayerBackwardFiveSec,
|
|
|
+ SvgPlayerBackwardTenSec,
|
|
|
+ SvgPlayerForwardFiveSec,
|
|
|
+ SvgPlayerForwardTenSec,
|
|
|
SvgPlayerFullScreen,
|
|
|
SvgPlayerPause,
|
|
|
SvgPlayerPip,
|
|
@@ -12,6 +16,7 @@ import {
|
|
|
SvgPlayerPlay,
|
|
|
SvgPlayerSmallScreen,
|
|
|
SvgPlayerSoundHalf,
|
|
|
+ SvgPlayerSoundOff,
|
|
|
SvgPlayerSoundOn,
|
|
|
} from '@/shared/icons'
|
|
|
import { Logger } from '@/utils/logger'
|
|
@@ -20,7 +25,12 @@ import { formatDurationShort } from '@/utils/time'
|
|
|
import {
|
|
|
Container,
|
|
|
ControlButton,
|
|
|
+ ControlsIndicator,
|
|
|
+ ControlsIndicatorTooltip,
|
|
|
+ ControlsIndicatorWrapper,
|
|
|
+ ControlsOverlay,
|
|
|
CurrentTime,
|
|
|
+ CurrentTimeWrapper,
|
|
|
CustomControls,
|
|
|
PlayOverlay,
|
|
|
ScreenControls,
|
|
@@ -30,7 +40,9 @@ import {
|
|
|
VolumeSlider,
|
|
|
VolumeSliderContainer,
|
|
|
} from './VideoPlayer.style'
|
|
|
-import { VOLUME_STEP, VideoJsConfig, useVideoJsPlayer } from './videoJsPlayer'
|
|
|
+import { CustomVideojsEvents, VOLUME_STEP, VideoJsConfig, useVideoJsPlayer } from './videoJsPlayer'
|
|
|
+
|
|
|
+import { Text } from '../Text'
|
|
|
|
|
|
export type VideoPlayerProps = {
|
|
|
className?: string
|
|
@@ -47,6 +59,14 @@ declare global {
|
|
|
}
|
|
|
|
|
|
const isPiPSupported = 'pictureInPictureEnabled' in document
|
|
|
+type VideoEvent = CustomVideojsEvents | null
|
|
|
+
|
|
|
+type EventState = {
|
|
|
+ type: VideoEvent
|
|
|
+ description: string | null
|
|
|
+ icon: React.ReactNode | null
|
|
|
+ isVisible: boolean
|
|
|
+}
|
|
|
|
|
|
const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, VideoPlayerProps> = (
|
|
|
{ className, autoplay, isInBackground, playing, ...videoJsConfig },
|
|
@@ -55,6 +75,7 @@ const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, Vid
|
|
|
const [player, playerRef] = useVideoJsPlayer(videoJsConfig)
|
|
|
const cachedPlayerVolume = usePersonalDataStore((state) => state.cachedPlayerVolume)
|
|
|
const updateCachedPlayerVolume = usePersonalDataStore((state) => state.actions.updateCachedPlayerVolume)
|
|
|
+ const [indicator, setIndicator] = useState<EventState | null>(null)
|
|
|
|
|
|
const [volume, setVolume] = useState(cachedPlayerVolume)
|
|
|
const [isPlaying, setIsPlaying] = useState(false)
|
|
@@ -66,10 +87,31 @@ const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, Vid
|
|
|
|
|
|
const displayPlayOverlay = playOverlayVisible && !isInBackground
|
|
|
|
|
|
+ // handle showing player indicators
|
|
|
+ useEffect(() => {
|
|
|
+ if (!player || isInBackground) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const indicatorEvents = Object.values(CustomVideojsEvents)
|
|
|
+ const handler = (e: Event) => {
|
|
|
+ const playerVolume = e.type === CustomVideojsEvents.Unmuted ? cachedPlayerVolume || VOLUME_STEP : player.volume()
|
|
|
+ const indicator = createIndicator(e.type as VideoEvent, playerVolume, player.muted())
|
|
|
+ if (indicator) {
|
|
|
+ setIndicator({ ...indicator, isVisible: true })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ player.on(indicatorEvents, handler)
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ player.off(indicatorEvents, handler)
|
|
|
+ }
|
|
|
+ }, [cachedPlayerVolume, isInBackground, player])
|
|
|
+
|
|
|
const playVideo = useCallback(() => {
|
|
|
if (!player) {
|
|
|
return
|
|
|
}
|
|
|
+ player.trigger(CustomVideojsEvents.PlayControl)
|
|
|
const playPromise = player.play()
|
|
|
if (playPromise) {
|
|
|
playPromise.catch((e) => {
|
|
@@ -200,23 +242,29 @@ const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, Vid
|
|
|
if (!player) {
|
|
|
return
|
|
|
}
|
|
|
+ const events = [
|
|
|
+ CustomVideojsEvents.VolumeIncrease,
|
|
|
+ CustomVideojsEvents.VolumeDecrease,
|
|
|
+ CustomVideojsEvents.Muted,
|
|
|
+ CustomVideojsEvents.Unmuted,
|
|
|
+ ]
|
|
|
|
|
|
const handler = (event: Event) => {
|
|
|
- if (event.type === 'mute') {
|
|
|
+ if (event.type === CustomVideojsEvents.Muted) {
|
|
|
setVolume(0)
|
|
|
return
|
|
|
}
|
|
|
- if (event.type === 'unmute') {
|
|
|
+ if (event.type === CustomVideojsEvents.Unmuted) {
|
|
|
setVolume(cachedPlayerVolume || VOLUME_STEP)
|
|
|
return
|
|
|
}
|
|
|
- if (event.type === 'volumechange') {
|
|
|
+ if (event.type === CustomVideojsEvents.VolumeIncrease || CustomVideojsEvents.VolumeDecrease) {
|
|
|
setVolume(player.volume())
|
|
|
}
|
|
|
}
|
|
|
- player.on(['volumechange', 'mute', 'unmute'], handler)
|
|
|
+ player.on(events, handler)
|
|
|
return () => {
|
|
|
- player.off(['volumechange', 'mute', 'unmute'], handler)
|
|
|
+ player.off(events, handler)
|
|
|
}
|
|
|
}, [cachedPlayerVolume, volume, player])
|
|
|
|
|
@@ -242,7 +290,12 @@ const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, Vid
|
|
|
|
|
|
// button/input handlers
|
|
|
const handlePlayPause = () => {
|
|
|
- isPlaying ? player?.pause() : playVideo()
|
|
|
+ if (isPlaying) {
|
|
|
+ player?.pause()
|
|
|
+ player?.trigger(CustomVideojsEvents.PauseControl)
|
|
|
+ } else {
|
|
|
+ playVideo()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const handleChangeVolume = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -296,36 +349,137 @@ const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, Vid
|
|
|
</PlayOverlay>
|
|
|
)}
|
|
|
<div data-vjs-player>
|
|
|
- <video ref={playerRef} className="video-js" />
|
|
|
+ <video
|
|
|
+ ref={playerRef}
|
|
|
+ className="video-js"
|
|
|
+ onClick={() => {
|
|
|
+ if (player?.paused()) {
|
|
|
+ player?.trigger(CustomVideojsEvents.PauseControl)
|
|
|
+ } else {
|
|
|
+ player?.trigger(CustomVideojsEvents.PlayControl)
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
{!isInBackground && !playOverlayVisible && (
|
|
|
- <CustomControls isFullScreen={isFullScreen}>
|
|
|
- <ControlButton onClick={handlePlayPause}>
|
|
|
- {isPlaying ? <SvgPlayerPause /> : <SvgPlayerPlay />}
|
|
|
- </ControlButton>
|
|
|
- <VolumeControl>
|
|
|
- <VolumeButton onClick={handleMute}>{renderVolumeButton()}</VolumeButton>
|
|
|
- <VolumeSliderContainer>
|
|
|
- <VolumeSlider step={0.01} max={1} min={0} value={volume} onChange={handleChangeVolume} type="range" />
|
|
|
- </VolumeSliderContainer>
|
|
|
- </VolumeControl>
|
|
|
- <CurrentTime variant="body2">
|
|
|
- {formatDurationShort(videoTime)} / {formatDurationShort(Math.floor(player?.duration() || 0))}
|
|
|
- </CurrentTime>
|
|
|
- <ScreenControls>
|
|
|
- {isPiPSupported && (
|
|
|
- <ControlButton onClick={handlePictureInPicture}>
|
|
|
- {isPiPEnabled ? <SvgPlayerPipDisable /> : <SvgPlayerPip />}
|
|
|
- </ControlButton>
|
|
|
- )}
|
|
|
- <ControlButton onClick={handleFullScreen}>
|
|
|
- {isFullScreen ? <SvgPlayerSmallScreen /> : <SvgPlayerFullScreen />}
|
|
|
+ <>
|
|
|
+ <ControlsOverlay isFullScreen={isFullScreen} />
|
|
|
+ <CustomControls isFullScreen={isFullScreen}>
|
|
|
+ <ControlButton onClick={handlePlayPause}>
|
|
|
+ {isPlaying ? <SvgPlayerPause /> : <SvgPlayerPlay />}
|
|
|
</ControlButton>
|
|
|
- </ScreenControls>
|
|
|
- </CustomControls>
|
|
|
+ <VolumeControl>
|
|
|
+ <VolumeButton onClick={handleMute}>{renderVolumeButton()}</VolumeButton>
|
|
|
+ <VolumeSliderContainer>
|
|
|
+ <VolumeSlider step={0.01} max={1} min={0} value={volume} onChange={handleChangeVolume} type="range" />
|
|
|
+ </VolumeSliderContainer>
|
|
|
+ </VolumeControl>
|
|
|
+ <CurrentTimeWrapper>
|
|
|
+ <CurrentTime variant="body2">
|
|
|
+ {formatDurationShort(videoTime)} / {formatDurationShort(Math.floor(player?.duration() || 0))}
|
|
|
+ </CurrentTime>
|
|
|
+ </CurrentTimeWrapper>
|
|
|
+ <ScreenControls>
|
|
|
+ {isPiPSupported && (
|
|
|
+ <ControlButton onClick={handlePictureInPicture}>
|
|
|
+ {isPiPEnabled ? <SvgPlayerPipDisable /> : <SvgPlayerPip />}
|
|
|
+ </ControlButton>
|
|
|
+ )}
|
|
|
+ <ControlButton onClick={handleFullScreen}>
|
|
|
+ {isFullScreen ? <SvgPlayerSmallScreen /> : <SvgPlayerFullScreen />}
|
|
|
+ </ControlButton>
|
|
|
+ </ScreenControls>
|
|
|
+ </CustomControls>
|
|
|
+ </>
|
|
|
)}
|
|
|
+ <CSSTransition
|
|
|
+ in={indicator?.isVisible}
|
|
|
+ timeout={indicator?.isVisible ? 0 : 750}
|
|
|
+ classNames="indicator"
|
|
|
+ mountOnEnter
|
|
|
+ unmountOnExit
|
|
|
+ onEntered={() => setIndicator((indicator) => (indicator ? { ...indicator, isVisible: false } : null))}
|
|
|
+ onExited={() => setIndicator(null)}
|
|
|
+ >
|
|
|
+ <ControlsIndicatorWrapper>
|
|
|
+ <ControlsIndicator>{indicator?.icon}</ControlsIndicator>
|
|
|
+ <ControlsIndicatorTooltip>
|
|
|
+ <Text variant="caption">{indicator?.description}</Text>
|
|
|
+ </ControlsIndicatorTooltip>
|
|
|
+ </ControlsIndicatorWrapper>
|
|
|
+ </CSSTransition>
|
|
|
</div>
|
|
|
</Container>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
export const VideoPlayer = React.forwardRef(VideoPlayerComponent)
|
|
|
+
|
|
|
+const createIndicator = (type: VideoEvent | null, playerVolume: number, playerMuted: boolean) => {
|
|
|
+ const formattedVolume = Math.floor(playerVolume * 100) + '%'
|
|
|
+ const isMuted = playerMuted || !Number(playerVolume.toFixed(2))
|
|
|
+
|
|
|
+ switch (type) {
|
|
|
+ case CustomVideojsEvents.PauseControl:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerPause />,
|
|
|
+ description: 'Pause',
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.PlayControl:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerPlay />,
|
|
|
+ description: 'Play',
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.BackwardFiveSec:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerBackwardFiveSec />,
|
|
|
+ description: 'Backward 5s',
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.ForwardFiveSec:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerForwardFiveSec />,
|
|
|
+ description: 'Forward 5s',
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.BackwardTenSec:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerBackwardTenSec />,
|
|
|
+ description: 'Backward 10s',
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.ForwardTenSec:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerForwardTenSec />,
|
|
|
+ description: 'Forward 10s',
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.Unmuted:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerSoundOn />,
|
|
|
+ description: formattedVolume,
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.Muted:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerSoundOff />,
|
|
|
+ description: 'Mute',
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.VolumeIncrease:
|
|
|
+ return {
|
|
|
+ icon: <SvgPlayerSoundOn />,
|
|
|
+ description: formattedVolume,
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ case CustomVideojsEvents.VolumeDecrease:
|
|
|
+ return {
|
|
|
+ icon: isMuted ? <SvgPlayerSoundOff /> : <SvgPlayerSoundHalf />,
|
|
|
+ description: isMuted ? 'Mute' : formattedVolume,
|
|
|
+ type,
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|