import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import './Controls.css'; import { formatTime } from '../../utils'; import { LIVE, VOD, VOD_LOADING_TIMEOUT } from '../../constants'; import BackToLiveBtn from './BackToLiveBtn'; import Backward60SVG from '../../assets/icons/backward-60'; import Forward60SVG from '../../assets/icons/forward-60'; import PauseSVG from '../../assets/icons/pause'; import PlaySVG from '../../assets/icons/play'; import useControls from '../../contexts/Controls/useControls'; import usePlayback from '../../contexts/Playback/usePlayback'; import usePrevious from '../../hooks/usePrevious'; import useFirstMountState from '../../hooks/useFirstMountState'; import useSeekBar, { playerControlSeekBarWrapperId } from '../../hooks/useSeekBar'; const Controls = ({ isLive }) => { const { stopPropagAndResetTimeout } = useControls(); const { activePlayer, bufferPercent, currProgress, getVodDuration, getVodPosition, isLiveAvailable, isVodAvailable, recordingStartTime, resetVOD, seekVodToPos, setActivePlayerType, vodPlayerInstance } = usePlayback(); const prevProgress = usePrevious(currProgress); const { error, isLoading, isPaused, pause, play, type: activePlayerType } = activePlayer; const { isMouseDown, onPointerDownHandler, scrubRef, seekBarRef, updateProgress, timeSinceLive, timeSinceLiveRef } = useSeekBar(); const hasError = !!error; const isLiveAvailablePrev = usePrevious(isLiveAvailable); const isBackwardsDisabled = hasError || !isVodAvailable || currProgress === 0; const isForwardsDisabled = hasError || isLive; const isSeekBarDisabled = hasError; const isFirstMount = useFirstMountState(); const prevLivePosition = useRef(null); const [backToLiveStartProgress, setBackToLiveStartProgress] = useState(null); const [backToLivePosDiff, setBackToLivePosDiff] = useState(null); const isBackToLiveTransition = backToLiveStartProgress !== null && backToLivePosDiff !== null; const goBackwards = useCallback( (event) => { stopPropagAndResetTimeout(event); if (!isBackwardsDisabled) { const currentVODDuration = getVodDuration(); const currentVODPosition = getVodPosition(); const nextPosition = Math.max(0, currentVODPosition - 60); // position in seconds const nextProgress = (nextPosition / currentVODDuration) * 100; updateProgress(nextProgress); } }, [ getVodDuration, getVodPosition, isBackwardsDisabled, stopPropagAndResetTimeout, updateProgress ] ); const goForwards = useCallback( (event) => { stopPropagAndResetTimeout(event); if (!isForwardsDisabled) { const currentVODDuration = getVodDuration(); const currentVODPosition = getVodPosition(); let nextPosition = Math.min( currentVODPosition + 60, currentVODDuration ); // position in seconds let nextProgress = (nextPosition / currentVODDuration) * 100; updateProgress(nextProgress); } }, [ getVodDuration, getVodPosition, isForwardsDisabled, stopPropagAndResetTimeout, updateProgress ] ); const onKeyDownHandler = useCallback( (event) => { if (event.key === 'ArrowRight' && !event.repeat) { goForwards(); } else if (event.key === 'ArrowLeft' && !event.repeat) { goBackwards(); } }, [goBackwards, goForwards] ); const onPointerDownPlayPauseHandler = useCallback( (event) => { if (hasError) return; stopPropagAndResetTimeout(event); const currentVODDuration = getVodDuration() || 0; if (isPaused) { if ( activePlayer.type === LIVE && prevLivePosition.current && prevLivePosition.current < currentVODDuration ) { seekVodToPos(prevLivePosition.current); setActivePlayerType(VOD); } play(); } else { const now = Date.now(); prevLivePosition.current = (now - recordingStartTime) / 1000; pause(); } }, [ activePlayer.type, getVodDuration, hasError, isPaused, pause, play, recordingStartTime, seekVodToPos, setActivePlayerType, stopPropagAndResetTimeout ] ); const backToLive = useCallback( (event) => { if (event) { stopPropagAndResetTimeout(event); } const scrubStyle = getComputedStyle(scrubRef.current); const seekbarStyle = getComputedStyle(seekBarRef.current); setBackToLivePosDiff( parseFloat(seekbarStyle.width) - parseFloat(scrubStyle.left) - 14 ); setBackToLiveStartProgress(currProgress); updateProgress(100, false); }, [ currProgress, scrubRef, seekBarRef, stopPropagAndResetTimeout, updateProgress ] ); const onTransitionEndHandler = useCallback(() => { setBackToLivePosDiff(null); setBackToLiveStartProgress(null); updateProgress(100); }, [setBackToLiveStartProgress, updateProgress]); // Set the playback position based on currProgress, defined by user input useEffect(() => { const playerType = currProgress < 100 ? VOD : LIVE; if (!isMouseDown && !isFirstMount && vodPlayerInstance) { const currentVODDuration = getVodDuration(); const currentVODPosition = getVodPosition(); const currentScrubberPosition = Math.max( 0.01, (currProgress / 100) * currentVODDuration ); const seekPosition = playerType === VOD ? currentScrubberPosition : currentVODDuration; // This condition prevents from calling the seekTo function more than necessary if (Math.abs(currentScrubberPosition - currentVODPosition) > 1) { seekVodToPos(seekPosition); } } setActivePlayerType(playerType); }, [ currProgress, getVodDuration, getVodPosition, isFirstMount, isMouseDown, seekVodToPos, setActivePlayerType, vodPlayerInstance ]); // Switch to the LIVE player if the VOD loading has stalled for longer than VOD_LOADING_TIMEOUT seconds const timeoutId = useRef(); useEffect(() => { if (activePlayerType === VOD && isLoading && !timeoutId.current) { timeoutId.current = setTimeout(backToLive, VOD_LOADING_TIMEOUT); } return () => { if (activePlayerType === LIVE || !isLoading) { clearTimeout(timeoutId.current); timeoutId.current = null; } }; }, [activePlayerType, backToLive, isLoading]); // Switch to the LIVE player if a new livestream useEffect(() => { if (isLiveAvailablePrev === false && isLiveAvailable) { backToLive(); resetVOD(); } }, [backToLive, isLiveAvailable, isLiveAvailablePrev, resetVOD]); const bufferPercentBg = prevProgress > currProgress ? currProgress : bufferPercent; const darkGrayColour = 'rgba(255, 255, 255, 0.3)'; const prevBgPercent = isBackToLiveTransition ? backToLiveStartProgress : currProgress; let seekbarBg = `linear-gradient( to right, ${!isPaused ? 'var(--color-pill-red)' : darkGrayColour} ${prevBgPercent}%, rgba(255, 255, 255, 0.5) ${prevBgPercent}% ${bufferPercentBg}%, ${darkGrayColour} ${bufferPercentBg}% )`; return ( <> {!isLive && }

-{formatTime(timeSinceLive)}

); }; Controls.propTypes = { isLive: PropTypes.bool }; Controls.defaultProps = { isLive: true }; export default Controls;