parent
ea00f09c7e
commit
a56c44521b
@ -1,139 +0,0 @@ |
|||||||
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, |
|
||||||
isFullMatchChapter: true, |
|
||||||
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 = { |
|
||||||
count: episode.c, |
|
||||||
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) |
|
||||||
} |
|
||||||
@ -1,71 +0,0 @@ |
|||||||
import { useEffect } from 'react' |
|
||||||
|
|
||||||
import { useToggle, useObjectState } from 'hooks' |
|
||||||
|
|
||||||
import type { Settings } from 'features/MatchPopup' |
|
||||||
import { useMatchPopupStore } from 'features/MatchPopup' |
|
||||||
import { useMatchPageStore } from 'features/MatchPage/store' |
|
||||||
|
|
||||||
import { usePlayerLogger } from './usePlayerLogger' |
|
||||||
import { useEpisodes } from './useEpisodes' |
|
||||||
import { useChapters } from './useChapters' |
|
||||||
|
|
||||||
const initPausedData = { |
|
||||||
activeChapterIndex: 0, |
|
||||||
activePlayer: 0 || 1, |
|
||||||
playedProgress: 0, |
|
||||||
} |
|
||||||
|
|
||||||
export type PausedData = typeof initPausedData |
|
||||||
|
|
||||||
export const useFinishedMatch = () => { |
|
||||||
const [pausedData, setPausedData] = useObjectState<PausedData>(initPausedData) |
|
||||||
|
|
||||||
const { setChapters, setSettings } = useMatchPopupStore() |
|
||||||
const { |
|
||||||
handlePlaylistClick, |
|
||||||
matchPlaylists, |
|
||||||
selectedPlaylist, |
|
||||||
} = useMatchPageStore() |
|
||||||
const { |
|
||||||
close: closeSettingsPopup, |
|
||||||
isOpen: isSettingsPopupOpen, |
|
||||||
open: openSettingsPopup, |
|
||||||
} = useToggle() |
|
||||||
|
|
||||||
const { episodes } = useEpisodes() |
|
||||||
|
|
||||||
const { logPlaylistChange, onPlayingChange } = usePlayerLogger() |
|
||||||
|
|
||||||
const setEpisodesSettings = (newSettings: Settings) => { |
|
||||||
setSettings(newSettings) |
|
||||||
closeSettingsPopup() |
|
||||||
} |
|
||||||
// @ts-expect-error
|
|
||||||
const onPlaylistSelect: typeof handlePlaylistClick = (playlist, e) => { |
|
||||||
if (selectedPlaylist) { |
|
||||||
logPlaylistChange(selectedPlaylist) |
|
||||||
} |
|
||||||
// @ts-expect-error
|
|
||||||
handlePlaylistClick(playlist, e) |
|
||||||
} |
|
||||||
const { chapters } = useChapters({ episodes, selectedPlaylist }) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
setChapters(chapters) |
|
||||||
}, [chapters, setChapters]) |
|
||||||
|
|
||||||
return { |
|
||||||
chapters, |
|
||||||
closeSettingsPopup, |
|
||||||
isSettingsPopupOpen, |
|
||||||
onPlayingChange, |
|
||||||
onPlaylistSelect, |
|
||||||
openSettingsPopup, |
|
||||||
pausedData, |
|
||||||
playlists: matchPlaylists, |
|
||||||
selectedPlaylist, |
|
||||||
setEpisodesSettings, |
|
||||||
setPausedData, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,46 +0,0 @@ |
|||||||
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 } |
|
||||||
} |
|
||||||
@ -1,71 +0,0 @@ |
|||||||
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 { useMatchPageStore } from 'features/MatchPage/store' |
|
||||||
|
|
||||||
import { |
|
||||||
defaultSettings, |
|
||||||
Settings, |
|
||||||
useMatchPopupStore, |
|
||||||
} from 'features/MatchPopup' |
|
||||||
|
|
||||||
export const useEpisodes = () => { |
|
||||||
const { settings } = useMatchPopupStore() |
|
||||||
const { |
|
||||||
handlePlaylistClick, |
|
||||||
matchPlaylists: playlists, |
|
||||||
selectedPlaylist, |
|
||||||
} = useMatchPageStore() |
|
||||||
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 } |
|
||||||
} |
|
||||||
@ -1,72 +0,0 @@ |
|||||||
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 } |
|
||||||
} |
|
||||||
@ -1,74 +0,0 @@ |
|||||||
import { Fragment } from 'react' |
|
||||||
|
|
||||||
import isEmpty from 'lodash/isEmpty' |
|
||||||
|
|
||||||
import { MatchSidePlaylists } from 'features/MatchSidePlaylists' |
|
||||||
import { MultiSourcePlayer } from 'features/MultiSourcePlayer' |
|
||||||
|
|
||||||
import { SettingsPopup } from '../SettingsPopup' |
|
||||||
|
|
||||||
import { useFinishedMatch } from './hooks' |
|
||||||
import { Container } from '../../styled' |
|
||||||
import { Modal } from './styled' |
|
||||||
import { MatchDescription } from '../MatchDescription' |
|
||||||
import { useMatchPageStore } from '../../store' |
|
||||||
|
|
||||||
export const FinishedMatch = () => { |
|
||||||
const { |
|
||||||
access, |
|
||||||
isOpenFiltersPopup, |
|
||||||
profile, |
|
||||||
profileCardShown, |
|
||||||
} = useMatchPageStore() |
|
||||||
const { |
|
||||||
chapters, |
|
||||||
closeSettingsPopup, |
|
||||||
isSettingsPopupOpen, |
|
||||||
onPlayingChange, |
|
||||||
onPlaylistSelect, |
|
||||||
pausedData, |
|
||||||
selectedPlaylist, |
|
||||||
setEpisodesSettings, |
|
||||||
setPausedData, |
|
||||||
} = useFinishedMatch() |
|
||||||
|
|
||||||
return ( |
|
||||||
<Fragment> |
|
||||||
<Modal |
|
||||||
close={closeSettingsPopup} |
|
||||||
isOpen={isSettingsPopupOpen} |
|
||||||
withCloseButton={false} |
|
||||||
> |
|
||||||
<SettingsPopup |
|
||||||
onWatchEpisodesClick={setEpisodesSettings} |
|
||||||
closePopup={closeSettingsPopup} |
|
||||||
profile={profile} |
|
||||||
selectedPlaylist={selectedPlaylist} |
|
||||||
/> |
|
||||||
</Modal> |
|
||||||
|
|
||||||
<Container isHidden={!profileCardShown}> |
|
||||||
{!isEmpty(chapters) && ( |
|
||||||
<Fragment> |
|
||||||
<MultiSourcePlayer |
|
||||||
pausedData={pausedData} |
|
||||||
setPausedData={setPausedData} |
|
||||||
access={access} |
|
||||||
isOpenPopup={isOpenFiltersPopup} |
|
||||||
chapters={chapters} |
|
||||||
onPlayingChange={onPlayingChange} |
|
||||||
profile={profile} |
|
||||||
/> |
|
||||||
<MatchDescription /> |
|
||||||
</Fragment> |
|
||||||
)} |
|
||||||
</Container> |
|
||||||
|
|
||||||
<MatchSidePlaylists |
|
||||||
selectedPlaylist={selectedPlaylist} |
|
||||||
// @ts-expect-error
|
|
||||||
onSelect={onPlaylistSelect} |
|
||||||
/> |
|
||||||
</Fragment> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,26 +0,0 @@ |
|||||||
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; |
|
||||||
border-radius: 5px; |
|
||||||
|
|
||||||
@media ${devices.tablet} { |
|
||||||
width: 100vw; |
|
||||||
} |
|
||||||
|
|
||||||
@media ${devices.mobile} { |
|
||||||
height: 100vh; |
|
||||||
padding: 0; |
|
||||||
background-color: transparent; |
|
||||||
} |
|
||||||
} |
|
||||||
` |
|
||||||
@ -1,47 +0,0 @@ |
|||||||
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]) |
|
||||||
}) |
|
||||||
@ -1,59 +0,0 @@ |
|||||||
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, |
|
||||||
] |
|
||||||
} |
|
||||||
@ -1,51 +0,0 @@ |
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */ |
|
||||||
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, |
|
||||||
onTouchEnd?: () => any, |
|
||||||
onTouchStart?: () => any, |
|
||||||
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), |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,52 +0,0 @@ |
|||||||
import { useSlider } from 'features/StreamPlayer/hooks/useSlider' |
|
||||||
import { TimeTooltip } from 'features/StreamPlayer/components/TimeTooltip' |
|
||||||
import { Scrubber, ScrubberContainer } from 'features/StreamPlayer/components/ProgressBar/styled' |
|
||||||
|
|
||||||
import { Chapters } from 'features/StreamPlayer/components/Chapters' |
|
||||||
import type { Props } from './hooks' |
|
||||||
import { useProgressBar } from './hooks' |
|
||||||
import { ProgressBarList } from './styled' |
|
||||||
import { isIOS } from '../../../../config/userAgent' |
|
||||||
|
|
||||||
export interface ProgressBarProps extends Props { |
|
||||||
isScrubberVisible?: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
export const ProgressBar = (props: ProgressBarProps) => { |
|
||||||
const { |
|
||||||
isScrubberVisible, |
|
||||||
onPlayedProgressChange, |
|
||||||
onTouchEnd, |
|
||||||
onTouchStart, |
|
||||||
} = props |
|
||||||
|
|
||||||
const progressBarRef = useSlider({ |
|
||||||
onChange: onPlayedProgressChange, |
|
||||||
onTouchEnd, |
|
||||||
onTouchStart, |
|
||||||
}) |
|
||||||
|
|
||||||
const { |
|
||||||
calculatedChapters, |
|
||||||
playedProgressInPercent, |
|
||||||
time, |
|
||||||
} = useProgressBar(props) |
|
||||||
|
|
||||||
return ( |
|
||||||
<ProgressBarList |
|
||||||
ref={progressBarRef} |
|
||||||
isIOS={isIOS} |
|
||||||
> |
|
||||||
<Chapters chapters={calculatedChapters} /> |
|
||||||
{isScrubberVisible && ( |
|
||||||
<ScrubberContainer> |
|
||||||
<Scrubber |
|
||||||
style={{ left: `${playedProgressInPercent}%` }} |
|
||||||
> |
|
||||||
<TimeTooltip time={time} /> |
|
||||||
</Scrubber> |
|
||||||
</ScrubberContainer> |
|
||||||
)} |
|
||||||
</ProgressBarList> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,126 +0,0 @@ |
|||||||
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} |
|
||||||
/>, |
|
||||||
) |
|
||||||
@ -1,20 +0,0 @@ |
|||||||
import styled, { css } from 'styled-components/macro' |
|
||||||
import { isMobileDevice } from 'config/userAgent' |
|
||||||
|
|
||||||
export const ProgressBarList = styled.div<{isIOS?: boolean}>` |
|
||||||
flex-grow: 1; |
|
||||||
height: 4px; |
|
||||||
position: relative; |
|
||||||
|
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
height: 1px; |
|
||||||
` |
|
||||||
: ''} |
|
||||||
|
|
||||||
${({ isIOS }) => (isIOS && isMobileDevice |
|
||||||
&& css` |
|
||||||
height: 3px; |
|
||||||
margin: 0 24px; |
|
||||||
`)}
|
|
||||||
` |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
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, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,47 +0,0 @@ |
|||||||
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} id='match_video_quality' /> |
|
||||||
{ |
|
||||||
isOpen && ( |
|
||||||
<OutsideClick onClick={close}> |
|
||||||
<QualitiesList> |
|
||||||
{ |
|
||||||
map(videoQualities, (quality) => ( |
|
||||||
<QualityItem |
|
||||||
key={quality} |
|
||||||
active={quality === selectedQuality} |
|
||||||
onClick={() => onItemClick(quality)} |
|
||||||
> |
|
||||||
{quality} |
|
||||||
</QualityItem> |
|
||||||
)) |
|
||||||
} |
|
||||||
</QualitiesList> |
|
||||||
</OutsideClick> |
|
||||||
) |
|
||||||
} |
|
||||||
</Fragment> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,78 +0,0 @@ |
|||||||
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-right: 25px; |
|
||||||
background-image: url(/images/settings.svg); |
|
||||||
|
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
width: 20px; |
|
||||||
height: 18px; |
|
||||||
margin-right: 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 : '')} |
|
||||||
` |
|
||||||
@ -1,5 +0,0 @@ |
|||||||
import { isMobileDevice } from 'config/userAgent' |
|
||||||
|
|
||||||
export const REWIND_SECONDS = isMobileDevice ? 10 : 5 |
|
||||||
|
|
||||||
export const HOUR_IN_MILLISECONDS = 60 * 60 * 1000 |
|
||||||
@ -1,35 +0,0 @@ |
|||||||
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 |
|
||||||
@ -1,494 +0,0 @@ |
|||||||
import type { MouseEvent } from 'react' |
|
||||||
import { |
|
||||||
useCallback, |
|
||||||
useEffect, |
|
||||||
useRef, |
|
||||||
useState, |
|
||||||
} from 'react' |
|
||||||
|
|
||||||
import { useTour } from '@reactour/tour' |
|
||||||
|
|
||||||
import size from 'lodash/size' |
|
||||||
import findIndex from 'lodash/findIndex' |
|
||||||
import inRange from 'lodash/inRange' |
|
||||||
import sum from 'lodash/sum' |
|
||||||
import values from 'lodash/values' |
|
||||||
|
|
||||||
import { KEYBOARD_KEYS } from 'config' |
|
||||||
|
|
||||||
import { useControlsVisibility } from 'features/StreamPlayer/hooks/useControlsVisibility' |
|
||||||
import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen' |
|
||||||
import { useVolume } from 'features/VideoPlayer/hooks/useVolume' |
|
||||||
import { useNoNetworkPopupStore } from 'features/NoNetworkPopup' |
|
||||||
import { useMatchPageStore } from 'features/MatchPage/store' |
|
||||||
import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists' |
|
||||||
|
|
||||||
import { |
|
||||||
useEventListener, |
|
||||||
useInterval, |
|
||||||
useObjectState, |
|
||||||
usePageParams, |
|
||||||
} from 'hooks' |
|
||||||
import type { SetPartialState } from 'hooks' |
|
||||||
|
|
||||||
import { |
|
||||||
MatchInfo, |
|
||||||
saveMatchStats, |
|
||||||
VIEW_INTERVAL_MS, |
|
||||||
} from 'requests' |
|
||||||
|
|
||||||
import type { PausedData } from 'features/MatchPage/components/FinishedMatch/hooks' |
|
||||||
|
|
||||||
import { useProgressChangeHandler } from './useProgressChangeHandler' |
|
||||||
import { usePlayingHandlers } from './usePlayingHandlers' |
|
||||||
import { useVideoQuality } from './useVideoQuality' |
|
||||||
import { useDuration } from './useDuration' |
|
||||||
import type { Chapters } from '../types' |
|
||||||
import { Players } from '../types' |
|
||||||
import { getNextPlayer } from '../helpers' |
|
||||||
import { REWIND_SECONDS } from '../config' |
|
||||||
|
|
||||||
const initialState = { |
|
||||||
activeChapterIndex: 0, |
|
||||||
activePlayer: Players.PLAYER1, |
|
||||||
loadedProgress: 0, |
|
||||||
playedProgress: 0, |
|
||||||
playing: false, |
|
||||||
ready: false, |
|
||||||
seek: { |
|
||||||
[Players.PLAYER1]: 0, |
|
||||||
[Players.PLAYER2]: 0, |
|
||||||
}, |
|
||||||
seeking: false, |
|
||||||
} |
|
||||||
|
|
||||||
export type PlayerState = typeof initialState |
|
||||||
|
|
||||||
export type Props = { |
|
||||||
access: boolean, |
|
||||||
chapters: Chapters, |
|
||||||
isOpenPopup?: boolean, |
|
||||||
onError?: () => void, |
|
||||||
onPlayingChange: (playing: boolean) => void, |
|
||||||
pausedData: PausedData, |
|
||||||
profile: MatchInfo, |
|
||||||
setPausedData: SetPartialState<PausedData>, |
|
||||||
} |
|
||||||
|
|
||||||
export const useMultiSourcePlayer = ({ |
|
||||||
chapters, |
|
||||||
onError, |
|
||||||
onPlayingChange, |
|
||||||
pausedData, |
|
||||||
setPausedData, |
|
||||||
}: Props) => { |
|
||||||
const [currentPlayingOrder, setCurrentPlayingOrder] = useState(0) |
|
||||||
const ordersObj = useRef<Record<string, number>>({}) |
|
||||||
|
|
||||||
const { |
|
||||||
disablePlayingEpisodes, |
|
||||||
handlePlaylistClick, |
|
||||||
isPlayFilterEpisodes, |
|
||||||
isPlayingEpisode, |
|
||||||
matchPlaylists, |
|
||||||
playingOrder, |
|
||||||
playingProgress, |
|
||||||
playNextEpisode, |
|
||||||
selectedPlaylist, |
|
||||||
setCircleAnimation, |
|
||||||
setPlayingProgress, |
|
||||||
} = useMatchPageStore() |
|
||||||
|
|
||||||
const { isOpen } = useTour() |
|
||||||
|
|
||||||
const { profileId, sportType } = usePageParams() |
|
||||||
|
|
||||||
/** время для сохранения статистики просмотра матча */ |
|
||||||
const timeForStatistics = useRef(0) |
|
||||||
|
|
||||||
const numberOfChapters = size(chapters) |
|
||||||
const [ |
|
||||||
{ |
|
||||||
activeChapterIndex: chapterIndex, |
|
||||||
activePlayer, |
|
||||||
loadedProgress, |
|
||||||
playedProgress, |
|
||||||
playing: statePlaying, |
|
||||||
ready, |
|
||||||
seek, |
|
||||||
seeking, |
|
||||||
}, |
|
||||||
setPlayerState, |
|
||||||
] = useObjectState(initialState) |
|
||||||
const video1Ref = useRef<HTMLVideoElement>(null) |
|
||||||
const video2Ref = useRef<HTMLVideoElement>(null) |
|
||||||
const activeChapterIndex = numberOfChapters >= chapterIndex ? chapterIndex : 0 |
|
||||||
const { |
|
||||||
onReady, |
|
||||||
playNextChapter, |
|
||||||
playPrevChapter, |
|
||||||
stopPlaying, |
|
||||||
togglePlaying, |
|
||||||
} = usePlayingHandlers(setPlayerState, numberOfChapters) |
|
||||||
|
|
||||||
const { |
|
||||||
selectedQuality, |
|
||||||
setSelectedQuality, |
|
||||||
videoQualities, |
|
||||||
} = useVideoQuality(chapters) |
|
||||||
|
|
||||||
const duration = useDuration(chapters) |
|
||||||
|
|
||||||
const playing = Boolean(statePlaying && !isOpen) |
|
||||||
|
|
||||||
const handleError = useCallback(() => { |
|
||||||
onError?.() |
|
||||||
}, [onError]) |
|
||||||
|
|
||||||
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, |
|
||||||
setPlayerState, |
|
||||||
}) |
|
||||||
|
|
||||||
const { |
|
||||||
isFullscreen, |
|
||||||
onFullscreenClick, |
|
||||||
wrapperRef, |
|
||||||
} = useFullscreen(videoRef) |
|
||||||
|
|
||||||
const getChapterUrl = useCallback((index: number, quality: string = selectedQuality) => ( |
|
||||||
getActiveChapter(index)?.urls[quality] |
|
||||||
), [selectedQuality, getActiveChapter]) |
|
||||||
|
|
||||||
const onQualitySelect = (quality: string) => { |
|
||||||
setPlayerState((state) => ({ |
|
||||||
seek: { |
|
||||||
...state.seek, |
|
||||||
[state.activePlayer]: videoRef.current?.currentTime ?? 0, |
|
||||||
}, |
|
||||||
})) |
|
||||||
setSelectedQuality(quality) |
|
||||||
} |
|
||||||
|
|
||||||
const onPlayerClick = (e: MouseEvent<HTMLDivElement>) => { |
|
||||||
if (e.target === videoRef.current) { |
|
||||||
togglePlaying() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const onLoadedProgress = (loadedMs: number) => { |
|
||||||
const chapter = getActiveChapter() |
|
||||||
const value = loadedMs - chapter.startOffsetMs |
|
||||||
|
|
||||||
setPlayerState({ loadedProgress: value }) |
|
||||||
} |
|
||||||
|
|
||||||
const onPlayedProgress = (playedMs: number) => { |
|
||||||
const chapter = getActiveChapter() |
|
||||||
const value = Math.max(playedMs - chapter.startOffsetMs, 0) |
|
||||||
|
|
||||||
timeForStatistics.current = (value + chapter.startMs) / 1000 |
|
||||||
|
|
||||||
setPlayerState({ playedProgress: value }) |
|
||||||
setPlayingProgress(Math.floor(value / 1000)) |
|
||||||
|
|
||||||
if (chapter.isFullMatchChapter) { |
|
||||||
setPausedData({ |
|
||||||
activeChapterIndex, |
|
||||||
activePlayer, |
|
||||||
playedProgress: value, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const backToPausedTime = useCallback(() => { |
|
||||||
if (selectedPlaylist?.id !== FULL_GAME_KEY) { |
|
||||||
// @ts-expect-error
|
|
||||||
handlePlaylistClick(matchPlaylists.match[0]) |
|
||||||
} |
|
||||||
|
|
||||||
setTimeout(() => { |
|
||||||
setPlayerState((state) => ({ |
|
||||||
activeChapterIndex: pausedData.activeChapterIndex, |
|
||||||
playedProgress: pausedData.playedProgress, |
|
||||||
seek: { |
|
||||||
...state.seek, |
|
||||||
[pausedData.activePlayer]: pausedData.playedProgress / 1000, |
|
||||||
}, |
|
||||||
})) |
|
||||||
}, 0) |
|
||||||
}, [ |
|
||||||
selectedPlaylist?.id, |
|
||||||
pausedData, |
|
||||||
handlePlaylistClick, |
|
||||||
matchPlaylists.match, |
|
||||||
setPlayerState, |
|
||||||
]) |
|
||||||
|
|
||||||
const onEnded = () => { |
|
||||||
playNextChapter() |
|
||||||
} |
|
||||||
const onPause = () => { |
|
||||||
setPlayerState({ playing: false }) |
|
||||||
} |
|
||||||
|
|
||||||
const restartVideo = () => { |
|
||||||
// @ts-expect-error
|
|
||||||
handlePlaylistClick(matchPlaylists.match[0]) |
|
||||||
} |
|
||||||
|
|
||||||
const stopPlayingEpisodes = () => { |
|
||||||
disablePlayingEpisodes() |
|
||||||
restartVideo() |
|
||||||
setTimeout(() => { |
|
||||||
setPlayerState((state) => ({ |
|
||||||
activeChapterIndex: pausedData.activeChapterIndex, |
|
||||||
playedProgress: pausedData.playedProgress, |
|
||||||
seek: { |
|
||||||
...state.seek, |
|
||||||
[pausedData.activePlayer]: pausedData.playedProgress / 1000, |
|
||||||
}, |
|
||||||
})) |
|
||||||
}, 1000) |
|
||||||
} |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
onPlayingChange(playing) |
|
||||||
}, [playing, onPlayingChange]) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
setPlayerState({ ...initialState }) |
|
||||||
}, [profileId, setPlayerState]) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
setCircleAnimation((state) => ({ |
|
||||||
...state, |
|
||||||
playedProgress, |
|
||||||
playing, |
|
||||||
ready, |
|
||||||
})) |
|
||||||
}, [ |
|
||||||
playedProgress, |
|
||||||
playing, |
|
||||||
ready, |
|
||||||
setCircleAnimation, |
|
||||||
]) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
setPlayerState((state) => ({ |
|
||||||
...initialState, |
|
||||||
activePlayer: getNextPlayer(state.activePlayer), |
|
||||||
playing: true, |
|
||||||
})) |
|
||||||
}, [chapters, setPlayerState]) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
const { duration: chapterDuration } = getActiveChapter() |
|
||||||
if (playedProgress >= chapterDuration && !seeking && isPlayFilterEpisodes) { |
|
||||||
setPlayerState({ playedProgress: 0 }) |
|
||||||
playNextEpisode() |
|
||||||
} |
|
||||||
if (playedProgress >= chapterDuration && !seeking && !isPlayFilterEpisodes) { |
|
||||||
if (isLastChapterPlaying) { |
|
||||||
backToPausedTime() |
|
||||||
} else { |
|
||||||
playNextChapter() |
|
||||||
} |
|
||||||
} |
|
||||||
}, [ |
|
||||||
isPlayFilterEpisodes, |
|
||||||
playNextEpisode, |
|
||||||
getActiveChapter, |
|
||||||
playedProgress, |
|
||||||
seeking, |
|
||||||
playNextChapter, |
|
||||||
setPlayerState, |
|
||||||
isLastChapterPlaying, |
|
||||||
backToPausedTime, |
|
||||||
]) |
|
||||||
|
|
||||||
useEventListener({ |
|
||||||
callback: (e: KeyboardEvent) => { |
|
||||||
if (isOpen) return |
|
||||||
if (e.code === KEYBOARD_KEYS.ArrowLeft) rewindBackward() |
|
||||||
else if (e.code === KEYBOARD_KEYS.ArrowRight) rewindForward() |
|
||||||
}, |
|
||||||
event: 'keydown', |
|
||||||
}) |
|
||||||
|
|
||||||
const { isOnline } = useNoNetworkPopupStore() |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (!isOnline) { |
|
||||||
stopPlaying() |
|
||||||
} |
|
||||||
}, [ |
|
||||||
isOnline, |
|
||||||
stopPlaying, |
|
||||||
]) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (ready && videoRef && !isPlayingEpisode) { |
|
||||||
videoRef.current?.scrollIntoView({ |
|
||||||
behavior: 'smooth', |
|
||||||
block: 'start', |
|
||||||
}) |
|
||||||
} |
|
||||||
}, [ready, videoRef, isPlayingEpisode]) |
|
||||||
|
|
||||||
// ведем статистику просмотра матча
|
|
||||||
const { start: startCollectingStats, stop: stopCollectingStats } = useInterval({ |
|
||||||
callback: () => { |
|
||||||
if (timeForStatistics.current !== 0) { |
|
||||||
saveMatchStats({ |
|
||||||
matchId: profileId, |
|
||||||
matchSecond: timeForStatistics.current, |
|
||||||
sportType, |
|
||||||
}) |
|
||||||
} |
|
||||||
}, |
|
||||||
intervalDuration: VIEW_INTERVAL_MS, |
|
||||||
startImmediate: false, |
|
||||||
}) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (playing) { |
|
||||||
startCollectingStats() |
|
||||||
} else { |
|
||||||
stopCollectingStats() |
|
||||||
} |
|
||||||
}, [ |
|
||||||
playing, |
|
||||||
startCollectingStats, |
|
||||||
stopCollectingStats, |
|
||||||
profileId, |
|
||||||
]) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
const splitEpisodeDuration = (n: number, m: number) => { |
|
||||||
const result: Array<[number, number]> = [] |
|
||||||
|
|
||||||
for (let i = 0; i < n; i += m) { |
|
||||||
const start = i |
|
||||||
const end = Math.min(i + m, n) |
|
||||||
result.push([start, end]) |
|
||||||
} |
|
||||||
|
|
||||||
return result |
|
||||||
} |
|
||||||
|
|
||||||
const getCurrentPlayingOrder = () => { |
|
||||||
const { count, duration: dur } = getActiveChapter() |
|
||||||
|
|
||||||
const arr = splitEpisodeDuration(dur, dur / count!) |
|
||||||
|
|
||||||
const index = findIndex(arr, (elem) => inRange( |
|
||||||
playingProgress * 1000, |
|
||||||
elem[0], |
|
||||||
elem[1] + 1, |
|
||||||
)) |
|
||||||
|
|
||||||
if (index !== -1 && ordersObj.current[playingOrder] !== index) { |
|
||||||
ordersObj.current = { |
|
||||||
...ordersObj.current, |
|
||||||
[playingOrder]: index, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return playingOrder + sum(values(ordersObj.current)) |
|
||||||
} |
|
||||||
|
|
||||||
setCurrentPlayingOrder(getCurrentPlayingOrder()) |
|
||||||
}, [getActiveChapter, isPlayingEpisode, playingOrder, playingProgress]) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (!isPlayingEpisode) { |
|
||||||
ordersObj.current = {} |
|
||||||
} |
|
||||||
}, [isPlayingEpisode]) |
|
||||||
|
|
||||||
return { |
|
||||||
activeChapterIndex, |
|
||||||
activePlayer, |
|
||||||
activeSrc: getChapterUrl(activeChapterIndex), |
|
||||||
allPlayedProgress: playedProgress + getActiveChapter().startMs, |
|
||||||
chapters, |
|
||||||
currentPlayingOrder, |
|
||||||
duration, |
|
||||||
isFirstChapterPlaying, |
|
||||||
isFullscreen, |
|
||||||
isLastChapterPlaying, |
|
||||||
loadedProgress, |
|
||||||
nextSrc: getChapterUrl((activeChapterIndex + 1) % numberOfChapters), |
|
||||||
numberOfChapters, |
|
||||||
onEnded, |
|
||||||
onError: handleError, |
|
||||||
onFullscreenClick, |
|
||||||
onLoadedProgress, |
|
||||||
onPause, |
|
||||||
onPlayedProgress, |
|
||||||
onPlayerClick, |
|
||||||
onProgressChange, |
|
||||||
onQualitySelect, |
|
||||||
onReady, |
|
||||||
playNextChapter, |
|
||||||
playPrevChapter, |
|
||||||
playedProgress, |
|
||||||
playing, |
|
||||||
ready, |
|
||||||
rewindBackward, |
|
||||||
rewindForward, |
|
||||||
seek, |
|
||||||
selectedQuality, |
|
||||||
stopPlayingEpisodes, |
|
||||||
togglePlaying, |
|
||||||
video1Ref, |
|
||||||
video2Ref, |
|
||||||
videoQualities, |
|
||||||
wrapperRef, |
|
||||||
...useControlsVisibility(isFullscreen, playing), |
|
||||||
...useVolume(), |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,102 +0,0 @@ |
|||||||
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, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,51 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
||||||
@ -1,38 +0,0 @@ |
|||||||
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, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,268 +0,0 @@ |
|||||||
import { Fragment } from 'react' |
|
||||||
|
|
||||||
import { Loader } from 'features/Loader' |
|
||||||
import { |
|
||||||
PlayerWrapper, |
|
||||||
CenterControls, |
|
||||||
PlayStop, |
|
||||||
LoaderWrapper, |
|
||||||
Backward, |
|
||||||
Forward, |
|
||||||
ControlsGradient, |
|
||||||
TeamsDetailsWrapper, |
|
||||||
EpisodeInfo, |
|
||||||
EpisodeInfoName, |
|
||||||
EpisodeInfoOrder, |
|
||||||
EpisodeInfoDivider, |
|
||||||
CloseButton, |
|
||||||
} from 'features/StreamPlayer/styled' |
|
||||||
import { VideoPlayer } from 'features/VideoPlayer' |
|
||||||
import { Controls } from 'features/StreamPlayer/components/Controls' |
|
||||||
import { Name } from 'features/Name' |
|
||||||
import RewindMobile from 'features/StreamPlayer/components/RewindMobile' |
|
||||||
import { FiltersPopup } from 'features/MatchSidePlaylists/components/FiltersPopup' |
|
||||||
import { useAuthStore } from 'features/AuthStore' |
|
||||||
import { useMatchPageStore } from 'features/MatchPage/store' |
|
||||||
|
|
||||||
import { isMobileDevice } from 'config/userAgent' |
|
||||||
|
|
||||||
import { AccessTimer } from 'components/AccessTimer' |
|
||||||
|
|
||||||
import type { Props } from './hooks' |
|
||||||
import { useMultiSourcePlayer } from './hooks' |
|
||||||
import { Players } from './types' |
|
||||||
import { REWIND_SECONDS } from './config' |
|
||||||
|
|
||||||
export const MultiSourcePlayer = (props: Props) => { |
|
||||||
const { |
|
||||||
access, |
|
||||||
isOpenPopup, |
|
||||||
profile, |
|
||||||
} = props |
|
||||||
|
|
||||||
const { |
|
||||||
activeChapterIndex, |
|
||||||
activePlayer, |
|
||||||
activeSrc, |
|
||||||
allPlayedProgress, |
|
||||||
centerControlsVisible, |
|
||||||
chapters, |
|
||||||
currentPlayingOrder, |
|
||||||
duration, |
|
||||||
hideCenterControls, |
|
||||||
isFirstChapterPlaying, |
|
||||||
isFullscreen, |
|
||||||
isLastChapterPlaying, |
|
||||||
loadedProgress, |
|
||||||
mainControlsVisible, |
|
||||||
muted, |
|
||||||
nextSrc, |
|
||||||
numberOfChapters, |
|
||||||
onEnded, |
|
||||||
onError, |
|
||||||
onFullscreenClick, |
|
||||||
onLoadedProgress, |
|
||||||
onMouseEnter, |
|
||||||
onMouseLeave, |
|
||||||
onMouseMove, |
|
||||||
onPause, |
|
||||||
onPlayedProgress, |
|
||||||
onPlayerClick, |
|
||||||
onProgressChange, |
|
||||||
onQualitySelect, |
|
||||||
onReady, |
|
||||||
onTouchEnd, |
|
||||||
onTouchStart, |
|
||||||
onVolumeChange, |
|
||||||
onVolumeClick, |
|
||||||
playedProgress, |
|
||||||
playing, |
|
||||||
playNextChapter, |
|
||||||
playPrevChapter, |
|
||||||
ready, |
|
||||||
rewindBackward, |
|
||||||
rewindForward, |
|
||||||
seek, |
|
||||||
selectedQuality, |
|
||||||
showCenterControls, |
|
||||||
stopPlayingEpisodes, |
|
||||||
togglePlaying, |
|
||||||
video1Ref, |
|
||||||
video2Ref, |
|
||||||
videoQualities, |
|
||||||
volume, |
|
||||||
volumeInPercent, |
|
||||||
wrapperRef, |
|
||||||
} = useMultiSourcePlayer(props) |
|
||||||
const { episodeInfo, isPlayingEpisode } = useMatchPageStore() |
|
||||||
|
|
||||||
const firstPlayerActive = activePlayer === Players.PLAYER1 |
|
||||||
const currentVideo = firstPlayerActive ? video1Ref : video2Ref |
|
||||||
const { user } = useAuthStore() |
|
||||||
|
|
||||||
return ( |
|
||||||
<PlayerWrapper |
|
||||||
ref={wrapperRef} |
|
||||||
playing={playing} |
|
||||||
onClick={isMobileDevice ? showCenterControls : onPlayerClick} |
|
||||||
onMouseMove={onMouseMove} |
|
||||||
onMouseEnter={onMouseEnter} |
|
||||||
onMouseLeave={onMouseLeave} |
|
||||||
onTouchStart={onTouchStart} |
|
||||||
onTouchEnd={onTouchEnd} |
|
||||||
> |
|
||||||
{isPlayingEpisode && ( |
|
||||||
<EpisodeInfo> |
|
||||||
<EpisodeInfoName> |
|
||||||
{isMobileDevice |
|
||||||
? ( |
|
||||||
<Fragment> |
|
||||||
{episodeInfo.playerOrTeamName} |
|
||||||
{episodeInfo.playerOrTeamName && <br />} |
|
||||||
{episodeInfo.paramName} |
|
||||||
</Fragment> |
|
||||||
) |
|
||||||
: `${episodeInfo.playerOrTeamName || ''}${episodeInfo.paramName && episodeInfo.playerOrTeamName ? ' - ' : ''}${episodeInfo.paramName || ''}`} |
|
||||||
</EpisodeInfoName> |
|
||||||
{currentPlayingOrder > 0 && ( |
|
||||||
<EpisodeInfoOrder> |
|
||||||
{currentPlayingOrder} |
|
||||||
<EpisodeInfoDivider>/</EpisodeInfoDivider> |
|
||||||
{episodeInfo.episodesCount} |
|
||||||
</EpisodeInfoOrder> |
|
||||||
)} |
|
||||||
<CloseButton onClick={stopPlayingEpisodes} /> |
|
||||||
</EpisodeInfo> |
|
||||||
)} |
|
||||||
{ |
|
||||||
!ready && ( |
|
||||||
<LoaderWrapper buffering> |
|
||||||
<Loader color='#515151' /> |
|
||||||
</LoaderWrapper> |
|
||||||
) |
|
||||||
} |
|
||||||
{isOpenPopup && <FiltersPopup />} |
|
||||||
<VideoPlayer |
|
||||||
src={firstPlayerActive ? activeSrc : nextSrc} |
|
||||||
playing={firstPlayerActive ? playing : false} |
|
||||||
hidden={!firstPlayerActive} |
|
||||||
muted={muted} |
|
||||||
volume={volume} |
|
||||||
ref={video1Ref} |
|
||||||
seek={seek[Players.PLAYER1]} |
|
||||||
isFullscreen={isFullscreen} |
|
||||||
onLoadedProgress={firstPlayerActive ? onLoadedProgress : undefined} |
|
||||||
onPlayedProgress={firstPlayerActive ? onPlayedProgress : undefined} |
|
||||||
onEnded={onEnded} |
|
||||||
onPause={firstPlayerActive ? onPause : undefined} |
|
||||||
onError={onError} |
|
||||||
onReady={onReady} |
|
||||||
/> |
|
||||||
<VideoPlayer |
|
||||||
src={!firstPlayerActive ? activeSrc : nextSrc} |
|
||||||
playing={!firstPlayerActive ? playing : false} |
|
||||||
hidden={firstPlayerActive} |
|
||||||
muted={muted} |
|
||||||
volume={volume} |
|
||||||
ref={video2Ref} |
|
||||||
seek={seek[Players.PLAYER2]} |
|
||||||
isFullscreen={isFullscreen} |
|
||||||
onLoadedProgress={!firstPlayerActive ? onLoadedProgress : undefined} |
|
||||||
onPlayedProgress={!firstPlayerActive ? onPlayedProgress : undefined} |
|
||||||
onEnded={onEnded} |
|
||||||
onPause={!firstPlayerActive ? onPause : undefined} |
|
||||||
onError={onError} |
|
||||||
onReady={onReady} |
|
||||||
/> |
|
||||||
|
|
||||||
{isMobileDevice && isFullscreen && mainControlsVisible && profile && ( |
|
||||||
<TeamsDetailsWrapper> |
|
||||||
<Name nameObj={profile.team1} /> |
|
||||||
{` ${profile.team1.score}-${profile.team2.score} `} |
|
||||||
<Name nameObj={profile.team2} /> |
|
||||||
</TeamsDetailsWrapper> |
|
||||||
)} |
|
||||||
|
|
||||||
{ready && ( |
|
||||||
<CenterControls |
|
||||||
controlsVisible={centerControlsVisible} |
|
||||||
playing={!currentVideo.current?.paused} |
|
||||||
> |
|
||||||
{isMobileDevice && playing ? ( |
|
||||||
<RewindMobile isBackward rewindCallback={rewindBackward} /> |
|
||||||
) : ( |
|
||||||
<Backward |
|
||||||
size={isMobileDevice ? 'sm' : 'lg'} |
|
||||||
onClick={rewindBackward} |
|
||||||
> |
|
||||||
{REWIND_SECONDS} |
|
||||||
</Backward> |
|
||||||
)} |
|
||||||
<PlayStop |
|
||||||
size='lg' |
|
||||||
marginRight={0} |
|
||||||
playing={playing} |
|
||||||
onClickCapture={() => { |
|
||||||
togglePlaying() |
|
||||||
hideCenterControls() |
|
||||||
}} |
|
||||||
/> |
|
||||||
{isMobileDevice && playing |
|
||||||
? <RewindMobile isForward rewindCallback={rewindForward} /> |
|
||||||
: ( |
|
||||||
<Forward |
|
||||||
size={isMobileDevice ? 'sm' : 'lg'} |
|
||||||
onClick={rewindForward} |
|
||||||
> |
|
||||||
{REWIND_SECONDS} |
|
||||||
</Forward> |
|
||||||
)} |
|
||||||
</CenterControls> |
|
||||||
)} |
|
||||||
<Controls |
|
||||||
activeChapterIndex={activeChapterIndex} |
|
||||||
activePlayer={activePlayer} |
|
||||||
allPlayedProgress={allPlayedProgress} |
|
||||||
multiSourceChapters={chapters} |
|
||||||
controlsVisible={mainControlsVisible} |
|
||||||
duration={duration} |
|
||||||
isFirstChapterPlaying={isFirstChapterPlaying} |
|
||||||
isFullscreen={isFullscreen} |
|
||||||
isLastChapterPlaying={isLastChapterPlaying} |
|
||||||
loadedProgress={loadedProgress} |
|
||||||
muted={muted} |
|
||||||
numberOfChapters={numberOfChapters} |
|
||||||
onFullscreenClick={onFullscreenClick} |
|
||||||
onProgressChange={onProgressChange} |
|
||||||
onQualitySelect={onQualitySelect} |
|
||||||
onTouchEnd={onTouchEnd} |
|
||||||
onTouchStart={onTouchStart} |
|
||||||
onVolumeChange={onVolumeChange} |
|
||||||
onVolumeClick={onVolumeClick} |
|
||||||
playNextChapter={playNextChapter} |
|
||||||
playPrevChapter={playPrevChapter} |
|
||||||
playedProgress={playedProgress} |
|
||||||
playing={playing} |
|
||||||
rewindBackward={rewindBackward} |
|
||||||
rewindForward={rewindForward} |
|
||||||
selectedQuality={selectedQuality} |
|
||||||
src={firstPlayerActive ? activeSrc : nextSrc} |
|
||||||
togglePlaying={togglePlaying} |
|
||||||
videoQualities={videoQualities} |
|
||||||
videoRef={currentVideo} |
|
||||||
volume={volume} |
|
||||||
volumeInPercent={volumeInPercent} |
|
||||||
/> |
|
||||||
<ControlsGradient isVisible={mainControlsVisible} /> |
|
||||||
{!user && ( |
|
||||||
<AccessTimer |
|
||||||
access={access} |
|
||||||
isFullscreen={isFullscreen} |
|
||||||
onFullscreenClick={onFullscreenClick} |
|
||||||
playing={ready && playing && !!playedProgress} |
|
||||||
videoRef={currentVideo} |
|
||||||
/> |
|
||||||
)} |
|
||||||
</PlayerWrapper> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,48 +0,0 @@ |
|||||||
import styled, { css } from 'styled-components/macro' |
|
||||||
|
|
||||||
import { isMobileDevice } from 'config/userAgent' |
|
||||||
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; |
|
||||||
|
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
margin: 0 5px; |
|
||||||
font-size: 12px; |
|
||||||
width: 25%; |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
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;' |
|
||||||
: '' |
|
||||||
)} |
|
||||||
|
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
width: 20px; |
|
||||||
height: 20px; |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
export const Next = styled(Prev)` |
|
||||||
margin-right: 10px; |
|
||||||
transform: rotate(180deg); |
|
||||||
` |
|
||||||
@ -1,20 +0,0 @@ |
|||||||
export type Urls = { [quality: string]: string } |
|
||||||
|
|
||||||
export type Chapter = { |
|
||||||
count?: number, |
|
||||||
duration: number, |
|
||||||
endMs: number, |
|
||||||
endOffsetMs: number, |
|
||||||
isFullMatchChapter?: boolean, |
|
||||||
period: number, |
|
||||||
startMs: number, |
|
||||||
startOffsetMs: number, |
|
||||||
urls: Urls, |
|
||||||
} |
|
||||||
|
|
||||||
export type Chapters = Array<Chapter> |
|
||||||
|
|
||||||
export enum Players { |
|
||||||
PLAYER1 = 0, |
|
||||||
PLAYER2 = 1, |
|
||||||
} |
|
||||||
@ -1,51 +0,0 @@ |
|||||||
import YouTube from 'react-youtube' |
|
||||||
|
|
||||||
import { useMatchPageStore } from 'features/MatchPage/store' |
|
||||||
import { FiltersPopup } from 'features/MatchSidePlaylists/components/FiltersPopup' |
|
||||||
|
|
||||||
import { PlayerWrapper } from '../../styled' |
|
||||||
import { useVideoPlayer, Props } from '../../hooks' |
|
||||||
|
|
||||||
export const YoutubePlayer = (props: Props) => { |
|
||||||
const { isOpenFiltersPopup, profile } = useMatchPageStore() |
|
||||||
|
|
||||||
const { |
|
||||||
onMouseMove, |
|
||||||
onPlayerClick, |
|
||||||
onTouchEnd, |
|
||||||
onTouchStart, |
|
||||||
playing, |
|
||||||
sizeOptions: { |
|
||||||
height, |
|
||||||
width, |
|
||||||
}, |
|
||||||
wrapperRef, |
|
||||||
} = useVideoPlayer(props) |
|
||||||
|
|
||||||
const youtube_link = profile?.youtube_link || '' |
|
||||||
const key = youtube_link.slice(32, youtube_link.length) |
|
||||||
|
|
||||||
return ( |
|
||||||
<PlayerWrapper |
|
||||||
ref={wrapperRef} |
|
||||||
playing={playing} |
|
||||||
onClick={onPlayerClick} |
|
||||||
onMouseMove={onMouseMove} |
|
||||||
onTouchStart={onTouchStart} |
|
||||||
onTouchEnd={onTouchEnd} |
|
||||||
> |
|
||||||
{isOpenFiltersPopup && <FiltersPopup />} |
|
||||||
<YouTube |
|
||||||
videoId={key} |
|
||||||
opts={{ |
|
||||||
height: String(height), |
|
||||||
playerVars: { |
|
||||||
autoplay: 1, |
|
||||||
modestbranding: 1, |
|
||||||
}, |
|
||||||
width: String(width), |
|
||||||
}} |
|
||||||
/> |
|
||||||
</PlayerWrapper> |
|
||||||
) |
|
||||||
} |
|
||||||
Loading…
Reference in new issue