Media Viewer: More smooth seekline animation (#3603)
This commit is contained in:
parent
422369f298
commit
c00aa8ef3f
@ -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}%`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
13
src/components/mediaViewer/hooks/useVideoWaitingSignal.ts
Normal file
13
src/components/mediaViewer/hooks/useVideoWaitingSignal.ts
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user