Ott 892 match profile imprs (#327)

* refactor(892): split match players into 2 components (#324)

* Ott 892 part 2 (#325)

* fix(892): micro fix

* fix(892): hls player ui impr.

* fix(892): finished match player ui impr. (#326)

* fix(892): rewind btw episodes (#334)

* refactor(892): removed resuming from MultiSourcePlayer (#335)
keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 5 years ago committed by GitHub
parent e3a683f37d
commit 20e58e7c5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      public/images/player-backward.svg
  2. 3
      public/images/player-forward.svg
  3. 9
      src/features/MatchPage/components/FinishedMatch/helpers.tsx
  4. 91
      src/features/MatchPage/components/FinishedMatch/hooks/index.tsx
  5. 17
      src/features/MatchPage/components/FinishedMatch/hooks/useChapters.tsx
  6. 0
      src/features/MatchPage/components/FinishedMatch/hooks/useEpisodes.tsx
  7. 72
      src/features/MatchPage/components/FinishedMatch/index.tsx
  8. 27
      src/features/MatchPage/components/FinishedMatch/styled.tsx
  9. 30
      src/features/MatchPage/components/LiveMatch/hooks/index.tsx
  10. 32
      src/features/MatchPage/components/LiveMatch/index.tsx
  11. 115
      src/features/MatchPage/hooks/useMatchPage.tsx
  12. 93
      src/features/MatchPage/index.tsx
  13. 34
      src/features/MatchPage/styled.tsx
  14. 10
      src/features/MatchPopup/store/hooks/usePopupNavigation.tsx
  15. 5
      src/features/MatchSidePlaylists/styled.tsx
  16. 7
      src/features/MultiSourcePlayer/components/ProgressBar/hooks.tsx
  17. 7
      src/features/MultiSourcePlayer/components/ProgressBar/stories.tsx
  18. 7
      src/features/MultiSourcePlayer/components/Settings/index.tsx
  19. 9
      src/features/MultiSourcePlayer/components/Settings/styled.tsx
  20. 3
      src/features/MultiSourcePlayer/config.tsx
  21. 92
      src/features/MultiSourcePlayer/hooks/index.tsx
  22. 45
      src/features/MultiSourcePlayer/hooks/usePlayingHandlers.tsx
  23. 123
      src/features/MultiSourcePlayer/index.tsx
  24. 32
      src/features/MultiSourcePlayer/styled.tsx
  25. 1
      src/features/MultiSourcePlayer/types.tsx
  26. 4
      src/features/StreamPlayer/components/ProgressBar/styled.tsx
  27. 2
      src/features/StreamPlayer/components/TimeTooltip/styled.tsx
  28. 10
      src/features/StreamPlayer/hooks/index.tsx
  29. 72
      src/features/StreamPlayer/index.tsx
  30. 136
      src/features/StreamPlayer/styled.tsx
  31. 1
      src/requests/getMatchInfo.tsx

@ -0,0 +1,3 @@
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 14.6151C1 22.0072 6.9925 27.9997 14.3846 27.9997C21.7767 27.9997 27.7692 22.0072 27.7692 14.6151C27.7692 7.22296 21.7767 1.23047 14.3846 1.23047C9.6128 1.23047 5.42422 3.72757 3.05433 7.48632M3.05433 7.48632V2.12278M3.05433 7.48632H8.4359" stroke="white" stroke-width="1.18974" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

@ -0,0 +1,3 @@
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.5391 14.6151C27.5391 22.0072 21.5466 27.9997 14.1544 27.9997C6.76233 27.9997 0.769833 22.0072 0.769833 14.6151C0.769833 7.22296 6.76233 1.23047 14.1544 1.23047C18.9263 1.23047 23.1148 3.72757 25.4847 7.48632M25.4847 7.48632V2.12278M25.4847 7.48632H20.1032" stroke="white" stroke-width="1.18974" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 436 B

@ -16,8 +16,8 @@ import type {
import type { Chapters, Urls } from 'features/MultiSourcePlayer/types'
import type { PlaylistOption } from '../types'
import { FULL_GAME_KEY } from './buildPlaylists'
import type { PlaylistOption } from '../../types'
import { FULL_GAME_KEY } from '../../helpers/buildPlaylists'
const getUniquePeriods = (videos: Videos) => uniq(map(videos, ({ period }) => period))
@ -64,9 +64,11 @@ const getFullMatchChapters = (videos: Array<Video>) => {
sortedVideos,
(acc: Chapters, video) => {
const prevVideoEndMs = last(acc)?.endMs || 0
const endMs = prevVideoEndMs + video.duration
const nextChapter = {
duration: video.duration,
endMs: prevVideoEndMs + video.duration,
endMs,
endOffsetMs: endMs,
period: video.period,
startMs: prevVideoEndMs,
startOffsetMs: 0,
@ -104,6 +106,7 @@ const getPlaylistChapters = (videos: Array<Video>, episodes: Episodes) => {
const nextChapter = {
duration: episodeDuration,
endMs: prevVideoEndMs + episodeDuration,
endOffsetMs: episode.e * 1000,
period: video.period,
startMs: prevVideoEndMs,
startOffsetMs: episode.s * 1000,

@ -0,0 +1,91 @@
import { useEffect } from 'react'
import isEqual from 'lodash/isEqual'
import type { MatchInfo } from 'requests'
import {
useSportNameParam,
usePageId,
useToggle,
} from 'hooks'
import type { Settings } from 'features/MatchPopup'
import { useMatchPopupStore } from 'features/MatchPopup'
import { usePlayerProgressReporter } from 'features/MatchPage/hooks/usePlayerProgressReporter'
import { useEpisodes } from './useEpisodes'
import { useChapters } from './useChapters'
export type Props = {
profile: MatchInfo,
}
export const useFinishedMatch = ({ profile }: Props) => {
const {
actions,
fetchMatchPlaylists,
handlePlaylistClick,
matchPlaylists,
selectedPlaylist,
setMatch,
setSettings,
settings,
} = useMatchPopupStore()
const { sportType } = useSportNameParam()
const matchId = usePageId()
const {
close: closeSettingsPopup,
isOpen: isSettingsPopupOpen,
open: openSettingsPopup,
} = useToggle()
const { episodes } = useEpisodes()
useEffect(() => {
if (profile) {
const match = {
calc: false,
id: matchId,
live: false,
sportType,
team1: profile.team1,
team2: profile.team2,
}
setMatch(match)
fetchMatchPlaylists(match)
}
}, [
matchId,
profile,
setMatch,
sportType,
fetchMatchPlaylists,
])
const setEpisodesSettings = (newSettings: Settings) => {
const settingsChanged = !isEqual(newSettings, settings)
if (settingsChanged) {
setSettings(newSettings)
}
closeSettingsPopup()
}
return {
actions,
closeSettingsPopup,
isSettingsPopupOpen,
onPlaylistSelect: handlePlaylistClick,
openSettingsPopup,
playlists: matchPlaylists,
profile,
selectedPlaylist,
setEpisodesSettings,
settings,
...useChapters({
episodes,
selectedPlaylist,
}),
...usePlayerProgressReporter(),
}
}

@ -9,18 +9,17 @@ import { getVideos } from 'requests'
import { usePageId, useSportNameParam } from 'hooks'
import type { PlaylistOption } from '../types'
import { buildChapters } from '../helpers/buildChapters'
import type { PlaylistOption } from 'features/MatchPage/types'
import { buildChapters } from '../helpers'
type Args = {
episodes: Episodes,
isFinishedMatch: boolean,
selectedPlaylist?: PlaylistOption,
}
export const useChapters = ({
episodes,
isFinishedMatch,
selectedPlaylist,
}: Args) => {
const [videos, setVideos] = useState<Videos>([])
@ -28,14 +27,8 @@ export const useChapters = ({
const matchId = usePageId()
useEffect(() => {
if (isFinishedMatch) {
getVideos(sportType, matchId).then(setVideos)
}
}, [
isFinishedMatch,
matchId,
sportType,
])
getVideos(sportType, matchId).then(setVideos)
}, [sportType, matchId])
const chapters = useMemo(
() => buildChapters({

@ -0,0 +1,72 @@
import { Fragment } from 'react'
import isEmpty from 'lodash/isEmpty'
import { MatchSidePlaylists } from 'features/MatchSidePlaylists'
import { MultiSourcePlayer } from 'features/MultiSourcePlayer'
import { MatchProfileCard } from '../MatchProfileCard'
import { SettingsPopup } from '../SettingsPopup'
import type { Props } from './hooks'
import { useFinishedMatch } from './hooks'
import { Container } from '../../styled'
import { Modal } from './styled'
export const FinishedMatch = (props: Props) => {
const { profile } = props
const {
actions,
chapters,
closeSettingsPopup,
isSettingsPopupOpen,
onPlayerProgressChange,
onPlayingChange,
onPlaylistSelect,
openSettingsPopup,
playlists,
selectedPlaylist,
setEpisodesSettings,
settings,
} = useFinishedMatch(props)
return (
<Fragment>
<Modal
close={closeSettingsPopup}
isOpen={isSettingsPopupOpen}
withCloseButton={false}
>
<SettingsPopup
actions={actions}
onWatchEpisodesClick={setEpisodesSettings}
settings={settings}
closePopup={closeSettingsPopup}
profile={profile}
selectedPlaylist={selectedPlaylist}
/>
</Modal>
<Container>
<MatchProfileCard profile={profile} />
{!isEmpty(chapters) && (
<MultiSourcePlayer
chapters={chapters}
onPlayingChange={onPlayingChange}
onProgressChange={onPlayerProgressChange}
/>
)}
</Container>
{playlists && (
<MatchSidePlaylists
playlists={playlists}
selectedPlaylist={selectedPlaylist}
onSelect={onPlaylistSelect}
profile={profile}
openPopup={openSettingsPopup}
/>
)}
</Fragment>
)
}

@ -0,0 +1,27 @@
import styled from 'styled-components/macro'
import { devices } from 'config/devices'
import { Modal as BaseModal } from 'features/Modal'
import { ModalWindow } from 'features/Modal/styled'
export const Modal = styled(BaseModal)`
background-color: rgba(0, 0, 0, 0.7);
${ModalWindow} {
width: 1222px;
padding: 20px 0;
background-color: #3F3F3F;
border-radius: 5px;
@media ${devices.tablet} {
width: 100vw;
}
@media ${devices.mobile} {
height: 100vh;
padding: 0;
background-color: transparent;
}
}
`

@ -0,0 +1,30 @@
import { useEffect, useState } from 'react'
import type { LiveVideos } from 'requests'
import { getLiveVideos } from 'requests'
import {
useSportNameParam,
usePageId,
} from 'hooks'
import { usePlayerProgressReporter } from 'features/MatchPage/hooks/usePlayerProgressReporter'
import { useLastPlayPosition } from 'features/MatchPage/hooks/useLastPlayPosition'
export const useLiveMatch = () => {
const { sportType } = useSportNameParam()
const matchId = usePageId()
const [liveVideos, setLiveVideos] = useState<LiveVideos>([])
useEffect(() => {
getLiveVideos(sportType, matchId).then(setLiveVideos)
},
[sportType, matchId])
return {
matchUrl: liveVideos[0] || '',
...usePlayerProgressReporter(),
...useLastPlayPosition(),
}
}

@ -0,0 +1,32 @@
import type { MatchInfo } from 'requests'
import { StreamPlayer } from 'features/StreamPlayer'
import { useLiveMatch } from './hooks'
import { MatchProfileCard } from '../MatchProfileCard'
import { Container } from '../../styled'
type Props = {
profile: MatchInfo,
}
export const LiveMatch = ({ profile }: Props) => {
const {
lastPlayPosition,
matchUrl,
onPlayerProgressChange,
onPlayingChange,
} = useLiveMatch()
return (
<Container marginRight={320}>
<MatchProfileCard profile={profile} />
<StreamPlayer
url={matchUrl}
onPlayingChange={onPlayingChange}
onProgressChange={onPlayerProgressChange}
resumeFrom={lastPlayPosition.second}
/>
</Container>
)
}

@ -1,115 +0,0 @@
import { useEffect, useState } from 'react'
import isEqual from 'lodash/isEqual'
import type { LiveVideos } from 'requests'
import { getLiveVideos } from 'requests'
import {
useSportNameParam,
usePageId,
useToggle,
} from 'hooks'
import { Settings, useMatchPopupStore } from 'features/MatchPopup'
import { useLastPlayPosition } from './useLastPlayPosition'
import { useEpisodes } from './useEpisodes'
import { useChapters } from './useChapters'
import { useMatchProfile } from './useMatchProfile'
export const useMatchPage = () => {
const {
actions,
closePopup,
fetchMatchPlaylists,
handlePlaylistClick,
matchPlaylists,
selectedPlaylist,
setMatch,
setSettings,
settings,
} = useMatchPopupStore()
const [isFinishedMatch, setFinishedMatch] = useState(Boolean(selectedPlaylist))
const [liveVideos, setLiveVideos] = useState<LiveVideos>([])
const profile = useMatchProfile()
const { sportType } = useSportNameParam()
const matchId = usePageId()
const {
close,
isOpen,
open,
} = useToggle()
const { episodes } = useEpisodes()
useEffect(() => {
if (!isFinishedMatch) {
getLiveVideos(sportType, matchId)
.then(setLiveVideos)
.catch(() => setFinishedMatch(true))
}
},
[
isFinishedMatch,
sportType,
matchId,
])
useEffect(() => {
if (profile && isFinishedMatch) {
const match = {
calc: false,
id: matchId,
live: false,
sportType,
team1: profile.team1,
team2: profile.team2,
}
setMatch(match)
fetchMatchPlaylists(match)
}
}, [
matchId,
profile,
setMatch,
sportType,
isFinishedMatch,
fetchMatchPlaylists,
])
const setEpisodesSettings = (values: Settings) => {
const isSettingsChanged = !isEqual(values, settings)
if (isSettingsChanged) {
setSettings({
episodeDuration: values.episodeDuration,
selectedActions: values.selectedActions,
selectedFormat: values.selectedFormat,
})
}
close()
}
useEffect(() => {
closePopup()
}, [closePopup])
return {
actions,
closeSettingsPopup: close,
isOpen,
onPlaylistSelect: handlePlaylistClick,
openSettingsPopup: open,
playlists: matchPlaylists,
profile,
selectedPlaylist,
setEpisodesSettings,
settings,
url: liveVideos[0] || '',
...useChapters({
episodes,
isFinishedMatch,
selectedPlaylist,
}),
...useLastPlayPosition(),
}
}

@ -1,103 +1,26 @@
import isEmpty from 'lodash/isEmpty'
import { ProfileHeader } from 'features/ProfileHeader'
import { MainWrapper } from 'features/MainWrapper'
import { UserFavorites } from 'features/UserFavorites'
import { MediaQuery } from 'features/MediaQuery'
import { MatchSidePlaylists } from 'features/MatchSidePlaylists'
import { MultiSourcePlayer } from 'features/MultiSourcePlayer'
import { StreamPlayer } from 'features/StreamPlayer'
import { MatchProfileCard } from './components/MatchProfileCard'
import {
MainWrapper as Wrapper,
Modal,
Container,
} from './styled'
import { useMatchPage } from './hooks/useMatchPage'
import { usePlayerProgressReporter } from './hooks/usePlayerProgressReporter'
import { SettingsPopup } from './components/SettingsPopup'
import { FinishedMatch } from './components/FinishedMatch'
import { LiveMatch } from './components/LiveMatch'
import { useMatchProfile } from './hooks/useMatchProfile'
import { Wrapper } from './styled'
const MatchPage = () => {
const {
actions,
chapters,
closeSettingsPopup,
isLastPlayPositionFetching,
isOpen,
lastPlayPosition,
onPlaylistSelect,
openSettingsPopup,
playlists,
profile,
selectedPlaylist,
setEpisodesSettings,
settings,
url,
} = useMatchPage()
const { onPlayerProgressChange, onPlayingChange } = usePlayerProgressReporter()
const isLiveMatch = Boolean(url) && !isLastPlayPositionFetching
const isFinishedMatch = !isEmpty(chapters) && !isLastPlayPositionFetching
const profile = useMatchProfile()
const isLiveMatch = !profile?.calc
return (
<MainWrapper>
<Modal
close={closeSettingsPopup}
isOpen={isOpen}
withCloseButton={false}
>
<SettingsPopup
actions={actions}
onWatchEpisodesClick={setEpisodesSettings}
settings={settings}
closePopup={closeSettingsPopup}
profile={profile}
selectedPlaylist={selectedPlaylist}
/>
</Modal>
<MediaQuery minDevice='laptop'>
<UserFavorites />
</MediaQuery>
<ProfileHeader />
<Wrapper>
<Container>
<MatchProfileCard profile={profile} />
{
isLiveMatch && (
<StreamPlayer
url={url}
onPlayingChange={onPlayingChange}
onProgressChange={onPlayerProgressChange}
resumeFrom={lastPlayPosition.second}
/>
)
}
{
isFinishedMatch && (
<MultiSourcePlayer
chapters={chapters}
onPlayingChange={onPlayingChange}
onProgressChange={onPlayerProgressChange}
resumeFrom={lastPlayPosition}
/>
)
}
</Container>
{
playlists && (
<MatchSidePlaylists
playlists={playlists}
selectedPlaylist={selectedPlaylist}
onSelect={onPlaylistSelect}
profile={profile}
openPopup={openSettingsPopup}
/>
)
}
{(isLiveMatch && profile) && <LiveMatch profile={profile} />}
{(!isLiveMatch && profile) && <FinishedMatch profile={profile} />}
</Wrapper>
</MainWrapper>
)

@ -2,10 +2,7 @@ import styled from 'styled-components/macro'
import { devices } from 'config/devices'
import { Modal as BaseModal } from 'features/Modal'
import { ModalWindow } from 'features/Modal/styled'
export const MainWrapper = styled.div`
export const Wrapper = styled.div`
margin: 63px 0px 0 22px;
display: flex;
@ -19,12 +16,17 @@ export const MainWrapper = styled.div`
}
`
export const Container = styled.div`
type ContainerProps = {
marginRight?: number,
}
export const Container = styled.div<ContainerProps>`
max-width: 2090px;
max-height: 896px;
display: flex;
flex-direction: column;
flex-grow: 1;
margin-right: ${({ marginRight = 0 }) => `${marginRight}px`};
@media ${devices.laptop} {
max-width: 80%;
@ -36,26 +38,6 @@ export const Container = styled.div`
@media ${devices.mobile} {
max-width: 100%;
}
`
export const Modal = styled(BaseModal)`
background-color: rgba(0, 0, 0, 0.7);
${ModalWindow} {
width: 1222px;
padding: 20px 0;
background-color: #3F3F3F;
border-radius: 5px;
@media ${devices.tablet} {
width: 100vw;
}
@media ${devices.mobile} {
height: 100vh;
padding: 0;
background-color: transparent;
}
margin-right: 0;
}
`

@ -1,11 +1,17 @@
import type { MouseEvent } from 'react'
import { useState, useCallback } from 'react'
import {
useState,
useEffect,
useCallback,
} from 'react'
import { useLocation } from 'react-router-dom'
import last from 'lodash/last'
import { PopupPages } from '../../types'
export const usePopupNavigation = () => {
const location = useLocation()
const [pages, setPages] = useState<Array<PopupPages>>([])
const goTo = useCallback(
@ -33,6 +39,8 @@ export const usePopupNavigation = () => {
setPages([])
}, [])
useEffect(closePopup, [location, closePopup])
const page = last(pages)
return {

@ -40,6 +40,11 @@ export const Button = styled.button<ButtonProps>`
&:hover {
background-color: #0c3ccc;
}
background-image: url(/images/player-play.svg);
background-repeat: no-repeat;
background-size: 12px;
background-position: 8px center;
`
: ''
)}

@ -7,6 +7,7 @@ import { calculateChapterStyles } from './helpers/calculateChapterStyles'
export type Props = {
activeChapterIndex: number,
allPlayedProgress: number,
chapters?: Chapters,
duration: number,
loadedProgress: number,
@ -16,6 +17,7 @@ export type Props = {
export const useProgressBar = ({
activeChapterIndex,
allPlayedProgress,
chapters = [],
duration,
loadedProgress,
@ -38,10 +40,9 @@ export const useProgressBar = ({
],
)
const played = playedProgress + chapters[activeChapterIndex].startMs
return {
calculatedChapters,
playedProgressInPercent: Math.min(played * 100 / duration, 100),
time: secondsToHms(played / 1000),
playedProgressInPercent: Math.min(allPlayedProgress * 100 / duration, 100),
time: secondsToHms(allPlayedProgress / 1000),
}
}

@ -51,6 +51,7 @@ const chapters = [
{
duration: 30000,
endMs: 30000,
endOffsetMs: 0,
period: 0,
startMs: 0,
startOffsetMs: 0,
@ -59,6 +60,7 @@ const chapters = [
{
duration: 30000,
endMs: 60000,
endOffsetMs: 0,
period: 0,
startMs: 30000,
startOffsetMs: 0,
@ -67,6 +69,7 @@ const chapters = [
{
duration: 10000,
endMs: 70000,
endOffsetMs: 0,
period: 0,
startMs: 60000,
startOffsetMs: 0,
@ -77,6 +80,7 @@ const chapters = [
export const Empty = () => renderInControls(
<ProgressBar
activeChapterIndex={0}
allPlayedProgress={0}
duration={duration}
chapters={chapters}
onPlayedProgressChange={callback}
@ -88,6 +92,7 @@ export const Empty = () => renderInControls(
export const HalfLoaded = () => renderInControls(
<ProgressBar
activeChapterIndex={0}
allPlayedProgress={0}
duration={duration}
chapters={chapters}
onPlayedProgressChange={callback}
@ -99,6 +104,7 @@ export const HalfLoaded = () => renderInControls(
export const HalfPlayed = () => renderInControls(
<ProgressBar
activeChapterIndex={1}
allPlayedProgress={1}
duration={duration}
chapters={chapters}
onPlayedProgressChange={callback}
@ -110,6 +116,7 @@ export const HalfPlayed = () => renderInControls(
export const Loaded40AndPlayed20 = () => renderInControls(
<ProgressBar
activeChapterIndex={0}
allPlayedProgress={0}
duration={duration}
chapters={chapters}
onPlayedProgressChange={callback}

@ -2,12 +2,15 @@ import { Fragment } from 'react'
import map from 'lodash/map'
import { SettingsButton } from 'features/StreamPlayer/styled'
import { OutsideClick } from 'features/OutsideClick'
import type { Props } from './hooks'
import { useSettings } from './hooks'
import { QualitiesList, QualityItem } from './styled'
import {
SettingsButton,
QualitiesList,
QualityItem,
} from './styled'
export const Settings = (props: Props) => {
const { selectedQuality, videoQualities } = props

@ -1,5 +1,14 @@
import styled, { css } from 'styled-components/macro'
import { ButtonBase } from 'features/StreamPlayer/styled'
export const SettingsButton = styled(ButtonBase)`
width: 22px;
height: 20px;
margin-left: 25px;
background-image: url(/images/player-settings.svg);
`
export const QualitiesList = styled.ul`
position: absolute;
z-index: 1;

@ -0,0 +1,3 @@
export const REWIND_SECONDS = 5
export const HOUR_IN_MILLISECONDS = 60 * 60 * 1000

@ -7,8 +7,6 @@ import {
import size from 'lodash/size'
import type { LastPlayPosition } from 'requests'
import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
@ -21,21 +19,11 @@ import { useDuration } from './useDuration'
import type { Chapters } from '../types'
import { Players } from '../types'
import { getNextPlayer } from '../helpers'
import { REWIND_SECONDS } from '../config'
export type PlayerState = {
activeChapterIndex: number,
activePlayer: Players,
loadedProgress: number,
playedProgress: number,
playing: boolean,
ready: boolean,
seek: Record<Players, number>,
seeking: boolean,
}
const initialState: PlayerState = {
const initialState = {
activeChapterIndex: 0,
activePlayer: 0,
activePlayer: Players.PLAYER1,
loadedProgress: 0,
playedProgress: 0,
playing: false,
@ -47,12 +35,13 @@ const initialState: PlayerState = {
seeking: false,
}
export type PlayerState = typeof initialState
export type Props = {
chapters: Chapters,
onError?: () => void,
onPlayingChange: (playing: boolean) => void,
onProgressChange: (seconds: number, period: number) => void,
resumeFrom: LastPlayPosition,
}
export const useMultiSourcePlayer = ({
@ -60,7 +49,6 @@ export const useMultiSourcePlayer = ({
onError,
onPlayingChange,
onProgressChange: onProgressChangeCallback,
resumeFrom,
}: Props) => {
const numberOfChapters = size(chapters)
const [
@ -75,7 +63,7 @@ export const useMultiSourcePlayer = ({
seeking,
},
setPlayerState,
] = useObjectState({ ...initialState, activeChapterIndex: resumeFrom.half })
] = useObjectState(initialState)
const video1Ref = useRef<HTMLVideoElement>(null)
const video2Ref = useRef<HTMLVideoElement>(null)
const activeChapterIndex = numberOfChapters >= chapterIndex ? chapterIndex : 0
@ -101,6 +89,46 @@ export const useMultiSourcePlayer = ({
onError?.()
}, [onError, stopPlaying])
const getActiveChapter = useCallback(
(index: number = activeChapterIndex) => chapters[index],
[chapters, activeChapterIndex],
)
const videoRef = [video1Ref, video2Ref][activePlayer]
const isFirstChapterPlaying = activeChapterIndex === 0
const isLastChapterPlaying = activeChapterIndex === numberOfChapters - 1
const setPlayerTime = (progressMs: number) => {
if (!videoRef.current) return
videoRef.current.currentTime = progressMs / 1000
}
const rewindForward = () => {
const chapter = getActiveChapter()
const newProgress = playedProgress + REWIND_SECONDS * 1000
if (newProgress <= chapter.duration) {
setPlayerTime(chapter.startOffsetMs + newProgress)
} else if (isLastChapterPlaying) {
playNextChapter()
} else {
const nextChapter = getActiveChapter(activeChapterIndex + 1)
const fromMs = newProgress - chapter.duration
playNextChapter(fromMs, nextChapter.startOffsetMs)
}
}
const rewindBackward = () => {
const chapter = getActiveChapter()
const newProgress = playedProgress - REWIND_SECONDS * 1000
if (newProgress >= 0) {
setPlayerTime(chapter.startOffsetMs + newProgress)
} else if (isFirstChapterPlaying) {
setPlayerTime(chapter.startOffsetMs)
} else {
const prevChapter = getActiveChapter(activeChapterIndex - 1)
const fromMs = prevChapter.duration + newProgress
playPrevChapter(fromMs, prevChapter.startOffsetMs)
}
}
const onProgressChange = useProgressChangeHandler({
chapters,
duration,
@ -108,10 +136,8 @@ export const useMultiSourcePlayer = ({
})
const getChapterUrl = useCallback((index: number, quality: string = selectedQuality) => (
chapters[index]?.urls[quality]
), [selectedQuality, chapters])
const videoRef = [video1Ref, video2Ref][activePlayer]
getActiveChapter(index)?.urls[quality]
), [selectedQuality, getActiveChapter])
const onQualitySelect = (quality: string) => {
setPlayerState((state) => ({
@ -130,14 +156,14 @@ export const useMultiSourcePlayer = ({
}
const onLoadedProgress = (loadedMs: number) => {
const chapter = chapters[activeChapterIndex]
const chapter = getActiveChapter()
const value = loadedMs - chapter.startOffsetMs
setPlayerState({ loadedProgress: value })
}
const onPlayedProgress = (playedMs: number) => {
const chapter = chapters[activeChapterIndex]
const chapter = getActiveChapter()
const value = Math.max(playedMs - chapter.startOffsetMs, 0)
setPlayerState({ playedProgress: value })
@ -158,38 +184,38 @@ export const useMultiSourcePlayer = ({
useEffect(() => {
const progressSeconds = playedProgress / 1000
const { period } = chapters[activeChapterIndex]
const { period } = getActiveChapter()
onProgressChangeCallback(progressSeconds, Number(period))
}, [
playedProgress,
chapters,
onProgressChangeCallback,
activeChapterIndex,
getActiveChapter,
])
useEffect(() => {
const { duration: chapterDuration } = chapters[activeChapterIndex]
const { duration: chapterDuration } = getActiveChapter()
if (playedProgress >= chapterDuration && !seeking) {
playNextChapter()
}
}, [
activeChapterIndex,
getActiveChapter,
playedProgress,
seeking,
playNextChapter,
chapters,
])
return {
activeChapterIndex,
activePlayer,
activeSrc: getChapterUrl(activeChapterIndex),
allPlayedProgress: playedProgress + getActiveChapter().startMs,
chapters,
duration,
isFirstChapterPlaying: activeChapterIndex === 0,
isLastChapterPlaying: activeChapterIndex === numberOfChapters - 1,
isFirstChapterPlaying,
isLastChapterPlaying,
loadedProgress,
nextSrc: getChapterUrl(activeChapterIndex + 1),
numberOfChapters,
onError: handleError,
onLoadedProgress,
onPlayedProgress,
@ -202,6 +228,8 @@ export const useMultiSourcePlayer = ({
playedProgress,
playing,
ready,
rewindBackward,
rewindForward,
seek,
selectedQuality,
startPlaying,

@ -1,6 +1,8 @@
import { useCallback } from 'react'
import { SetPartialState } from 'hooks'
import isUndefined from 'lodash/isUndefined'
import type { SetPartialState } from 'hooks'
import type { PlayerState } from '.'
import { getNextPlayer } from '../helpers'
@ -37,29 +39,54 @@ export const usePlayingHandlers = (
))
}, [setPlayerState])
const playNextChapter = useCallback(() => {
const playNextChapter = useCallback((fromMs?: number, startOffsetMs?: number) => {
setPlayerState((state) => {
if (!state.ready) return state
const isLastChapter = state.activeChapterIndex + 1 === numberOfChapters
if (isLastChapter || isUndefined(fromMs) || isUndefined(startOffsetMs)) {
return {
activeChapterIndex: isLastChapter ? 0 : state.activeChapterIndex + 1,
activePlayer: getNextPlayer(state.activePlayer),
loadedProgress: 0,
playedProgress: 0,
playing: isLastChapter ? false : state.playing,
}
}
return {
activeChapterIndex: isLastChapter ? 0 : state.activeChapterIndex + 1,
activePlayer: getNextPlayer(state.activeChapterIndex),
activeChapterIndex: state.activeChapterIndex + 1,
loadedProgress: 0,
playedProgress: 0,
playing: isLastChapter ? false : state.playing,
playedProgress: fromMs,
playing: state.playing,
seek: {
...state.seek,
[state.activePlayer]: (startOffsetMs + fromMs) / 1000,
},
}
})
}, [numberOfChapters, setPlayerState])
const playPrevChapter = useCallback(() => {
const playPrevChapter = useCallback((fromMs?: number, startOffsetMs?: number) => {
setPlayerState((state) => {
if (!state.ready || state.activeChapterIndex === 0) return state
if (isUndefined(fromMs) || isUndefined(startOffsetMs)) {
return {
activeChapterIndex: state.activeChapterIndex - 1,
loadedProgress: 0,
playedProgress: 0,
}
}
return {
activeChapterIndex: state.activeChapterIndex - 1,
activePlayer: getNextPlayer(state.activeChapterIndex),
loadedProgress: 0,
playedProgress: 0,
playedProgress: fromMs,
seek: {
...state.seek,
[state.activePlayer]: (startOffsetMs + fromMs) / 1000,
},
}
})
}, [setPlayerState])

@ -3,25 +3,40 @@ import { VolumeBar } from 'features/StreamPlayer/components/VolumeBar'
import {
PlayerWrapper,
Controls,
ControlsRow,
ControlsGroup,
CenterControls,
PlayStop,
Fullscreen,
Next,
Prev,
LoaderWrapper,
Backward,
Forward,
PlaybackTime,
} from 'features/StreamPlayer/styled'
import { VideoPlayer } from 'features/VideoPlayer'
import { secondsToHms } from 'helpers'
import { HOUR_IN_MILLISECONDS, REWIND_SECONDS } from './config'
import { ProgressBar } from './components/ProgressBar'
import { Settings } from './components/Settings'
import type { Props } from './hooks'
import { useMultiSourcePlayer } from './hooks'
import { Players } from './types'
import {
ChaptersText,
Next,
Prev,
} from './styled'
export const MultiSourcePlayer = (props: Props) => {
const {
activeChapterIndex,
activePlayer,
activeSrc,
allPlayedProgress,
chapters,
duration,
isFirstChapterPlaying,
@ -30,6 +45,7 @@ export const MultiSourcePlayer = (props: Props) => {
loadedProgress,
muted,
nextSrc,
numberOfChapters,
onError,
onFullscreenClick,
onLoadedProgress,
@ -45,6 +61,8 @@ export const MultiSourcePlayer = (props: Props) => {
playNextChapter,
playPrevChapter,
ready,
rewindBackward,
rewindForward,
seek,
selectedQuality,
togglePlaying,
@ -82,7 +100,7 @@ export const MultiSourcePlayer = (props: Props) => {
isFullscreen={isFullscreen}
onLoadedProgress={firstPlayerActive ? onLoadedProgress : undefined}
onPlayedProgress={firstPlayerActive ? onPlayedProgress : undefined}
onEnded={playNextChapter}
onEnded={() => playNextChapter()}
onError={onError}
onReady={onReady}
/>
@ -97,43 +115,76 @@ export const MultiSourcePlayer = (props: Props) => {
isFullscreen={isFullscreen}
onLoadedProgress={!firstPlayerActive ? onLoadedProgress : undefined}
onPlayedProgress={!firstPlayerActive ? onPlayedProgress : undefined}
onEnded={playNextChapter}
onEnded={() => playNextChapter()}
onError={onError}
onReady={onReady}
/>
{ready && (
<CenterControls playing={playing}>
<Backward size='lg' onClick={rewindBackward}>{REWIND_SECONDS}</Backward>
<PlayStop
size='lg'
marginRight={0}
playing={playing}
onClick={togglePlaying}
/>
<Forward size='lg' onClick={rewindForward}>{REWIND_SECONDS}</Forward>
</CenterControls>
)}
<Controls>
<Prev
disabled={isFirstChapterPlaying}
onClick={playPrevChapter}
/>
<PlayStop
playing={playing}
onClick={togglePlaying}
/>
<Next
disabled={isLastChapterPlaying}
onClick={playNextChapter}
/>
<VolumeBar
value={volumeInPercent}
muted={muted}
onChange={onVolumeChange}
onClick={onVolumeClick}
/>
<ProgressBar
activeChapterIndex={activeChapterIndex}
duration={duration}
chapters={chapters}
onPlayedProgressChange={onProgressChange}
playedProgress={playedProgress}
loadedProgress={loadedProgress}
/>
<Fullscreen onClick={onFullscreenClick} isFullscreen={isFullscreen} />
<Settings
onSelect={onQualitySelect}
selectedQuality={selectedQuality}
videoQualities={videoQualities}
/>
<ControlsRow>
<ProgressBar
activeChapterIndex={activeChapterIndex}
allPlayedProgress={allPlayedProgress}
duration={duration}
chapters={chapters}
onPlayedProgressChange={onProgressChange}
playedProgress={playedProgress}
loadedProgress={loadedProgress}
/>
</ControlsRow>
<ControlsRow>
<ControlsGroup>
<PlayStop
playing={playing}
onClick={togglePlaying}
/>
<Prev
disabled={isFirstChapterPlaying}
onClick={() => playPrevChapter()}
/>
<ChaptersText>
{activeChapterIndex + 1} / {numberOfChapters}
</ChaptersText>
<Next
disabled={isLastChapterPlaying}
onClick={() => playNextChapter()}
/>
<VolumeBar
value={volumeInPercent}
muted={muted}
onChange={onVolumeChange}
onClick={onVolumeClick}
/>
<PlaybackTime width={duration > HOUR_IN_MILLISECONDS ? 150 : 130}>
{secondsToHms(allPlayedProgress / 1000)}
{' / '}
{secondsToHms(duration / 1000)}
</PlaybackTime>
<Backward onClick={rewindBackward}>{REWIND_SECONDS}</Backward>
<Forward onClick={rewindForward}>{REWIND_SECONDS}</Forward>
</ControlsGroup>
<ControlsGroup>
<Fullscreen onClick={onFullscreenClick} isFullscreen={isFullscreen} />
<Settings
onSelect={onQualitySelect}
selectedQuality={selectedQuality}
videoQualities={videoQualities}
/>
</ControlsGroup>
</ControlsRow>
</Controls>
</PlayerWrapper>
)

@ -0,0 +1,32 @@
import styled from 'styled-components/macro'
import { ButtonBase } from 'features/StreamPlayer/styled'
export const ChaptersText = styled.span`
margin: 0 14px;
font-weight: 500;
font-size: 16px;
color: #fff;
text-align: center;
`
type PrevProps = {
disabled?: boolean,
}
export const Prev = styled(ButtonBase)<PrevProps>`
width: 29px;
height: 28px;
background-image: url(/images/player-prev.svg);
${({ disabled }) => (
disabled
? 'opacity: 0.5;'
: ''
)}
`
export const Next = styled(Prev)`
margin-right: 24px;
transform: rotate(180deg);
`

@ -3,6 +3,7 @@ export type Urls = { [quality: string]: string }
export type Chapter = {
duration: number,
endMs: number,
endOffsetMs: number,
period: number,
startMs: number,
startOffsetMs: number,

@ -20,7 +20,7 @@ export const LoadedProgress = styled.div`
export const PlayedProgress = styled.div`
position: absolute;
z-index: 2;
background-color: #F2C94C;
background-color: #CC0000;
height: 100%;
`
@ -33,7 +33,7 @@ export const Scrubber = styled.button`
z-index: 3;
width: 18px;
height: 18px;
background-color: #F2C94C;
background-color: #CC0000;
border-radius: 50%;
cursor: pointer;

@ -26,7 +26,7 @@ export const Wrapper = styled.div`
export const Time = styled.span`
padding: 0 6px;
color: #F2C94C;
color: #CC0000;
text-align: center;
font-weight: bold;
font-size: 16px;

@ -7,6 +7,7 @@ import { useObjectState } from 'hooks'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
import { REWIND_SECONDS } from 'features/MultiSourcePlayer/config'
import { useHlsPlayer } from './useHlsPlayer'
import { useVideoQuality } from './useVideoQuality'
import { useFullscreen } from './useFullscreen'
@ -61,6 +62,13 @@ export const useVideoPlayer = ({
}
}
const rewind = (seconds: number) => () => {
if (!videoRef.current) return
const { currentTime } = videoRef.current
const newProgress = currentTime + seconds
videoRef.current.currentTime = newProgress
}
const onError = useCallback(() => {
setPlayerState({ playing: false })
}, [setPlayerState])
@ -100,6 +108,8 @@ export const useVideoPlayer = ({
playedProgress,
playing,
ready,
rewindBackward: rewind(-REWIND_SECONDS),
rewindForward: rewind(REWIND_SECONDS),
seek,
startPlaying,
togglePlaying,

@ -1,5 +1,8 @@
import { secondsToHms } from 'helpers'
import { Loader } from 'features/Loader'
import { Settings } from 'features/MultiSourcePlayer/components/Settings'
import { REWIND_SECONDS } from 'features/MultiSourcePlayer/config'
import type { Props } from './hooks'
import { useVideoPlayer } from './hooks'
@ -9,9 +12,15 @@ import {
PlayerWrapper,
VideoPlayer,
Controls,
ControlsRow,
ControlsGroup,
CenterControls,
PlayStop,
Fullscreen,
LoaderWrapper,
Backward,
Forward,
PlaybackTime,
} from './styled'
export const StreamPlayer = (props: Props) => {
@ -34,6 +43,8 @@ export const StreamPlayer = (props: Props) => {
playedProgress,
playing,
ready,
rewindBackward,
rewindForward,
seek,
selectedQuality,
startPlaying,
@ -75,26 +86,49 @@ export const StreamPlayer = (props: Props) => {
onReady={startPlaying}
onError={onError}
/>
{ready && (
<CenterControls playing={playing}>
<Backward size='lg' onClick={rewindBackward}>{REWIND_SECONDS}</Backward>
<PlayStop
size='lg'
marginRight={0}
playing={playing}
onClick={togglePlaying}
/>
<Forward size='lg' onClick={rewindForward}>{REWIND_SECONDS}</Forward>
</CenterControls>
)}
<Controls>
<PlayStop onClick={togglePlaying} playing={playing} />
<VolumeBar
value={volumeInPercent}
muted={muted}
onChange={onVolumeChange}
onClick={onVolumeClick}
/>
<ProgressBar
duration={duration}
onPlayedProgressChange={onProgressChange}
playedProgress={playedProgress}
loadedProgress={loadedProgress}
/>
<Fullscreen onClick={onFullscreenClick} isFullscreen={isFullscreen} />
<Settings
onSelect={onQualitySelect}
selectedQuality={selectedQuality}
videoQualities={videoQualities}
/>
<ControlsRow>
<ProgressBar
duration={duration}
onPlayedProgressChange={onProgressChange}
playedProgress={playedProgress}
loadedProgress={loadedProgress}
/>
</ControlsRow>
<ControlsRow>
<ControlsGroup>
<PlayStop onClick={togglePlaying} playing={playing} />
<VolumeBar
value={volumeInPercent}
muted={muted}
onChange={onVolumeChange}
onClick={onVolumeClick}
/>
<PlaybackTime>
{secondsToHms(playedProgress / 1000)}
</PlaybackTime>
</ControlsGroup>
<ControlsGroup>
<Fullscreen onClick={onFullscreenClick} isFullscreen={isFullscreen} />
<Settings
onSelect={onQualitySelect}
selectedQuality={selectedQuality}
videoQualities={videoQualities}
/>
</ControlsGroup>
</ControlsRow>
</Controls>
</PlayerWrapper>
)

@ -1,4 +1,4 @@
import styled from 'styled-components/macro'
import styled, { css } from 'styled-components/macro'
import { VideoPlayer as VideoPlayerBase } from 'features/VideoPlayer'
@ -13,12 +13,33 @@ export const Controls = styled.div`
width: 100%;
bottom: 22px;
display: flex;
flex-direction: column;
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 ControlsRow = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
:first-child {
margin-bottom: 17px;
padding: 0 18px;
}
:last-child {
padding-left: 28px;
padding-right: 25px;
}
`
export const ControlsGroup = styled.div`
display: flex;
align-items: center;
`
export const PlayerWrapper = styled.div<PlayStopProps>`
position: relative;
width: 100%;
@ -53,7 +74,7 @@ export const LoaderWrapper = styled.div`
justify-content: center;
`
const ButtonBase = styled.button`
export const ButtonBase = styled.button`
outline: none;
border: none;
color: #fff;
@ -62,43 +83,33 @@ const ButtonBase = styled.button`
background-repeat: no-repeat;
background-position: center;
background-size: contain;
height: 100%;
`
const sizes = {
lg: 92,
sm: 24,
}
type PlayStopProps = {
marginRight?: number,
playing: boolean,
size?: keyof typeof sizes,
}
export const PlayStop = styled(ButtonBase)<PlayStopProps>`
width: 24px;
height: 24px;
margin-right: 18px;
margin-right: ${({ marginRight = 20 }) => `${marginRight}px`};
background-image: ${({ playing }) => (
playing
? 'url(/images/player-pause.svg)'
: 'url(/images/player-play.svg)'
)};
`
type PrevProps = {
disabled?: boolean,
}
export const Prev = styled(ButtonBase)<PrevProps>`
width: 29px;
height: 28px;
margin-right: 19px;
background-image: url(/images/player-prev.svg);
${({ disabled }) => (
disabled
? 'opacity: 0.5;'
: ''
)}
`
export const Next = styled(Prev)`
margin-right: 24px;
transform: rotate(180deg);
${({ size = 'sm' }) => (
css`
width: ${sizes[size]}px;
height: ${sizes[size]}px;
`
)};
`
export const Volume = styled(ButtonBase)`
@ -123,9 +134,68 @@ export const Fullscreen = styled(ButtonBase)<FullscreenProps>`
)};
`
export const SettingsButton = styled(ButtonBase)`
width: 22px;
height: 20px;
margin-left: 25px;
background-image: url(/images/player-settings.svg);
type ButtonProps = {
size?: 'sm' | 'lg',
}
const rewindButtonSizes = {
fontSizes: {
lg: 40,
sm: 14,
},
sides: {
lg: 75,
sm: 29,
},
}
export const Backward = styled(ButtonBase)<ButtonProps>`
padding: 0;
margin-right: 10px;
background-image: url(/images/player-backward.svg);
background-repeat: no-repeat;
background-size: contain;
font-weight: normal;
${({ size = 'sm' }) => (
css`
width: ${rewindButtonSizes.sides[size]}px;
height: ${rewindButtonSizes.sides[size]}px;
font-size: ${rewindButtonSizes.fontSizes[size]}px;
`
)}
`
export const Forward = styled(Backward)`
background-image: url(/images/player-forward.svg);
`
type PlaybackTimeProps = {
width?: number,
}
export const PlaybackTime = styled.span<PlaybackTimeProps>`
width: ${({ width = 130 }) => `${width}px`};
font-weight: 600;
font-size: 16px;
color: #fff;
`
type CenterControlsProps = {
playing: boolean,
}
export const CenterControls = styled.div<CenterControlsProps>`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 413px;
height: 109px;
display: flex;
justify-content: space-between;
align-items: center;
transition: opacity 0.3s ease-in-out;
opacity: ${({ playing }) => (playing ? 0 : 1)};
pointer-events: ${({ playing }) => (playing ? 'none' : 'auto')};
`

@ -17,6 +17,7 @@ type Team = {
}
export type MatchInfo = {
calc: boolean,
date: string,
stream_status: MatchStatuses,
team1: Team,

Loading…
Cancel
Save