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, {
|
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}%`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
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