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/StreamPlayer/hooks/index.tsx

363 lines
8.9 KiB

import type { MouseEvent } from 'react'
import {
useCallback,
useEffect,
useState,
} from 'react'
import size from 'lodash/size'
import isNumber from 'lodash/isNumber'
import isEmpty from 'lodash/isEmpty'
import { isIOS } from 'config/userAgent'
import { useObjectState } from 'hooks/useObjectState'
import { useEventListener } from 'hooks/useEventListener'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
import { useNoNetworkPopupStore } from 'features/NoNetworkPopup'
import { useLiveMatch } from 'features/MatchPage/components/LiveMatch/hooks'
import type { Chapters } from 'features/StreamPlayer/types'
import { MatchInfo } from 'requests/getMatchInfo'
import { REWIND_SECONDS } from '../config'
import { useHlsPlayer } from './useHlsPlayer'
import { useFullscreen } from './useFullscreen'
import { useVideoQuality } from './useVideoQuality'
import { useControlsVisibility } from './useControlsVisibility'
import { useProgressChangeHandler } from './useProgressChangeHandler'
import { usePlayingHandlers } from './usePlayingHandlers'
import { useDuration } from './useDuration'
export type PlayerState = typeof initialState
const toMilliSeconds = (seconds: number) => seconds * 1000
const initialState = {
activeChapterIndex: 0,
buffering: true,
chapters: [] as Chapters,
duration: 0,
loadedProgress: 0,
playedProgress: 0,
playing: false,
ready: false,
seek: 0,
seeking: false,
}
export type Props = {
chapters: Chapters,
isLive?: boolean,
onDurationChange?: (duration: number) => void,
onPlayingChange: (playing: boolean) => void,
onProgressChange: (seconds: number) => void,
profile?: MatchInfo,
resumeFrom?: number,
url?: string,
}
export const useVideoPlayer = ({
chapters: chaptersProps,
isLive,
onDurationChange,
onPlayingChange,
onProgressChange: progressChangeCallback,
resumeFrom,
}: Props) => {
const [{
activeChapterIndex,
buffering,
chapters,
duration: fullMatchDuration,
loadedProgress,
playedProgress,
playing,
ready,
seek,
seeking,
}, setPlayerState] = useObjectState({ ...initialState, chapters: chaptersProps })
const { onPlaylistSelect, selectedPlaylist } = useLiveMatch()
const { url } = chapters[0] ?? { url: '' }
const numberOfChapters = size(chapters)
const { hls, videoRef } = useHlsPlayer(url, resumeFrom)
const [isLiveTime, setIsLiveTime] = useState(false)
const chaptersDuration = useDuration(chapters)
const duration = (isLive && chapters[0]?.isFullMatchChapter)
? fullMatchDuration
: chaptersDuration
const {
onReady,
playNextChapter,
playPrevChapter,
stopPlaying,
togglePlaying,
} = usePlayingHandlers(setPlayerState, chapters)
const getActiveChapter = useCallback(
(index: number = activeChapterIndex) => chapters[index],
[chapters, activeChapterIndex],
)
const {
isFullscreen,
onFullscreenClick,
wrapperRef,
} = useFullscreen()
const [sizeOptions, setSizeOptions] = useState({
height: wrapperRef.current?.clientHeight,
width: wrapperRef.current?.clientWidth,
})
const isFirstChapterPlaying = activeChapterIndex === 0
const isLastChapterPlaying = activeChapterIndex === numberOfChapters - 1
const seekTo = useCallback((progressMs: number) => {
if (!videoRef.current) return
videoRef.current.currentTime = progressMs / 1000
}, [videoRef])
const rewindForward = () => {
const chapter = getActiveChapter()
const newProgress = playedProgress + REWIND_SECONDS * 1000
if (newProgress <= chapter.duration || isLive) {
seekTo(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) {
seekTo(chapter.startOffsetMs + newProgress)
} else if (isFirstChapterPlaying) {
seekTo(chapter.startOffsetMs)
} else {
const prevChapter = getActiveChapter(activeChapterIndex - 1)
const fromMs = prevChapter.duration + newProgress
playPrevChapter(fromMs, prevChapter.startOffsetMs)
}
}
const onError = useCallback(() => {
setPlayerState({ playing: false })
}, [setPlayerState])
const onPlayerClick = (e: MouseEvent<HTMLDivElement>) => {
if (e.target === videoRef.current) {
togglePlaying()
}
}
const onWaiting = () => {
setPlayerState({ buffering: true })
}
const onPlaying = () => {
setPlayerState({ buffering: false })
}
const onPause = () => {
setPlayerState({ playing: false })
}
const onPlay = () => {
setPlayerState({ playing: true })
}
const onDuration = (durationSeconds: number) => {
setPlayerState({ duration: toMilliSeconds(durationSeconds) })
onDurationChange?.(durationSeconds)
}
const onProgressChange = useProgressChangeHandler({
chapters,
duration,
setPlayerState,
})
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)
setPlayerState({ playedProgress: value })
progressChangeCallback(value / 1000)
}
const backToLive = useCallback(() => {
if (!duration) return
if (selectedPlaylist?.id !== 'full_game') {
onPlaylistSelect({
duration: 0,
episodes: [],
id: 'full_game',
lexic: 13028,
type: 0,
})
setIsLiveTime(true)
}
const liveProgressMs = Math.max(duration - 30000, 0)
setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 })
if (liveProgressMs > 0) setIsLiveTime(false)
}, [
duration,
onPlaylistSelect,
selectedPlaylist,
setPlayerState,
])
useEffect(() => {
if (duration && isLiveTime && chapters[0]?.isFullMatchChapter) {
backToLive()
}
// eslint-disable-next-line
}, [duration, isLiveTime])
useEventListener({
callback: (e: KeyboardEvent) => {
if (e.code === 'ArrowLeft') rewindBackward()
else if (e.code === 'ArrowRight') rewindForward()
},
event: 'keydown',
})
useEffect(() => {
if (isNumber(seek)) {
setPlayerState({ seek: undefined })
}
}, [seek, setPlayerState])
useEffect(() => {
onPlayingChange(playing)
if (playing) {
setPlayerState({ buffering: false })
}
// eslint-disable-next-line
}, [playing, onPlayingChange])
useEffect(() => {
setPlayerState({
...initialState,
chapters: chaptersProps,
playing: true,
seek: chaptersProps[0].startOffsetMs / 1000,
})
}, [
chapters,
chaptersProps,
isLive,
setPlayerState,
])
useEffect(() => {
if ((isLive && chapters[0]?.isFullMatchChapter) || isEmpty(chapters)) return
const { duration: chapterDuration } = getActiveChapter()
if (playedProgress >= chapterDuration && !seeking) {
playNextChapter()
}
}, [
isLive,
chapters,
getActiveChapter,
onPlaylistSelect,
playedProgress,
seeking,
playNextChapter,
])
const { isOnline } = useNoNetworkPopupStore()
useEffect(() => {
if (wrapperRef.current) {
setSizeOptions({
height: wrapperRef.current?.clientHeight,
width: wrapperRef.current?.clientWidth,
})
}
}, [wrapperRef])
useEffect(() => {
if (!isOnline) {
stopPlaying()
}
}, [
isOnline,
stopPlaying,
])
useEffect(() => {
if (!navigator.serviceWorker || !isIOS) return undefined
const listener = (event: MessageEvent) => {
setPlayerState({ duration: toMilliSeconds(event.data.duration) })
}
navigator.serviceWorker.addEventListener('message', listener)
return () => {
navigator.serviceWorker.removeEventListener('message', listener)
}
}, [setPlayerState])
return {
activeChapterIndex,
allPlayedProgress: playedProgress + getActiveChapter().startMs,
backToLive,
buffering,
chapters,
duration,
isFirstChapterPlaying,
isFullscreen,
isLastChapterPlaying,
loadedProgress,
numberOfChapters,
onDuration,
onError,
onFullscreenClick,
onLoadedProgress,
onPause,
onPlay,
onPlayedProgress,
onPlayerClick,
onPlaying,
onProgressChange,
onReady,
onWaiting,
playNextChapter,
playPrevChapter,
playedProgress,
playing,
ready,
rewindBackward,
rewindForward,
seek,
sizeOptions,
togglePlaying,
url,
videoRef,
wrapperRef,
...useControlsVisibility(isFullscreen),
...useVolume(),
...useVideoQuality(hls),
}
}