diff --git a/package-lock.json b/package-lock.json index edade4f4..7ad00beb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "babel-polyfill": "^6.26.0", "date-fns": "^2.14.0", "history": "^4.10.1", - "hls.js": "^0.14.15", + "hls.js": "^1.1.1", "lodash": "^4.17.15", "m3u8-parser": "^4.7.0", "oidc-client": "^1.11.5", @@ -46,7 +46,6 @@ "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^7.1.2", "@types/history": "^4.7.6", - "@types/hls.js": "^0.13.2", "@types/jest": "^26.0.15", "@types/lodash": "^4.14.154", "@types/node": "^12.0.0", @@ -8659,12 +8658,6 @@ "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==", "dev": true }, - "node_modules/@types/hls.js": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/@types/hls.js/-/hls.js-0.13.3.tgz", - "integrity": "sha512-Po8ZPCsAcPPuf5OODPEkb6cdWJ/w4BdX1veP7IIOc2WG0x1SW4GEQ1+FHKN1AMG2AePJfNUceJbh5PKtP92yRQ==", - "dev": true - }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -17659,13 +17652,9 @@ } }, "node_modules/hls.js": { - "version": "0.14.17", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-0.14.17.tgz", - "integrity": "sha512-25A7+m6qqp6UVkuzUQ//VVh2EEOPYlOBg32ypr34bcPO7liBMOkKFvbjbCBfiPAOTA/7BSx1Dujft3Th57WyFg==", - "dependencies": { - "eventemitter3": "^4.0.3", - "url-toolkit": "^2.1.6" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.0.tgz", + "integrity": "sha512-QIEQIUpBRhcpBMq3NA+/qozG8lbNfVekuX7kCMUlhiVu4382xFWsnwcuBe/CA4Gp/wB/pf2aRBaGRFlxh/FN8g==" }, "node_modules/hmac-drbg": { "version": "1.0.1", @@ -39870,12 +39859,6 @@ "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==", "dev": true }, - "@types/hls.js": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/@types/hls.js/-/hls.js-0.13.3.tgz", - "integrity": "sha512-Po8ZPCsAcPPuf5OODPEkb6cdWJ/w4BdX1veP7IIOc2WG0x1SW4GEQ1+FHKN1AMG2AePJfNUceJbh5PKtP92yRQ==", - "dev": true - }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -47032,13 +47015,9 @@ } }, "hls.js": { - "version": "0.14.17", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-0.14.17.tgz", - "integrity": "sha512-25A7+m6qqp6UVkuzUQ//VVh2EEOPYlOBg32ypr34bcPO7liBMOkKFvbjbCBfiPAOTA/7BSx1Dujft3Th57WyFg==", - "requires": { - "eventemitter3": "^4.0.3", - "url-toolkit": "^2.1.6" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.0.tgz", + "integrity": "sha512-QIEQIUpBRhcpBMq3NA+/qozG8lbNfVekuX7kCMUlhiVu4382xFWsnwcuBe/CA4Gp/wB/pf2aRBaGRFlxh/FN8g==" }, "hmac-drbg": { "version": "1.0.1", diff --git a/package.json b/package.json index 2436e172..1a69883f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "babel-polyfill": "^6.26.0", "date-fns": "^2.14.0", "history": "^4.10.1", - "hls.js": "^0.14.15", + "hls.js": "^1.1.1", "lodash": "^4.17.15", "m3u8-parser": "^4.7.0", "oidc-client": "^1.11.5", @@ -56,7 +56,6 @@ "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^7.1.2", "@types/history": "^4.7.6", - "@types/hls.js": "^0.13.2", "@types/jest": "^26.0.15", "@types/lodash": "^4.14.154", "@types/node": "^12.0.0", diff --git a/src/features/HeaderFilters/components/DateFilter/styled.tsx b/src/features/HeaderFilters/components/DateFilter/styled.tsx index df2ac7a8..cd3fad07 100644 --- a/src/features/HeaderFilters/components/DateFilter/styled.tsx +++ b/src/features/HeaderFilters/components/DateFilter/styled.tsx @@ -73,11 +73,10 @@ type DateButtonProps = { export const DateButton = styled(BaseButton)` position: absolute; top: 0.2rem; - right: 3.6rem; + right: 4.6rem; width: 1.3rem; height: 1.26rem; color: ${({ theme: { colors } }) => colors.dateButton}; - margin-left: 10px; ${isMobileDevice ? css` diff --git a/src/features/HeaderFilters/components/DatePicker/styled.tsx b/src/features/HeaderFilters/components/DatePicker/styled.tsx index fa6f1f2b..acbf0268 100644 --- a/src/features/HeaderFilters/components/DatePicker/styled.tsx +++ b/src/features/HeaderFilters/components/DatePicker/styled.tsx @@ -6,7 +6,7 @@ import { BaseButton } from '../DateFilter/styled' export const Wrapper = styled.div` position: absolute; top: 3.5rem; - right: 11rem; + right: 12rem; z-index: 10; ${isMobileDevice ? css` diff --git a/src/features/MatchCard/CardFrontside/index.tsx b/src/features/MatchCard/CardFrontside/index.tsx index 302e01e9..87ffa181 100644 --- a/src/features/MatchCard/CardFrontside/index.tsx +++ b/src/features/MatchCard/CardFrontside/index.tsx @@ -94,8 +94,9 @@ export const CardFrontside = ({ - + {previewImage && ( @@ -107,7 +108,7 @@ export const CardFrontside = ({ {access === MatchAccess.NoCountryAccess ? : ( - + - - + + @@ -154,11 +155,13 @@ export const CardFrontside = ({ {showScore && {team2.score}} - + {!isMatchPage && ( + + )} diff --git a/src/features/MatchCard/styled.tsx b/src/features/MatchCard/styled.tsx index 3a00b256..42963a85 100644 --- a/src/features/MatchCard/styled.tsx +++ b/src/features/MatchCard/styled.tsx @@ -6,11 +6,16 @@ import { isMobileDevice } from 'config/userAgent' import { Name } from 'features/Name' import { ProfileLogo } from 'features/ProfileLogo' +type CardProps = { + isMatchPage?: boolean, +} + export const CardWrapperOuter = styled.li.attrs({ tabIndex: 0, -})` - padding-top: 100%; +})` + padding-top: ${({ isMatchPage }) => (isMatchPage ? 0 : '100%')}; position: relative; + ${isMobileDevice ? css` width: 100%; @@ -28,13 +33,13 @@ export const CardWrapperOuter = styled.li.attrs({ : ''}; ` -export const CardWrapper = styled.div` - position: absolute; +export const CardWrapper = styled.div` + position: ${({ isMatchPage }) => (isMatchPage ? 'relative' : 'absolute')}; top: 0; left: 0; bottom: 0; right: 0; - padding-bottom: 0.75rem; + padding-bottom: ${({ isMatchPage }) => (isMatchPage ? '0' : '0.75rem')}; border-radius: 2px; background-color: #3F3F3F; box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4); @@ -133,10 +138,10 @@ export const Time = styled.span` margin: 0 0.2rem; ` -export const Info = styled.div` +export const Info = styled.div` display: flex; flex-direction: column; - padding: 0.85rem 0.472rem 0 0.519rem; + padding: ${({ isMatchPage }) => (isMatchPage ? '1.2rem 0.472rem 1.2rem 0.519rem' : '0.85rem 0.472rem 0 0.519rem')}; color: #fff; text-overflow: ellipsis; overflow: hidden; @@ -178,8 +183,8 @@ const nameStyles = css` overflow: hidden; ` -export const Teams = styled.div` - margin-bottom: 0.567rem; +export const Teams = styled.div` + margin-bottom: ${({ isMatchPage }) => (isMatchPage ? '0' : '0.567rem')}; ${isMobileDevice ? css` margin-bottom: 15px; @@ -215,11 +220,11 @@ export const TeamName = styled(Name)` export const Score = styled.div`` -export const TeamLogos = styled.div` +export const TeamLogos = styled.div` display: flex; align-items: center; justify-content: center; - margin: 0.71rem auto 0 auto; + margin: ${({ isMatchPage }) => (isMatchPage ? '1.5rem auto 1rem auto' : '0.71rem auto 0 auto')}; z-index: 1; ` diff --git a/src/features/MatchPage/components/FinishedMatch/hooks/useEpisodes.tsx b/src/features/MatchPage/components/FinishedMatch/hooks/useEpisodes.tsx index 04a2edaa..152638cf 100644 --- a/src/features/MatchPage/components/FinishedMatch/hooks/useEpisodes.tsx +++ b/src/features/MatchPage/components/FinishedMatch/hooks/useEpisodes.tsx @@ -41,7 +41,7 @@ export const useEpisodes = () => { }).then(setEpisodes) } else if (playlistOption.type === PlaylistTypes.MATCH || playlistOption.type === PlaylistTypes.EVENT) { - setEpisodes(playlistOption.data) + setEpisodes(playlistOption.episodes) } }, [matchId, sportType]) diff --git a/src/features/MatchPage/components/FinishedMatch/index.tsx b/src/features/MatchPage/components/FinishedMatch/index.tsx index 10b87181..f8190c7f 100644 --- a/src/features/MatchPage/components/FinishedMatch/index.tsx +++ b/src/features/MatchPage/components/FinishedMatch/index.tsx @@ -2,14 +2,14 @@ import { Fragment } from 'react' import isEmpty from 'lodash/isEmpty' +import { isMobileDevice } from 'config/userAgent' + import type { Events } from 'requests/getMatchEvents' import type { MatchInfo } from 'requests/getMatchInfo' import { MatchSidePlaylists } from 'features/MatchSidePlaylists' import { MultiSourcePlayer } from 'features/MultiSourcePlayer' -import { isMobileDevice } from 'config/userAgent' - import { SettingsPopup } from '../SettingsPopup' import { useFinishedMatch } from './hooks' diff --git a/src/features/MatchPage/components/LiveMatch/helpers.tsx b/src/features/MatchPage/components/LiveMatch/helpers.tsx new file mode 100644 index 00000000..d5492b1f --- /dev/null +++ b/src/features/MatchPage/components/LiveMatch/helpers.tsx @@ -0,0 +1,104 @@ +import last from 'lodash/last' +import find from 'lodash/find' +import reduce from 'lodash/reduce' +import concat from 'lodash/concat' + +import type { Episodes } from 'requests/getMatchPlaylists' +import type { MatchInfo } from 'requests/getMatchInfo' + +import type { Chapters, Chapter } from 'features/StreamPlayer/types' + +import type { MatchPlaylistOption, PlaylistOption } from '../../types' +import { FULL_GAME_KEY } from '../../helpers/buildPlaylists' + +export const FULL_MATCH_BOUNDARY = '0' + +/** + * Формирует эпизоды плейлиста Полный матч + * API не выдает полный матч как плейлист, формируем на фронте + * */ +const getFullMatchChapters = ( + profile: MatchInfo, + url: string, + playlist: MatchPlaylistOption, +) => { + const bound = find(profile?.video_bounds, { h: FULL_MATCH_BOUNDARY }) + const durationMs = (playlist.duration ?? 0) * 1000 + return [ + { + duration: durationMs, + endMs: durationMs, + endOffsetMs: bound ? Number(bound.e) * 1000 : durationMs, + index: 0, + isFullMatchChapter: true, + startMs: 0, + startOffsetMs: bound ? Number(bound.s) * 1000 : 0, + url, + }, + ] +} + +/** + * Формирует эпизоды плейлистов матча и игроков + * */ +const getPlaylistChapters = ( + profile: MatchInfo, + url: string, + episodes: Episodes, +) => reduce( + episodes, + ( + acc: Chapters, + episode, + index, + ) => { + if (episode.s >= episode.e) return acc + + const bound = find(profile?.video_bounds, { h: String(episode.h) }) + const boundStart = bound ? Number(bound.s) : 0 + + const episodeDuration = (episode.e - episode.s) * 1000 + const prevVideoEndMs = last(acc)?.endMs ?? 0 + + const nextChapter: Chapter = { + duration: episodeDuration, + endMs: prevVideoEndMs + episodeDuration, + endOffsetMs: (boundStart + episode.e) * 1000, + index, + startMs: prevVideoEndMs, + startOffsetMs: (boundStart + episode.s) * 1000, + url, + } + return concat(acc, nextChapter) + }, + [], +) + +type Args = { + profile: MatchInfo, + selectedPlaylist?: PlaylistOption, + url: string, +} + +/** + * Формирует список эпизодов из выбранного плейлиста для плеера + */ +export const buildChapters = ({ + profile, + selectedPlaylist, + url, +}: Args): Chapters => { + if (!selectedPlaylist) return [] + if (selectedPlaylist.id === FULL_GAME_KEY) { + return getFullMatchChapters( + profile, + url, + selectedPlaylist, + ) + } + return getPlaylistChapters( + profile, + url, + selectedPlaylist.episodes, + ) +} diff --git a/src/features/MatchPage/components/LiveMatch/hooks/index.tsx b/src/features/MatchPage/components/LiveMatch/hooks/index.tsx index 05cb804b..eeda07dc 100644 --- a/src/features/MatchPage/components/LiveMatch/hooks/index.tsx +++ b/src/features/MatchPage/components/LiveMatch/hooks/index.tsx @@ -2,25 +2,24 @@ import { useMemo } from 'react' import { API_ROOT } from 'config' -import type { MatchInfo } from 'requests/getMatchInfo' - import { usePageParams } from 'hooks/usePageParams' -import { useMatchPopupStore } from 'features/MatchPopup' +import { useMatchPageStore } from 'features/MatchPage/store' import { usePlayerProgressReporter } from './usePlayerProgressReporter' -import { useLastPlayPosition } from './useLastPlayPosition' -import { useUrlParam } from './useUrlParam' +import { useResumeUrlParam } from './useResumeUrlParam' +import { useChapters } from './useChapters' +import { usePlaylistLogger } from './usePlaylistLogger' -export const useLiveMatch = (profile: MatchInfo) => { +export const useLiveMatch = () => { const { handlePlaylistClick, - matchPlaylists, + profile, selectedPlaylist, - } = useMatchPopupStore() - + setFullMatchPlaylistDuration, + } = useMatchPageStore() const { profileId: matchId, sportType } = usePageParams() - const resume = useUrlParam() + const resume = useResumeUrlParam() const fromStartIfStreamPaused = useMemo( () => (profile && !profile.live ? 0 : undefined), @@ -30,13 +29,50 @@ export const useLiveMatch = (profile: MatchInfo) => { [], ) + const { chapters } = useChapters({ + profile, + selectedPlaylist, + url: `${API_ROOT}/video/stream/${sportType}/${matchId}.m3u8`, + }) + + const { + logPlaylistChange, + onPlayingChange: notifyPlaylistLogger, + } = usePlaylistLogger() + + const { + onPlayerProgressChange, + onPlayingChange: notifyProgressLogger, + } = usePlayerProgressReporter() + + const onDurationChange = (duration: number) => { + if (profile?.live || profile?.video_bounds) return + setFullMatchPlaylistDuration(duration) + } + + const onPlayingChange = (playing: boolean) => { + notifyPlaylistLogger(playing) + notifyProgressLogger(playing) + } + + const onPlaylistSelect: typeof handlePlaylistClick = (playlist, e) => { + if (selectedPlaylist) { + logPlaylistChange(selectedPlaylist) + } + handlePlaylistClick(playlist, e) + } + return { - matchPlaylists, - onPlaylistSelect: handlePlaylistClick, + chapters, + onDurationChange, + onPlayerProgressChange, + onPlayingChange, + onPlaylistSelect, resume: resume ?? fromStartIfStreamPaused, selectedPlaylist, - streamUrl: `${API_ROOT}/video/stream/${sportType}/${matchId}.m3u8`, - ...usePlayerProgressReporter(), - ...useLastPlayPosition(), + streamUrl: ( + profile?.playbackUrl + || `${API_ROOT}/video/stream/${sportType}/${matchId}.m3u8` + ), } } diff --git a/src/features/MatchPage/components/LiveMatch/hooks/useChapters.tsx b/src/features/MatchPage/components/LiveMatch/hooks/useChapters.tsx new file mode 100644 index 00000000..146134aa --- /dev/null +++ b/src/features/MatchPage/components/LiveMatch/hooks/useChapters.tsx @@ -0,0 +1,33 @@ +import { useMemo } from 'react' + +import type { PlaylistOption } from 'features/MatchPage/types' +import type { MatchInfo } from 'requests/getMatchInfo' + +import { buildChapters } from '../helpers' + +type Args = { + profile: MatchInfo, + selectedPlaylist?: PlaylistOption, + url: string, +} + +export const useChapters = ({ + profile, + selectedPlaylist, + url, +}: Args) => { + const chapters = useMemo( + () => buildChapters({ + profile, + selectedPlaylist, + url, + }), + [ + profile, + selectedPlaylist, + url, + ], + ) + + return { chapters } +} diff --git a/src/features/MatchPage/components/LiveMatch/hooks/usePlaylistLogger.tsx b/src/features/MatchPage/components/LiveMatch/hooks/usePlaylistLogger.tsx new file mode 100644 index 00000000..49659866 --- /dev/null +++ b/src/features/MatchPage/components/LiveMatch/hooks/usePlaylistLogger.tsx @@ -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 usePlaylistLogger = () => { + 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 } +} diff --git a/src/features/MatchPage/components/LiveMatch/hooks/useResumeUrlParam.tsx b/src/features/MatchPage/components/LiveMatch/hooks/useResumeUrlParam.tsx new file mode 100644 index 00000000..2f799cfa --- /dev/null +++ b/src/features/MatchPage/components/LiveMatch/hooks/useResumeUrlParam.tsx @@ -0,0 +1,23 @@ +import { useMemo } from 'react' +import { useLocation } from 'react-router' + +import isNumber from 'lodash/isNumber' + +export const RESUME_KEY = 'resume' + +const readResumeParam = (search: string) => { + const params = new URLSearchParams(search) + const rawValue = params.get(RESUME_KEY) + if (!rawValue) return undefined + + const value = JSON.parse(rawValue) + return isNumber(value) ? value : 0 +} + +export const useResumeUrlParam = () => { + const { search } = useLocation() + + const resume = useMemo(() => readResumeParam(search), [search]) + + return resume +} diff --git a/src/features/MatchPage/components/LiveMatch/index.tsx b/src/features/MatchPage/components/LiveMatch/index.tsx index 00492228..26cf6789 100644 --- a/src/features/MatchPage/components/LiveMatch/index.tsx +++ b/src/features/MatchPage/components/LiveMatch/index.tsx @@ -1,8 +1,8 @@ import { Fragment } from 'react' -import type { Events } from 'requests/getMatchEvents' -import type { MatchInfo } from 'requests/getMatchInfo' +import isEmpty from 'lodash/isEmpty' +import { useMatchPageStore } from 'features/MatchPage/store' import { StreamPlayer } from 'features/StreamPlayer' import { YoutubePlayer } from 'features/StreamPlayer/components/YoutubePlayer' import { MatchSidePlaylists } from 'features/MatchSidePlaylists' @@ -13,42 +13,60 @@ import { Container } from '../../styled' import { useLiveMatch } from './hooks' import { TournamentData } from '../../types' +import { MatchDescription } from '../MatchDescription' import { MatchProfileCardMobile } from '../MatchProfileCardMobile' type Props = { - events: Events, - profile: MatchInfo, tournamentData: TournamentData, } export const LiveMatch = ({ - events, - profile, tournamentData, }: Props) => { const { + events, matchPlaylists, + profile, + selectedPlaylist, + } = useMatchPageStore() + + const { + chapters, + onDurationChange, onPlayerProgressChange, onPlayingChange, onPlaylistSelect, resume, - selectedPlaylist, streamUrl, - } = useLiveMatch(profile) - - const Player = profile?.youtube_link ? YoutubePlayer : StreamPlayer + } = useLiveMatch() return ( - - {isMobileDevice ? : null} + {profile?.youtube_link ? ( + + ) : ( + !isEmpty(chapters) && ( + + ) + )} + {isMobileDevice + ? : ( + )} @@ -69,9 +65,13 @@ export const MatchDescription = ({ > - {!isScoreHidden && {team1.score}} - - - {!isScoreHidden && {team2.score}} + + { + isScoreHidden || !team1.score || !team2.score + ? '-' + : `${team1.score} - ${team2.score}` + } + - {live ? '\u00a0|\u00a0LIVE STREAM' : ''} @@ -96,11 +95,7 @@ export const MatchDescription = ({ - { - live - ? - : - } + {localDate} diff --git a/src/features/MatchPage/components/MatchDescription/styled.tsx b/src/features/MatchPage/components/MatchDescription/styled.tsx index d29c2153..3045b861 100644 --- a/src/features/MatchPage/components/MatchDescription/styled.tsx +++ b/src/features/MatchPage/components/MatchDescription/styled.tsx @@ -35,11 +35,6 @@ export const StyledLink = styled(ProfileLink)` } ` -export const StyledDash = styled.span<{isScoreHidden?: boolean}>` - color: #fff; - margin: ${({ isScoreHidden }) => (isScoreHidden ? '0 10px' : '0')}; -` - export const Title = styled.div` display: flex; flex-direction: row; @@ -56,11 +51,6 @@ export const Title = styled.div` opacity: 0.7; pointer-events: none; } - - &:hover > ${StyledDash}:not(:hover){ - opacity: 0.7; - pointer-events: none; - } ` export const Tournament = styled.span` diff --git a/src/features/MatchPage/components/SubscriptionGuard/index.tsx b/src/features/MatchPage/components/SubscriptionGuard/index.tsx index b341261e..01286d17 100644 --- a/src/features/MatchPage/components/SubscriptionGuard/index.tsx +++ b/src/features/MatchPage/components/SubscriptionGuard/index.tsx @@ -1,20 +1,19 @@ import type { ReactNode } from 'react' import { Fragment, useEffect } from 'react' -import type { MatchInfo } from 'requests/getMatchInfo' - import { usePageParams } from 'hooks/usePageParams' import { useBuyMatchPopupStore } from 'features/BuyMatchPopup' +import { useMatchPageStore } from 'features/MatchPage/store' import { prepareMatchProfile } from '../../helpers/prepareMatchProfile' type Props = { children: ReactNode, - matchProfile: MatchInfo, } -export const SubscriptionGuard = ({ children, matchProfile }: Props) => { +export const SubscriptionGuard = ({ children }: Props) => { + const { profile: matchProfile } = useMatchPageStore() const { open: openBuyMatchPopup } = useBuyMatchPopupStore() const { profileId: matchId, sportType } = usePageParams() diff --git a/src/features/MatchPage/helpers/buildPlaylists.tsx b/src/features/MatchPage/helpers/buildPlaylists.tsx index 89b17c0d..f5aeafc3 100644 --- a/src/features/MatchPage/helpers/buildPlaylists.tsx +++ b/src/features/MatchPage/helpers/buildPlaylists.tsx @@ -1,7 +1,7 @@ import map from 'lodash/map' import sortBy from 'lodash/sortBy' -import type { MatchPlaylists, Players } from 'requests' +import type { MatchPlaylists, Players } from 'requests/getMatchPlaylists' import type { Playlists, @@ -25,10 +25,10 @@ const getMatchPlaylists = (matchPlaylists: MatchPlaylists | null): MatchPlaylist return map(MATCH_KEYS, (key) => { const playlist = matchPlaylists[key] - const lexic = matchPlaylists.lexics[key] + const lexic = matchPlaylists.lexics[key] ?? '' return { - data: sortBy(playlist?.data, ['h', 's']), duration: playlist?.dur, + episodes: sortBy(playlist?.data, ['h', 's']), id: key, lexic, type: PlaylistTypes.MATCH, @@ -39,6 +39,7 @@ const getMatchPlaylists = (matchPlaylists: MatchPlaylists | null): MatchPlaylist const getPlayerPlaylists = (players?: Players): PlayerPlaylistOptions => ( map(players, (player) => ({ ...player, + episodes: [], type: PlaylistTypes.PLAYER, })) ) diff --git a/src/features/MatchPage/helpers/fullMatchDuration.tsx b/src/features/MatchPage/helpers/fullMatchDuration.tsx new file mode 100644 index 00000000..f31b20b2 --- /dev/null +++ b/src/features/MatchPage/helpers/fullMatchDuration.tsx @@ -0,0 +1,11 @@ +import find from 'lodash/find' + +import type { MatchInfo } from 'requests/getMatchInfo' + +import { FULL_MATCH_BOUNDARY } from 'features/MatchPage/components/LiveMatch/helpers' + +export const calculateDuration = (profile: MatchInfo) => { + const bound = find(profile?.video_bounds, { h: FULL_MATCH_BOUNDARY }) + if (!bound) return 0 + return Number(bound.e) - Number(bound.s) +} diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index 703245a7..58f8bdeb 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -10,21 +10,20 @@ import { } from 'features/PageLayout' import { FavoritesActions } from 'requests' + import { ProfileTypes } from 'config' -import { isMobileDevice } from 'config/userAgent' import { usePageLogger } from 'hooks/usePageLogger' import { usePageParams } from 'hooks/usePageParams' +import { MatchPageStore } from './store' import { SubscriptionGuard } from './components/SubscriptionGuard' -import { MatchProfileCard } from './components/MatchProfileCard' -import { FinishedMatch } from './components/FinishedMatch' import { LiveMatch } from './components/LiveMatch' import { useMatchProfile } from './hooks/useMatchProfile' import { Wrapper } from './styled' -import { MatchPageStore } from './store' +import { FinishedMatch } from './components/FinishedMatch' -const MatchPage = () => { +const MatchPageComponent = () => { usePageLogger() const history = useHistory() const { addRemoveFavorite, userFavorites } = useUserFavoritesStore() @@ -64,7 +63,6 @@ const MatchPage = () => { const playFromScout = profile?.has_video && !profile?.live const playFromOTT = !profile?.has_video && (profile?.live || profile?.storage) - // TODO Добавить попап 'Данный матч ещё не начался' if (!isStarted && profile?.live === false) { const sportName = history.location.pathname.split('/')[1] @@ -72,35 +70,35 @@ const MatchPage = () => { } return ( - - - - {isMobileDevice ? null : } - -
- - - - {playFromOTT && ( - - )} - {playFromScout && ( - - )} - - -
-
-
+ + +
+ + + + {playFromOTT && ( + + )} + {playFromScout && ( + + )} + + +
+
) } +const MatchPage = () => ( + + + +) + export default MatchPage diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx new file mode 100644 index 00000000..5e17b1a0 --- /dev/null +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -0,0 +1,92 @@ +import { + useEffect, + useState, + useMemo, +} from 'react' +import { useToggle } from 'hooks' + +import type { MatchInfo } from 'requests/getMatchInfo' +import { getMatchInfo } from 'requests/getMatchInfo' + +import { usePageParams } from 'hooks/usePageParams' + +import { parseDate } from 'helpers/parseDate' + +import { useMatchData } from './useMatchData' + +import type { Playlists } from '../../types' + +const addScoresFromPlaylists = ( + profile: MatchInfo, + playlists: Playlists, +): MatchInfo => ( + profile + ? { + ...profile, + team1: { + ...profile?.team1, + score: playlists.score1, + }, + team2: { + ...profile?.team2, + score: playlists.score2, + }, + } + : null +) + +export const useMatchPage = () => { + const [matchProfile, setMatchProfile] = useState(null) + const { profileId: matchId, sportType } = usePageParams() + const { + close: hideProfileCard, + isOpen: profileCardShown, + open: showProfileCard, + } = useToggle(true) + + useEffect(() => { + getMatchInfo(sportType, matchId).then(setMatchProfile) + }, [sportType, matchId]) + + useEffect(() => { + let getIntervalMatch: ReturnType + if (matchProfile?.live && !matchProfile.youtube_link) { + getIntervalMatch = setInterval( + () => getMatchInfo(sportType, matchId).then(setMatchProfile), 1000 * 60 * 3, + ) + } + return () => clearInterval(getIntervalMatch) + }, [matchProfile, sportType, matchId]) + + const { + events, + handlePlaylistClick, + matchPlaylists, + selectedPlaylist, + setFullMatchPlaylistDuration, + } = useMatchData(matchProfile) + + const profile = useMemo( + () => addScoresFromPlaylists(matchProfile, matchPlaylists), + [matchProfile, matchPlaylists], + ) + + const isStarted = useMemo(() => ( + profile?.date + ? parseDate(profile.date) < new Date() + : true + ), [profile?.date]) + + return { + events, + handlePlaylistClick, + hideProfileCard, + isStarted, + matchPlaylists, + profile, + profileCardShown, + selectedPlaylist, + setFullMatchPlaylistDuration, + showProfileCard, + } +} diff --git a/src/features/MatchPage/store/hooks/useEvents.tsx b/src/features/MatchPage/store/hooks/useEvents.tsx new file mode 100644 index 00000000..14f7095c --- /dev/null +++ b/src/features/MatchPage/store/hooks/useEvents.tsx @@ -0,0 +1,28 @@ +import { useCallback, useState } from 'react' + +import type { Events } from 'requests' +import { getMatchEvents } from 'requests' + +import { usePageParams } from 'hooks/usePageParams' + +import { useEventsLexics } from './useEventsLexics' + +export const useEvents = () => { + const [events, setEvents] = useState([]) + const { fetchLexics } = useEventsLexics() + const { profileId: matchId, sportType } = usePageParams() + + const fetchMatchEvents = useCallback(() => { + getMatchEvents({ + matchId, + sportType, + }).then(fetchLexics) + .then(setEvents) + }, [ + fetchLexics, + matchId, + sportType, + ]) + + return { events, fetchMatchEvents } +} diff --git a/src/features/MatchPage/store/hooks/useEventsLexics.tsx b/src/features/MatchPage/store/hooks/useEventsLexics.tsx new file mode 100644 index 00000000..cc886d0f --- /dev/null +++ b/src/features/MatchPage/store/hooks/useEventsLexics.tsx @@ -0,0 +1,25 @@ +import { useCallback } from 'react' + +import isEmpty from 'lodash/isEmpty' +import map from 'lodash/map' +import uniq from 'lodash/uniq' + +import type { Events } from 'requests' + +import { useLexicsStore } from 'features/LexicsStore' + +export const useEventsLexics = () => { + const { addLexicsConfig } = useLexicsStore() + + const fetchLexics = useCallback((events: Events) => { + const lexics = uniq(map(events, ({ l }) => l)) + + if (!isEmpty(lexics)) { + addLexicsConfig(lexics) + } + + return events + }, [addLexicsConfig]) + + return { fetchLexics } +} diff --git a/src/features/MatchPage/store/hooks/useMatchData.tsx b/src/features/MatchPage/store/hooks/useMatchData.tsx new file mode 100644 index 00000000..4696407a --- /dev/null +++ b/src/features/MatchPage/store/hooks/useMatchData.tsx @@ -0,0 +1,82 @@ +import { useEffect, useMemo } from 'react' + +import debounce from 'lodash/debounce' + +import { MatchInfo } from 'requests/getMatchInfo' + +import { usePageParams } from 'hooks/usePageParams' +import { useInterval } from 'hooks/useInterval' + +import { calculateDuration } from '../../helpers/fullMatchDuration' +import { useMatchPlaylists } from './useMatchPlaylists' +import { useEvents } from './useEvents' + +const MATCH_DATA_POLL_INTERVAL = 60000 +const MATCH_PLAYLISTS_DELAY = 5000 + +export const useMatchData = (profile: MatchInfo) => { + const { profileId: matchId, sportType } = usePageParams() + const { + fetchMatchPlaylists, + handlePlaylistClick, + matchPlaylists, + selectedPlaylist, + setFullMatchPlaylistDuration, + } = useMatchPlaylists() + const { events, fetchMatchEvents } = useEvents() + + const fetchPlaylistsDebounced = useMemo( + () => debounce(fetchMatchPlaylists, MATCH_PLAYLISTS_DELAY), + [fetchMatchPlaylists], + ) + + const fullMatchDuration = useMemo(() => calculateDuration(profile), [profile]) + + useEffect(() => { + if (!profile) return + fetchMatchPlaylists({ + fullMatchDuration, + id: matchId, + sportType, + }) + fetchMatchEvents() + }, [ + profile, + fullMatchDuration, + matchId, + sportType, + fetchMatchPlaylists, + fetchMatchEvents, + ]) + + const intervalCallback = () => { + fetchPlaylistsDebounced({ + fullMatchDuration, + id: matchId, + sportType, + }) + fetchMatchEvents() + } + + const { start, stop } = useInterval({ + callback: intervalCallback, + intervalDuration: MATCH_DATA_POLL_INTERVAL, + startImmediate: false, + }) + + useEffect(() => { + if (profile?.live) { + start() + } else { + stop() + } + }, [profile?.live, start, stop]) + + return { + events, + handlePlaylistClick, + matchPlaylists, + selectedPlaylist, + setFullMatchPlaylistDuration, + } +} diff --git a/src/features/MatchPage/store/hooks/useMatchPlaylists.tsx b/src/features/MatchPage/store/hooks/useMatchPlaylists.tsx new file mode 100644 index 00000000..5667b8e2 --- /dev/null +++ b/src/features/MatchPage/store/hooks/useMatchPlaylists.tsx @@ -0,0 +1,89 @@ +import { + useState, + useCallback, +} from 'react' + +import isEmpty from 'lodash/isEmpty' + +import type { SportTypes } from 'config/sportTypes' + +import { getMatchPlaylists } from 'requests/getMatchPlaylists' + +import type { Playlists } from 'features/MatchPage/types' +import { buildPlaylists } from 'features/MatchPage/helpers/buildPlaylists' + +import { usePlaylistLexics } from './usePlaylistLexics' +import { useSelectedPlaylist } from './useSelectedPlaylist' + +type ArgsFetchMatchPlaylists = { + fullMatchDuration: number, + id: number, + sportType: SportTypes, +} + +const initialPlaylists = buildPlaylists(null) + +export const useMatchPlaylists = () => { + const [matchPlaylists, setMatchPlaylists] = useState(initialPlaylists) + + const { fetchLexics } = usePlaylistLexics() + const { + handlePlaylistClick, + selectedPlaylist, + setSelectedPlaylist, + } = useSelectedPlaylist() + + const setInitialSeletedPlaylist = useCallback((playlists: Playlists) => { + setSelectedPlaylist((playlist) => { + if (!playlist && !isEmpty(playlists.match)) { + return playlists.match[0] + } + return playlist + }) + return playlists + }, [setSelectedPlaylist]) + + const fetchMatchPlaylists = useCallback(({ + fullMatchDuration, + id, + sportType, + }: ArgsFetchMatchPlaylists) => { + getMatchPlaylists({ + fullMatchDuration, + matchId: id, + selectedActions: [], + sportType, + }).then(fetchLexics) + .then(buildPlaylists) + .then(setInitialSeletedPlaylist) + .then(setMatchPlaylists) + }, [fetchLexics, setInitialSeletedPlaylist]) + + /** + * API не выдает длительность Полного матча + * Здесь получаем его из самого видео + * и обновляем длительность плейлиста Полный матч + */ + const setFullMatchPlaylistDuration = (duration: number) => { + const playlists = [...matchPlaylists.match] + if (!playlists[0]) return + + playlists[0].duration = duration + setMatchPlaylists({ + ...matchPlaylists, + match: playlists, + }) + + if (selectedPlaylist) { + setSelectedPlaylist({ ...selectedPlaylist }) + } + } + + return { + fetchMatchPlaylists, + handlePlaylistClick, + matchPlaylists, + selectedPlaylist, + setFullMatchPlaylistDuration, + } +} diff --git a/src/features/MatchPage/store/hooks/usePlaylistLexics.tsx b/src/features/MatchPage/store/hooks/usePlaylistLexics.tsx new file mode 100644 index 00000000..e2f4cc5d --- /dev/null +++ b/src/features/MatchPage/store/hooks/usePlaylistLexics.tsx @@ -0,0 +1,22 @@ +import { useCallback } from 'react' + +import isEmpty from 'lodash/isEmpty' +import compact from 'lodash/compact' +import values from 'lodash/values' + +import type { MatchPlaylists } from 'requests' + +import { useLexicsStore } from 'features/LexicsStore' + +export const usePlaylistLexics = () => { + const { addLexicsConfig } = useLexicsStore() + const fetchLexics = useCallback((playlist: MatchPlaylists) => { + const lexics = compact(values(playlist.lexics)) + if (!isEmpty(lexics)) { + addLexicsConfig(lexics) + } + return playlist + }, [addLexicsConfig]) + + return { fetchLexics } +} diff --git a/src/features/MatchPage/store/hooks/useSelectedPlaylist.tsx b/src/features/MatchPage/store/hooks/useSelectedPlaylist.tsx new file mode 100644 index 00000000..ee732c5b --- /dev/null +++ b/src/features/MatchPage/store/hooks/useSelectedPlaylist.tsx @@ -0,0 +1,49 @@ +import type { MouseEvent } from 'react' +import { useState, useCallback } from 'react' + +import { getPlayerPlaylists } from 'requests/getPlayerPlaylists' + +import { usePageParams } from 'hooks/usePageParams' + +import { + PlayerPlaylistOption, + PlaylistOption, + PlaylistTypes, +} from 'features/MatchPage/types' +import { defaultSettings } from 'features/MatchPopup/types' + +export const useSelectedPlaylist = () => { + const { profileId: matchId, sportType } = usePageParams() + const [selectedPlaylist, setSelectedPlaylist] = useState() + + const fetchPlayerEpisodes = useCallback((playlistOption: PlayerPlaylistOption) => ( + getPlayerPlaylists({ + matchId, + playerId: playlistOption.id, + settings: defaultSettings, + sportType, + }) + ), [matchId, sportType]) + + const handlePlaylistClick = useCallback((playlist: PlaylistOption, e?: MouseEvent) => { + e?.stopPropagation() + if (playlist === selectedPlaylist) return + + if (playlist.type === PlaylistTypes.PLAYER) { + fetchPlayerEpisodes(playlist).then((episodes) => { + setSelectedPlaylist({ + ...playlist, + episodes, + }) + }) + } else { + setSelectedPlaylist(playlist) + } + }, [fetchPlayerEpisodes, selectedPlaylist]) + + return { + handlePlaylistClick, + selectedPlaylist, + setSelectedPlaylist, + } +} diff --git a/src/features/MatchPage/store/index.tsx b/src/features/MatchPage/store/index.tsx index 674efdc2..778a5853 100644 --- a/src/features/MatchPage/store/index.tsx +++ b/src/features/MatchPage/store/index.tsx @@ -1,7 +1,10 @@ import type { ReactNode } from 'react' -import { createContext, useContext } from 'react' +import { + createContext, + useContext, +} from 'react' -import { useMatchPage } from '../hooks' +import { useMatchPage } from './hooks' type Context = ReturnType type Props = { children: ReactNode } @@ -9,12 +12,8 @@ type Props = { children: ReactNode } const MatchPageContext = createContext({} as Context) export const MatchPageStore = ({ children }: Props) => { - const value = useMatchPage() - return ( - - {children} - - ) + const values = useMatchPage() + return {children} } export const useMatchPageStore = () => useContext(MatchPageContext) diff --git a/src/features/MatchPage/types.tsx b/src/features/MatchPage/types.tsx index 8a5b7892..8d19fd04 100644 --- a/src/features/MatchPage/types.tsx +++ b/src/features/MatchPage/types.tsx @@ -12,8 +12,8 @@ export enum PlaylistTypes { } export type MatchPlaylistOption = { - data: Episodes, duration?: number, + episodes: Episodes, id: MatchPlaylistIds, lexic: number | string, type: PlaylistTypes.MATCH, @@ -21,6 +21,7 @@ export type MatchPlaylistOption = { export type PlayerPlaylistOption = { dur: number, + episodes: Episodes, gk?: boolean, id: number, name_eng: string, @@ -31,6 +32,7 @@ export type PlayerPlaylistOption = { } export type InterviewPlaylistOption = { + episodes: Episodes, id: number, name_eng: string, name_rus: string, @@ -38,7 +40,7 @@ export type InterviewPlaylistOption = { } export type EventPlaylistOption = { - data: Episodes, + episodes: Episodes, id: number, type: PlaylistTypes.EVENT, } diff --git a/src/features/MatchPopup/types.tsx b/src/features/MatchPopup/types.tsx index c83a1afa..c611b094 100644 --- a/src/features/MatchPopup/types.tsx +++ b/src/features/MatchPopup/types.tsx @@ -38,8 +38,8 @@ export type SettingsBySport = Partial> export const defaultSettings: Settings = { episodeDuration: { - after: 6, - before: 6, + after: 30, + before: 10, }, selectedActions: [], selectedFormat: PlayerPlaylistFormats.ALL_MATCH_TIME, diff --git a/src/features/MatchSidePlaylists/components/EventsList/index.tsx b/src/features/MatchSidePlaylists/components/EventsList/index.tsx index 18cdfc5b..3a1eed3a 100644 --- a/src/features/MatchSidePlaylists/components/EventsList/index.tsx +++ b/src/features/MatchSidePlaylists/components/EventsList/index.tsx @@ -45,7 +45,7 @@ export const EventsList = ({ const repeatedEpisodes = event.rep || [] const eventPlaylist = { - data: [{ + episodes: [{ e: event.e, h: event.h, s: event.s, diff --git a/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx b/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx index 306d54f7..77d640b8 100644 --- a/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx @@ -16,6 +16,7 @@ import { T9n } from 'features/T9n' import { PlayButton } from '../PlayButton' type Props = { + live?: boolean, onSelect?: (selectedMathPlaylist: PlaylistOption) => void, playlists: MatchPlaylistOptions, selectedMathPlaylist?: PlaylistOption, @@ -36,6 +37,7 @@ const Item = styled.li` ` export const MatchPlaylists = ({ + live, onSelect, playlists, selectedMathPlaylist, @@ -52,7 +54,7 @@ export const MatchPlaylists = ({ onSelect?.(playlist)} > diff --git a/src/features/MatchSidePlaylists/components/PlayButton/index.tsx b/src/features/MatchSidePlaylists/components/PlayButton/index.tsx index a34374e6..119e1cab 100644 --- a/src/features/MatchSidePlaylists/components/PlayButton/index.tsx +++ b/src/features/MatchSidePlaylists/components/PlayButton/index.tsx @@ -14,6 +14,7 @@ type Props = { disabled?: boolean, duration?: number, leftContent?: ReactNode, + live?: boolean, onClick: () => void, } @@ -28,6 +29,7 @@ export const PlayButton = ({ disabled, duration, leftContent, + live, onClick, }: Props) => ( ) diff --git a/src/features/MatchSidePlaylists/components/TabVideo/styled.tsx b/src/features/MatchSidePlaylists/components/TabVideo/styled.tsx index 18a0733c..d10607e1 100644 --- a/src/features/MatchSidePlaylists/components/TabVideo/styled.tsx +++ b/src/features/MatchSidePlaylists/components/TabVideo/styled.tsx @@ -9,5 +9,6 @@ export const MatchesWrapper = styled.div` margin-bottom: 15px; } + ${customScrollbar} ` diff --git a/src/features/MatchSidePlaylists/components/TabWatch/index.tsx b/src/features/MatchSidePlaylists/components/TabWatch/index.tsx index 1831f322..d6b94831 100644 --- a/src/features/MatchSidePlaylists/components/TabWatch/index.tsx +++ b/src/features/MatchSidePlaylists/components/TabWatch/index.tsx @@ -29,6 +29,7 @@ export const TabWatch = ({ playlists={playlists.match} selectedMathPlaylist={selectedPlaylist} onSelect={onSelect} + live={profile?.live} /> ` } ` : ''}; - + &:hover { opacity: 1; } diff --git a/src/features/MultiSourcePlayer/components/Chapters/index.tsx b/src/features/MultiSourcePlayer/components/Chapters/index.tsx deleted file mode 100644 index bde90511..00000000 --- a/src/features/MultiSourcePlayer/components/Chapters/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import map from 'lodash/map' - -import { - LoadedProgress, - PlayedProgress, -} from 'features/StreamPlayer/components/ProgressBar/styled' - -import type { Chapter } from '../../types' -import { - ChapterList, - ChapterContainer, -} from './styled' - -type ChapterWithStyles = Chapter & { - loaded: number, - played: number, - width: number, -} - -type Props = { - chapters: Array, -} - -export const Chapters = ({ chapters }: Props) => ( - - { - map( - chapters, - ({ - loaded, - played, - width, - }, index) => ( - - - - - ), - ) - } - -) diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/index.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/index.tsx index a1132956..2882700b 100644 --- a/src/features/MultiSourcePlayer/components/ProgressBar/index.tsx +++ b/src/features/MultiSourcePlayer/components/ProgressBar/index.tsx @@ -2,7 +2,7 @@ import { useSlider } from 'features/StreamPlayer/hooks/useSlider' import { TimeTooltip } from 'features/StreamPlayer/components/TimeTooltip' import { Scrubber } from 'features/StreamPlayer/components/ProgressBar/styled' -import { Chapters } from '../Chapters' +import { Chapters } from 'features/StreamPlayer/components/Chapters' import type { Props } from './hooks' import { useProgressBar } from './hooks' import { ProgressBarList } from './styled' diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/styled.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/styled.tsx index cc84193a..8a0a9c0e 100644 --- a/src/features/MultiSourcePlayer/components/ProgressBar/styled.tsx +++ b/src/features/MultiSourcePlayer/components/ProgressBar/styled.tsx @@ -1,5 +1,5 @@ import styled, { css } from 'styled-components/macro' -import { isMobileDevice } from '../../../../config/userAgent' +import { isMobileDevice } from 'config/userAgent' export const ProgressBarList = styled.div` flex-grow: 1; diff --git a/src/features/MultiSourcePlayer/index.tsx b/src/features/MultiSourcePlayer/index.tsx index 2ebce53d..1ad21b9b 100644 --- a/src/features/MultiSourcePlayer/index.tsx +++ b/src/features/MultiSourcePlayer/index.tsx @@ -22,7 +22,6 @@ import { REWIND_SECONDS } from './config' export const MultiSourcePlayer = (props: Props) => { const { profile } = props - const { activeChapterIndex, activePlayer, diff --git a/src/features/StreamPlayer/components/Chapters/index.tsx b/src/features/StreamPlayer/components/Chapters/index.tsx new file mode 100644 index 00000000..0a8182b6 --- /dev/null +++ b/src/features/StreamPlayer/components/Chapters/index.tsx @@ -0,0 +1,36 @@ +import map from 'lodash/map' + +import type { Chapter } from '../../types' +import { + ChapterList, + ChapterContainer, + LoadedProgress, + PlayedProgress, +} from './styled' + +type ChapterWithStyles = Chapter & { + loaded: number, + played: number, + width: number, +} + +type Props = { + chapters: Array, +} + +export const Chapters = ({ chapters }: Props) => ( + + {map(chapters, ({ + loaded, + played, + width, + }, index) => ( + + + + + ))} + +) diff --git a/src/features/MultiSourcePlayer/components/Chapters/styled.tsx b/src/features/StreamPlayer/components/Chapters/styled.tsx similarity index 52% rename from src/features/MultiSourcePlayer/components/Chapters/styled.tsx rename to src/features/StreamPlayer/components/Chapters/styled.tsx index 54060a58..7a20409c 100644 --- a/src/features/MultiSourcePlayer/components/Chapters/styled.tsx +++ b/src/features/StreamPlayer/components/Chapters/styled.tsx @@ -16,3 +16,19 @@ export const ChapterContainer = styled.div` margin-right: 3px; } ` + +export const LoadedProgress = styled.div` + position: absolute; + z-index: 1; + background-color: rgba(255, 255, 255, 0.6); + height: 100%; + max-width: 100%; +` + +export const PlayedProgress = styled.div` + position: absolute; + z-index: 2; + background-color: #CC0000; + height: 100%; + max-width: 100%; +` diff --git a/src/features/StreamPlayer/components/Controls/index.tsx b/src/features/StreamPlayer/components/Controls/index.tsx index feb525a6..9542d828 100644 --- a/src/features/StreamPlayer/components/Controls/index.tsx +++ b/src/features/StreamPlayer/components/Controls/index.tsx @@ -11,7 +11,7 @@ import { Chapters } from 'features/MultiSourcePlayer/types' import { ControlsMobile } from './Components/ControlsMobile' import { ControlsWeb } from './Components/ControlsWeb' -import { ProgressBar } from '../ProgressBar' +// import { ProgressBar } from '../ProgressBar' export type ControlsProps = { activeChapterIndex?: number, @@ -65,7 +65,7 @@ export const Controls = (props: ControlsProps) => { isStorage, loadedProgress, onProgressChange, - onProgressChangeLive, + // onProgressChangeLive, onTouchEnd, onTouchStart, playedProgress, @@ -84,18 +84,17 @@ export const Controls = (props: ControlsProps) => { isLive, isStorage, ]) - const progressBarElement = useMemo(() => { if (isLive || isStorage) { - return ( - - ) + // return ( + // + // ) } return ( { isStorage, loadedProgress, onProgressChange, - onProgressChangeLive, + // onProgressChangeLive, onTouchEnd, onTouchStart, playedProgress, diff --git a/src/features/StreamPlayer/components/ProgressBar/helpers/calculateChapterStyles/__tests__/index.tsx b/src/features/StreamPlayer/components/ProgressBar/helpers/calculateChapterStyles/__tests__/index.tsx new file mode 100644 index 00000000..30c3c457 --- /dev/null +++ b/src/features/StreamPlayer/components/ProgressBar/helpers/calculateChapterStyles/__tests__/index.tsx @@ -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]) +}) diff --git a/src/features/StreamPlayer/components/ProgressBar/helpers/calculateChapterStyles/index.tsx b/src/features/StreamPlayer/components/ProgressBar/helpers/calculateChapterStyles/index.tsx new file mode 100644 index 00000000..8d6dd2f3 --- /dev/null +++ b/src/features/StreamPlayer/components/ProgressBar/helpers/calculateChapterStyles/index.tsx @@ -0,0 +1,62 @@ +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/StreamPlayer/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: playedProgress * 100 / videoDuration, + width: chapter.isFullMatchChapter + ? 100 + : chapter.duration * 100 / videoDuration, + } + return [ + ...playedChapters, + activeChapter, + ...comingChapters, + ] +} diff --git a/src/features/StreamPlayer/components/ProgressBar/hooks.tsx b/src/features/StreamPlayer/components/ProgressBar/hooks.tsx new file mode 100644 index 00000000..33633606 --- /dev/null +++ b/src/features/StreamPlayer/components/ProgressBar/hooks.tsx @@ -0,0 +1,52 @@ +import { useMemo, RefObject } from 'react' + +import { secondsToHms } from 'helpers' + +import type { Chapters } from '../../../StreamPlayer/types' +import { calculateChapterStyles } from './helpers/calculateChapterStyles' + +export type Props = { + activeChapterIndex: number, + allPlayedProgress: number, + chapters: Chapters, + duration: number, + isScrubberVisible?: boolean, + loadedProgress: number, + onPlayedProgressChange: (progress: number, seeking: boolean) => void, + onTouchEnd?: () => any, + onTouchStart?: () => any, + playedProgress: number, + videoRef?: RefObject, +} + +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), + } +} diff --git a/src/features/StreamPlayer/components/ProgressBar/index.tsx b/src/features/StreamPlayer/components/ProgressBar/index.tsx index 2fc3f39c..0475d15d 100644 --- a/src/features/StreamPlayer/components/ProgressBar/index.tsx +++ b/src/features/StreamPlayer/components/ProgressBar/index.tsx @@ -1,43 +1,26 @@ -import { secondsToHms } from 'helpers' - import { useSlider } from 'features/StreamPlayer/hooks/useSlider' import { TimeTooltip } from 'features/StreamPlayer/components/TimeTooltip' +import { Scrubber } from 'features/StreamPlayer/components/ProgressBar/styled' -import { - ProgressBarList, - LoadedProgress, - PlayedProgress, - Scrubber, -} from './styled' - -type Props = { - duration: number, - isScrubberVisible?: boolean, - loadedProgress: number, - onPlayedProgressChange: (progress: number) => void, - playedProgress: number, -} +import { Chapters } from '../Chapters' +import type { Props } from './hooks' +import { useProgressBar } from './hooks' +import { ProgressBarList } from './styled' -export const ProgressBar = ({ - duration, - isScrubberVisible, - loadedProgress, - onPlayedProgressChange, - playedProgress, -}: Props) => { +export const ProgressBar = (props: Props) => { + const { onPlayedProgressChange } = props const progressBarRef = useSlider({ onChange: onPlayedProgressChange }) - const loadedFraction = Math.min(loadedProgress * 100 / duration, 100) - const playedFraction = Math.min(playedProgress * 100 / duration, 100) - + const { + calculatedChapters, + playedProgressInPercent, + time, + } = useProgressBar(props) return ( - - - {isScrubberVisible === false ? null : ( - - - - )} + + + + ) } diff --git a/src/features/StreamPlayer/components/ProgressBar/stories.tsx b/src/features/StreamPlayer/components/ProgressBar/stories.tsx index d62dfce6..bd301b29 100644 --- a/src/features/StreamPlayer/components/ProgressBar/stories.tsx +++ b/src/features/StreamPlayer/components/ProgressBar/stories.tsx @@ -13,7 +13,7 @@ import { ProgressBar } from '.' const Story = { component: ProgressBar, - title: 'ProgressBar', + title: 'ProgressBarWithChapters', } export default Story @@ -47,9 +47,42 @@ const renderInControls = (progressBarElement: ReactElement) => ( const duration = 70000 +const chapters = [ + { + duration: 30000, + endMs: 30000, + endOffsetMs: 0, + period: 0, + startMs: 0, + startOffsetMs: 0, + url: '', + }, + { + duration: 30000, + endMs: 60000, + endOffsetMs: 0, + period: 0, + startMs: 30000, + startOffsetMs: 0, + url: '', + }, + { + duration: 10000, + endMs: 70000, + endOffsetMs: 0, + period: 0, + startMs: 60000, + startOffsetMs: 0, + url: '', + }, +] + export const Empty = () => renderInControls( renderInControls( export const HalfLoaded = () => renderInControls( renderInControls( export const HalfPlayed = () => renderInControls( renderInControls( export const Loaded40AndPlayed20 = () => renderInControls( void, + selectedQuality: string, + videoQualities: Array, +} + +export const useSettings = ({ onSelect }: Props) => { + const { + close, + isOpen, + open, + } = useToggle() + + const onItemClick = (quality: string) => { + onSelect(quality) + close() + } + + return { + close, + isOpen, + onItemClick, + open, + } +} diff --git a/src/features/StreamPlayer/components/Settings/index.tsx b/src/features/StreamPlayer/components/Settings/index.tsx new file mode 100644 index 00000000..d0e62e75 --- /dev/null +++ b/src/features/StreamPlayer/components/Settings/index.tsx @@ -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 ( + + + { + isOpen && ( + + + { + map(videoQualities, (quality) => ( + onItemClick(quality)} + > + {quality} + + )) + } + + + ) + } + + ) +} diff --git a/src/features/StreamPlayer/components/Settings/styled.tsx b/src/features/StreamPlayer/components/Settings/styled.tsx new file mode 100644 index 00000000..1df22dad --- /dev/null +++ b/src/features/StreamPlayer/components/Settings/styled.tsx @@ -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: 10px; + 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` + 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 : '')} +` diff --git a/src/features/StreamPlayer/config.tsx b/src/features/StreamPlayer/config.tsx index 72bec30c..77418095 100644 --- a/src/features/StreamPlayer/config.tsx +++ b/src/features/StreamPlayer/config.tsx @@ -1,8 +1,8 @@ -import Hls from 'hls.js' +import type { HlsConfig } from 'hls.js' import { readToken } from 'helpers/token' -export const streamConfig: Partial = { +export const streamConfig: Partial = { liveSyncDuration: 30, maxBufferLength: 30, xhrSetup: (xhr, urlString) => { @@ -11,3 +11,7 @@ export const streamConfig: Partial = { xhr.open('GET', url.toString()) }, } + +export const REWIND_SECONDS = 5 + +export const HOUR_IN_MILLISECONDS = 60 * 60 * 1000 diff --git a/src/features/StreamPlayer/helpers/index.tsx b/src/features/StreamPlayer/helpers/index.tsx new file mode 100644 index 00000000..854ce2c3 --- /dev/null +++ b/src/features/StreamPlayer/helpers/index.tsx @@ -0,0 +1,13 @@ +import findIndex from 'lodash/findIndex' +import size from 'lodash/size' + +import type { Chapters } from '../types' + +export const findChapterByProgress = (chapters: Chapters, progressMs: number) => { + if (size(chapters) === 1 && chapters[0].isFullMatchChapter) return 0 + return ( + findIndex(chapters, ({ endMs, startMs }) => ( + startMs <= progressMs && progressMs <= endMs + )) + ) +} diff --git a/src/features/StreamPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx index b2a90b8b..6eb9f291 100644 --- a/src/features/StreamPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -2,94 +2,156 @@ import type { MouseEvent } from 'react' import { useCallback, useEffect, - useMemo, useState, } from 'react' -import once from 'lodash/once' +import size from 'lodash/size' +import isNumber from 'lodash/isNumber' +import isEmpty from 'lodash/isEmpty' + +import { isIOS } from 'config/userAgent' + +import { useObjectState } from 'hooks/useObjectState' +import { useEventListener } from 'hooks/useEventListener' import { useVolume } from 'features/VideoPlayer/hooks/useVolume' -import { REWIND_SECONDS } from 'features/MultiSourcePlayer/config' import { useNoNetworkPopupStore } from 'features/NoNetworkPopup' +import { useLiveMatch } from 'features/MatchPage/components/LiveMatch/hooks' -import { useObjectState } from 'hooks' +import type { Chapters } from 'features/StreamPlayer/types' -import type { MatchInfo } from 'requests/getMatchInfo' - -import { isIOS } from 'config/userAgent' +import { MatchInfo } from 'requests/getMatchInfo' +import { REWIND_SECONDS } from '../config' import { useHlsPlayer } from './useHlsPlayer' import { useFullscreen } from './useFullscreen' import { useVideoQuality } from './useVideoQuality' import { useControlsVisibility } from './useControlsVisibility' +import { useProgressChangeHandler } from './useProgressChangeHandler' +import { usePlayingHandlers } from './usePlayingHandlers' +import { useDuration } from './useDuration' + +export type PlayerState = typeof initialState const toMilliSeconds = (seconds: number) => seconds * 1000 const initialState = { + activeChapterIndex: 0, + buffering: true, + chapters: [] as Chapters, duration: 0, loadedProgress: 0, playedProgress: 0, playing: false, ready: false, seek: 0, + seeking: false, } export type Props = { + chapters: Chapters, + isLive?: boolean, + onDurationChange?: (duration: number) => void, onPlayingChange: (playing: boolean) => void, onProgressChange: (seconds: number) => void, - profile: MatchInfo, + profile?: MatchInfo, resumeFrom?: number, - url: string, + url?: string, } export const useVideoPlayer = ({ + chapters: chaptersProps, + isLive, + onDurationChange, onPlayingChange, onProgressChange: progressChangeCallback, resumeFrom, - url, }: Props) => { - const { hls, videoRef } = useHlsPlayer(url, resumeFrom) - const [{ - duration, + activeChapterIndex, + buffering, + chapters, + duration: fullMatchDuration, loadedProgress, playedProgress, playing, ready, seek, - }, setPlayerState] = useObjectState({ - ...initialState, - playedProgress: toMilliSeconds(resumeFrom || 0), - seek: resumeFrom || 0, - }) + seeking, + }, setPlayerState] = useObjectState({ ...initialState, chapters: chaptersProps }) + + const { onPlaylistSelect, selectedPlaylist } = useLiveMatch() - const startPlaying = useMemo(() => once(() => { - setPlayerState({ playing: true, ready: true }) - onPlayingChange(true) - }), [onPlayingChange, setPlayerState]) + const { url } = chapters[0] ?? { url: '' } + const numberOfChapters = size(chapters) + const { hls, videoRef } = useHlsPlayer(url, resumeFrom) + const [isLiveTime, setIsLiveTime] = useState(false) + const [isPausedTime, setIsPausedTime] = useState(false) + const [pausedProgress, setPausedProgress] = useState(0) + + const chaptersDuration = useDuration(chapters) + + const duration = (isLive && chapters[0]?.isFullMatchChapter) + ? fullMatchDuration + : chaptersDuration + + const { + onReady, + playNextChapter, + playPrevChapter, + stopPlaying, + togglePlaying, + } = usePlayingHandlers(setPlayerState, chapters) + + const getActiveChapter = useCallback( + (index: number = activeChapterIndex) => chapters[index], + [chapters, activeChapterIndex], + ) const { isFullscreen, onFullscreenClick, wrapperRef, } = useFullscreen() + const [sizeOptions, setSizeOptions] = useState({ height: wrapperRef.current?.clientHeight, width: wrapperRef.current?.clientWidth, }) - const togglePlaying = () => { - if (ready) { - setPlayerState({ playing: !playing }) - onPlayingChange(!playing) + const isFirstChapterPlaying = activeChapterIndex === 0 + const isLastChapterPlaying = activeChapterIndex === numberOfChapters - 1 + const seekTo = useCallback((progressMs: number) => { + if (!videoRef.current) return + videoRef.current.currentTime = progressMs / 1000 + }, [videoRef]) + + const rewindForward = () => { + const chapter = getActiveChapter() + const newProgress = playedProgress + REWIND_SECONDS * 1000 + if (newProgress <= chapter.duration || isLive) { + seekTo(chapter.startOffsetMs + newProgress) + } else if (isLastChapterPlaying) { + playNextChapter() + } else { + const nextChapter = getActiveChapter(activeChapterIndex + 1) + const fromMs = newProgress - chapter.duration + playNextChapter(fromMs, nextChapter.startOffsetMs) } } - const rewind = (seconds: number) => () => { - if (!videoRef.current) return - const { currentTime } = videoRef.current - const newProgress = currentTime + seconds - videoRef.current.currentTime = newProgress + const rewindBackward = () => { + const chapter = getActiveChapter() + const newProgress = playedProgress - REWIND_SECONDS * 1000 + if (newProgress >= 0) { + seekTo(chapter.startOffsetMs + newProgress) + } else if (isFirstChapterPlaying) { + seekTo(chapter.startOffsetMs) + } else { + const prevChapter = getActiveChapter(activeChapterIndex - 1) + const fromMs = prevChapter.duration + newProgress + playPrevChapter(fromMs, prevChapter.startOffsetMs) + } } const onError = useCallback(() => { @@ -102,28 +164,177 @@ export const useVideoPlayer = ({ } } + const onWaiting = () => { + setPlayerState({ buffering: true }) + } + + const onPlaying = () => { + setPlayerState({ buffering: false }) + } + + const onPause = () => { + setPlayerState({ playing: false }) + } + + const onPlay = () => { + setPlayerState({ playing: true }) + } + const onDuration = (durationSeconds: number) => { setPlayerState({ duration: toMilliSeconds(durationSeconds) }) + onDurationChange?.(durationSeconds) } - const onProgressChange = useCallback((progress: number) => { - const progressMs = progress * duration - setPlayerState({ playedProgress: progressMs, seek: progressMs / 1000 }) - }, [duration, setPlayerState]) + const onProgressChange = useProgressChangeHandler({ + chapters, + duration, + setPlayerState, + }) const onLoadedProgress = (loadedMs: number) => { - setPlayerState({ loadedProgress: loadedMs }) + const chapter = getActiveChapter() + const value = loadedMs - chapter.startOffsetMs + setPlayerState({ loadedProgress: value }) } const onPlayedProgress = (playedMs: number) => { - setPlayerState({ playedProgress: playedMs }) - progressChangeCallback(playedMs / 1000) + const chapter = getActiveChapter() + const value = Math.max(playedMs - chapter.startOffsetMs, 0) + setPlayerState({ playedProgress: value }) + + progressChangeCallback(value / 1000) } const backToLive = useCallback(() => { - const liveProgressMs = Math.max(duration - 10000, 0) + if (!duration) return + + if (selectedPlaylist?.id !== 'full_game') { + onPlaylistSelect({ + duration: 0, + episodes: [], + id: 'full_game', + lexic: 13028, + type: 0, + }) + setIsLiveTime(true) + } + + const liveProgressMs = Math.max(duration - 30000, 0) setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 }) - }, [duration, setPlayerState]) + if (liveProgressMs > 0) setIsLiveTime(false) + }, [ + duration, + onPlaylistSelect, + selectedPlaylist, + setPlayerState, + ]) + + const backToPausedTime = useCallback(() => { + if (!duration) return + + if (selectedPlaylist?.id !== 'full_game') { + onPlaylistSelect({ + duration: 0, + episodes: [], + id: 'full_game', + lexic: 13028, + type: 0, + }) + setIsPausedTime(true) + } + + const liveProgressMs = Math.max(duration - 30000, 0) + setPlayerState({ playedProgress: pausedProgress, seek: pausedProgress / 1000 }) + if (liveProgressMs > 0) setIsPausedTime(false) + // eslint-disable-next-line + }, [ + duration, + onPlaylistSelect, + selectedPlaylist, + setPlayerState, + ]) + + useEffect(() => { + if (chapters[0]?.isFullMatchChapter) { + setPausedProgress(playedProgress) + } + // eslint-disable-next-line + }, [selectedPlaylist]) + + useEffect(() => { + if (duration && isLiveTime && chapters[0]?.isFullMatchChapter) { + backToLive() + } + // eslint-disable-next-line + }, [duration, isLiveTime]) + + useEffect(() => { + if (duration && isPausedTime && chapters[0]?.isFullMatchChapter) { + backToPausedTime() + } + // eslint-disable-next-line + }, [duration, isPausedTime]) + + useEventListener({ + callback: (e: KeyboardEvent) => { + if (e.code === 'ArrowLeft') rewindBackward() + else if (e.code === 'ArrowRight') rewindForward() + }, + event: 'keydown', + }) + + useEffect(() => { + if (isNumber(seek)) { + setPlayerState({ seek: undefined }) + } + }, [seek, setPlayerState]) + + useEffect(() => { + onPlayingChange(playing) + if (playing) { + setPlayerState({ buffering: false }) + } + // eslint-disable-next-line + }, [playing, onPlayingChange]) + + useEffect(() => { + if ((isLive + && chapters[0]?.endOffsetMs === chaptersProps[0]?.endOffsetMs) + || isEmpty(chapters)) return + setPlayerState({ + ...initialState, + chapters: chaptersProps, + playing: true, + seek: chaptersProps[0].startOffsetMs / 1000, + }) + }, [ + chapters, + chaptersProps, + isLive, + setPlayerState, + ]) + + useEffect(() => { + if ((isLive && chapters[0]?.isFullMatchChapter) || isEmpty(chapters)) return + + const { duration: chapterDuration } = getActiveChapter() + if (playedProgress >= chapterDuration && !seeking) { + if (isLive) { + backToPausedTime() + } else { + playNextChapter() + } + } + // eslint-disable-next-line + }, [ + isLive, + chapters, + getActiveChapter, + onPlaylistSelect, + playedProgress, + seeking, + playNextChapter, + ]) const { isOnline } = useNoNetworkPopupStore() @@ -138,13 +349,11 @@ export const useVideoPlayer = ({ useEffect(() => { if (!isOnline) { - setPlayerState({ playing: false }) - onPlayingChange(false) + stopPlaying() } }, [ isOnline, - onPlayingChange, - setPlayerState, + stopPlaying, ]) useEffect(() => { @@ -159,27 +368,50 @@ export const useVideoPlayer = ({ } }, [setPlayerState]) + useEffect(() => { + if (ready && videoRef) { + videoRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }) + } + }, [ready, videoRef]) + return { + activeChapterIndex, + allPlayedProgress: playedProgress + getActiveChapter().startMs, backToLive, + buffering, + chapters, duration, + isFirstChapterPlaying, isFullscreen, + isLastChapterPlaying, loadedProgress, + numberOfChapters, onDuration, onError, onFullscreenClick, onLoadedProgress, + onPause, + onPlay, onPlayedProgress, onPlayerClick, + onPlaying, onProgressChange, + onReady, + onWaiting, + playNextChapter, + playPrevChapter, playedProgress, playing, ready, - rewindBackward: rewind(-REWIND_SECONDS), - rewindForward: rewind(REWIND_SECONDS), + rewindBackward, + rewindForward, seek, sizeOptions, - startPlaying, togglePlaying, + url, videoRef, wrapperRef, ...useControlsVisibility(isFullscreen), diff --git a/src/features/StreamPlayer/hooks/useDuration.tsx b/src/features/StreamPlayer/hooks/useDuration.tsx new file mode 100644 index 00000000..ae416a0e --- /dev/null +++ b/src/features/StreamPlayer/hooks/useDuration.tsx @@ -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]) +) diff --git a/src/features/StreamPlayer/hooks/useFullscreen.tsx b/src/features/StreamPlayer/hooks/useFullscreen.tsx index ffbfb4a5..2a73e4f1 100644 --- a/src/features/StreamPlayer/hooks/useFullscreen.tsx +++ b/src/features/StreamPlayer/hooks/useFullscreen.tsx @@ -43,11 +43,8 @@ export const useFullscreen = () => { } } - /** - * В обертке могут быть 2 плеера, находим тот который играет сейчас, т.е. не скрыт - */ const getPlayingVideoElement = () => ( - wrapperRef.current?.querySelector('video:not([hidden])') as HTMLVideoElement | null + wrapperRef.current?.querySelector('video') as HTMLVideoElement | null ) const toggleIOSFullscreen = () => { diff --git a/src/features/StreamPlayer/hooks/usePlayingHandlers.tsx b/src/features/StreamPlayer/hooks/usePlayingHandlers.tsx new file mode 100644 index 00000000..7d7c698f --- /dev/null +++ b/src/features/StreamPlayer/hooks/usePlayingHandlers.tsx @@ -0,0 +1,113 @@ +import { useCallback } from 'react' + +import isUndefined from 'lodash/isUndefined' + +import type { SetPartialState } from 'hooks' + +import type { PlayerState } from '.' +import type { Chapters } from '../types' + +export const usePlayingHandlers = ( + setPlayerState: SetPartialState, + chapters: Chapters, +) => { + const onReady = useCallback(() => { + setPlayerState((state) => ( + state.ready + ? state + : { + buffering: false, + 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 nextChapterIndex = state.activeChapterIndex + 1 + const nextChapter = chapters[nextChapterIndex] + if (!nextChapter) { + return { + activeChapterIndex: 0, + loadedProgress: 0, + playedProgress: 0, + playing: false, + seek: chapters[0].startOffsetMs / 1000, + seeking: false, + } + } + if (isUndefined(fromMs) || isUndefined(startOffsetMs)) { + return { + activeChapterIndex: nextChapterIndex, + loadedProgress: 0, + playedProgress: 0, + seek: nextChapter.startOffsetMs / 1000, + } + } + + return { + activeChapterIndex: nextChapterIndex, + loadedProgress: 0, + playedProgress: fromMs, + playing: state.playing, + seek: (startOffsetMs + fromMs) / 1000, + } + }) + }, [chapters, setPlayerState]) + + const playPrevChapter = useCallback((fromMs?: number, startOffsetMs?: number) => { + setPlayerState((state) => { + if (!state.ready || state.activeChapterIndex === 0) return state + + const prevChapterIndex = state.activeChapterIndex - 1 + const prevChapter = chapters[prevChapterIndex] + if (isUndefined(fromMs) || isUndefined(startOffsetMs)) { + return { + activeChapterIndex: prevChapterIndex, + loadedProgress: 0, + playedProgress: 0, + seek: prevChapter.startOffsetMs / 1000, + } + } + + return { + activeChapterIndex: prevChapterIndex, + loadedProgress: 0, + playedProgress: fromMs, + seek: (startOffsetMs + fromMs) / 1000, + } + }) + }, [chapters, setPlayerState]) + + return { + onReady, + playNextChapter, + playPrevChapter, + startPlaying, + stopPlaying, + togglePlaying, + } +} diff --git a/src/features/StreamPlayer/hooks/useProgressChangeHandler.tsx b/src/features/StreamPlayer/hooks/useProgressChangeHandler.tsx new file mode 100644 index 00000000..17788fe8 --- /dev/null +++ b/src/features/StreamPlayer/hooks/useProgressChangeHandler.tsx @@ -0,0 +1,48 @@ +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, +} + +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: seekMs / 1000, + seeking, + } + }) + }, [duration, chapters, setPlayerState]) + + return onProgressChange +} diff --git a/src/features/StreamPlayer/hooks/useVideoQuality.tsx b/src/features/StreamPlayer/hooks/useVideoQuality.tsx index c8617513..43b968c1 100644 --- a/src/features/StreamPlayer/hooks/useVideoQuality.tsx +++ b/src/features/StreamPlayer/hooks/useVideoQuality.tsx @@ -4,7 +4,7 @@ import { useCallback, } from 'react' -import Hls from 'hls.js' +import Hls, { Level } from 'hls.js' import map from 'lodash/map' import find from 'lodash/find' @@ -28,9 +28,9 @@ const autoQuality = { * непонятное качество без свойств height, width и тд для определения * какое это качество */ -const filterOutUnknownQualities = filter(({ height }: Hls.Level) => Boolean(height)) +const filterOutUnknownQualities = filter(({ height }: Level) => Boolean(height)) -const getVideoQualities = (levels: Array) => { +const getVideoQualities = (levels: Array) => { if (isEmpty(levels)) return [] const filteredQualities = filterOutUnknownQualities(levels) diff --git a/src/features/StreamPlayer/index.tsx b/src/features/StreamPlayer/index.tsx index d6f9bd8b..67bcec54 100644 --- a/src/features/StreamPlayer/index.tsx +++ b/src/features/StreamPlayer/index.tsx @@ -1,34 +1,57 @@ +import { Fragment } from 'react' + +import { T9n } from 'features/T9n' import { Loader } from 'features/Loader' -import { REWIND_SECONDS } from 'features/MultiSourcePlayer/config' import { VideoPlayer } from 'features/VideoPlayer' -import { Name } from 'features/Name' -import { isMobileDevice } from 'config/userAgent' +import { secondsToHms } from 'helpers' + +import { HOUR_IN_MILLISECONDS, REWIND_SECONDS } from './config' +import { VolumeBar } from './components/VolumeBar' +import { Settings } from './components/Settings' +import { ProgressBar } from './components/ProgressBar' import { PlayerWrapper, - LoaderWrapper, - ControlsGradient, + Controls, + ControlsRow, + ControlsGroup, CenterControls, - Backward, PlayStop, + Fullscreen, + LoaderWrapper, + Backward, Forward, - TeamsDetailsWrapper, + PlaybackTime, + ControlsGradient, + LiveBtn, + ChaptersText, + Next, + Prev, } from './styled' import type { Props } from './hooks' import { useVideoPlayer } from './hooks' -import { Controls } from './components/Controls' +/** + * HLS плеер, применяется на лайв и завершенных матчах + */ export const StreamPlayer = (props: Props) => { - const { profile, url } = props + const { isLive } = props const { + activeChapterIndex, + allPlayedProgress, backToLive, + buffering, + chapters, controlsVisible, duration, + isFirstChapterPlaying, isFullscreen, + isLastChapterPlaying, loadedProgress, muted, + numberOfChapters, onDuration, onError, onFullscreenClick, @@ -36,23 +59,30 @@ export const StreamPlayer = (props: Props) => { onMouseEnter, onMouseLeave, onMouseMove, + onPause, + onPlay, onPlayedProgress, onPlayerClick, + onPlaying, onProgressChange, onQualitySelect, + onReady, onTouchEnd, onTouchStart, onVolumeChange, onVolumeClick, + onWaiting, playedProgress, playing, + playNextChapter, + playPrevChapter, ready, rewindBackward, rewindForward, seek, selectedQuality, - startPlaying, togglePlaying, + url, videoQualities, videoRef, volume, @@ -71,11 +101,9 @@ export const StreamPlayer = (props: Props) => { onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - {!ready && ( - - - - )} + + + { onPlayedProgress={onPlayedProgress} onDurationChange={onDuration} onEnded={togglePlaying} - onReady={startPlaying} + onReady={onReady} + onPause={onPause} + onPlay={onPlay} + onPlaying={onPlaying} + onWaiting={onWaiting} onError={onError} crossOrigin='use-credentials' /> - - {isMobileDevice && isFullscreen && controlsVisible && profile && ( - - - {` ${profile.team1.score}-${profile.team2.score} `} - - - )} - {ready && ( @@ -120,34 +143,83 @@ export const StreamPlayer = (props: Props) => { )} - - + + + + + + + + { + numberOfChapters > 1 && ( + + playPrevChapter()} + /> + + {activeChapterIndex + 1} / {numberOfChapters} + + playNextChapter()} + /> + + ) + } + + { + isLive + ? ( + + {secondsToHms(allPlayedProgress / 1000)} + + ) + : ( + HOUR_IN_MILLISECONDS ? 150 : 130}> + {secondsToHms(allPlayedProgress / 1000)} + {' / '} + {secondsToHms(duration / 1000)} + + ) + } + {REWIND_SECONDS} + {REWIND_SECONDS} + + + { + isLive && ( + + + + ) + } + + + + + + ) } diff --git a/src/features/StreamPlayer/styled.tsx b/src/features/StreamPlayer/styled.tsx index 9b1b11e4..d99334aa 100644 --- a/src/features/StreamPlayer/styled.tsx +++ b/src/features/StreamPlayer/styled.tsx @@ -1,4 +1,4 @@ -import styled, { css } from 'styled-components' +import styled, { css } from 'styled-components/macro' import { isMobileDevice } from 'config/userAgent' @@ -9,14 +9,6 @@ export const ControlsGradient = styled.div<{ isVisible?: boolean }>` width: 100%; pointer-events: none; height: 145px; - - ${({ isVisible }) => (isMobileDevice - ? css` - height: 100%; - background: ${(isVisible - ? 'linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0) 25%, rgba(0,0,0,0) 75%, rgba(0,0,0,0.7) 100%)' - : 'initial')} - ` : '')} ` type HoverStylesProps = { @@ -108,11 +100,9 @@ export const PlayerWrapper = styled.div` position: relative; background-color: #000; min-height: 100%; - :fullscreen { padding-top: 0; } - ${supportsAspectRatio ? css`aspect-ratio: 16 / 9;` : css` @@ -128,15 +118,18 @@ export const PlayerWrapper = styled.div` : ''}; ` -export const LoaderWrapper = styled.div` +type LoaderWrapperProps = { + buffering?: boolean, +} + +export const LoaderWrapper = styled.div` position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1; + transition: opacity 0.3s ease-in-out; + opacity: ${({ buffering }) => (buffering ? 1 : 0)}; ` export const ButtonBase = styled.button` @@ -153,7 +146,7 @@ export const ButtonBase = styled.button` const sizes = { lg: 92, - sm: 29, + sm: 24, } type PlayStopProps = { @@ -171,18 +164,17 @@ export const PlayStop = styled(ButtonBase)` )}; ${({ size = 'sm' }) => ( css` - width: ${sizes[size]}px; - height: ${sizes[size]}px; - ` + width: ${sizes[size]}px; + height: ${sizes[size]}px; + ` )}; - ${isMobileDevice ? css` - width: ${sizes.sm}px; - height: ${sizes.sm}px; - margin-right: 0; - padding: 0; - ` + width: 20%; + height: 60%; + margin-right: 0; + padding: 0; + ` : ''}; ` @@ -206,13 +198,12 @@ export const Fullscreen = styled(ButtonBase)` ? 'url(/images/player-fullscreen-off.svg)' : 'url(/images/player-fullscreen-on.svg)' )}; - ${isMobileDevice ? css` - width: 20px; - height: 18px; - margin-left: 15px; - ` + width: 20px; + height: 18px; + margin-left: 15px; + ` : ''}; ` @@ -241,41 +232,41 @@ export const Backward = styled(ButtonBase)` font-weight: normal; ${({ size = 'sm' }) => ( css` - width: ${rewindButtonSizes.sides[size]}px; - height: ${rewindButtonSizes.sides[size]}px; - font-size: ${rewindButtonSizes.fontSizes[size]}px; - ` + width: ${rewindButtonSizes.sides[size]}px; + height: ${rewindButtonSizes.sides[size]}px; + font-size: ${rewindButtonSizes.fontSizes[size]}px; + ` )} display: ${({ isHidden }) => (isHidden ? 'none' : 'block')}; - ${isMobileDevice ? css` - width: ${rewindButtonSizes.sides.sm}px; - height: ${rewindButtonSizes.sides.sm}px; - font-size: ${rewindButtonSizes.fontSizes.sm}px; - ` + width: 15%; + font-size: 14px; + margin-right: 6px; + ` : ''}; ` export const Forward = styled(Backward)` background-image: url(/images/player-forward.svg); - margin-left: 10px; - margin-right: 0; ` -export const PlaybackTime = styled.span` +type PlaybackTimeProps = { + width?: number, +} + +export const PlaybackTime = styled.span` + width: ${({ width = 130 }) => `${width}px`}; font-weight: 600; font-size: 16px; color: #fff; - margin-right: 10px; - ${isMobileDevice ? css` - margin-left: 5px; - font-size: 10px; - width: 100px; - white-space: nowrap; - ` + margin-left: 5px; + font-size: 10px; + width: 100px; + white-space: nowrap; + ` : ''}; ` @@ -294,13 +285,12 @@ export const CenterControls = styled.div` justify-content: space-between; align-items: center; transition: opacity 0.3s ease-in-out; - opacity: ${({ playing }) => (playing ? 0 : 1)}; pointer-events: ${({ playing }) => (playing ? 'none' : 'auto')}; ${isMobileDevice ? css` - width: 135px; - ` + width: 70%; + ` : ''}; ` @@ -314,6 +304,51 @@ export const LiveBtn = styled(ButtonBase)` padding: 4.5px 8px; background-color: #CC0000; border-radius: 1.3px; + ${isMobileDevice + ? css` + ` + : ''}; +` + +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: 15%; + ` + : ''}; +` + +type PrevProps = { + disabled?: boolean, +} + +export const Prev = styled(ButtonBase)` + 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); ` export const TeamsDetailsWrapper = styled.div` diff --git a/src/features/StreamPlayer/types.tsx b/src/features/StreamPlayer/types.tsx new file mode 100644 index 00000000..e90a6fb1 --- /dev/null +++ b/src/features/StreamPlayer/types.tsx @@ -0,0 +1,33 @@ +/** + * для примера матч с двумя эпизодами в плейлисте Голы, время в мс: + * [{start: 0, end: 20000}, {start: 60000, end: 80000}] +*/ + +export type Chapter = { + duration: number, + + /** + * конец эпизода в плейлисте + * в первом эпизоде - 20000, во втором - 40000 + */ + endMs: number, + + /** конец эпизода как отмечено в матче */ + endOffsetMs: number, + + /** индекс эпизода для дебага */ + index?: number, + isFullMatchChapter?: boolean, + + /** + * начало эпизода в плейлисте + * в первом эпизоде - 0, во втором - 20000 + */ + startMs: number, + + /** начало эпизода как отмечено в матче */ + startOffsetMs: number, + url: string, +} + +export type Chapters = Array diff --git a/src/features/TournamentsPopup/index.tsx b/src/features/TournamentsPopup/index.tsx index 65f1e1b1..fd99f52b 100644 --- a/src/features/TournamentsPopup/index.tsx +++ b/src/features/TournamentsPopup/index.tsx @@ -31,7 +31,7 @@ export const TournamentsPopup = () => { { isFetching ? ( - + ) : ( diff --git a/src/features/VideoPlayer/hooks/index.tsx b/src/features/VideoPlayer/hooks/index.tsx index bff60797..d0484023 100644 --- a/src/features/VideoPlayer/hooks/index.tsx +++ b/src/features/VideoPlayer/hooks/index.tsx @@ -25,8 +25,11 @@ export type Props = { onError?: (e?: SyntheticEvent) => void, onLoadedProgress?: (loadedMs: number) => void, onPause?: (e: SyntheticEvent) => void, + onPlay?: (e: SyntheticEvent) => void, onPlayedProgress?: (playedMs: number) => void, + onPlaying?: () => void, onReady?: () => void, + onWaiting?: () => void, playing?: boolean, ref?: Ref, seek?: number | null, diff --git a/src/features/VideoPlayer/index.tsx b/src/features/VideoPlayer/index.tsx index 05dac725..bc56c661 100644 --- a/src/features/VideoPlayer/index.tsx +++ b/src/features/VideoPlayer/index.tsx @@ -4,6 +4,16 @@ import type { Props } from './hooks' import { useVideoPlayer } from './hooks' import { Video } from './styled' +/** + * Низкоуровневый компонент для декларативной работы с HTMLVideoElement + * ```ts + * например старт и пауза плеера вместо + * video.play() | video.pause() + * + * контролируем через пропс playing + * + * ``` + */ export const VideoPlayer = forwardRef((props: Props, ref) => { const { className, @@ -15,6 +25,9 @@ export const VideoPlayer = forwardRef((props: Props, re onEnded, onError, onPause, + onPlay, + onPlaying, + onWaiting, src, width, } = props @@ -40,8 +53,11 @@ export const VideoPlayer = forwardRef((props: Props, re onProgress={handleLoadedChange} onEnded={onEnded} onDurationChange={handleDurationChange} + onPlay={onPlay} onPause={onPause} onError={onError} + onWaiting={onWaiting} + onPlaying={onPlaying} crossOrigin={crossOrigin} controls={controls} /> diff --git a/src/requests/getFullMatchDuration.tsx b/src/requests/getFullMatchDuration.tsx deleted file mode 100644 index dd9a1e90..00000000 --- a/src/requests/getFullMatchDuration.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import pipe from 'lodash/fp/pipe' -import orderBy from 'lodash/fp/orderBy' -import sumBy from 'lodash/fp/sumBy' -import uniqBy from 'lodash/fp/uniqBy' - -import type { Videos, Video } from './getVideos' -import { getVideos } from './getVideos' - -const calculateDuration = (videos: Videos) => { - const durationMs = pipe( - orderBy(({ quality }: Video) => Number(quality), 'desc'), - uniqBy(({ period }: Video) => period), - sumBy(({ duration }: Video) => duration), - )(videos) - return durationMs / 1000 -} - -/** - * Временный способ получения длительности матча - */ -export const getFullMatchDuration = async (...args: Parameters) => { - const videos = await getVideos(...args) - return calculateDuration(videos) -} diff --git a/src/requests/getMatchEvents.tsx b/src/requests/getMatchEvents.tsx index 8b95a494..51af4761 100644 --- a/src/requests/getMatchEvents.tsx +++ b/src/requests/getMatchEvents.tsx @@ -76,5 +76,7 @@ export const getMatchEvents = async ({ url: `${DATA_URL}/${getSportLexic(sportType)}`, }) + if (!response?.data) return Promise.reject(response) + return response?.data || [] } diff --git a/src/requests/getMatchInfo.tsx b/src/requests/getMatchInfo.tsx index 31960455..1cb9a087 100644 --- a/src/requests/getMatchInfo.tsx +++ b/src/requests/getMatchInfo.tsx @@ -16,6 +16,14 @@ export type Team = { score: number, } +export type VideoBound = { + e: string, + h: string, + s: string, +} + +type VideoBounds = Array + export type MatchInfo = { access?: boolean, calc: boolean, @@ -33,6 +41,7 @@ export type MatchInfo = { name_eng: string, name_rus: string, }, + video_bounds?: VideoBounds, youtube_link?: string, } | null diff --git a/src/requests/getMatchPlaylists.tsx b/src/requests/getMatchPlaylists.tsx index 9c395904..e2c01cdd 100644 --- a/src/requests/getMatchPlaylists.tsx +++ b/src/requests/getMatchPlaylists.tsx @@ -7,11 +7,10 @@ import { } from 'config' import { callApi, getSportLexic } from 'helpers' -import { getFullMatchDuration } from './getFullMatchDuration' - const proc = PROCEDURES.ott_match_popup type Args = { + fullMatchDuration?: number, matchId: number, selectedActions: Array, sportType: SportTypes, @@ -49,12 +48,12 @@ type Player = { export type Players = Array export type Lexics = { - ball_in_play: number, - full_game: number, - goals: number, - highlights: number, - interview: number, - players: number, + ball_in_play?: number, + full_game?: number, + goals?: number, + highlights?: number, + interview?: number, + players?: number, } export type MatchPlaylists = { @@ -75,11 +74,11 @@ type Response = { } export const getMatchPlaylists = async ({ + fullMatchDuration, matchId, selectedActions, sportType, - withFullMatchDuration, -}: Args) => { +}: Args): Promise => { const actions = isEmpty(selectedActions) ? null : selectedActions const config = { @@ -92,25 +91,29 @@ export const getMatchPlaylists = async ({ }, } - const playlistPromise: Promise = callApi({ + const playlist: Response = await callApi({ config, url: `${DATA_URL}/${getSportLexic(sportType)}`, }) - const matchDurationPromise = withFullMatchDuration - ? getFullMatchDuration(sportType, matchId) - : Promise.resolve(undefined) - - const [playlist, fullMatchDuration] = await Promise.all( - [playlistPromise, matchDurationPromise], - ) - const full_game: PlaylistWithDuration = { data: [], dur: fullMatchDuration, } - return playlist.data - ? { ...playlist.data, full_game } - : null + if (playlist.data) { + return { ...playlist.data, full_game } + } + + return { + ball_in_play: {}, + full_game, + goals: {}, + highlights: {}, + lexics: {}, + players1: [], + players2: [], + score1: 0, + score2: 0, + } } diff --git a/src/requests/index.tsx b/src/requests/index.tsx index b5d0cd7b..f4f0e777 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -11,9 +11,9 @@ export * from './getTournamentInfo' export * from './getTeamInfo' export * from './getUserInfo' export * from './getMatchInfo' +export * from './getVideos' export * from './getUnauthenticatedMatch' export * from './reportPlayerProgress' -export * from './getVideos' export * from './saveUserInfo' export * from './getPlayerInfo' export * from './getMatchLastWatchSeconds'