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.
 
 
 
 
spa_instat_tv/src/features/MultiSourcePlayer/hooks/index.tsx

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(),
}
}