From 194d09e0091e8d3f037c51bf0ba58e794144e99a Mon Sep 17 00:00:00 2001 From: Mirlan Date: Thu, 17 Sep 2020 16:28:39 +0600 Subject: [PATCH] Ott 403 finished game playing (#151) * Ott 403 finished game playing part 1 (#134) * fix(#430): fixed styled-components warnings on often changing css rules * refactor(#430): moved match profile hook to MatchPage level * feat(#430): added video request * feat(#430): added playing match video link Co-authored-by: mirlan.maksitaliev * Ott 403 finished game playing part 2 (#136) * refactor(#403): updated getVideos request return type * feat(#403): added displaying video chapters in player progressbar * feat(#430): added progressBar stories * feat(#403): added tests to helpers Co-authored-by: mirlan.maksitaliev * Ott 403 finished game playing part 3 (#144) * feat(#403): added useEventListener hook * refactor(#403): player progress reporting * refactor(#403): added MultiSourcePlayer component Co-authored-by: mirlan.maksitaliev * Ott 403 finished game playing part 4 (#147) * refactor(#403): renamed VideoPlayer -> StreamPlayer * refactor(#403): restored previous simple progress bar of StreamPlayer * fix(#403): storybook component name Co-authored-by: mirlan.maksitaliev * Ott 403 finished game playing part 5 (#150) * fix(#403): added player ready state * refactor(#403): added converting videos response to chapters Co-authored-by: mirlan.maksitaliev Co-authored-by: mirlan.maksitaliev --- .../MatchPage/MatchProfileCard/hooks.tsx | 46 ------ .../MatchPage/MatchProfileCard/index.tsx | 82 +++++----- .../MatchPage/hooks/useMatchProfile.tsx | 58 +++++++ .../usePlayerProgressReporter.tsx} | 11 +- src/features/MatchPage/hooks/useVideoData.tsx | 30 ++++ src/features/MatchPage/index.tsx | 37 +++-- .../components/Chapters/index.tsx | 44 +++++ .../components/Chapters/styled.tsx | 18 ++ .../__tests__/index.tsx | 36 ++++ .../calculateChapterProgress/index.tsx | 8 + .../__tests__/index.tsx | 43 +++++ .../helpers/calculateChapterStyles/index.tsx | 26 +++ .../components/ProgressBar/hooks.tsx | 38 +++++ .../components/ProgressBar/index.tsx | 27 +++ .../components/ProgressBar/stories.tsx | 111 +++++++++++++ .../components/ProgressBar/styled.tsx | 7 + .../MultiSourcePlayer/helpers/index.tsx | 24 +++ .../MultiSourcePlayer/hooks/index.tsx | 154 ++++++++++++++++++ .../MultiSourcePlayer/hooks/useDuration.tsx | 9 + .../hooks/usePlayingState.tsx | 42 +++++ .../hooks/useProgressChangeHandler.tsx | 76 +++++++++ .../hooks/useProgressEvents.tsx | 68 ++++++++ .../MultiSourcePlayer/hooks/useVideos.tsx | 62 +++++++ .../MultiSourcePlayer/hooks/useVolume.tsx | 50 ++++++ src/features/MultiSourcePlayer/index.tsx | 64 ++++++++ src/features/MultiSourcePlayer/styled.tsx | 9 + src/features/MultiSourcePlayer/types.tsx | 9 + .../components/ProgressBar/index.tsx | 18 +- .../components/ProgressBar/stories.tsx | 83 ++++++++++ .../components/ProgressBar/styled.tsx | 13 +- .../components/TimeTooltip/index.tsx | 0 .../components/TimeTooltip/styled.tsx | 0 .../components/VolumeBar/index.tsx | 2 +- .../components/VolumeBar/styled.tsx | 0 .../{VideoPlayer => StreamPlayer}/config.tsx | 4 +- .../hooks/index.tsx | 30 ++-- .../hooks/useFullscreen.tsx | 0 .../hooks/useSlider.tsx | 0 .../hooks/useVolume.tsx | 2 +- .../{VideoPlayer => StreamPlayer}/index.tsx | 14 +- .../{VideoPlayer => StreamPlayer}/styled.tsx | 0 src/hooks/index.tsx | 1 + src/hooks/useEventListener.tsx | 39 +++++ src/requests/getMatches.tsx | 1 + src/requests/getVideos.tsx | 31 ++++ src/requests/index.tsx | 1 + src/requests/reportPlayerProgress.tsx | 2 +- 47 files changed, 1286 insertions(+), 144 deletions(-) delete mode 100644 src/features/MatchPage/MatchProfileCard/hooks.tsx create mode 100644 src/features/MatchPage/hooks/useMatchProfile.tsx rename src/features/MatchPage/{hooks.tsx => hooks/usePlayerProgressReporter.tsx} (72%) create mode 100644 src/features/MatchPage/hooks/useVideoData.tsx create mode 100644 src/features/MultiSourcePlayer/components/Chapters/index.tsx create mode 100644 src/features/MultiSourcePlayer/components/Chapters/styled.tsx create mode 100644 src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterProgress/__tests__/index.tsx create mode 100644 src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterProgress/index.tsx create mode 100644 src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/__tests__/index.tsx create mode 100644 src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/index.tsx create mode 100644 src/features/MultiSourcePlayer/components/ProgressBar/hooks.tsx create mode 100644 src/features/MultiSourcePlayer/components/ProgressBar/index.tsx create mode 100644 src/features/MultiSourcePlayer/components/ProgressBar/stories.tsx create mode 100644 src/features/MultiSourcePlayer/components/ProgressBar/styled.tsx create mode 100644 src/features/MultiSourcePlayer/helpers/index.tsx create mode 100644 src/features/MultiSourcePlayer/hooks/index.tsx create mode 100644 src/features/MultiSourcePlayer/hooks/useDuration.tsx create mode 100644 src/features/MultiSourcePlayer/hooks/usePlayingState.tsx create mode 100644 src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx create mode 100644 src/features/MultiSourcePlayer/hooks/useProgressEvents.tsx create mode 100644 src/features/MultiSourcePlayer/hooks/useVideos.tsx create mode 100644 src/features/MultiSourcePlayer/hooks/useVolume.tsx create mode 100644 src/features/MultiSourcePlayer/index.tsx create mode 100644 src/features/MultiSourcePlayer/styled.tsx create mode 100644 src/features/MultiSourcePlayer/types.tsx rename src/features/{VideoPlayer => StreamPlayer}/components/ProgressBar/index.tsx (51%) create mode 100644 src/features/StreamPlayer/components/ProgressBar/stories.tsx rename src/features/{VideoPlayer => StreamPlayer}/components/ProgressBar/styled.tsx (66%) rename src/features/{VideoPlayer => StreamPlayer}/components/TimeTooltip/index.tsx (100%) rename src/features/{VideoPlayer => StreamPlayer}/components/TimeTooltip/styled.tsx (100%) rename src/features/{VideoPlayer => StreamPlayer}/components/VolumeBar/index.tsx (90%) rename src/features/{VideoPlayer => StreamPlayer}/components/VolumeBar/styled.tsx (100%) rename src/features/{VideoPlayer => StreamPlayer}/config.tsx (69%) rename src/features/{VideoPlayer => StreamPlayer}/hooks/index.tsx (78%) rename src/features/{VideoPlayer => StreamPlayer}/hooks/useFullscreen.tsx (100%) rename src/features/{VideoPlayer => StreamPlayer}/hooks/useSlider.tsx (100%) rename src/features/{VideoPlayer => StreamPlayer}/hooks/useVolume.tsx (96%) rename src/features/{VideoPlayer => StreamPlayer}/index.tsx (86%) rename src/features/{VideoPlayer => StreamPlayer}/styled.tsx (100%) create mode 100644 src/hooks/useEventListener.tsx create mode 100644 src/requests/getVideos.tsx diff --git a/src/features/MatchPage/MatchProfileCard/hooks.tsx b/src/features/MatchPage/MatchProfileCard/hooks.tsx deleted file mode 100644 index 7f0aa0c5..00000000 --- a/src/features/MatchPage/MatchProfileCard/hooks.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect, useState } from 'react' - -import type { MatchInfo } from 'requests' -import { getMatchInfo } from 'requests' - -import { useLexicsStore } from 'features/LexicsStore' - -import { useSportNameParam, usePageId } from 'hooks' - -type Name = 'name_rus' | 'name_eng' - -export const useMatchProfileCard = () => { - const [matchProfile, setMatchProfile] = useState(null) - const { sportType } = useSportNameParam() - const pageId = usePageId() - const { suffix } = useLexicsStore() - - const addNames = (profile: MatchInfo, suffixArg: string) => ( - profile - ? ({ - ...profile, - team1: { - ...profile.team1, - name: profile.team1[`name_${suffixArg}` as Name], - }, - team2: { - ...profile.team2, - name: profile.team2[`name_${suffixArg}` as Name], - }, - tournament: { - ...profile.tournament, - name: profile.tournament[`name_${suffixArg}` as Name], - }, - }) - : null - ) - - useEffect(() => { - getMatchInfo(sportType, pageId) - .then(setMatchProfile) - }, [sportType, pageId]) - - return { - matchProfile: addNames(matchProfile, suffix), - } -} diff --git a/src/features/MatchPage/MatchProfileCard/index.tsx b/src/features/MatchPage/MatchProfileCard/index.tsx index 22575c6d..6cd983f4 100644 --- a/src/features/MatchPage/MatchProfileCard/index.tsx +++ b/src/features/MatchPage/MatchProfileCard/index.tsx @@ -11,8 +11,8 @@ import { SportName } from 'features/Common/SportName' import { useScoreStore } from 'features/ToggleScore' import { useSportNameParam } from 'hooks/useSportNameParam' -import { useMatchProfileCard } from './hooks' +import type { MatchProfile } from '../hooks/useMatchProfile' import { Wrapper, Teams, @@ -22,55 +22,55 @@ import { StyledLink, } from './styled' -export const MatchProfileCard = () => { - const { - matchProfile, - } = useMatchProfileCard() +type Props = { + profile: MatchProfile, +} +export const MatchProfileCard = ({ profile }: Props) => { const { sportName, sportType } = useSportNameParam() const { isHidden } = useScoreStore() + if (!profile) return + const color = getSportColor(sportType) const { team1, team2, tournament, - } = matchProfile || {} + } = profile return ( - {!isNull(matchProfile) + {!isNull(profile) && ( - {team1 && ( - - {team1.name} - - )} - {!isHidden - ? ( - - {team1?.score} : {team2?.score} - - ) - : } - {team2 && ( - - {team2.name} - - )} + > + {team1.name} + + { + !isHidden + ? ( + + {team1?.score} : {team2?.score} + + ) + : + } + + {team2.name} + @@ -78,16 +78,14 @@ export const MatchProfileCard = () => { color={color} t={sportName} /> - {tournament && ( - - {tournament?.name} - - )} + + {tournament?.name} + )} diff --git a/src/features/MatchPage/hooks/useMatchProfile.tsx b/src/features/MatchPage/hooks/useMatchProfile.tsx new file mode 100644 index 00000000..4c3145a4 --- /dev/null +++ b/src/features/MatchPage/hooks/useMatchProfile.tsx @@ -0,0 +1,58 @@ +import { + useEffect, + useState, + useMemo, +} from 'react' + +import type { MatchInfo } from 'requests' +import { getMatchInfo } from 'requests' + +import { useLexicsStore } from 'features/LexicsStore' + +import { useSportNameParam, usePageId } from 'hooks' + +type Name = 'name_rus' | 'name_eng' + +const addNames = (profile: MatchInfo, suffixArg: string) => ( + profile + ? ({ + ...profile, + team1: { + ...profile.team1, + name: profile.team1[`name_${suffixArg}` as Name], + }, + team2: { + ...profile.team2, + name: profile.team2[`name_${suffixArg}` as Name], + }, + tournament: { + ...profile.tournament, + name: profile.tournament[`name_${suffixArg}` as Name], + }, + }) + : null +) + +export type MatchProfile = ReturnType + +export const useMatchProfile = () => { + const [rawMatchProfile, setMatchProfile] = useState(null) + const { sportType } = useSportNameParam() + const matchId = usePageId() + const { suffix } = useLexicsStore() + + useEffect(() => { + getMatchInfo(sportType, matchId).then(setMatchProfile) + }, + [ + sportType, + matchId, + ]) + + const matchProfile = useMemo( + () => addNames(rawMatchProfile, suffix), + [rawMatchProfile, suffix], + ) + + return matchProfile +} diff --git a/src/features/MatchPage/hooks.tsx b/src/features/MatchPage/hooks/usePlayerProgressReporter.tsx similarity index 72% rename from src/features/MatchPage/hooks.tsx rename to src/features/MatchPage/hooks/usePlayerProgressReporter.tsx index e721993b..fae19c75 100644 --- a/src/features/MatchPage/hooks.tsx +++ b/src/features/MatchPage/hooks/usePlayerProgressReporter.tsx @@ -13,18 +13,21 @@ const reportRequestInterval = 30000 export const usePlayerProgressReporter = () => { const { sportType } = useSportNameParam() const matchId = usePageId() - const secondsRef = useRef(0) + const playerData = useRef({ period: 0, seconds: 0 }) const intervalCallback = () => { + const { period, seconds } = playerData.current reportPlayerProgress({ + half: period, matchId, - seconds: secondsRef.current, + seconds, sport: sportType, }) } const { start, stop } = useInterval({ callback: intervalCallback, intervalDuration: reportRequestInterval, + startImmediate: false, }) const onPlayingChange = useCallback((playing: boolean) => { @@ -35,8 +38,8 @@ export const usePlayerProgressReporter = () => { } }, [start, stop]) - const onPlayerProgressChange = useCallback((seconds: number) => { - secondsRef.current = seconds + const onPlayerProgressChange = useCallback((seconds: number, period = 0) => { + playerData.current = { period, seconds } }, []) return { onPlayerProgressChange, onPlayingChange } diff --git a/src/features/MatchPage/hooks/useVideoData.tsx b/src/features/MatchPage/hooks/useVideoData.tsx new file mode 100644 index 00000000..860ee647 --- /dev/null +++ b/src/features/MatchPage/hooks/useVideoData.tsx @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react' + +import type { Videos } from 'requests' +import { getVideos } from 'requests' + +import { MatchStatuses } from 'features/HeaderFilters' + +import { useSportNameParam, usePageId } from 'hooks' + +export const useVideoData = (matchStatus?: MatchStatuses) => { + const [videos, setVideos] = useState([]) + const { sportType } = useSportNameParam() + const matchId = usePageId() + + useEffect(() => { + if (matchStatus === MatchStatuses.Finished) { + getVideos(sportType, matchId).then(setVideos) + } + }, + [ + matchStatus, + sportType, + matchId, + ]) + + return { + url: '', + videos, + } +} diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index 770972a5..3ec24104 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -1,25 +1,42 @@ import React from 'react' -import { VideoPlayer } from 'features/VideoPlayer' +import { StreamPlayer } from 'features/StreamPlayer' +import { MatchStatuses } from 'features/HeaderFilters' +import { MultiSourcePlayer } from 'features/MultiSourcePlayer' import { MatchProfileCard } from './MatchProfileCard' -import { usePlayerProgressReporter } from './hooks' +import { usePlayerProgressReporter } from './hooks/usePlayerProgressReporter' +import { useMatchProfile } from './hooks/useMatchProfile' +import { useVideoData } from './hooks/useVideoData' import { MainWrapper, Container } from './styled' -const url = 'https://bserv.instatfootball.tv/common/outhls.m3u8' - export const MatchPage = () => { + const profile = useMatchProfile() + const { url, videos } = useVideoData(profile?.stream_status) const { onPlayerProgressChange, onPlayingChange } = usePlayerProgressReporter() return ( - + - + { + profile?.stream_status === MatchStatuses.Live && ( + + ) + } + { + profile?.stream_status === MatchStatuses.Finished && ( + + ) + } ) diff --git a/src/features/MultiSourcePlayer/components/Chapters/index.tsx b/src/features/MultiSourcePlayer/components/Chapters/index.tsx new file mode 100644 index 00000000..5a5ea594 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/Chapters/index.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +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/Chapters/styled.tsx b/src/features/MultiSourcePlayer/components/Chapters/styled.tsx new file mode 100644 index 00000000..54060a58 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/Chapters/styled.tsx @@ -0,0 +1,18 @@ +import styled from 'styled-components/macro' + +export const ChapterList = styled.div` + width: 100%; + height: 100%; + display: flex; +` + +export const ChapterContainer = styled.div` + position: relative; + height: 100%; + background-color: rgba(255, 255, 255, 0.3); + cursor: pointer; + + :not(:last-child) { + margin-right: 3px; + } +` diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterProgress/__tests__/index.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterProgress/__tests__/index.tsx new file mode 100644 index 00000000..e98e169e --- /dev/null +++ b/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterProgress/__tests__/index.tsx @@ -0,0 +1,36 @@ +import { calculateChapterProgress } from '..' + +it('calculates zero chapter progress for zero progress or less than chapter start ms', () => { + const chapter = { + duration: 10000, + endMs: 15000, + period: '', + startMs: 5000, + url: '', + } + expect(calculateChapterProgress(0, chapter)).toBe(0) + expect(calculateChapterProgress(4999, chapter)).toBe(0) +}) + +it('calculates half chapter progress', () => { + const chapter = { + duration: 10000, + endMs: 10000, + period: '', + startMs: 0, + url: '', + } + expect(calculateChapterProgress(5000, chapter)).toBe(50) +}) + +it('calculates full chapter progress for full progress or more than chapter end ms', () => { + const chapter = { + duration: 10000, + endMs: 10000, + period: '', + startMs: 0, + url: '', + } + expect(calculateChapterProgress(10000, chapter)).toBe(100) + expect(calculateChapterProgress(99999, chapter)).toBe(100) +}) diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterProgress/index.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterProgress/index.tsx new file mode 100644 index 00000000..8142cea7 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterProgress/index.tsx @@ -0,0 +1,8 @@ +import type { Chapter } from 'features/MultiSourcePlayer/types' + +export const calculateChapterProgress = (progress: number, chapter: Chapter) => { + if (progress <= chapter.startMs) return 0 + if (progress >= chapter.endMs) return 100 + const progressInChapter = progress - chapter.startMs + return progressInChapter * 100 / chapter.duration +} diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/__tests__/index.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/__tests__/index.tsx new file mode 100644 index 00000000..8ddc8bfb --- /dev/null +++ b/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/__tests__/index.tsx @@ -0,0 +1,43 @@ +import { calculateChapterStyles } from '..' + +const videoDuration = 60000 + +it('return correct progress and width lengthes', () => { + let chapter = { + duration: 15000, + endMs: 20000, + period: '', + startMs: 5000, + } + let expected = { + ...chapter, + loaded: 100, + played: 100, + width: 25, + } + expect(calculateChapterStyles({ + chapters: [chapter], + loadedProgress: 30000, + playedProgress: 30000, + videoDuration, + })).toEqual([expected]) + + chapter = { + duration: 30000, + endMs: 30000, + period: '', + startMs: 0, + } + expected = { + ...chapter, + loaded: 50, + played: 50, + width: 50, + } + expect(calculateChapterStyles({ + chapters: [chapter], + loadedProgress: 15000, + playedProgress: 15000, + videoDuration, + })).toEqual([expected]) +}) diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/index.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/index.tsx new file mode 100644 index 00000000..488273a8 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/ProgressBar/helpers/calculateChapterStyles/index.tsx @@ -0,0 +1,26 @@ +import map from 'lodash/map' + +import type { Chapters } from 'features/MultiSourcePlayer/types' + +import { calculateChapterProgress } from '../calculateChapterProgress' + +type Args = { + chapters: Chapters, + loadedProgress: number, + playedProgress: number, + videoDuration: number, +} + +export const calculateChapterStyles = ({ + chapters, + loadedProgress, + playedProgress, + videoDuration, +}: Args) => ( + map(chapters, (chapter) => ({ + ...chapter, + loaded: calculateChapterProgress(loadedProgress, chapter), + played: calculateChapterProgress(playedProgress, chapter), + width: chapter.duration * 100 / videoDuration, + })) +) diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/hooks.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/hooks.tsx new file mode 100644 index 00000000..25350bd6 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/ProgressBar/hooks.tsx @@ -0,0 +1,38 @@ +import { useMemo } from 'react' + +import type { Chapters } from '../../types' +import { calculateChapterStyles } from './helpers/calculateChapterStyles' + +export type Props = { + chapters?: Chapters, + duration: number, + loadedProgress: number, + onPlayedProgressChange: (progress: number) => void, + playedProgress: number, +} + +export const useProgressBar = ({ + chapters = [], + duration, + loadedProgress, + playedProgress, +}: Props) => { + const calculatedChapters = useMemo( + () => calculateChapterStyles({ + chapters, + loadedProgress, + playedProgress, + videoDuration: duration, + }), + [ + loadedProgress, + playedProgress, + duration, + chapters, + ], + ) + return { + calculatedChapters, + playedProgressInPercent: playedProgress * 100 / duration, + } +} diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/index.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/index.tsx new file mode 100644 index 00000000..cac206b9 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/ProgressBar/index.tsx @@ -0,0 +1,27 @@ +import React from 'react' + +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 { Chapters } from '../Chapters' +import type { Props } from './hooks' +import { useProgressBar } from './hooks' +import { ProgressBarList } from './styled' + +export const ProgressBar = (props: Props) => { + const { onPlayedProgressChange, playedProgress } = props + const progressBarRef = useSlider({ onChange: onPlayedProgressChange }) + const { calculatedChapters, playedProgressInPercent } = useProgressBar(props) + + return ( + + + + + + + ) +} diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/stories.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/stories.tsx new file mode 100644 index 00000000..c57c4f06 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/ProgressBar/stories.tsx @@ -0,0 +1,111 @@ +import type { ReactElement } from 'react' +import React 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 '.' + +export default { + component: ProgressBar, + title: 'ProgressBarWithChapters', +} + +const Wrapper = styled.div` + position: relative; + width: 95vw; + height: 50vh; + left: 50%; + transform: translateX(-50%); + background-color: #000; +` + +const callback = () => {} + +const renderInControls = (progressBarElement: ReactElement) => ( + + + + + {progressBarElement} + + + +) + +const duration = 70000 + +const chapters = [ + { + duration: 30000, + endMs: 30000, + period: 0, + startMs: 0, + url: '', + }, + { + duration: 30000, + endMs: 60000, + period: 0, + startMs: 30000, + url: '', + }, + { + duration: 10000, + endMs: 70000, + period: 0, + startMs: 60000, + url: '', + }, +] + +export const Empty = () => renderInControls( + , +) + +export const HalfLoaded = () => renderInControls( + , +) + +export const HalfPlayed = () => renderInControls( + , +) + +export const Loaded40AndPlayed20 = () => renderInControls( + , +) diff --git a/src/features/MultiSourcePlayer/components/ProgressBar/styled.tsx b/src/features/MultiSourcePlayer/components/ProgressBar/styled.tsx new file mode 100644 index 00000000..8c959a19 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/ProgressBar/styled.tsx @@ -0,0 +1,7 @@ +import styled from 'styled-components/macro' + +export const ProgressBarList = styled.div` + flex-grow: 1; + height: 4px; + position: relative; +` diff --git a/src/features/MultiSourcePlayer/helpers/index.tsx b/src/features/MultiSourcePlayer/helpers/index.tsx new file mode 100644 index 00000000..638dfc48 --- /dev/null +++ b/src/features/MultiSourcePlayer/helpers/index.tsx @@ -0,0 +1,24 @@ +import { RefObject } from 'react' + +import findIndex from 'lodash/findIndex' + +import type { Chapters, Chapter } from '../types' + +export const preparePlayer = (videoRef: RefObject, { url }: Chapter) => { + const video = videoRef?.current + if (!video) return + // eslint-disable-next-line no-param-reassign + video.src = url + video.load() +} + +export const startPlayer = (videoRef: RefObject, chapter: Chapter) => { + preparePlayer(videoRef, chapter) + videoRef.current?.play() +} + +export const findChapterByProgress = (chapters: Chapters, progress: number) => ( + findIndex(chapters, ({ endMs, startMs }) => ( + startMs / 1000 <= progress && progress <= endMs / 1000 + )) +) diff --git a/src/features/MultiSourcePlayer/hooks/index.tsx b/src/features/MultiSourcePlayer/hooks/index.tsx new file mode 100644 index 00000000..c9a67bc4 --- /dev/null +++ b/src/features/MultiSourcePlayer/hooks/index.tsx @@ -0,0 +1,154 @@ +import type { MouseEvent } from 'react' +import { + useEffect, + useState, + useRef, +} from 'react' + +import size from 'lodash/size' +import isEmpty from 'lodash/isEmpty' + +import type { Videos } from 'requests' + +import { useEventListener } from 'hooks' +import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen' + +import { useProgressChangeHandler } from './useProgressChangeHandler' +import { useLoadedEvent, usePlayedEvent } from './useProgressEvents' +import { usePlayingState } from './usePlayingState' +import { useDuration } from './useDuration' +import { useVolume } from './useVolume' +import { useVideos } from './useVideos' +import { + preparePlayer, + startPlayer, +} from '../helpers' + +export type Props = { + onError?: () => void, + onPlayingChange: (playing: boolean) => void, + onProgressChange: (seconds: number, period: number) => void, + videos: Videos, +} + +export const useMultiSourcePlayer = ({ + onError = () => {}, + videos, + onPlayingChange, + onProgressChange: onProgressChangeCallback, +}: Props) => { + const activeChapterIndex = useRef(0) + const wrapperRef = useRef(null) + const videoRef = useRef(null) + const [loadedProgress, setLoadedProgress] = useState(0) + const [playedProgress, setPlayedProgress] = useState(0) + const { + playing, + ready, + togglePlaying, + } = usePlayingState(videoRef) + const { chapters } = useVideos(videos) + const duration = useDuration(chapters) + + const onProgressChange = useProgressChangeHandler({ + activeChapterIndex, + chapters, + duration, + playing, + setPlayedProgress, + videoRef, + }) + + const playNextChapter = () => { + activeChapterIndex.current += 1 + const isLastChapterPlayed = activeChapterIndex.current === size(chapters) + if (isLastChapterPlayed) { + activeChapterIndex.current = 0 + preparePlayer(videoRef, chapters[activeChapterIndex.current]) + togglePlaying() + } else { + startPlayer(videoRef, chapters[activeChapterIndex.current]) + } + } + + const onPlayerClick = (e: MouseEvent) => { + if (e.target === videoRef.current) { + togglePlaying() + } + } + + const getActiveChapterStart = () => ( + chapters[activeChapterIndex.current]?.startMs || 0 + ) + + const onLoadedChange = (loadedMs: number) => { + const chapterStart = getActiveChapterStart() + setLoadedProgress(chapterStart + loadedMs) + } + + const onPlayedChange = (playedMs: number) => { + const chapterStart = getActiveChapterStart() + setPlayedProgress(chapterStart + playedMs) + } + + useEffect(() => { + if (isEmpty(chapters)) return + + startPlayer(videoRef, chapters[activeChapterIndex.current]) + togglePlaying() + }, [chapters, togglePlaying]) + + useLoadedEvent({ + onChange: onLoadedChange, + videoRef, + }) + + usePlayedEvent({ + onChange: onPlayedChange, + videoRef, + }) + + useEventListener({ + callback: playNextChapter, + event: 'ended', + ref: videoRef, + }) + + useEventListener({ + callback: onError, + event: 'error', + ref: videoRef, + }) + + useEffect(() => { + onPlayingChange(playing) + }, [playing, onPlayingChange]) + + useEffect(() => { + if (!ready) return + + const progressSeconds = playedProgress / 1000 + const { period } = chapters[activeChapterIndex.current] + onProgressChangeCallback(progressSeconds, Number(period)) + }, [ + ready, + playedProgress, + chapters, + onProgressChangeCallback, + ]) + + return { + chapters, + duration, + loadedProgress, + onPlayerClick, + onProgressChange, + playedProgress, + playing, + togglePlaying, + videoRef, + wrapperRef, + ...useFullscreen(wrapperRef), + ...useVolume(videoRef), + } +} diff --git a/src/features/MultiSourcePlayer/hooks/useDuration.tsx b/src/features/MultiSourcePlayer/hooks/useDuration.tsx new file mode 100644 index 00000000..ae416a0e --- /dev/null +++ b/src/features/MultiSourcePlayer/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/MultiSourcePlayer/hooks/usePlayingState.tsx b/src/features/MultiSourcePlayer/hooks/usePlayingState.tsx new file mode 100644 index 00000000..f079b126 --- /dev/null +++ b/src/features/MultiSourcePlayer/hooks/usePlayingState.tsx @@ -0,0 +1,42 @@ +import type { RefObject } from 'react' +import { useState, useCallback } from 'react' + +import once from 'lodash/once' + +import { useEventListener } from 'hooks' + +export const usePlayingState = (videoRef: RefObject) => { + const [playing, setPlaying] = useState(false) + const [ready, setReady] = useState(false) + + const togglePlaying = useCallback(() => { + setPlaying((isPlaying) => { + const video = videoRef.current + if (!ready || !video) return false + + const nextIsPlaying = !isPlaying + if (nextIsPlaying) { + video.play() + } else { + video.pause() + } + return nextIsPlaying + }) + }, [videoRef, ready]) + + const onReady = useCallback(once(() => { + setReady(true) + }), []) + + useEventListener({ + callback: onReady, + event: 'canplay', + ref: videoRef, + }) + + return { + playing, + ready, + togglePlaying, + } +} diff --git a/src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx b/src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx new file mode 100644 index 00000000..2208a46c --- /dev/null +++ b/src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx @@ -0,0 +1,76 @@ +import type { MutableRefObject, RefObject } from 'react' +import { + useCallback, +} from 'react' + +import throttle from 'lodash/throttle' + +import type { Chapters } from '../types' +import { + findChapterByProgress, + preparePlayer, + startPlayer, +} from '../helpers' + +type Args = { + activeChapterIndex: MutableRefObject, + chapters: Chapters, + duration: number, + playing: boolean, + setPlayedProgress: (value: number) => void, + videoRef: RefObject, +} + +export const useProgressChangeHandler = ({ + activeChapterIndex, + chapters, + duration, + playing, + setPlayedProgress, + videoRef, +}: Args) => { + const setPlayerProgress = useCallback( + throttle((value: number) => { + const video = videoRef.current + if (!video) return + video.currentTime = value + }, 100), + [], + ) + + const onProgressChange = useCallback((progress: number) => { + // значение новой позиции ползунка в миллисекундах + const progressMs = progress * duration + const chapterIndex = findChapterByProgress(chapters, progressMs / 1000) + const chapter = chapters[chapterIndex] + const isProgressOnDifferentChapter = ( + chapterIndex !== -1 + && chapterIndex !== activeChapterIndex.current + ) + // если ползунок остановили на другой главе + if (isProgressOnDifferentChapter) { + // если проигрывали видео то продолжаем проигрывание, + // если нет то только загружаем видео + const continueOrPreparePlayer = playing ? startPlayer : preparePlayer + continueOrPreparePlayer(videoRef, chapter) + // eslint-disable-next-line no-param-reassign + activeChapterIndex.current = chapterIndex + } + setPlayedProgress(progressMs) + + // отнимаем начало главы на котором остановились от общего прогресса + // чтобы получить прогресс текущей главы + const chapterProgressSec = (progressMs - chapter.startMs) / 1000 + setPlayerProgress(chapterProgressSec) + }, [ + playing, + chapters, + duration, + setPlayedProgress, + setPlayerProgress, + activeChapterIndex, + videoRef, + ]) + + return onProgressChange +} diff --git a/src/features/MultiSourcePlayer/hooks/useProgressEvents.tsx b/src/features/MultiSourcePlayer/hooks/useProgressEvents.tsx new file mode 100644 index 00000000..f3ca7993 --- /dev/null +++ b/src/features/MultiSourcePlayer/hooks/useProgressEvents.tsx @@ -0,0 +1,68 @@ +import type { RefObject } from 'react' + +import size from 'lodash/size' + +import { useEventListener } from 'hooks' + +/** + * Возвращает значение отрезка буфера которое ближе + * к проигрываемому времени + */ +const getLoadedSeconds = (video: HTMLVideoElement | null) => { + const bufferLength = size(video?.buffered) + if (!video || bufferLength === 0) return 0 + + for (let i = 0; i < bufferLength; i++) { + const bufferPosition = bufferLength - 1 - i + if (video.buffered.start(bufferPosition) < video.currentTime) { + return video.buffered.end(bufferPosition) + } + } + return 0 +} + +type Args = { + onChange: (millis: number) => void, + videoRef: RefObject, +} + +/** + * Подписывается на прогресс буферизации и вызывает колбек + * со значением прогресса в милисекундах + */ +export const useLoadedEvent = ({ + onChange, + videoRef, +}: Args) => { + const listenLoadedProgress = () => { + const loadedSeconds = getLoadedSeconds(videoRef.current) + onChange(loadedSeconds * 1000) + } + + useEventListener({ + callback: listenLoadedProgress, + event: 'progress', + ref: videoRef, + }) +} + +/** + * Подписывается на прогресс проигывания и вызывает колбек + * со значением прогресса в милисекундах + */ +export const usePlayedEvent = ({ + onChange, + videoRef, +}: Args) => { + const listenPlayedProgresses = () => { + const video = videoRef.current + if (!video) return + onChange(video.currentTime * 1000) + } + + useEventListener({ + callback: listenPlayedProgresses, + event: 'timeupdate', + ref: videoRef, + }) +} diff --git a/src/features/MultiSourcePlayer/hooks/useVideos.tsx b/src/features/MultiSourcePlayer/hooks/useVideos.tsx new file mode 100644 index 00000000..a59d08d5 --- /dev/null +++ b/src/features/MultiSourcePlayer/hooks/useVideos.tsx @@ -0,0 +1,62 @@ +import { useMemo } from 'react' + +import map from 'lodash/map' +import last from 'lodash/last' +import uniq from 'lodash/uniq' +import maxBy from 'lodash/maxBy' +import filter from 'lodash/filter' +import reduce from 'lodash/reduce' +import concat from 'lodash/concat' +import orderBy from 'lodash/orderBy' + +import type { Videos } from 'requests' + +import type { Chapters } from '../types' + +const getUniquePeriods = (videos: Videos) => uniq(map(videos, 'period')) + +const getHighestQualityVideo = (videos: Videos, period: number) => { + const videosWithSamePeriod = filter(videos, { period }) + return maxBy(videosWithSamePeriod, ({ quality }) => Number(quality)) +} + +const getHighQualityVideos = (videos: Videos, periods: Array) => ( + reduce( + periods, + (acc: Videos, period) => { + const video = getHighestQualityVideo(videos, period) + return video ? concat(acc, video) : acc + }, + [], + ) +) + +const getChapters = (videos: Videos) => { + const sortedVideos = orderBy(videos, 'period') + return reduce( + sortedVideos, + (acc: Chapters, video) => { + const prevVideoEndMs = last(acc)?.endMs || 0 + const nextChapter = { + duration: video.duration, + endMs: prevVideoEndMs + video.duration, + period: video.period, + startMs: prevVideoEndMs, + url: video.url, + } + return concat(acc, nextChapter) + }, + [], + ) +} + +const buildChapters = (videos: Videos) => { + const periods = getUniquePeriods(videos) + const highQualityVideos = getHighQualityVideos(videos, periods) + return getChapters(highQualityVideos) +} + +export const useVideos = (videos: Videos) => { + const chapters = useMemo(() => buildChapters(videos), [videos]) + return { chapters } +} diff --git a/src/features/MultiSourcePlayer/hooks/useVolume.tsx b/src/features/MultiSourcePlayer/hooks/useVolume.tsx new file mode 100644 index 00000000..96e6788f --- /dev/null +++ b/src/features/MultiSourcePlayer/hooks/useVolume.tsx @@ -0,0 +1,50 @@ +import type { RefObject } from 'react' +import { useState, useCallback } from 'react' + +import { useToggle } from 'hooks' + +const useVolumeState = (videoRef: RefObject) => { + const [volume, setVolume] = useState(0.5) + const setVideoVolume = useCallback((value: number) => { + if (videoRef.current) { + // eslint-disable-next-line no-param-reassign + videoRef.current.volume = value + } + setVolume(value) + }, [videoRef]) + return [volume, setVideoVolume] as const +} + +export const useVolume = (videoRef: RefObject) => { + const { + close: unmute, + isOpen: muted, + open: mute, + toggle: toggleMuted, + } = useToggle(true) + const [volume, setVolume] = useVolumeState(videoRef) + + const onVolumeChange = (value: number) => { + if (value === 0) { + mute() + } else { + unmute() + } + setVolume(value) + } + + const onVolumeClick = () => { + if (muted && volume === 0) { + setVolume(0.1) + } + toggleMuted() + } + + return { + muted, + onVolumeChange, + onVolumeClick, + volume, + volumeInPercent: volume * 100, + } +} diff --git a/src/features/MultiSourcePlayer/index.tsx b/src/features/MultiSourcePlayer/index.tsx new file mode 100644 index 00000000..a684882b --- /dev/null +++ b/src/features/MultiSourcePlayer/index.tsx @@ -0,0 +1,64 @@ +import React from 'react' + +import { VolumeBar } from 'features/StreamPlayer/components/VolumeBar' +import { + PlayerWrapper, + Controls, + PlayStop, + Fullscreen, +} from 'features/StreamPlayer/styled' + +import { ProgressBar } from './components/ProgressBar' +import type { Props } from './hooks' +import { useMultiSourcePlayer } from './hooks' +import { Video } from './styled' + +export const MultiSourcePlayer = (props: Props) => { + const { + chapters, + duration, + isFullscreen, + loadedProgress, + muted, + onFullscreenClick, + onPlayerClick, + onProgressChange, + onVolumeChange, + onVolumeClick, + playedProgress, + playing, + togglePlaying, + videoRef, + volumeInPercent, + wrapperRef, + } = useMultiSourcePlayer(props) + return ( + + + ) +} diff --git a/src/features/MultiSourcePlayer/styled.tsx b/src/features/MultiSourcePlayer/styled.tsx new file mode 100644 index 00000000..f35c8ac2 --- /dev/null +++ b/src/features/MultiSourcePlayer/styled.tsx @@ -0,0 +1,9 @@ +import styled from 'styled-components/macro' + +export const Video = styled.video` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +` diff --git a/src/features/MultiSourcePlayer/types.tsx b/src/features/MultiSourcePlayer/types.tsx new file mode 100644 index 00000000..eb54f486 --- /dev/null +++ b/src/features/MultiSourcePlayer/types.tsx @@ -0,0 +1,9 @@ +export type Chapter = { + duration: number, + endMs: number, + period: number, + startMs: number, + url: string, +} + +export type Chapters = Array diff --git a/src/features/VideoPlayer/components/ProgressBar/index.tsx b/src/features/StreamPlayer/components/ProgressBar/index.tsx similarity index 51% rename from src/features/VideoPlayer/components/ProgressBar/index.tsx rename to src/features/StreamPlayer/components/ProgressBar/index.tsx index 1bc9e6c7..8e5c7193 100644 --- a/src/features/VideoPlayer/components/ProgressBar/index.tsx +++ b/src/features/StreamPlayer/components/ProgressBar/index.tsx @@ -2,9 +2,9 @@ import React from 'react' import { secondsToHms } from 'helpers' -import { useSlider } from 'features/VideoPlayer/hooks/useSlider' +import { useSlider } from 'features/StreamPlayer/hooks/useSlider' +import { TimeTooltip } from 'features/StreamPlayer/components/TimeTooltip' -import { TimeTooltip } from '../TimeTooltip' import { ProgressBarList, LoadedProgress, @@ -13,26 +13,28 @@ import { } from './styled' type Props = { + duration: number, loadedProgress: number, onPlayedProgressChange: (progress: number) => void, playedProgress: number, - playedSeconds: number, } export const ProgressBar = ({ + duration, loadedProgress, onPlayedProgressChange, playedProgress, - playedSeconds, }: Props) => { const progressBarRef = useSlider({ onChange: onPlayedProgressChange }) + const loadedFraction = loadedProgress * 100 / duration + const playedFraction = playedProgress * 100 / duration return ( - - - - + + + + ) diff --git a/src/features/StreamPlayer/components/ProgressBar/stories.tsx b/src/features/StreamPlayer/components/ProgressBar/stories.tsx new file mode 100644 index 00000000..b1f66b7a --- /dev/null +++ b/src/features/StreamPlayer/components/ProgressBar/stories.tsx @@ -0,0 +1,83 @@ +import type { ReactElement } from 'react' +import React 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 '.' + +export default { + component: ProgressBar, + title: 'ProgressBar', +} + +const Wrapper = styled.div` + position: relative; + width: 95vw; + height: 50vh; + left: 50%; + transform: translateX(-50%); + background-color: #000; +` + +const callback = () => {} + +const renderInControls = (progressBarElement: ReactElement) => ( + + + + + {progressBarElement} + + + +) + +const duration = 70000 + +export const Empty = () => renderInControls( + , +) + +export const HalfLoaded = () => renderInControls( + , +) + +export const HalfPlayed = () => renderInControls( + , +) + +export const Loaded40AndPlayed20 = () => renderInControls( + , +) diff --git a/src/features/VideoPlayer/components/ProgressBar/styled.tsx b/src/features/StreamPlayer/components/ProgressBar/styled.tsx similarity index 66% rename from src/features/VideoPlayer/components/ProgressBar/styled.tsx rename to src/features/StreamPlayer/components/ProgressBar/styled.tsx index d43ab47c..cb919940 100644 --- a/src/features/VideoPlayer/components/ProgressBar/styled.tsx +++ b/src/features/StreamPlayer/components/ProgressBar/styled.tsx @@ -8,32 +8,25 @@ export const ProgressBarList = styled.div` cursor: pointer; ` -type ProgressProps = { - value: number, -} - -export const LoadedProgress = styled.div` +export const LoadedProgress = styled.div` position: absolute; z-index: 1; background-color: rgba(255, 255, 255, 0.6);; height: 100%; - width: ${({ value }) => value}%; ` -export const PlayedProgress = styled.div` +export const PlayedProgress = styled.div` position: absolute; z-index: 2; background-color: #F2C94C; height: 100%; - width: ${({ value }) => value}%; ` -export const Scrubber = styled.button` +export const Scrubber = styled.button` border: none; outline: none; position: absolute; top: 0; - left: ${({ value }) => value}%; transform: translate(-50%, -38%); z-index: 3; width: 18px; diff --git a/src/features/VideoPlayer/components/TimeTooltip/index.tsx b/src/features/StreamPlayer/components/TimeTooltip/index.tsx similarity index 100% rename from src/features/VideoPlayer/components/TimeTooltip/index.tsx rename to src/features/StreamPlayer/components/TimeTooltip/index.tsx diff --git a/src/features/VideoPlayer/components/TimeTooltip/styled.tsx b/src/features/StreamPlayer/components/TimeTooltip/styled.tsx similarity index 100% rename from src/features/VideoPlayer/components/TimeTooltip/styled.tsx rename to src/features/StreamPlayer/components/TimeTooltip/styled.tsx diff --git a/src/features/VideoPlayer/components/VolumeBar/index.tsx b/src/features/StreamPlayer/components/VolumeBar/index.tsx similarity index 90% rename from src/features/VideoPlayer/components/VolumeBar/index.tsx rename to src/features/StreamPlayer/components/VolumeBar/index.tsx index 51d2806a..7196544c 100644 --- a/src/features/VideoPlayer/components/VolumeBar/index.tsx +++ b/src/features/StreamPlayer/components/VolumeBar/index.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { useSlider } from 'features/VideoPlayer/hooks/useSlider' +import { useSlider } from 'features/StreamPlayer/hooks/useSlider' import { Wrapper, diff --git a/src/features/VideoPlayer/components/VolumeBar/styled.tsx b/src/features/StreamPlayer/components/VolumeBar/styled.tsx similarity index 100% rename from src/features/VideoPlayer/components/VolumeBar/styled.tsx rename to src/features/StreamPlayer/components/VolumeBar/styled.tsx diff --git a/src/features/VideoPlayer/config.tsx b/src/features/StreamPlayer/config.tsx similarity index 69% rename from src/features/VideoPlayer/config.tsx rename to src/features/StreamPlayer/config.tsx index 490efda1..a415d6b9 100644 --- a/src/features/VideoPlayer/config.tsx +++ b/src/features/StreamPlayer/config.tsx @@ -1,4 +1,6 @@ -export const playerConfig = { +import type { Config } from 'react-player' + +export const streamConfig: Config = { file: { forceHLS: true, hlsOptions: { diff --git a/src/features/VideoPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx similarity index 78% rename from src/features/VideoPlayer/hooks/index.tsx rename to src/features/StreamPlayer/hooks/index.tsx index ffcb7489..fb9365d9 100644 --- a/src/features/VideoPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -25,6 +25,8 @@ export type Props = { url: string, } +const toMilliSeconds = (seconds: number) => seconds * 1000 + export const useVideoPlayer = ({ onPlayingChange, onProgressChange: progressChangeCallback, @@ -34,7 +36,6 @@ export const useVideoPlayer = ({ const [duration, setDuration] = useState(0) const [loadedProgress, setLoadedProgress] = useState(0) const [playedProgress, setPlayedProgress] = useState(0) - const [playedSeconds, setPlayedSeconds] = useState(0) const wrapperRef = useRef(null) const playerRef = useRef(null) @@ -59,41 +60,44 @@ export const useVideoPlayer = ({ const setPlayerProgress = useCallback( throttle((value: number) => { - playerRef.current?.seekTo(value, 'fraction') + playerRef.current?.seekTo(value, 'seconds') }, 100), [], ) + const onDuration = (durationSeconds: number) => { + setDuration(toMilliSeconds(durationSeconds)) + } + const onProgressChange = useCallback((progress: number) => { - setPlayedProgress(progress * 100) - setPlayerProgress(progress) + const progressSeconds = progress * duration + setPlayedProgress(toMilliSeconds(progressSeconds)) + setPlayerProgress(progressSeconds) }, [ + duration, setPlayedProgress, setPlayerProgress, ]) const setProgress = ({ - loaded, - played, - playedSeconds: seconds, + loadedSeconds, + playedSeconds, }: ProgressState) => { - setLoadedProgress(loaded * 100) - setPlayedProgress(played * 100) - setPlayedSeconds(seconds) + setLoadedProgress(toMilliSeconds(loadedSeconds)) + setPlayedProgress(toMilliSeconds(playedSeconds)) - progressChangeCallback(seconds) + progressChangeCallback(playedSeconds) } return { duration, loadedProgress, + onDuration, onPlayerClick, onProgressChange, playedProgress, - playedSeconds, playerRef, playing, - setDuration, setProgress, setReady, startPlaying, diff --git a/src/features/VideoPlayer/hooks/useFullscreen.tsx b/src/features/StreamPlayer/hooks/useFullscreen.tsx similarity index 100% rename from src/features/VideoPlayer/hooks/useFullscreen.tsx rename to src/features/StreamPlayer/hooks/useFullscreen.tsx diff --git a/src/features/VideoPlayer/hooks/useSlider.tsx b/src/features/StreamPlayer/hooks/useSlider.tsx similarity index 100% rename from src/features/VideoPlayer/hooks/useSlider.tsx rename to src/features/StreamPlayer/hooks/useSlider.tsx diff --git a/src/features/VideoPlayer/hooks/useVolume.tsx b/src/features/StreamPlayer/hooks/useVolume.tsx similarity index 96% rename from src/features/VideoPlayer/hooks/useVolume.tsx rename to src/features/StreamPlayer/hooks/useVolume.tsx index 9d3802bf..b29a9db6 100644 --- a/src/features/VideoPlayer/hooks/useVolume.tsx +++ b/src/features/StreamPlayer/hooks/useVolume.tsx @@ -8,7 +8,7 @@ export const useVolume = () => { isOpen: muted, open: mute, toggle: toggleMuted, - } = useToggle() + } = useToggle(true) const [volume, setVolume] = useState(0.5) const onVolumeChange = (value: number) => { diff --git a/src/features/VideoPlayer/index.tsx b/src/features/StreamPlayer/index.tsx similarity index 86% rename from src/features/VideoPlayer/index.tsx rename to src/features/StreamPlayer/index.tsx index 0931d33e..f6c98d2d 100644 --- a/src/features/VideoPlayer/index.tsx +++ b/src/features/StreamPlayer/index.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { playerConfig, progressCallbackInterval } from './config' +import { streamConfig, progressCallbackInterval } from './config' import type { Props } from './hooks' import { useVideoPlayer } from './hooks' import { ProgressBar } from './components/ProgressBar' @@ -13,22 +13,22 @@ import { Fullscreen, } from './styled' -export const VideoPlayer = (props: Props) => { +export const StreamPlayer = (props: Props) => { const { url } = props const { + duration, isFullscreen, loadedProgress, muted, + onDuration, onFullscreenClick, onPlayerClick, onProgressChange, onVolumeChange, onVolumeClick, playedProgress, - playedSeconds, playerRef, playing, - setDuration, setProgress, startPlaying, togglePlaying, @@ -52,10 +52,10 @@ export const VideoPlayer = (props: Props) => { playing={playing} volume={volume} muted={muted} - config={playerConfig} + config={streamConfig} progressInterval={progressCallbackInterval} onProgress={setProgress} - onDuration={setDuration} + onDuration={onDuration} onEnded={togglePlaying} onReady={startPlaying} /> @@ -68,8 +68,8 @@ export const VideoPlayer = (props: Props) => { onClick={onVolumeClick} /> diff --git a/src/features/VideoPlayer/styled.tsx b/src/features/StreamPlayer/styled.tsx similarity index 100% rename from src/features/VideoPlayer/styled.tsx rename to src/features/StreamPlayer/styled.tsx diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index 379dcba7..095acf5d 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -4,3 +4,4 @@ export * from './useRequest' export * from './useSportNameParam' export * from './useStorage' export * from './useInterval' +export * from './useEventListener' diff --git a/src/hooks/useEventListener.tsx b/src/hooks/useEventListener.tsx new file mode 100644 index 00000000..cb79baa5 --- /dev/null +++ b/src/hooks/useEventListener.tsx @@ -0,0 +1,39 @@ +import type { RefObject } from 'react' +import { useEffect, useRef } from 'react' + +type Args = { + callback: (e: Event) => void, + element?: HTMLElement, + event: E, + ref?: RefObject, +} + +/** + * Хук для подписки и отписки на события + * на html element, window и react ref + */ +export const useEventListener = ({ + callback, + element, + event, + ref, +}: Args) => { + const callbackRef = useRef(callback) + + useEffect(() => { + callbackRef.current = callback + }, [callback]) + + useEffect(() => { + const htmlElement = ref?.current || element || window + if (!htmlElement) return undefined + + const listener = (e: Event) => { + callbackRef.current(e) + } + htmlElement.addEventListener(event, listener) + return () => { + htmlElement.removeEventListener(event, listener) + } + }, [event, ref, element]) +} diff --git a/src/requests/getMatches.tsx b/src/requests/getMatches.tsx index 32bd498d..f4dfd31e 100644 --- a/src/requests/getMatches.tsx +++ b/src/requests/getMatches.tsx @@ -36,6 +36,7 @@ export type Content = { type Match = { date: string, + has_video: boolean, id: number, round_id: number | null, stream_status: MatchStatuses, diff --git a/src/requests/getVideos.tsx b/src/requests/getVideos.tsx new file mode 100644 index 00000000..e503737d --- /dev/null +++ b/src/requests/getVideos.tsx @@ -0,0 +1,31 @@ +import { API_ROOT, SportTypes } from 'config' +import { callApi } from 'helpers' + +type Video = { + duration: number, + match_id: number, + name: string, + period: number, + quality: string, + start_ms: number, + url: string, +} + +export type Videos = Array