Ott 138 match page player (#97)
* Ott 138 match page player part 1 (#93) * feat(#138): added svg icons * feat(#138): added video player control components Co-authored-by: mirlan.maksitaliev <mirlan.maksitaliev@instatsport.com> * feat(#138): added player (#96) Co-authored-by: mirlan.maksitaliev <mirlan.maksitaliev@instatsport.com> Co-authored-by: mirlan.maksitaliev <mirlan.maksitaliev@instatsport.com>keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
|
After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 441 B |
|
After Width: | Height: | Size: 218 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 440 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 643 B |
|
After Width: | Height: | Size: 452 B |
@ -1,13 +1,17 @@ |
|||||||
import React from 'react' |
import React from 'react' |
||||||
|
|
||||||
|
import { VideoPlayer } from 'features/VideoPlayer' |
||||||
|
|
||||||
import { MatchProfileCard } from './MatchProfileCard' |
import { MatchProfileCard } from './MatchProfileCard' |
||||||
|
import { MainWrapper, Container } from './styled' |
||||||
|
|
||||||
import { |
const url = 'https://bserv.instatfootball.tv/common/outhls.m3u8' |
||||||
MainWrapper, |
|
||||||
} from './styled' |
|
||||||
|
|
||||||
export const MatchPage = () => ( |
export const MatchPage = () => ( |
||||||
<MainWrapper> |
<MainWrapper> |
||||||
<MatchProfileCard /> |
<MatchProfileCard /> |
||||||
|
<Container> |
||||||
|
<VideoPlayer url={url} /> |
||||||
|
</Container> |
||||||
</MainWrapper> |
</MainWrapper> |
||||||
) |
) |
||||||
|
|||||||
@ -1,6 +1,13 @@ |
|||||||
import styled from 'styled-components/macro' |
import styled from 'styled-components/macro' |
||||||
|
|
||||||
export const MainWrapper = styled.div` |
export const MainWrapper = styled.div` |
||||||
margin-left: 18px; |
margin: 63px 16px 0 16px; |
||||||
margin-top: 63px; |
` |
||||||
|
|
||||||
|
export const Container = styled.div` |
||||||
|
max-width: 1504px; |
||||||
|
max-height: 896px; |
||||||
|
margin-top: 14px; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
` |
` |
||||||
|
|||||||
@ -0,0 +1,39 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { secondsToHms } from 'helpers' |
||||||
|
|
||||||
|
import { useSlider } from 'features/VideoPlayer/hooks/useSlider' |
||||||
|
|
||||||
|
import { TimeTooltip } from '../TimeTooltip' |
||||||
|
import { |
||||||
|
ProgressBarList, |
||||||
|
LoadedProgress, |
||||||
|
PlayedProgress, |
||||||
|
Scrubber, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
loadedProgress: number, |
||||||
|
onPlayedProgressChange: (progress: number) => void, |
||||||
|
playedProgress: number, |
||||||
|
playedSeconds: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const ProgressBar = ({ |
||||||
|
loadedProgress, |
||||||
|
onPlayedProgressChange, |
||||||
|
playedProgress, |
||||||
|
playedSeconds, |
||||||
|
}: Props) => { |
||||||
|
const progressBarRef = useSlider({ onChange: onPlayedProgressChange }) |
||||||
|
|
||||||
|
return ( |
||||||
|
<ProgressBarList ref={progressBarRef}> |
||||||
|
<LoadedProgress value={loadedProgress} /> |
||||||
|
<PlayedProgress value={playedProgress} /> |
||||||
|
<Scrubber value={playedProgress}> |
||||||
|
<TimeTooltip time={secondsToHms(playedSeconds)} /> |
||||||
|
</Scrubber> |
||||||
|
</ProgressBarList> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
export const ProgressBarList = styled.div` |
||||||
|
flex-grow: 1; |
||||||
|
height: 4px; |
||||||
|
position: relative; |
||||||
|
background-color: rgba(255, 255, 255, 0.3); |
||||||
|
cursor: pointer; |
||||||
|
` |
||||||
|
|
||||||
|
type ProgressProps = { |
||||||
|
value: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const LoadedProgress = styled.div<ProgressProps>` |
||||||
|
position: absolute; |
||||||
|
z-index: 1; |
||||||
|
background-color: rgba(255, 255, 255, 0.6);; |
||||||
|
height: 100%; |
||||||
|
width: ${({ value }) => value}%; |
||||||
|
` |
||||||
|
|
||||||
|
export const PlayedProgress = styled.div<ProgressProps>` |
||||||
|
position: absolute; |
||||||
|
z-index: 2; |
||||||
|
background-color: #F2C94C; |
||||||
|
height: 100%; |
||||||
|
width: ${({ value }) => value}%; |
||||||
|
` |
||||||
|
|
||||||
|
export const Scrubber = styled.button<ProgressProps>` |
||||||
|
border: none; |
||||||
|
outline: none; |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: ${({ value }) => value}%; |
||||||
|
transform: translate(-50%, -38%); |
||||||
|
z-index: 3; |
||||||
|
width: 18px; |
||||||
|
height: 18px; |
||||||
|
background-color: #F2C94C; |
||||||
|
border-radius: 50%; |
||||||
|
cursor: pointer; |
||||||
|
` |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { Wrapper, Time } from './styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
time: string, |
||||||
|
} |
||||||
|
|
||||||
|
export const TimeTooltip = ({ time }: Props) => ( |
||||||
|
<Wrapper> |
||||||
|
<Time>{time}</Time> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
export const Wrapper = styled.div` |
||||||
|
position: absolute; |
||||||
|
bottom: 100%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
background-color: #000; |
||||||
|
min-width: 60px; |
||||||
|
height: 32px; |
||||||
|
|
||||||
|
::after { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
bottom: 0; |
||||||
|
left: 50%; |
||||||
|
width: 9px; |
||||||
|
height: 9px; |
||||||
|
transform: rotate(45deg) translateY(50%); |
||||||
|
background-color: #000; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Time = styled.span` |
||||||
|
padding: 0 6px; |
||||||
|
color: #F2C94C; |
||||||
|
text-align: center; |
||||||
|
font-weight: bold; |
||||||
|
font-size: 16px; |
||||||
|
line-height: 20px; |
||||||
|
` |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { useSlider } from 'features/VideoPlayer/hooks/useSlider' |
||||||
|
|
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
VolumeButton, |
||||||
|
VolumeProgressList, |
||||||
|
VolumeProgress, |
||||||
|
Scrubber, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
muted: boolean, |
||||||
|
onChange: (progress: number) => void, |
||||||
|
onClick: () => void, |
||||||
|
value: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const VolumeBar = ({ |
||||||
|
muted, |
||||||
|
onChange, |
||||||
|
onClick, |
||||||
|
value, |
||||||
|
}: Props) => { |
||||||
|
const progressRef = useSlider({ onChange }) |
||||||
|
return ( |
||||||
|
<Wrapper> |
||||||
|
<VolumeButton onClick={onClick} muted={muted} /> |
||||||
|
<VolumeProgressList ref={progressRef}> |
||||||
|
<VolumeProgress value={muted ? 0 : value} /> |
||||||
|
<Scrubber value={muted ? 0 : value} /> |
||||||
|
</VolumeProgressList> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,61 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
export const Wrapper = styled.div` |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
margin-right: 18px; |
||||||
|
` |
||||||
|
|
||||||
|
type VolumeButtonProps = { |
||||||
|
muted: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const VolumeButton = styled.button<VolumeButtonProps>` |
||||||
|
outline: none; |
||||||
|
border: none; |
||||||
|
padding: 0; |
||||||
|
width: 40px; |
||||||
|
height: 26px; |
||||||
|
margin-right: 9px; |
||||||
|
cursor: pointer; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-position: center; |
||||||
|
background-size: cover; |
||||||
|
background-color: transparent; |
||||||
|
background-image: ${({ muted }) => ( |
||||||
|
muted |
||||||
|
? 'url(/images/player-volume-off.svg)' |
||||||
|
: 'url(/images/player-volume-on.svg)' |
||||||
|
)}; |
||||||
|
` |
||||||
|
|
||||||
|
export const VolumeProgressList = styled.div` |
||||||
|
width: 77px; |
||||||
|
height: 4px; |
||||||
|
position: relative; |
||||||
|
background-color: rgba(255, 255, 255, 0.3); |
||||||
|
cursor: pointer; |
||||||
|
` |
||||||
|
|
||||||
|
type VolumeProgressProps = { |
||||||
|
value: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const VolumeProgress = styled.div<VolumeProgressProps>` |
||||||
|
height: 100%; |
||||||
|
width: ${({ value }) => value}%; |
||||||
|
background-color: #fff; |
||||||
|
` |
||||||
|
|
||||||
|
export const Scrubber = styled.button<VolumeProgressProps>` |
||||||
|
border: none; |
||||||
|
outline: none; |
||||||
|
width: 13px; |
||||||
|
height: 13px; |
||||||
|
left: ${({ value }) => value}%;; |
||||||
|
position: absolute; |
||||||
|
border-radius: 50%; |
||||||
|
background-color: #fff; |
||||||
|
transform: translate(-50%, -65%); |
||||||
|
cursor: pointer; |
||||||
|
` |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
export const playerConfig = { |
||||||
|
file: { |
||||||
|
forceHLS: true, |
||||||
|
hlsOptions: { |
||||||
|
liveSyncDuration: 10, |
||||||
|
maxBufferLength: 10, |
||||||
|
maxBufferSize: 0, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
export const progressCallbackInterval = 100 |
||||||
@ -0,0 +1,91 @@ |
|||||||
|
import type { MouseEvent } from 'react' |
||||||
|
import { |
||||||
|
useCallback, |
||||||
|
useState, |
||||||
|
useRef, |
||||||
|
} from 'react' |
||||||
|
import ReactPlayer from 'react-player' |
||||||
|
|
||||||
|
import throttle from 'lodash/throttle' |
||||||
|
|
||||||
|
import { useFullscreen } from './useFullscreen' |
||||||
|
import { useVolume } from './useVolume' |
||||||
|
|
||||||
|
type ProgressState = { |
||||||
|
loaded: number, |
||||||
|
loadedSeconds: number, |
||||||
|
played: number, |
||||||
|
playedSeconds: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const useVideoPlayer = () => { |
||||||
|
const [ready, setReady] = useState(false) |
||||||
|
const [playing, setPlaying] = useState(false) |
||||||
|
const [duration, setDuration] = useState(0) |
||||||
|
const [loadedProgress, setLoadedProgress] = useState(0) |
||||||
|
const [playedProgress, setPlayedProgress] = useState(0) |
||||||
|
const [playedSeconds, setPlayedSeconds] = useState(0) |
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null) |
||||||
|
const playerRef = useRef<ReactPlayer>(null) |
||||||
|
|
||||||
|
const startPlaying = () => { |
||||||
|
setReady(true) |
||||||
|
setPlaying(true) |
||||||
|
} |
||||||
|
|
||||||
|
const togglePlaying = () => { |
||||||
|
if (ready) { |
||||||
|
setPlaying(!playing) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onPlayerClick = (e: MouseEvent<HTMLDivElement>) => { |
||||||
|
if (e.target === playerRef.current?.getInternalPlayer()) { |
||||||
|
togglePlaying() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const setPlayerProgress = useCallback( |
||||||
|
throttle((value: number) => { |
||||||
|
playerRef.current?.seekTo(value, 'fraction') |
||||||
|
}, 100), |
||||||
|
[], |
||||||
|
) |
||||||
|
|
||||||
|
const onProgressChange = useCallback((progress: number) => { |
||||||
|
setPlayedProgress(progress * 100) |
||||||
|
setPlayerProgress(progress) |
||||||
|
}, [ |
||||||
|
setPlayedProgress, |
||||||
|
setPlayerProgress, |
||||||
|
]) |
||||||
|
|
||||||
|
const setProgress = ({ |
||||||
|
loaded, |
||||||
|
played, |
||||||
|
playedSeconds: seconds, |
||||||
|
}: ProgressState) => { |
||||||
|
setLoadedProgress(loaded * 100) |
||||||
|
setPlayedProgress(played * 100) |
||||||
|
setPlayedSeconds(seconds) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
duration, |
||||||
|
loadedProgress, |
||||||
|
onPlayerClick, |
||||||
|
onProgressChange, |
||||||
|
playedProgress, |
||||||
|
playedSeconds, |
||||||
|
playerRef, |
||||||
|
playing, |
||||||
|
setDuration, |
||||||
|
setProgress, |
||||||
|
setReady, |
||||||
|
startPlaying, |
||||||
|
togglePlaying, |
||||||
|
wrapperRef, |
||||||
|
...useFullscreen(wrapperRef), |
||||||
|
...useVolume(), |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
import type { RefObject } from 'react' |
||||||
|
import { useEffect } from 'react' |
||||||
|
import screenfull from 'screenfull' |
||||||
|
|
||||||
|
import noop from 'lodash/noop' |
||||||
|
|
||||||
|
import { useToggle } from 'hooks' |
||||||
|
|
||||||
|
export const useFullscreen = (wrapperRef: RefObject<HTMLDivElement>) => { |
||||||
|
const { |
||||||
|
isOpen: isFullscreen, |
||||||
|
setIsOpen: setFullscreen, |
||||||
|
} = useToggle() |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!screenfull.isEnabled) return noop |
||||||
|
const changeListener = () => { |
||||||
|
setFullscreen(screenfull.isEnabled && screenfull.isFullscreen) |
||||||
|
} |
||||||
|
screenfull.onchange(changeListener) |
||||||
|
|
||||||
|
return () => { |
||||||
|
if (screenfull.isEnabled) { |
||||||
|
screenfull.off('change', changeListener) |
||||||
|
} |
||||||
|
} |
||||||
|
}, [setFullscreen]) |
||||||
|
|
||||||
|
const onFullscreenClick = () => { |
||||||
|
if (screenfull.isEnabled && wrapperRef.current) { |
||||||
|
screenfull.toggle(wrapperRef.current) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { isFullscreen, onFullscreenClick } |
||||||
|
} |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
import { |
||||||
|
useCallback, |
||||||
|
useEffect, |
||||||
|
useRef, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
const getNormalizedProgress = (fraction: number) => { |
||||||
|
if (fraction > 1) return 1 |
||||||
|
if (fraction < 0) return 0 |
||||||
|
return fraction |
||||||
|
} |
||||||
|
|
||||||
|
const useMouseState = () => { |
||||||
|
const mouseDownRef = useRef(false) |
||||||
|
const setMouseDown = useCallback(() => { |
||||||
|
mouseDownRef.current = true |
||||||
|
}, []) |
||||||
|
const setMouseUp = useCallback(() => { |
||||||
|
mouseDownRef.current = false |
||||||
|
}, []) |
||||||
|
|
||||||
|
return { |
||||||
|
mouseDownRef, |
||||||
|
setMouseDown, |
||||||
|
setMouseUp, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type Args = { |
||||||
|
onChange: (progress: number) => void, |
||||||
|
} |
||||||
|
|
||||||
|
export const useSlider = ({ onChange }: Args) => { |
||||||
|
const ref = useRef<HTMLDivElement>(null) |
||||||
|
const { |
||||||
|
mouseDownRef, |
||||||
|
setMouseDown, |
||||||
|
setMouseUp, |
||||||
|
} = useMouseState() |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const handleProgress = (mouseX: number) => { |
||||||
|
const track = ref.current |
||||||
|
if (!mouseDownRef.current || !track) return |
||||||
|
|
||||||
|
const x = mouseX - track.getBoundingClientRect().left |
||||||
|
const progress = getNormalizedProgress(x / track.offsetWidth) |
||||||
|
onChange(progress) |
||||||
|
} |
||||||
|
|
||||||
|
const mouseDownListener = ({ clientX, target }: MouseEvent) => { |
||||||
|
const track = ref.current |
||||||
|
if (target === track || track?.contains(target as Node)) { |
||||||
|
setMouseDown() |
||||||
|
handleProgress(clientX) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mouseUpListener = setMouseUp |
||||||
|
|
||||||
|
const mouseMoveListener = (e: MouseEvent) => { |
||||||
|
handleProgress(e.clientX) |
||||||
|
} |
||||||
|
|
||||||
|
window.addEventListener('mousedown', mouseDownListener) |
||||||
|
window.addEventListener('mouseup', mouseUpListener) |
||||||
|
window.addEventListener('mousemove', mouseMoveListener) |
||||||
|
return () => { |
||||||
|
window.removeEventListener('mousedown', mouseDownListener) |
||||||
|
window.removeEventListener('mouseup', mouseUpListener) |
||||||
|
window.removeEventListener('mousemove', mouseMoveListener) |
||||||
|
} |
||||||
|
}, [ |
||||||
|
onChange, |
||||||
|
mouseDownRef, |
||||||
|
setMouseDown, |
||||||
|
setMouseUp, |
||||||
|
]) |
||||||
|
|
||||||
|
return ref |
||||||
|
} |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
import { useState } from 'react' |
||||||
|
|
||||||
|
import { useToggle } from 'hooks' |
||||||
|
|
||||||
|
export const useVolume = () => { |
||||||
|
const { |
||||||
|
close: unmute, |
||||||
|
isOpen: muted, |
||||||
|
open: mute, |
||||||
|
toggle: toggleMuted, |
||||||
|
} = useToggle() |
||||||
|
const [volume, setVolume] = useState(0.5) |
||||||
|
|
||||||
|
const onVolumeChange = (value: number) => { |
||||||
|
if (value === 0) { |
||||||
|
mute() |
||||||
|
} else { |
||||||
|
unmute() |
||||||
|
} |
||||||
|
setVolume(value) |
||||||
|
} |
||||||
|
|
||||||
|
const onVolumeClick = () => { |
||||||
|
if (muted && volume === 0) { |
||||||
|
setVolume(0.1) |
||||||
|
} |
||||||
|
toggleMuted() |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
muted, |
||||||
|
onVolumeChange, |
||||||
|
onVolumeClick, |
||||||
|
volume, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { playerConfig, progressCallbackInterval } from './config' |
||||||
|
import { useVideoPlayer } from './hooks' |
||||||
|
import { ProgressBar } from './components/ProgressBar' |
||||||
|
import { VolumeBar } from './components/VolumeBar' |
||||||
|
import { |
||||||
|
PlayerWrapper, |
||||||
|
ReactPlayer, |
||||||
|
Controls, |
||||||
|
Prev, |
||||||
|
Next, |
||||||
|
PlayStop, |
||||||
|
Fullscreen, |
||||||
|
Settings, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
url: string, |
||||||
|
} |
||||||
|
|
||||||
|
export const VideoPlayer = ({ url }: Props) => { |
||||||
|
const { |
||||||
|
isFullscreen, |
||||||
|
loadedProgress, |
||||||
|
muted, |
||||||
|
onFullscreenClick, |
||||||
|
onPlayerClick, |
||||||
|
onProgressChange, |
||||||
|
onVolumeChange, |
||||||
|
onVolumeClick, |
||||||
|
playedProgress, |
||||||
|
playedSeconds, |
||||||
|
playerRef, |
||||||
|
playing, |
||||||
|
setDuration, |
||||||
|
setProgress, |
||||||
|
startPlaying, |
||||||
|
togglePlaying, |
||||||
|
volume, |
||||||
|
wrapperRef, |
||||||
|
} = useVideoPlayer() |
||||||
|
|
||||||
|
const volumeInPercent = volume * 100 |
||||||
|
|
||||||
|
return ( |
||||||
|
<PlayerWrapper |
||||||
|
ref={wrapperRef} |
||||||
|
playing={playing} |
||||||
|
onClick={onPlayerClick} |
||||||
|
> |
||||||
|
<ReactPlayer |
||||||
|
width='100%' |
||||||
|
height='100%' |
||||||
|
url={url} |
||||||
|
ref={playerRef} |
||||||
|
playing={playing} |
||||||
|
volume={volume} |
||||||
|
config={playerConfig} |
||||||
|
progressInterval={progressCallbackInterval} |
||||||
|
onProgress={setProgress} |
||||||
|
onDuration={setDuration} |
||||||
|
onEnded={togglePlaying} |
||||||
|
onReady={startPlaying} |
||||||
|
/> |
||||||
|
<Controls> |
||||||
|
<Prev /> |
||||||
|
<PlayStop onClick={togglePlaying} playing={playing} /> |
||||||
|
<Next /> |
||||||
|
<VolumeBar |
||||||
|
value={volumeInPercent} |
||||||
|
muted={muted} |
||||||
|
onChange={onVolumeChange} |
||||||
|
onClick={onVolumeClick} |
||||||
|
/> |
||||||
|
<ProgressBar |
||||||
|
onPlayedProgressChange={onProgressChange} |
||||||
|
playedSeconds={playedSeconds} |
||||||
|
playedProgress={playedProgress} |
||||||
|
loadedProgress={loadedProgress} |
||||||
|
/> |
||||||
|
<Fullscreen onClick={onFullscreenClick} isFullscreen={isFullscreen} /> |
||||||
|
<Settings /> |
||||||
|
</Controls> |
||||||
|
</PlayerWrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,106 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
import ReactPlayerBase from 'react-player' |
||||||
|
|
||||||
|
export const ReactPlayer = styled(ReactPlayerBase)` |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
` |
||||||
|
|
||||||
|
export const Controls = styled.div` |
||||||
|
position: absolute; |
||||||
|
width: 100%; |
||||||
|
bottom: 22px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 0 25px; |
||||||
|
transition: opacity 0.3s ease-in-out; |
||||||
|
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25)); |
||||||
|
` |
||||||
|
|
||||||
|
export const PlayerWrapper = styled.div<PlayStopProps>` |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
height: 0px; |
||||||
|
padding-top: 56.25%; |
||||||
|
background-color: #000; |
||||||
|
|
||||||
|
:hover ${Controls} { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
${Controls} { |
||||||
|
opacity: ${({ playing }) => ( |
||||||
|
playing |
||||||
|
? '0' |
||||||
|
: '1' |
||||||
|
)}; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
const ButtonBase = styled.button` |
||||||
|
outline: none; |
||||||
|
border: none; |
||||||
|
color: #fff; |
||||||
|
cursor: pointer; |
||||||
|
background-color: transparent; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-position: center; |
||||||
|
background-size: contain; |
||||||
|
` |
||||||
|
|
||||||
|
type PlayStopProps = { |
||||||
|
playing: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const PlayStop = styled(ButtonBase)<PlayStopProps>` |
||||||
|
width: 24px; |
||||||
|
height: 24px; |
||||||
|
margin-right: 18px; |
||||||
|
background-image: ${({ playing }) => ( |
||||||
|
playing |
||||||
|
? 'url(/images/player-pause.svg)' |
||||||
|
: 'url(/images/player-play.svg)' |
||||||
|
)}; |
||||||
|
` |
||||||
|
|
||||||
|
export const Prev = styled(ButtonBase)` |
||||||
|
width: 29px; |
||||||
|
height: 28px; |
||||||
|
margin-right: 19px; |
||||||
|
background-image: url(/images/player-prev.svg); |
||||||
|
` |
||||||
|
|
||||||
|
export const Next = styled(Prev)` |
||||||
|
margin-right: 24px; |
||||||
|
transform: rotate(180deg); |
||||||
|
` |
||||||
|
|
||||||
|
export const Volume = styled(ButtonBase)` |
||||||
|
width: 40px; |
||||||
|
height: 26px; |
||||||
|
margin-right: 23px; |
||||||
|
background-image: url(/images/player-volume-off.svg); |
||||||
|
` |
||||||
|
|
||||||
|
type FullscreenProps = { |
||||||
|
isFullscreen: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const Fullscreen = styled(ButtonBase)<FullscreenProps>` |
||||||
|
width: 22px; |
||||||
|
height: 20px; |
||||||
|
margin-left: 31px; |
||||||
|
margin-right: 23px; |
||||||
|
background-image: ${({ isFullscreen }) => ( |
||||||
|
isFullscreen |
||||||
|
? 'url(/images/player-fullscreen-off.svg)' |
||||||
|
: 'url(/images/player-fullscreen-on.svg)' |
||||||
|
)}; |
||||||
|
` |
||||||
|
|
||||||
|
export const Settings = styled(ButtonBase)` |
||||||
|
width: 22px; |
||||||
|
height: 20px; |
||||||
|
background-image: url(/images/player-settings.svg); |
||||||
|
` |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
import { secondsToHms } from '..' |
||||||
|
|
||||||
|
describe('secondsToHms helper', () => { |
||||||
|
it('returns correct formatted time', () => { |
||||||
|
expect(secondsToHms(0)).toBe('00:00') |
||||||
|
expect(secondsToHms(120)).toBe('02:00') |
||||||
|
expect(secondsToHms(600)).toBe('10:00') |
||||||
|
expect(secondsToHms(3600)).toBe('01:00:00') |
||||||
|
expect(secondsToHms(3700)).toBe('01:01:40') |
||||||
|
}) |
||||||
|
}) |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
const prependZero = (value: number | string) => ( |
||||||
|
`0${value}`.slice(-2) |
||||||
|
) |
||||||
|
|
||||||
|
/** |
||||||
|
* Форматирует секунды в hh:mm:ss |
||||||
|
*/ |
||||||
|
export const secondsToHms = (seconds: number) => { |
||||||
|
const hours = prependZero(Math.floor(seconds / 3600)) |
||||||
|
const minutes = prependZero(Math.floor(seconds % 3600 / 60)) |
||||||
|
const secondsStr = prependZero(Math.floor(seconds % 3600 % 60)) |
||||||
|
|
||||||
|
if (Number(hours) > 0) return `${hours}:${minutes}:${secondsStr}` |
||||||
|
|
||||||
|
return `${minutes}:${secondsStr}` |
||||||
|
} |
||||||