import type { FC } from '../../../lib/teact/teact'; import React, { useCallback, useLayoutEffect, useRef, useState, } from '../../../lib/teact/teact'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { getActions } from '../../../global'; import type { ApiMessage } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { ApiMediaFormat } from '../../../api/types'; import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions'; import { getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri } from '../../../global/helpers'; import { formatMediaDuration } from '../../../util/dateFormat'; import buildClassName from '../../../util/buildClassName'; import { stopCurrentAudio } from '../../../util/audioPlayer'; import safePlay from '../../../util/safePlay'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress'; import useShowTransition from '../../../hooks/useShowTransition'; import useMediaTransition from '../../../hooks/useMediaTransition'; import usePrevious from '../../../hooks/usePrevious'; import useFlag from '../../../hooks/useFlag'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import ProgressSpinner from '../../ui/ProgressSpinner'; import OptimizedVideo from '../../ui/OptimizedVideo'; import './RoundVideo.scss'; type OwnProps = { message: ApiMessage; observeIntersection: ObserveFn; canAutoLoad?: boolean; lastSyncTime?: number; isDownloading?: boolean; }; let stopPrevious: NoneToVoidFunction; const RoundVideo: FC = ({ message, observeIntersection, canAutoLoad, lastSyncTime, isDownloading, }) => { // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null const playingProgressRef = useRef(null); // eslint-disable-next-line no-null/no-null const playerRef = useRef(null); const video = message.content.video!; const isIntersecting = useIsIntersecting(ref, observeIntersection); const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad); const shouldLoad = Boolean(isLoadAllowed && isIntersecting && lastSyncTime); const { mediaData, loadProgress } = useMediaWithLoadProgress( getMessageMediaHash(message, 'inline'), !shouldLoad, getMessageMediaFormat(message, 'inline'), lastSyncTime, ); const { loadProgress: downloadProgress } = useMediaWithLoadProgress( getMessageMediaHash(message, 'download'), !isDownloading, ApiMediaFormat.BlobUrl, lastSyncTime, ); const [isPlayerReady, markPlayerReady] = useFlag(); const hasThumb = Boolean(getMessageMediaThumbDataUri(message)); const noThumb = !hasThumb || isPlayerReady; const thumbRef = useBlurredMediaThumbRef(message, noThumb); const thumbClassNames = useMediaTransition(!noThumb); const isTransferring = (isLoadAllowed && !isPlayerReady) || isDownloading; const wasLoadDisabled = usePrevious(isLoadAllowed) === false; const { shouldRender: shouldSpinnerRender, transitionClassNames: spinnerClassNames, } = useShowTransition(isTransferring, undefined, wasLoadDisabled); const [isActivated, setIsActivated] = useState(false); const [progress, setProgress] = useState(0); useLayoutEffect(() => { if (!isActivated) { return; } const svgCenter = ROUND_VIDEO_DIMENSIONS_PX / 2; const svgMargin = 6; const circumference = (svgCenter - svgMargin) * 2 * Math.PI; const strokeDashOffset = circumference - progress * circumference; const playerEl = playerRef.current!; const playingProgressEl = playingProgressRef.current!; const svgEl = playingProgressEl.firstElementChild; if (!svgEl) { playingProgressEl.innerHTML = ` `; } else { (svgEl.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', strokeDashOffset.toString()); } setProgress(playerEl.currentTime / playerEl.duration); }, [isActivated, progress]); const shouldPlay = Boolean(mediaData && isIntersecting); const stopPlaying = useCallback(() => { if (!playerRef.current) { return; } setIsActivated(false); setProgress(0); safePlay(playerRef.current); requestMutation(() => { playingProgressRef.current!.innerHTML = ''; }); }, []); const capturePlaying = useCallback(() => { stopPrevious?.(); stopPrevious = stopPlaying; }, [stopPlaying]); const handleClick = useCallback(() => { if (!mediaData) { setIsLoadAllowed((isAllowed) => !isAllowed); return; } if (isDownloading) { getActions().cancelMessageMediaDownload({ message }); return; } const playerEl = playerRef.current!; if (isActivated) { if (playerEl.paused) { safePlay(playerEl); stopCurrentAudio(); } else { playerEl.pause(); } } else { capturePlaying(); // Pause is a workaround for iOS Safari – otherwise it stops video after several frames playerEl.pause(); playerEl.currentTime = 0; safePlay(playerEl); stopCurrentAudio(); setIsActivated(true); } }, [capturePlaying, isActivated, isDownloading, mediaData, message]); const handleTimeUpdate = useCallback((e: React.UIEvent) => { const playerEl = e.currentTarget; setProgress(playerEl.currentTime / playerEl.duration); }, []); return (
{mediaData && (
)}
{shouldSpinnerRender && (
)} {!mediaData && !isLoadAllowed && ( )}
{isActivated ? formatMediaDuration(playerRef.current!.currentTime) : formatMediaDuration(video.duration)} {(!isActivated || playerRef.current!.paused) && }
); }; export default RoundVideo;