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(null) const video2Ref = useRef(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) => { 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(), } }