Media Viewer: More smooth seekline animation (#3603)

This commit is contained in:
Alexander Zinchuk 2023-07-20 15:58:53 +02:00
parent 422369f298
commit c00aa8ef3f
5 changed files with 82 additions and 8 deletions

View File

@ -1,3 +1,4 @@
import type { FC } from '../../lib/teact/teact';
import React, { import React, {
useRef, useState, useEffect, memo, useMemo, useLayoutEffect, useRef, useState, useEffect, memo, useMemo, useLayoutEffect,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
@ -7,13 +8,16 @@ import type { ApiDimensions } from '../../api/types';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useSignal from '../../hooks/useSignal'; 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 { captureEvents } from '../../util/captureEvents';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatMediaDuration } from '../../util/dateFormat'; import { formatMediaDuration } from '../../util/dateFormat';
import { clamp, round } from '../../util/math'; import { clamp, round } from '../../util/math';
import { animateNumber } from '../../util/animation';
import { createVideoPreviews, renderVideoPreview, getPreviewDimensions } from '../../lib/video-preview/VideoPreview'; import { createVideoPreviews, renderVideoPreview, getPreviewDimensions } from '../../lib/video-preview/VideoPreview';
@ -25,7 +29,9 @@ type OwnProps = {
url?: string; url?: string;
duration: number; duration: number;
bufferedRanges: BufferedRange[]; bufferedRanges: BufferedRange[];
playbackRate: number;
isActive?: boolean; isActive?: boolean;
isPlaying?: boolean;
isPreviewDisabled?: boolean; isPreviewDisabled?: boolean;
isReady: boolean; isReady: boolean;
posterSize?: ApiDimensions; posterSize?: ApiDimensions;
@ -33,20 +39,27 @@ type OwnProps = {
onSeekStart: () => void; onSeekStart: () => void;
}; };
const SeekLine: React.FC<OwnProps> = ({ const LOCK_TIMEOUT = 250;
let cancelAnimation: Function | undefined;
const SeekLine: FC<OwnProps> = ({
duration, duration,
bufferedRanges, bufferedRanges,
isReady, isReady,
posterSize, posterSize,
playbackRate,
url, url,
isActive, isActive,
isPlaying,
isPreviewDisabled, isPreviewDisabled,
onSeek, onSeek,
onSeekStart, onSeekStart,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const seekerRef = useRef<HTMLDivElement>(null); const seekerRef = useRef<HTMLDivElement>(null);
const [getCurrentTime] = useCurrentTimeSignal(); const [getCurrentTimeSignal] = useCurrentTimeSignal();
const [getIsWaiting] = useVideoWaitingSignal();
const getCurrentTime = useThrottledSignal(getCurrentTimeSignal, LOCK_TIMEOUT);
const [getSelectedTime, setSelectedTime] = useSignal(getCurrentTime()); const [getSelectedTime, setSelectedTime] = useSignal(getCurrentTime());
const [getPreviewOffset, setPreviewOffset] = useSignal(0); const [getPreviewOffset, setPreviewOffset] = useSignal(0);
const [getPreviewTime, setPreviewTime] = useSignal(0); const [getPreviewTime, setPreviewTime] = useSignal(0);
@ -82,10 +95,42 @@ const SeekLine: React.FC<OwnProps> = ({
}, [isActive]); }, [isActive]);
useEffect(() => { useEffect(() => {
if (cancelAnimation) cancelAnimation();
cancelAnimation = undefined;
if (!isLockedRef.current && !isSeeking) { 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(() => { useLayoutEffect(() => {
if (!progressRef.current) return; if (!progressRef.current) return;
@ -123,7 +168,13 @@ const SeekLine: React.FC<OwnProps> = ({
return [t, o]; return [t, o];
}; };
const stopAnimation = () => {
if (cancelAnimation) cancelAnimation();
cancelAnimation = undefined;
};
const handleSeek = (e: MouseEvent | TouchEvent) => { const handleSeek = (e: MouseEvent | TouchEvent) => {
stopAnimation();
setPreviewVisible(true); setPreviewVisible(true);
([time, offset] = getPreviewProps(e)); ([time, offset] = getPreviewProps(e));
void setPreview(time); void setPreview(time);
@ -132,12 +183,14 @@ const SeekLine: React.FC<OwnProps> = ({
}; };
const handleStartSeek = () => { const handleStartSeek = () => {
stopAnimation();
setPreviewVisible(true); setPreviewVisible(true);
setIsSeeking(true); setIsSeeking(true);
onSeekStart(); onSeekStart();
}; };
const handleStopSeek = () => { const handleStopSeek = () => {
stopAnimation();
isLockedRef.current = true; isLockedRef.current = true;
setPreviewVisible(false); setPreviewVisible(false);
setIsSeeking(false); setIsSeeking(false);
@ -146,7 +199,7 @@ const SeekLine: React.FC<OwnProps> = ({
// Prevent current time updates from overriding the selected time // Prevent current time updates from overriding the selected time
setTimeout(() => { setTimeout(() => {
isLockedRef.current = false; isLockedRef.current = false;
}, 500); }, LOCK_TIMEOUT);
}; };
const cleanup = captureEvents(seeker, { const cleanup = captureEvents(seeker, {
@ -191,6 +244,7 @@ const SeekLine: React.FC<OwnProps> = ({
setSelectedTime, setSelectedTime,
setIsSeeking, setIsSeeking,
isPreviewDisabled, isPreviewDisabled,
playbackRate,
]); ]);
return ( return (
@ -217,6 +271,7 @@ const SeekLine: React.FC<OwnProps> = ({
<div <div
key={`${start}-${end}`} key={`${start}-${end}`}
className={styles.buffered} className={styles.buffered}
// @ts-ignore
style={`left: ${start * 100}%; right: ${100 - end * 100}%`} style={`left: ${start * 100}%; right: ${100 - end * 100}%`}
/> />
))} ))}

View File

@ -18,8 +18,9 @@ import usePictureInPicture from '../../hooks/usePictureInPicture';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useVideoCleanup from '../../hooks/useVideoCleanup'; import useVideoCleanup from '../../hooks/useVideoCleanup';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useCurrentTimeSignal from './hooks/currentTimeSignal'; import useCurrentTimeSignal from './hooks/useCurrentTimeSignal';
import useControlsSignal from './hooks/useControlsSignal'; import useControlsSignal from './hooks/useControlsSignal';
import useVideoWaitingSignal from './hooks/useVideoWaitingSignal';
import Button from '../ui/Button'; import Button from '../ui/Button';
import ProgressSpinner from '../ui/ProgressSpinner'; import ProgressSpinner from '../ui/ProgressSpinner';
@ -176,10 +177,12 @@ const VideoPlayer: FC<OwnProps> = ({
useVideoCleanup(videoRef, []); useVideoCleanup(videoRef, []);
const [, setCurrentTime] = useCurrentTimeSignal(); const [, setCurrentTime] = useCurrentTimeSignal();
const [, setIsVideoWaiting] = useVideoWaitingSignal();
const handleTimeUpdate = useLastCallback((e: React.SyntheticEvent<HTMLVideoElement>) => { const handleTimeUpdate = useLastCallback((e: React.SyntheticEvent<HTMLVideoElement>) => {
const video = e.currentTarget; const video = e.currentTarget;
if (video.readyState >= MIN_READY_STATE) { if (video.readyState >= MIN_READY_STATE) {
setIsVideoWaiting(false);
setCurrentTime(video.currentTime); setCurrentTime(video.currentTime);
} }
if (!isLooped && video.currentTime === video.duration) { if (!isLooped && video.currentTime === video.duration) {
@ -292,6 +295,7 @@ const VideoPlayer: FC<OwnProps> = ({
muted={isGif || isMuted} muted={isGif || isMuted}
id="media-viewer-video" id="media-viewer-video"
style={videoStyle} style={videoStyle}
onWaiting={() => setIsVideoWaiting(true)}
onPlay={() => setIsPlaying(true)} onPlay={() => setIsPlaying(true)}
onEnded={handleEnded} onEnded={handleEnded}
onClick={!isMobile && !isFullscreen ? handleClick : undefined} onClick={!isMobile && !isFullscreen ? handleClick : undefined}

View File

@ -12,7 +12,7 @@ import useFlag from '../../hooks/useFlag';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useDerivedState from '../../hooks/useDerivedState'; import useDerivedState from '../../hooks/useDerivedState';
import useSignal from '../../hooks/useSignal'; import useSignal from '../../hooks/useSignal';
import useCurrentTimeSignal from './hooks/currentTimeSignal'; import useCurrentTimeSignal from './hooks/useCurrentTimeSignal';
import useControlsSignal from './hooks/useControlsSignal'; import useControlsSignal from './hooks/useControlsSignal';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -162,9 +162,11 @@ const VideoPlayerControls: FC<OwnProps> = ({
url={url} url={url}
duration={duration} duration={duration}
isReady={isReady} isReady={isReady}
isPlaying={isPlaying}
isPreviewDisabled={isPreviewDisabled} isPreviewDisabled={isPreviewDisabled}
posterSize={posterSize} posterSize={posterSize}
bufferedRanges={bufferedRanges} bufferedRanges={bufferedRanges}
playbackRate={playbackRate}
onSeek={handleSeek} onSeek={handleSeek}
onSeekStart={handleSeekStart} onSeekStart={handleSeekStart}
isActive={isVisible} isActive={isVisible}

View File

@ -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;
}