You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
8.5 KiB
347 lines
8.5 KiB
import type { MouseEvent } from 'react'
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
} from 'react'
|
|
|
|
import size from 'lodash/size'
|
|
|
|
import type { TSetCircleAnimation } from 'features/CircleAnimationBar'
|
|
import { useControlsVisibility } from 'features/StreamPlayer/hooks/useControlsVisibility'
|
|
import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen'
|
|
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
|
|
import { useNoNetworkPopupStore } from 'features/NoNetworkPopup'
|
|
import { useMatchPageStore } from 'features/MatchPage/store'
|
|
|
|
import {
|
|
useEventListener,
|
|
useInterval,
|
|
useObjectState,
|
|
usePageParams,
|
|
} from 'hooks'
|
|
|
|
import {
|
|
MatchInfo,
|
|
saveMatchStats,
|
|
VIEW_INTERVAL_MS,
|
|
} from 'requests'
|
|
|
|
import { useProgressChangeHandler } from './useProgressChangeHandler'
|
|
import { usePlayingHandlers } from './usePlayingHandlers'
|
|
import { useVideoQuality } from './useVideoQuality'
|
|
import { useDuration } from './useDuration'
|
|
import type { Chapters } from '../types'
|
|
import { Players } from '../types'
|
|
import { getNextPlayer } from '../helpers'
|
|
import { REWIND_SECONDS } from '../config'
|
|
|
|
const initialState = {
|
|
activeChapterIndex: 0,
|
|
activePlayer: Players.PLAYER1,
|
|
loadedProgress: 0,
|
|
playedProgress: 0,
|
|
playing: false,
|
|
ready: false,
|
|
seek: {
|
|
[Players.PLAYER1]: 0,
|
|
[Players.PLAYER2]: 0,
|
|
},
|
|
seeking: false,
|
|
}
|
|
|
|
export type PlayerState = typeof initialState
|
|
|
|
export type Props = {
|
|
chapters: Chapters,
|
|
isOpenPopup?: boolean,
|
|
onError?: () => void,
|
|
onPlayingChange: (playing: boolean) => void,
|
|
profile: MatchInfo,
|
|
setCircleAnimation: TSetCircleAnimation,
|
|
}
|
|
|
|
export const useMultiSourcePlayer = ({
|
|
chapters,
|
|
onError,
|
|
onPlayingChange,
|
|
setCircleAnimation,
|
|
}: Props) => {
|
|
const {
|
|
isPlayFilterEpisodes,
|
|
playNextEpisode,
|
|
} = useMatchPageStore()
|
|
|
|
const { profileId, sportType } = usePageParams()
|
|
|
|
/** время для сохранения статистики просмотра матча */
|
|
const timeForStatistics = useRef(0)
|
|
|
|
const numberOfChapters = size(chapters)
|
|
const [
|
|
{
|
|
activeChapterIndex: chapterIndex,
|
|
activePlayer,
|
|
loadedProgress,
|
|
playedProgress,
|
|
playing,
|
|
ready,
|
|
seek,
|
|
seeking,
|
|
},
|
|
setPlayerState,
|
|
] = useObjectState(initialState)
|
|
const video1Ref = useRef<HTMLVideoElement>(null)
|
|
const video2Ref = useRef<HTMLVideoElement>(null)
|
|
const activeChapterIndex = numberOfChapters >= chapterIndex ? chapterIndex : 0
|
|
const {
|
|
onReady,
|
|
playNextChapter,
|
|
playPrevChapter,
|
|
stopPlaying,
|
|
togglePlaying,
|
|
} = usePlayingHandlers(setPlayerState, numberOfChapters)
|
|
|
|
const {
|
|
selectedQuality,
|
|
setSelectedQuality,
|
|
videoQualities,
|
|
} = useVideoQuality(chapters)
|
|
|
|
const duration = useDuration(chapters)
|
|
|
|
const handleError = useCallback(() => {
|
|
onError?.()
|
|
}, [onError])
|
|
|
|
const getActiveChapter = useCallback(
|
|
(index: number = activeChapterIndex) => chapters[index],
|
|
[chapters, activeChapterIndex],
|
|
)
|
|
|
|
const videoRef = [video1Ref, video2Ref][activePlayer]
|
|
const isFirstChapterPlaying = activeChapterIndex === 0
|
|
const isLastChapterPlaying = activeChapterIndex === numberOfChapters - 1
|
|
const setPlayerTime = (progressMs: number) => {
|
|
if (!videoRef.current) return
|
|
videoRef.current.currentTime = progressMs / 1000
|
|
}
|
|
|
|
const rewindForward = () => {
|
|
const chapter = getActiveChapter()
|
|
const newProgress = playedProgress + REWIND_SECONDS * 1000
|
|
if (newProgress <= chapter.duration) {
|
|
setPlayerTime(chapter.startOffsetMs + newProgress)
|
|
} else if (isLastChapterPlaying) {
|
|
playNextChapter()
|
|
} else {
|
|
const nextChapter = getActiveChapter(activeChapterIndex + 1)
|
|
const fromMs = newProgress - chapter.duration
|
|
playNextChapter(fromMs, nextChapter.startOffsetMs)
|
|
}
|
|
}
|
|
|
|
const rewindBackward = () => {
|
|
const chapter = getActiveChapter()
|
|
const newProgress = playedProgress - REWIND_SECONDS * 1000
|
|
if (newProgress >= 0) {
|
|
setPlayerTime(chapter.startOffsetMs + newProgress)
|
|
} else if (isFirstChapterPlaying) {
|
|
setPlayerTime(chapter.startOffsetMs)
|
|
} else {
|
|
const prevChapter = getActiveChapter(activeChapterIndex - 1)
|
|
const fromMs = prevChapter.duration + newProgress
|
|
playPrevChapter(fromMs, prevChapter.startOffsetMs)
|
|
}
|
|
}
|
|
|
|
const onProgressChange = useProgressChangeHandler({
|
|
chapters,
|
|
duration,
|
|
setPlayerState,
|
|
})
|
|
|
|
const {
|
|
isFullscreen,
|
|
onFullscreenClick,
|
|
wrapperRef,
|
|
} = useFullscreen(videoRef)
|
|
|
|
const getChapterUrl = useCallback((index: number, quality: string = selectedQuality) => (
|
|
getActiveChapter(index)?.urls[quality]
|
|
), [selectedQuality, getActiveChapter])
|
|
|
|
const onQualitySelect = (quality: string) => {
|
|
setPlayerState((state) => ({
|
|
seek: {
|
|
...state.seek,
|
|
[state.activePlayer]: videoRef.current?.currentTime ?? 0,
|
|
},
|
|
}))
|
|
setSelectedQuality(quality)
|
|
}
|
|
|
|
const onPlayerClick = (e: MouseEvent<HTMLDivElement>) => {
|
|
if (e.target === videoRef.current) {
|
|
togglePlaying()
|
|
}
|
|
}
|
|
|
|
const onLoadedProgress = (loadedMs: number) => {
|
|
const chapter = getActiveChapter()
|
|
const value = loadedMs - chapter.startOffsetMs
|
|
|
|
setPlayerState({ loadedProgress: value })
|
|
}
|
|
|
|
const onPlayedProgress = (playedMs: number) => {
|
|
const chapter = getActiveChapter()
|
|
const value = Math.max(playedMs - chapter.startOffsetMs, 0)
|
|
|
|
timeForStatistics.current = (value + chapter.startMs) / 1000
|
|
|
|
setPlayerState({ playedProgress: value })
|
|
}
|
|
|
|
const onEnded = () => {
|
|
playNextChapter()
|
|
}
|
|
const onPause = () => {
|
|
setPlayerState({ playing: false })
|
|
}
|
|
|
|
useEffect(() => {
|
|
onPlayingChange(playing)
|
|
}, [playing, onPlayingChange])
|
|
|
|
useEffect(() => {
|
|
setCircleAnimation((state) => ({
|
|
...state,
|
|
playedProgress,
|
|
playing,
|
|
ready,
|
|
}))
|
|
}, [
|
|
playedProgress,
|
|
playing,
|
|
ready,
|
|
setCircleAnimation,
|
|
])
|
|
|
|
useEffect(() => {
|
|
setPlayerState((state) => ({
|
|
...initialState,
|
|
activePlayer: getNextPlayer(state.activePlayer),
|
|
playing: true,
|
|
}))
|
|
}, [chapters, setPlayerState])
|
|
|
|
useEffect(() => {
|
|
const { duration: chapterDuration } = getActiveChapter()
|
|
if (playedProgress >= chapterDuration && !seeking && isPlayFilterEpisodes) {
|
|
setPlayerState({ playedProgress: 0 })
|
|
playNextEpisode()
|
|
}
|
|
if (playedProgress >= chapterDuration && !seeking && !isPlayFilterEpisodes) {
|
|
playNextChapter()
|
|
}
|
|
}, [
|
|
isPlayFilterEpisodes,
|
|
playNextEpisode,
|
|
getActiveChapter,
|
|
playedProgress,
|
|
seeking,
|
|
playNextChapter,
|
|
setPlayerState,
|
|
])
|
|
|
|
useEventListener({
|
|
callback: (e: KeyboardEvent) => {
|
|
if (e.code === 'ArrowLeft') rewindBackward()
|
|
else if (e.code === 'ArrowRight') rewindForward()
|
|
},
|
|
event: 'keydown',
|
|
})
|
|
|
|
const { isOnline } = useNoNetworkPopupStore()
|
|
|
|
useEffect(() => {
|
|
if (!isOnline) {
|
|
stopPlaying()
|
|
}
|
|
}, [
|
|
isOnline,
|
|
stopPlaying,
|
|
])
|
|
|
|
useEffect(() => {
|
|
if (ready && videoRef) {
|
|
videoRef.current?.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start',
|
|
})
|
|
}
|
|
}, [ready, videoRef])
|
|
|
|
// ведем статистику просмотра матча
|
|
const { start: startCollectingStats, stop: stopCollectingStats } = useInterval({
|
|
callback: () => {
|
|
saveMatchStats({
|
|
matchId: profileId,
|
|
matchSecond: timeForStatistics.current,
|
|
sportType,
|
|
})
|
|
},
|
|
intervalDuration: VIEW_INTERVAL_MS,
|
|
startImmediate: false,
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (playing) {
|
|
startCollectingStats()
|
|
} else {
|
|
stopCollectingStats()
|
|
}
|
|
}, [playing, startCollectingStats, stopCollectingStats])
|
|
|
|
return {
|
|
activeChapterIndex,
|
|
activePlayer,
|
|
activeSrc: getChapterUrl(activeChapterIndex),
|
|
allPlayedProgress: playedProgress + getActiveChapter().startMs,
|
|
chapters,
|
|
duration,
|
|
isFirstChapterPlaying,
|
|
isFullscreen,
|
|
isLastChapterPlaying,
|
|
loadedProgress,
|
|
nextSrc: getChapterUrl((activeChapterIndex + 1) % numberOfChapters),
|
|
numberOfChapters,
|
|
onEnded,
|
|
onError: handleError,
|
|
onFullscreenClick,
|
|
onLoadedProgress,
|
|
onPause,
|
|
onPlayedProgress,
|
|
onPlayerClick,
|
|
onProgressChange,
|
|
onQualitySelect,
|
|
onReady,
|
|
playNextChapter,
|
|
playPrevChapter,
|
|
playedProgress,
|
|
playing,
|
|
ready,
|
|
rewindBackward,
|
|
rewindForward,
|
|
seek,
|
|
selectedQuality,
|
|
togglePlaying,
|
|
video1Ref,
|
|
video2Ref,
|
|
videoQualities,
|
|
wrapperRef,
|
|
...useControlsVisibility(isFullscreen, playing),
|
|
...useVolume(),
|
|
}
|
|
}
|
|
|