diff --git a/src/components/mediaViewer/SeekLine.tsx b/src/components/mediaViewer/SeekLine.tsx index 8bc900301..aa1e4a0ff 100644 --- a/src/components/mediaViewer/SeekLine.tsx +++ b/src/components/mediaViewer/SeekLine.tsx @@ -1,3 +1,4 @@ +import type { FC } from '../../lib/teact/teact'; import React, { useRef, useState, useEffect, memo, useMemo, useLayoutEffect, } from '../../lib/teact/teact'; @@ -7,13 +8,16 @@ import type { ApiDimensions } from '../../api/types'; import useLastCallback from '../../hooks/useLastCallback'; import useSignal from '../../hooks/useSignal'; -import useCurrentTimeSignal from './hooks/currentTimeSignal'; +import { useThrottledSignal } from '../../hooks/useAsyncResolvers'; +import useCurrentTimeSignal from './hooks/useCurrentTimeSignal'; +import useVideoWaitingSignal from './hooks/useVideoWaitingSignal'; import { captureEvents } from '../../util/captureEvents'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import buildClassName from '../../util/buildClassName'; import { formatMediaDuration } from '../../util/dateFormat'; import { clamp, round } from '../../util/math'; +import { animateNumber } from '../../util/animation'; import { createVideoPreviews, renderVideoPreview, getPreviewDimensions } from '../../lib/video-preview/VideoPreview'; @@ -25,7 +29,9 @@ type OwnProps = { url?: string; duration: number; bufferedRanges: BufferedRange[]; + playbackRate: number; isActive?: boolean; + isPlaying?: boolean; isPreviewDisabled?: boolean; isReady: boolean; posterSize?: ApiDimensions; @@ -33,20 +39,27 @@ type OwnProps = { onSeekStart: () => void; }; -const SeekLine: React.FC = ({ +const LOCK_TIMEOUT = 250; +let cancelAnimation: Function | undefined; + +const SeekLine: FC = ({ duration, bufferedRanges, isReady, posterSize, + playbackRate, url, isActive, + isPlaying, isPreviewDisabled, onSeek, onSeekStart, }) => { // eslint-disable-next-line no-null/no-null const seekerRef = useRef(null); - const [getCurrentTime] = useCurrentTimeSignal(); + const [getCurrentTimeSignal] = useCurrentTimeSignal(); + const [getIsWaiting] = useVideoWaitingSignal(); + const getCurrentTime = useThrottledSignal(getCurrentTimeSignal, LOCK_TIMEOUT); const [getSelectedTime, setSelectedTime] = useSignal(getCurrentTime()); const [getPreviewOffset, setPreviewOffset] = useSignal(0); const [getPreviewTime, setPreviewTime] = useSignal(0); @@ -82,10 +95,42 @@ const SeekLine: React.FC = ({ }, [isActive]); useEffect(() => { + if (cancelAnimation) cancelAnimation(); + cancelAnimation = undefined; if (!isLockedRef.current && !isSeeking) { - setSelectedTime(getCurrentTime()); + const time = getCurrentTime(); + const remaining = duration - time; + cancelAnimation = animateNumber({ + from: time, + to: duration, + duration: (remaining * 1000) / playbackRate, + onUpdate: setSelectedTime, + }); } - }, [getCurrentTime, isSeeking, setSelectedTime]); + }, [getCurrentTime, isSeeking, setSelectedTime, playbackRate, duration]); + + useEffect(() => { + if (!isPlaying || getIsWaiting()) { + if (cancelAnimation) cancelAnimation(); + cancelAnimation = undefined; + } + }, [isPlaying, getSelectedTime, getIsWaiting]); + + useEffect(() => { + if (isPlaying) { + if (cancelAnimation) cancelAnimation(); + cancelAnimation = undefined; + const time = getCurrentTime(); + const remaining = duration - time; + cancelAnimation = animateNumber({ + from: time, + to: duration, + duration: (remaining * 1000) / playbackRate, + onUpdate: setSelectedTime, + }); + } + // eslint-disable-next-line + }, [isPlaying, playbackRate, duration]); useLayoutEffect(() => { if (!progressRef.current) return; @@ -123,7 +168,13 @@ const SeekLine: React.FC = ({ return [t, o]; }; + const stopAnimation = () => { + if (cancelAnimation) cancelAnimation(); + cancelAnimation = undefined; + }; + const handleSeek = (e: MouseEvent | TouchEvent) => { + stopAnimation(); setPreviewVisible(true); ([time, offset] = getPreviewProps(e)); void setPreview(time); @@ -132,12 +183,14 @@ const SeekLine: React.FC = ({ }; const handleStartSeek = () => { + stopAnimation(); setPreviewVisible(true); setIsSeeking(true); onSeekStart(); }; const handleStopSeek = () => { + stopAnimation(); isLockedRef.current = true; setPreviewVisible(false); setIsSeeking(false); @@ -146,7 +199,7 @@ const SeekLine: React.FC = ({ // Prevent current time updates from overriding the selected time setTimeout(() => { isLockedRef.current = false; - }, 500); + }, LOCK_TIMEOUT); }; const cleanup = captureEvents(seeker, { @@ -191,6 +244,7 @@ const SeekLine: React.FC = ({ setSelectedTime, setIsSeeking, isPreviewDisabled, + playbackRate, ]); return ( @@ -217,6 +271,7 @@ const SeekLine: React.FC = ({
))} diff --git a/src/components/mediaViewer/VideoPlayer.tsx b/src/components/mediaViewer/VideoPlayer.tsx index 205b712bc..075d9263b 100644 --- a/src/components/mediaViewer/VideoPlayer.tsx +++ b/src/components/mediaViewer/VideoPlayer.tsx @@ -18,8 +18,9 @@ import usePictureInPicture from '../../hooks/usePictureInPicture'; import useShowTransition from '../../hooks/useShowTransition'; import useVideoCleanup from '../../hooks/useVideoCleanup'; import useAppLayout from '../../hooks/useAppLayout'; -import useCurrentTimeSignal from './hooks/currentTimeSignal'; +import useCurrentTimeSignal from './hooks/useCurrentTimeSignal'; import useControlsSignal from './hooks/useControlsSignal'; +import useVideoWaitingSignal from './hooks/useVideoWaitingSignal'; import Button from '../ui/Button'; import ProgressSpinner from '../ui/ProgressSpinner'; @@ -176,10 +177,12 @@ const VideoPlayer: FC = ({ useVideoCleanup(videoRef, []); const [, setCurrentTime] = useCurrentTimeSignal(); + const [, setIsVideoWaiting] = useVideoWaitingSignal(); const handleTimeUpdate = useLastCallback((e: React.SyntheticEvent) => { const video = e.currentTarget; if (video.readyState >= MIN_READY_STATE) { + setIsVideoWaiting(false); setCurrentTime(video.currentTime); } if (!isLooped && video.currentTime === video.duration) { @@ -292,6 +295,7 @@ const VideoPlayer: FC = ({ muted={isGif || isMuted} id="media-viewer-video" style={videoStyle} + onWaiting={() => setIsVideoWaiting(true)} onPlay={() => setIsPlaying(true)} onEnded={handleEnded} onClick={!isMobile && !isFullscreen ? handleClick : undefined} diff --git a/src/components/mediaViewer/VideoPlayerControls.tsx b/src/components/mediaViewer/VideoPlayerControls.tsx index e41b43dd5..edad7a292 100644 --- a/src/components/mediaViewer/VideoPlayerControls.tsx +++ b/src/components/mediaViewer/VideoPlayerControls.tsx @@ -12,7 +12,7 @@ import useFlag from '../../hooks/useFlag'; import useAppLayout from '../../hooks/useAppLayout'; import useDerivedState from '../../hooks/useDerivedState'; import useSignal from '../../hooks/useSignal'; -import useCurrentTimeSignal from './hooks/currentTimeSignal'; +import useCurrentTimeSignal from './hooks/useCurrentTimeSignal'; import useControlsSignal from './hooks/useControlsSignal'; import buildClassName from '../../util/buildClassName'; @@ -162,9 +162,11 @@ const VideoPlayerControls: FC = ({ url={url} duration={duration} isReady={isReady} + isPlaying={isPlaying} isPreviewDisabled={isPreviewDisabled} posterSize={posterSize} bufferedRanges={bufferedRanges} + playbackRate={playbackRate} onSeek={handleSeek} onSeekStart={handleSeekStart} isActive={isVisible} diff --git a/src/components/mediaViewer/hooks/currentTimeSignal.ts b/src/components/mediaViewer/hooks/useCurrentTimeSignal.ts similarity index 100% rename from src/components/mediaViewer/hooks/currentTimeSignal.ts rename to src/components/mediaViewer/hooks/useCurrentTimeSignal.ts diff --git a/src/components/mediaViewer/hooks/useVideoWaitingSignal.ts b/src/components/mediaViewer/hooks/useVideoWaitingSignal.ts new file mode 100644 index 000000000..c2246aaaa --- /dev/null +++ b/src/components/mediaViewer/hooks/useVideoWaitingSignal.ts @@ -0,0 +1,13 @@ +import { createSignal } from '../../../util/signals'; +import { useEffect } from '../../../lib/teact/teact'; + +export const [getIsVideoWaiting, setIsVideoWaiting] = createSignal(false); + +export default function useVideoWaitingSignal() { + useEffect(() => { + return () => { + setIsVideoWaiting(false); + }; + }, []); + return [getIsVideoWaiting, setIsVideoWaiting] as const; +}