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 <mirlan.maksitaliev@instatsport.com> * 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 <mirlan.maksitaliev@instatsport.com> * 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 <mirlan.maksitaliev@instatsport.com> * 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 <mirlan.maksitaliev@instatsport.com> * 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 <mirlan.maksitaliev@instatsport.com> Co-authored-by: mirlan.maksitaliev <mirlan.maksitaliev@instatsport.com>keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
parent
264a7acab7
commit
194d09e009
@ -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<MatchInfo>(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), |
|
||||||
} |
|
||||||
} |
|
||||||
@ -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<typeof useMatchProfile> |
||||||
|
|
||||||
|
export const useMatchProfile = () => { |
||||||
|
const [rawMatchProfile, setMatchProfile] = useState<MatchInfo>(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 |
||||||
|
} |
||||||
@ -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<Videos>([]) |
||||||
|
const { sportType } = useSportNameParam() |
||||||
|
const matchId = usePageId() |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (matchStatus === MatchStatuses.Finished) { |
||||||
|
getVideos(sportType, matchId).then(setVideos) |
||||||
|
} |
||||||
|
}, |
||||||
|
[ |
||||||
|
matchStatus, |
||||||
|
sportType, |
||||||
|
matchId, |
||||||
|
]) |
||||||
|
|
||||||
|
return { |
||||||
|
url: '', |
||||||
|
videos, |
||||||
|
} |
||||||
|
} |
||||||
@ -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<ChapterWithStyles>, |
||||||
|
} |
||||||
|
|
||||||
|
export const Chapters = ({ chapters }: Props) => ( |
||||||
|
<ChapterList> |
||||||
|
{ |
||||||
|
map( |
||||||
|
chapters, |
||||||
|
({ |
||||||
|
loaded, |
||||||
|
played, |
||||||
|
width, |
||||||
|
}, index) => ( |
||||||
|
<ChapterContainer key={index} style={{ width: `${width}%` }}> |
||||||
|
<LoadedProgress style={{ width: `${loaded}%` }} /> |
||||||
|
<PlayedProgress style={{ width: `${played}%` }} /> |
||||||
|
</ChapterContainer> |
||||||
|
), |
||||||
|
) |
||||||
|
} |
||||||
|
</ChapterList> |
||||||
|
) |
||||||
@ -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; |
||||||
|
} |
||||||
|
` |
||||||
@ -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) |
||||||
|
}) |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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]) |
||||||
|
}) |
||||||
@ -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, |
||||||
|
})) |
||||||
|
) |
||||||
@ -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, |
||||||
|
} |
||||||
|
} |
||||||
@ -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 ( |
||||||
|
<ProgressBarList ref={progressBarRef}> |
||||||
|
<Chapters chapters={calculatedChapters} /> |
||||||
|
<Scrubber style={{ left: `${playedProgressInPercent}%` }}> |
||||||
|
<TimeTooltip time={secondsToHms(playedProgress / 1000)} /> |
||||||
|
</Scrubber> |
||||||
|
</ProgressBarList> |
||||||
|
) |
||||||
|
} |
||||||
@ -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) => ( |
||||||
|
<Wrapper> |
||||||
|
<Controls> |
||||||
|
<PlayStop onClick={callback} playing={false} /> |
||||||
|
<VolumeBar |
||||||
|
value={50} |
||||||
|
muted={false} |
||||||
|
onChange={callback} |
||||||
|
onClick={callback} |
||||||
|
/> |
||||||
|
{progressBarElement} |
||||||
|
<Fullscreen onClick={callback} isFullscreen={false} /> |
||||||
|
</Controls> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
|
||||||
|
const duration = 70000 |
||||||
|
|
||||||
|
const chapters = [ |
||||||
|
{ |
||||||
|
duration: 30000, |
||||||
|
endMs: 30000, |
||||||
|
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( |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
chapters={chapters} |
||||||
|
onPlayedProgressChange={callback} |
||||||
|
playedProgress={0} |
||||||
|
loadedProgress={0} |
||||||
|
/>, |
||||||
|
) |
||||||
|
|
||||||
|
export const HalfLoaded = () => renderInControls( |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
chapters={chapters} |
||||||
|
onPlayedProgressChange={callback} |
||||||
|
playedProgress={0} |
||||||
|
loadedProgress={30000} |
||||||
|
/>, |
||||||
|
) |
||||||
|
|
||||||
|
export const HalfPlayed = () => renderInControls( |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
chapters={chapters} |
||||||
|
onPlayedProgressChange={callback} |
||||||
|
playedProgress={30000} |
||||||
|
loadedProgress={0} |
||||||
|
/>, |
||||||
|
) |
||||||
|
|
||||||
|
export const Loaded40AndPlayed20 = () => renderInControls( |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
chapters={chapters} |
||||||
|
onPlayedProgressChange={callback} |
||||||
|
playedProgress={20000} |
||||||
|
loadedProgress={40000} |
||||||
|
/>, |
||||||
|
) |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
export const ProgressBarList = styled.div` |
||||||
|
flex-grow: 1; |
||||||
|
height: 4px; |
||||||
|
position: relative; |
||||||
|
` |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
import { RefObject } from 'react' |
||||||
|
|
||||||
|
import findIndex from 'lodash/findIndex' |
||||||
|
|
||||||
|
import type { Chapters, Chapter } from '../types' |
||||||
|
|
||||||
|
export const preparePlayer = (videoRef: RefObject<HTMLVideoElement>, { 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<HTMLVideoElement>, 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 |
||||||
|
)) |
||||||
|
) |
||||||
@ -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<HTMLDivElement>(null) |
||||||
|
const videoRef = useRef<HTMLVideoElement>(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<HTMLDivElement>) => { |
||||||
|
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), |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
import { useMemo } from 'react' |
||||||
|
|
||||||
|
import sumBy from 'lodash/sumBy' |
||||||
|
|
||||||
|
import type { Chapters } from '../types' |
||||||
|
|
||||||
|
export const useDuration = (chapters: Chapters) => ( |
||||||
|
useMemo(() => sumBy(chapters, 'duration'), [chapters]) |
||||||
|
) |
||||||
@ -0,0 +1,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<HTMLVideoElement>) => { |
||||||
|
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, |
||||||
|
} |
||||||
|
} |
||||||
@ -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<number>, |
||||||
|
chapters: Chapters, |
||||||
|
duration: number, |
||||||
|
playing: boolean, |
||||||
|
setPlayedProgress: (value: number) => void, |
||||||
|
videoRef: RefObject<HTMLVideoElement>, |
||||||
|
} |
||||||
|
|
||||||
|
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 |
||||||
|
} |
||||||
@ -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<HTMLVideoElement>, |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Подписывается на прогресс буферизации и вызывает колбек |
||||||
|
* со значением прогресса в милисекундах |
||||||
|
*/ |
||||||
|
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, |
||||||
|
}) |
||||||
|
} |
||||||
@ -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<number>) => ( |
||||||
|
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 } |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
import type { RefObject } from 'react' |
||||||
|
import { useState, useCallback } from 'react' |
||||||
|
|
||||||
|
import { useToggle } from 'hooks' |
||||||
|
|
||||||
|
const useVolumeState = (videoRef: RefObject<HTMLVideoElement>) => { |
||||||
|
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<HTMLVideoElement>) => { |
||||||
|
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, |
||||||
|
} |
||||||
|
} |
||||||
@ -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 ( |
||||||
|
<PlayerWrapper |
||||||
|
ref={wrapperRef} |
||||||
|
playing={playing} |
||||||
|
onClick={onPlayerClick} |
||||||
|
> |
||||||
|
<Video |
||||||
|
muted={muted} |
||||||
|
ref={videoRef} |
||||||
|
/> |
||||||
|
<Controls> |
||||||
|
<PlayStop onClick={togglePlaying} playing={playing} /> |
||||||
|
<VolumeBar |
||||||
|
value={volumeInPercent} |
||||||
|
muted={muted} |
||||||
|
onChange={onVolumeChange} |
||||||
|
onClick={onVolumeClick} |
||||||
|
/> |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
chapters={chapters} |
||||||
|
onPlayedProgressChange={onProgressChange} |
||||||
|
playedProgress={playedProgress} |
||||||
|
loadedProgress={loadedProgress} |
||||||
|
/> |
||||||
|
<Fullscreen onClick={onFullscreenClick} isFullscreen={isFullscreen} /> |
||||||
|
</Controls> |
||||||
|
</PlayerWrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -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%; |
||||||
|
` |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
export type Chapter = { |
||||||
|
duration: number, |
||||||
|
endMs: number, |
||||||
|
period: number, |
||||||
|
startMs: number, |
||||||
|
url: string, |
||||||
|
} |
||||||
|
|
||||||
|
export type Chapters = Array<Chapter> |
||||||
@ -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) => ( |
||||||
|
<Wrapper> |
||||||
|
<Controls> |
||||||
|
<PlayStop onClick={callback} playing={false} /> |
||||||
|
<VolumeBar |
||||||
|
value={50} |
||||||
|
muted={false} |
||||||
|
onChange={callback} |
||||||
|
onClick={callback} |
||||||
|
/> |
||||||
|
{progressBarElement} |
||||||
|
<Fullscreen onClick={callback} isFullscreen={false} /> |
||||||
|
</Controls> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
|
||||||
|
const duration = 70000 |
||||||
|
|
||||||
|
export const Empty = () => renderInControls( |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
onPlayedProgressChange={callback} |
||||||
|
playedProgress={0} |
||||||
|
loadedProgress={0} |
||||||
|
/>, |
||||||
|
) |
||||||
|
|
||||||
|
export const HalfLoaded = () => renderInControls( |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
onPlayedProgressChange={callback} |
||||||
|
playedProgress={0} |
||||||
|
loadedProgress={30000} |
||||||
|
/>, |
||||||
|
) |
||||||
|
|
||||||
|
export const HalfPlayed = () => renderInControls( |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
onPlayedProgressChange={callback} |
||||||
|
playedProgress={30000} |
||||||
|
loadedProgress={0} |
||||||
|
/>, |
||||||
|
) |
||||||
|
|
||||||
|
export const Loaded40AndPlayed20 = () => renderInControls( |
||||||
|
<ProgressBar |
||||||
|
duration={duration} |
||||||
|
onPlayedProgressChange={callback} |
||||||
|
playedProgress={20000} |
||||||
|
loadedProgress={40000} |
||||||
|
/>, |
||||||
|
) |
||||||
@ -1,6 +1,6 @@ |
|||||||
import React from 'react' |
import React from 'react' |
||||||
|
|
||||||
import { useSlider } from 'features/VideoPlayer/hooks/useSlider' |
import { useSlider } from 'features/StreamPlayer/hooks/useSlider' |
||||||
|
|
||||||
import { |
import { |
||||||
Wrapper, |
Wrapper, |
||||||
@ -1,4 +1,6 @@ |
|||||||
export const playerConfig = { |
import type { Config } from 'react-player' |
||||||
|
|
||||||
|
export const streamConfig: Config = { |
||||||
file: { |
file: { |
||||||
forceHLS: true, |
forceHLS: true, |
||||||
hlsOptions: { |
hlsOptions: { |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
import type { RefObject } from 'react' |
||||||
|
import { useEffect, useRef } from 'react' |
||||||
|
|
||||||
|
type Args<E> = { |
||||||
|
callback: (e: Event) => void, |
||||||
|
element?: HTMLElement, |
||||||
|
event: E, |
||||||
|
ref?: RefObject<HTMLElement>, |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Хук для подписки и отписки на события |
||||||
|
* на html element, window и react ref |
||||||
|
*/ |
||||||
|
export const useEventListener = <E extends keyof HTMLElementEventMap>({ |
||||||
|
callback, |
||||||
|
element, |
||||||
|
event, |
||||||
|
ref, |
||||||
|
}: Args<E>) => { |
||||||
|
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]) |
||||||
|
} |
||||||
@ -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<Video> |
||||||
|
|
||||||
|
export const getVideos = ( |
||||||
|
sportType: SportTypes, |
||||||
|
matchId: number, |
||||||
|
): Promise<Videos> => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
match_id: matchId, |
||||||
|
sport_id: sportType, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: `${API_ROOT}/videoapi`, |
||||||
|
}) |
||||||
|
} |
||||||
Loading…
Reference in new issue