fix(#1701): return multisource and finished matches players

keep-around/fdb88b04b32b9392e76795099e2ec47c9856b38b
Andrei Dekterev 4 years ago committed by Andrei Dekterev
parent c52a48383d
commit 602050f347
  1. 137
      src/features/MatchPage/components/FinishedMatch/helpers.tsx
  2. 53
      src/features/MatchPage/components/FinishedMatch/hooks/index.tsx
  3. 46
      src/features/MatchPage/components/FinishedMatch/hooks/useChapters.tsx
  4. 69
      src/features/MatchPage/components/FinishedMatch/hooks/useEpisodes.tsx
  5. 72
      src/features/MatchPage/components/FinishedMatch/hooks/usePlayerLogger.tsx
  6. 27
      src/features/MatchPage/components/FinishedMatch/styled.tsx
  7. 2
      src/features/MatchPage/index.tsx
  8. 47
      src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/__tests__/index.tsx
  9. 59
      src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/index.tsx
  10. 48
      src/features/MultiSourcePlayer/components/ProgressBar/hooks.tsx
  11. 126
      src/features/MultiSourcePlayer/components/ProgressBar/stories.tsx
  12. 27
      src/features/MultiSourcePlayer/components/Settings/hooks.tsx
  13. 47
      src/features/MultiSourcePlayer/components/Settings/index.tsx
  14. 78
      src/features/MultiSourcePlayer/components/Settings/styled.tsx
  15. 3
      src/features/MultiSourcePlayer/config.tsx
  16. 35
      src/features/MultiSourcePlayer/helpers/index.tsx
  17. 9
      src/features/MultiSourcePlayer/hooks/useDuration.tsx
  18. 102
      src/features/MultiSourcePlayer/hooks/usePlayingHandlers.tsx
  19. 51
      src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx
  20. 38
      src/features/MultiSourcePlayer/hooks/useVideoQuality.tsx
  21. 18
      src/features/MultiSourcePlayer/types.tsx
  22. 40
      src/requests/getVideos.tsx
  23. 1
      src/requests/index.tsx

@ -0,0 +1,137 @@
import map from 'lodash/map'
import last from 'lodash/last'
import uniq from 'lodash/uniq'
import filter from 'lodash/filter'
import reduce from 'lodash/reduce'
import concat from 'lodash/concat'
import orderBy from 'lodash/orderBy'
import isEmpty from 'lodash/isEmpty'
import groupBy from 'lodash/groupBy'
import type {
Videos,
Episodes,
Episode,
} from 'requests'
import type { Chapters, Urls } from 'features/MultiSourcePlayer/types'
import type { PlaylistOption } from '../../types'
import { FULL_GAME_KEY } from '../../helpers/buildPlaylists'
const getUniquePeriods = (videos: Videos) => uniq(map(videos, ({ period }) => period))
type Video = {
duration: number,
period: number,
urls: Urls,
}
const getVideoByPeriod = (videos: Videos, period: number) => {
const videosWithSamePeriod = filter(videos, { period })
if (isEmpty(videosWithSamePeriod)) return null
const urls = reduce(
videosWithSamePeriod,
(acc: Urls, video) => ({
...acc,
[video.quality]: video.url,
}),
{},
)
const [video] = videosWithSamePeriod
return {
duration: video.duration,
period: video.period,
urls,
}
}
const getVideoByPeriods = (videos: Videos, periods: Array<number>) => (
reduce(
periods,
(acc: Array<Video>, period) => {
const video = getVideoByPeriod(videos, period)
return video ? concat(acc, video) : acc
},
[],
)
)
const getFullMatchChapters = (videos: Array<Video>) => {
const sortedVideos = orderBy(videos, ({ period }) => period)
return reduce(
sortedVideos,
(acc: Chapters, video) => {
const prevVideoEndMs = last(acc)?.endMs || 0
const endMs = prevVideoEndMs + video.duration
const nextChapter = {
duration: video.duration,
endMs,
endOffsetMs: endMs,
period: video.period,
startMs: prevVideoEndMs,
startOffsetMs: 0,
urls: video.urls,
}
return concat(acc, nextChapter)
},
[],
)
}
const getEpisodeUrls = (urls: Urls, episode: Episode) => reduce(
urls,
(
acc: Urls,
url,
qulaity,
) => {
acc[qulaity] = `${url}#t=${episode.s},${episode.e}`
return acc
},
{},
)
const getPlaylistChapters = (videos: Array<Video>, episodes: Episodes) => {
const groupedByPeriods = groupBy(videos, ({ period }) => period)
return reduce(
episodes,
(acc: Chapters, episode) => {
const video = groupedByPeriods[episode.h]?.[0]
if (!video || episode.s >= episode.e) return acc
const episodeDuration = (episode.e - episode.s) * 1000
const prevVideoEndMs = last(acc)?.endMs || 0
const nextChapter = {
duration: episodeDuration,
endMs: prevVideoEndMs + episodeDuration,
endOffsetMs: episode.e * 1000,
period: video.period,
startMs: prevVideoEndMs,
startOffsetMs: episode.s * 1000,
urls: getEpisodeUrls(video.urls, episode),
}
return concat(acc, nextChapter)
},
[],
)
}
type Args = {
episodes: Episodes,
selectedPlaylist?: PlaylistOption,
videos: Videos,
}
export const buildChapters = ({
episodes,
selectedPlaylist,
videos,
}: Args) => {
const periods = getUniquePeriods(videos)
const highQualityVideos = getVideoByPeriods(videos, periods)
return selectedPlaylist?.id === FULL_GAME_KEY
? getFullMatchChapters(highQualityVideos)
: getPlaylistChapters(highQualityVideos, episodes)
}

@ -0,0 +1,53 @@
import { useToggle } from 'hooks/useToggle'
import type { Settings } from 'features/MatchPopup'
import { useMatchPopupStore } from 'features/MatchPopup'
import { usePlayerLogger } from './usePlayerLogger'
import { useEpisodes } from './useEpisodes'
import { useChapters } from './useChapters'
export const useFinishedMatch = () => {
const {
handlePlaylistClick,
matchPlaylists,
selectedPlaylist,
setSettings,
} = useMatchPopupStore()
const {
close: closeSettingsPopup,
isOpen: isSettingsPopupOpen,
open: openSettingsPopup,
} = useToggle()
const { episodes } = useEpisodes()
const { logPlaylistChange, onPlayingChange } = usePlayerLogger()
const setEpisodesSettings = (newSettings: Settings) => {
setSettings(newSettings)
closeSettingsPopup()
}
const onPlaylistSelect: typeof handlePlaylistClick = (playlist, e) => {
if (selectedPlaylist) {
logPlaylistChange(selectedPlaylist)
}
handlePlaylistClick(playlist, e)
}
return {
closeSettingsPopup,
isSettingsPopupOpen,
onPlayingChange,
onPlaylistSelect,
openSettingsPopup,
playlists: matchPlaylists,
selectedPlaylist,
setEpisodesSettings,
...useChapters({
episodes,
selectedPlaylist,
}),
}
}

@ -0,0 +1,46 @@
import {
useEffect,
useMemo,
useState,
} from 'react'
import type { Episodes, Videos } from 'requests'
import { getVideos } from 'requests'
import { usePageParams } from 'hooks/usePageParams'
import type { PlaylistOption } from 'features/MatchPage/types'
import { buildChapters } from '../helpers'
type Args = {
episodes: Episodes,
selectedPlaylist?: PlaylistOption,
}
export const useChapters = ({
episodes,
selectedPlaylist,
}: Args) => {
const [videos, setVideos] = useState<Videos>([])
const { profileId: matchId, sportType } = usePageParams()
useEffect(() => {
getVideos(sportType, matchId).then(setVideos)
}, [sportType, matchId])
const chapters = useMemo(
() => buildChapters({
episodes,
selectedPlaylist,
videos,
}),
[
selectedPlaylist,
episodes,
videos,
],
)
return { chapters }
}

@ -0,0 +1,69 @@
import {
useCallback,
useEffect,
useState,
} from 'react'
import isEmpty from 'lodash/isEmpty'
import type { Episodes } from 'requests'
import { getPlayerPlaylists } from 'requests'
import { usePageParams } from 'hooks/usePageParams'
import { PlaylistOption, PlaylistTypes } from 'features/MatchPage/types'
import {
defaultSettings,
Settings,
useMatchPopupStore,
} from 'features/MatchPopup'
export const useEpisodes = () => {
const {
handlePlaylistClick,
matchPlaylists: playlists,
selectedPlaylist,
settings,
} = useMatchPopupStore()
const [episodes, setEpisodes] = useState<Episodes>([])
const { profileId: matchId, sportType } = usePageParams()
const fetchEpisodes = useCallback((
playlistOption: PlaylistOption,
popupSettings: Settings = defaultSettings,
) => {
if (playlistOption.type === PlaylistTypes.PLAYER) {
getPlayerPlaylists({
matchId,
playerId: playlistOption.id,
settings: popupSettings,
sportType,
}).then(setEpisodes)
} else if (playlistOption.type === PlaylistTypes.MATCH
|| playlistOption.type === PlaylistTypes.EVENT) {
setEpisodes(playlistOption.episodes)
}
}, [matchId, sportType])
useEffect(() => {
if (!selectedPlaylist && playlists && !isEmpty(playlists.match)) {
handlePlaylistClick(playlists.match[0])
}
}, [
selectedPlaylist,
playlists,
handlePlaylistClick,
])
useEffect(() => {
if (selectedPlaylist) {
fetchEpisodes(selectedPlaylist, settings)
}
}, [
settings,
selectedPlaylist,
fetchEpisodes,
])
return { episodes }
}

@ -0,0 +1,72 @@
import {
useCallback,
useRef,
} from 'react'
import { useLocation } from 'react-router'
import { LogActions, logUserAction } from 'requests/logUserAction'
import { useInterval } from 'hooks/useInterval'
import { usePageParams } from 'hooks/usePageParams'
import { PlaylistOption, PlaylistTypes } from 'features/MatchPage/types'
const playlistTypeConfig = {
ball_in_play: 2,
full_game: 1,
goals: 4,
highlights: 3,
players: 5,
}
const getInitialData = () => ({ dateVisit: new Date().toISOString(), seconds: 0 })
export const usePlayerLogger = () => {
const location = useLocation()
const { profileId, sportType } = usePageParams()
const data = useRef(getInitialData())
const incrementSeconds = () => data.current.seconds++
const resetData = () => {
data.current = getInitialData()
}
const { start, stop } = useInterval({
callback: incrementSeconds,
intervalDuration: 1000,
startImmediate: false,
})
const onPlayingChange = useCallback((playing: boolean) => {
if (playing) {
start()
} else {
stop()
}
}, [start, stop])
const logPlaylistChange = (prevPlaylist: PlaylistOption) => {
const args = prevPlaylist.type === PlaylistTypes.MATCH
? {
playlistType: playlistTypeConfig[prevPlaylist.id],
}
: {
playerId: prevPlaylist.id,
playlistType: playlistTypeConfig.players,
}
logUserAction({
actionType: LogActions.VideoChange,
dateVisit: data.current.dateVisit,
duration: data.current.seconds,
matchId: profileId,
sportType,
url: location.pathname,
...args,
})
resetData()
}
return { logPlaylistChange, onPlayingChange }
}

@ -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;
}
}
`

@ -21,6 +21,7 @@ import { MatchProfileCard } from './components/MatchProfileCard'
import { LiveMatch } from './components/LiveMatch'
import { useMatchProfile } from './hooks/useMatchProfile'
import { Wrapper } from './styled'
import { FinishedMatch } from './components/FinishedMatch'
const MatchPageComponent = () => {
usePageLogger()
@ -28,6 +29,7 @@ const MatchPageComponent = () => {
const { addRemoveFavorite, userFavorites } = useUserFavoritesStore()
const {
events,
isStarted,
profile,
tournamentData,

@ -0,0 +1,47 @@
import { calculateChapterStyles } from '..'
const videoDuration = 60000
it('return correct progress and width lengthes', () => {
let chapter = {
duration: 15000,
endMs: 20000,
period: 0,
startMs: 5000,
urls: {},
}
let expected = {
...chapter,
loaded: 100,
played: 100,
width: 25,
}
expect(calculateChapterStyles({
activeChapterIndex: 0,
chapters: [chapter],
loadedProgress: 30000,
playedProgress: 30000,
videoDuration,
})).toEqual([expected])
chapter = {
duration: 30000,
endMs: 30000,
period: 0,
startMs: 0,
urls: {},
}
expected = {
...chapter,
loaded: 50,
played: 50,
width: 50,
}
expect(calculateChapterStyles({
activeChapterIndex: 0,
chapters: [chapter],
loadedProgress: 15000,
playedProgress: 15000,
videoDuration,
})).toEqual([expected])
})

@ -0,0 +1,59 @@
import map from 'lodash/fp/map'
import pipe from 'lodash/fp/pipe'
import size from 'lodash/fp/size'
import slice from 'lodash/fp/slice'
import type { Chapters, Chapter } from 'features/MultiSourcePlayer/types'
const calculateChapterProgress = (progress: number, chapter: Chapter) => (
Math.min(progress * 100 / chapter.duration, 100)
)
type Args = {
activeChapterIndex: number,
chapters: Chapters,
loadedProgress: number,
playedProgress: number,
videoDuration: number,
}
export const calculateChapterStyles = ({
activeChapterIndex,
chapters,
loadedProgress,
playedProgress,
videoDuration,
}: Args) => {
const playedChapters = pipe(
slice(0, activeChapterIndex),
map((chapter: Chapter) => ({
...chapter,
loaded: 100,
played: 100,
width: chapter.duration * 100 / videoDuration,
})),
)(chapters)
const comingChapters = pipe(
slice(activeChapterIndex + 1, size(chapters)),
map((chapter: Chapter) => ({
...chapter,
loaded: 0,
played: 0,
width: chapter.duration * 100 / videoDuration,
})),
)(chapters)
const chapter = chapters[activeChapterIndex]
const activeChapter = {
...chapter,
loaded: calculateChapterProgress(loadedProgress, chapter),
played: calculateChapterProgress(playedProgress, chapter),
width: chapter.duration * 100 / videoDuration,
}
return [
...playedChapters,
activeChapter,
...comingChapters,
]
}

@ -0,0 +1,48 @@
import { useMemo } from 'react'
import { secondsToHms } from 'helpers'
import type { Chapters } from '../../types'
import { calculateChapterStyles } from './helpers/calculateChapterStyles'
export type Props = {
activeChapterIndex: number,
allPlayedProgress: number,
chapters?: Chapters,
duration: number,
loadedProgress: number,
onPlayedProgressChange: (progress: number, seeking: boolean) => void,
playedProgress: number,
}
export const useProgressBar = ({
activeChapterIndex,
allPlayedProgress,
chapters = [],
duration,
loadedProgress,
playedProgress,
}: Props) => {
const calculatedChapters = useMemo(
() => calculateChapterStyles({
activeChapterIndex,
chapters,
loadedProgress,
playedProgress,
videoDuration: duration,
}),
[
activeChapterIndex,
loadedProgress,
playedProgress,
duration,
chapters,
],
)
return {
calculatedChapters,
playedProgressInPercent: Math.min(allPlayedProgress * 100 / duration, 100),
time: secondsToHms(allPlayedProgress / 1000),
}
}

@ -0,0 +1,126 @@
import type { ReactElement } from 'react'
import styled from 'styled-components/macro'
import { VolumeBar } from 'features/StreamPlayer/components/VolumeBar'
import {
Controls,
Fullscreen,
PlayStop,
} from 'features/StreamPlayer/styled'
import { ProgressBar } from '.'
const Story = {
component: ProgressBar,
title: 'ProgressBarWithChapters',
}
export default Story
const Wrapper = styled.div`
position: relative;
width: 95vw;
height: 50vh;
left: 50%;
transform: translateX(-50%);
background-color: #000;
`
const callback = () => {}
const renderInControls = (progressBarElement: ReactElement) => (
<Wrapper>
<Controls visible>
<PlayStop onClick={callback} playing={false} />
<VolumeBar
value={50}
muted={false}
onChange={callback}
onClick={callback}
/>
{progressBarElement}
<Fullscreen onClick={callback} isFullscreen={false} />
</Controls>
</Wrapper>
)
const duration = 70000
const chapters = [
{
duration: 30000,
endMs: 30000,
endOffsetMs: 0,
period: 0,
startMs: 0,
startOffsetMs: 0,
urls: {},
},
{
duration: 30000,
endMs: 60000,
endOffsetMs: 0,
period: 0,
startMs: 30000,
startOffsetMs: 0,
urls: {},
},
{
duration: 10000,
endMs: 70000,
endOffsetMs: 0,
period: 0,
startMs: 60000,
startOffsetMs: 0,
urls: {},
},
]
export const Empty = () => renderInControls(
<ProgressBar
activeChapterIndex={0}
allPlayedProgress={0}
duration={duration}
chapters={chapters}
onPlayedProgressChange={callback}
playedProgress={0}
loadedProgress={0}
/>,
)
export const HalfLoaded = () => renderInControls(
<ProgressBar
activeChapterIndex={0}
allPlayedProgress={0}
duration={duration}
chapters={chapters}
onPlayedProgressChange={callback}
playedProgress={0}
loadedProgress={30000}
/>,
)
export const HalfPlayed = () => renderInControls(
<ProgressBar
activeChapterIndex={1}
allPlayedProgress={1}
duration={duration}
chapters={chapters}
onPlayedProgressChange={callback}
playedProgress={30000}
loadedProgress={0}
/>,
)
export const Loaded40AndPlayed20 = () => renderInControls(
<ProgressBar
activeChapterIndex={0}
allPlayedProgress={0}
duration={duration}
chapters={chapters}
onPlayedProgressChange={callback}
playedProgress={20000}
loadedProgress={40000}
/>,
)

@ -0,0 +1,27 @@
import { useToggle } from 'hooks'
export type Props = {
onSelect: (quality: string) => void,
selectedQuality: string,
videoQualities: Array<string>,
}
export const useSettings = ({ onSelect }: Props) => {
const {
close,
isOpen,
open,
} = useToggle()
const onItemClick = (quality: string) => {
onSelect(quality)
close()
}
return {
close,
isOpen,
onItemClick,
open,
}
}

@ -0,0 +1,47 @@
import { Fragment } from 'react'
import map from 'lodash/map'
import { OutsideClick } from 'features/OutsideClick'
import type { Props } from './hooks'
import { useSettings } from './hooks'
import {
SettingsButton,
QualitiesList,
QualityItem,
} from './styled'
export const Settings = (props: Props) => {
const { selectedQuality, videoQualities } = props
const {
close,
isOpen,
onItemClick,
open,
} = useSettings(props)
return (
<Fragment>
<SettingsButton onClick={open} />
{
isOpen && (
<OutsideClick onClick={close}>
<QualitiesList>
{
map(videoQualities, (quality) => (
<QualityItem
key={quality}
active={quality === selectedQuality}
onClick={() => onItemClick(quality)}
>
{quality}
</QualityItem>
))
}
</QualitiesList>
</OutsideClick>
)
}
</Fragment>
)
}

@ -0,0 +1,78 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { ButtonBase } from 'features/StreamPlayer/styled'
export const SettingsButton = styled(ButtonBase)`
width: 22px;
height: 20px;
margin-left: 25px;
background-image: url(/images/settings.svg);
${isMobileDevice
? css`
width: 20px;
height: 18px;
margin-left: 15px;
cursor: pointer;
`
: ''};
`
export const QualitiesList = styled.ul`
position: absolute;
z-index: 1;
bottom: calc(100% + 14px);
right: 24px;
width: 52px;
list-style: none;
border-radius: 2px;
background-color: rgba(0, 0, 0, 0.5);
overflow: hidden;
${isMobileDevice
? css`
right: 0;
bottom: 35px;
`
: ''};
`
type QualityItemProps = {
active: boolean,
}
const activeIcon = css`
:before {
position: absolute;
top: 45%;
transform: rotate(-45deg);
content: '';
left: 8px;
width: 5px;
height: 3px;
border-left: 1px solid #fff;
border-bottom: 1px solid #fff;
}
`
export const QualityItem = styled.li<QualityItemProps>`
width: 100%;
padding: 5px 8px;
text-align: right;
font-style: normal;
font-weight: normal;
/* stylelint-disable-next-line */
font-family: Montserrat;
font-size: 10px;
line-height: 12px;
letter-spacing: 0.01em;
color: #fff;
cursor: pointer;
position: relative;
:hover, :focus {
background-color: rgba(255, 255, 255, 0.1);
}
${({ active }) => (active ? activeIcon : '')}
`

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

@ -0,0 +1,35 @@
import { RefObject } from 'react'
import findIndex from 'lodash/findIndex'
import type { Chapters, Players } from '../types'
type Args = {
from?: number,
url: string,
videoRef: RefObject<HTMLVideoElement>,
}
export const preparePlayer = ({
from = 0,
url,
videoRef,
}: Args) => {
const video = videoRef?.current
if (!video) return
// eslint-disable-next-line no-param-reassign
video.src = url
if (from) {
video.currentTime = from
}
video.load()
}
export const findChapterByProgress = (chapters: Chapters, progressMs: number) => (
findIndex(chapters, ({ endMs, startMs }) => (
startMs <= progressMs && progressMs <= endMs
))
)
export const getNextPlayer = (player: Players): Players => (player + 1) % 2

@ -0,0 +1,9 @@
import { useMemo } from 'react'
import sumBy from 'lodash/sumBy'
import type { Chapters } from '../types'
export const useDuration = (chapters: Chapters) => (
useMemo(() => sumBy(chapters, 'duration'), [chapters])
)

@ -0,0 +1,102 @@
import { useCallback } from 'react'
import isUndefined from 'lodash/isUndefined'
import type { SetPartialState } from 'hooks'
import type { PlayerState } from '.'
import { getNextPlayer } from '../helpers'
export const usePlayingHandlers = (
setPlayerState: SetPartialState<PlayerState>,
numberOfChapters: number,
) => {
const onReady = useCallback(() => {
setPlayerState((state) => (
state.ready
? state
: { playing: true, ready: true }
))
}, [setPlayerState])
const togglePlaying = useCallback(() => {
setPlayerState((state) => (
state.ready
? { playing: !state.playing }
: state
))
}, [setPlayerState])
const stopPlaying = useCallback(() => {
setPlayerState({ playing: false })
}, [setPlayerState])
const startPlaying = useCallback(() => {
setPlayerState((state) => (
state.ready
? { playing: true }
: state
))
}, [setPlayerState])
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: state.activeChapterIndex + 1,
loadedProgress: 0,
playedProgress: fromMs,
playing: state.playing,
seek: {
...state.seek,
[state.activePlayer]: (startOffsetMs + fromMs) / 1000,
},
}
})
}, [numberOfChapters, setPlayerState])
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,
loadedProgress: 0,
playedProgress: fromMs,
seek: {
...state.seek,
[state.activePlayer]: (startOffsetMs + fromMs) / 1000,
},
}
})
}, [setPlayerState])
return {
onReady,
playNextChapter,
playPrevChapter,
startPlaying,
stopPlaying,
togglePlaying,
}
}

@ -0,0 +1,51 @@
import { useCallback } from 'react'
import type { SetPartialState } from 'hooks'
import type { Chapters } from '../types'
import type { PlayerState } from '.'
import { findChapterByProgress } from '../helpers'
type Args = {
chapters: Chapters,
duration: number,
setPlayerState: SetPartialState<PlayerState>,
}
export const useProgressChangeHandler = ({
chapters,
duration,
setPlayerState,
}: Args) => {
const onProgressChange = useCallback((progress: number, seeking: boolean) => {
setPlayerState((state) => {
// значение новой позиции ползунка в миллисекундах
const progressMs = progress * duration
const chapterIndex = findChapterByProgress(chapters, progressMs)
const chapter = chapters[chapterIndex]
const isProgressOnDifferentChapter = (
chapterIndex !== -1
&& chapterIndex !== state.activeChapterIndex
)
const nextChapter = isProgressOnDifferentChapter
? chapterIndex
: state.activeChapterIndex
// отнимаем начало главы на котором остановились от общего прогресса
// чтобы получить прогресс текущей главы
const chapterProgressMs = (progressMs - chapter.startMs)
const seekMs = chapterProgressMs + chapter.startOffsetMs
return {
activeChapterIndex: nextChapter,
playedProgress: chapterProgressMs,
seek: {
...state.seek,
[state.activePlayer]: seekMs / 1000,
},
seeking,
}
})
}, [duration, chapters, setPlayerState])
return onProgressChange
}

@ -0,0 +1,38 @@
import keys from 'lodash/keys'
import uniq from 'lodash/uniq'
import orderBy from 'lodash/orderBy'
import includes from 'lodash/includes'
import { useLocalStore } from 'hooks'
import type { Chapters } from '../types'
const getVideoQualities = (chapters: Chapters) => {
const qualities = uniq(keys(chapters[0]?.urls))
return orderBy(
qualities,
Number,
'desc',
)
}
export const useVideoQuality = (chapters: Chapters) => {
const videoQualities = getVideoQualities(chapters)
const qualityValidator = (localStorageQuality: string) => (
includes(videoQualities, localStorageQuality)
)
const [selectedQuality, setSelectedQuality] = useLocalStore({
// по умолчанию наилучшее качество
defaultValue: videoQualities[0],
key: 'player_quality',
validator: qualityValidator,
})
return {
selectedQuality,
setSelectedQuality,
videoQualities,
}
}

@ -0,0 +1,18 @@
export type Urls = { [quality: string]: string }
export type Chapter = {
duration: number,
endMs: number,
endOffsetMs: number,
period: number,
startMs: number,
startOffsetMs: number,
urls: Urls,
}
export type Chapters = Array<Chapter>
export enum Players {
PLAYER1 = 0,
PLAYER2 = 1,
}

@ -0,0 +1,40 @@
import isEmpty from 'lodash/isEmpty'
import filter from 'lodash/filter'
import { API_ROOT, SportTypes } from 'config'
import { callApi } from 'helpers'
const filterByIds = (videos: Videos) => {
const zeroIdVideos = filter(videos, { abc: '0' })
return isEmpty(zeroIdVideos) ? videos : zeroIdVideos
}
export type Video = {
/** id дорожки */
abc: string,
duration: number,
period: number,
quality: string,
start_ms: number,
url: string,
}
export type Videos = Array<Video>
export const getVideos = (
sportType: SportTypes,
matchId: number,
): Promise<Videos> => {
const config = {
body: {
match_id: matchId,
sport_id: sportType,
},
}
return callApi({
config,
// эндпоинт удалили со стейджинга, временно ссылаемся на прод
url: `${API_ROOT}/videoapi`,
}).then(filterByIds)
}

@ -11,6 +11,7 @@ export * from './getTournamentInfo'
export * from './getTeamInfo'
export * from './getUserInfo'
export * from './getMatchInfo'
export * from './getVideos'
export * from './getUnauthenticatedMatch'
export * from './reportPlayerProgress'
export * from './saveUserInfo'

Loading…
Cancel
Save