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, {
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<OwnProps> = ({
const LOCK_TIMEOUT = 250;
let cancelAnimation: Function | undefined;
const SeekLine: FC<OwnProps> = ({
duration,
bufferedRanges,
isReady,
posterSize,
playbackRate,
url,
isActive,
isPlaying,
isPreviewDisabled,
onSeek,
onSeekStart,
}) => {
// eslint-disable-next-line no-null/no-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 [getPreviewOffset, setPreviewOffset] = useSignal(0);
const [getPreviewTime, setPreviewTime] = useSignal(0);
@ -82,10 +95,42 @@ const SeekLine: React.FC<OwnProps> = ({
}, [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<OwnProps> = ({
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<OwnProps> = ({
};
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<OwnProps> = ({
// 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<OwnProps> = ({
setSelectedTime,
setIsSeeking,
isPreviewDisabled,
playbackRate,
]);
return (
@ -217,6 +271,7 @@ const SeekLine: React.FC<OwnProps> = ({
<div
key={`${start}-${end}`}
className={styles.buffered}
// @ts-ignore
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 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<OwnProps> = ({
useVideoCleanup(videoRef, []);
const [, setCurrentTime] = useCurrentTimeSignal();
const [, setIsVideoWaiting] = useVideoWaitingSignal();
const handleTimeUpdate = useLastCallback((e: React.SyntheticEvent<HTMLVideoElement>) => {
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<OwnProps> = ({
muted={isGif || isMuted}
id="media-viewer-video"
style={videoStyle}
onWaiting={() => setIsVideoWaiting(true)}
onPlay={() => setIsPlaying(true)}
onEnded={handleEnded}
onClick={!isMobile && !isFullscreen ? handleClick : undefined}

View File

@ -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<OwnProps> = ({
url={url}
duration={duration}
isReady={isReady}
isPlaying={isPlaying}
isPreviewDisabled={isPreviewDisabled}
posterSize={posterSize}
bufferedRanges={bufferedRanges}
playbackRate={playbackRate}
onSeek={handleSeek}
onSeekStart={handleSeekStart}
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;
}