Round Video: Fix smooth progress indicator

This commit is contained in:
Alexander Zinchuk 2023-04-26 21:15:46 +04:00
parent 1e0f4e654a
commit 9bc780c1f9
2 changed files with 49 additions and 13 deletions

View File

@ -1,6 +1,7 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { import React, {
useCallback, useCallback,
useEffect,
useLayoutEffect, useLayoutEffect,
useRef, useRef,
useState, useState,
@ -25,6 +26,8 @@ import useMediaTransition from '../../../hooks/useMediaTransition';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import useSignal from '../../../hooks/useSignal';
import { useThrottledSignal } from '../../../hooks/useAsyncResolvers';
import ProgressSpinner from '../../ui/ProgressSpinner'; import ProgressSpinner from '../../ui/ProgressSpinner';
import OptimizedVideo from '../../ui/OptimizedVideo'; import OptimizedVideo from '../../ui/OptimizedVideo';
@ -39,6 +42,8 @@ type OwnProps = {
isDownloading?: boolean; isDownloading?: boolean;
}; };
const PROGRESS_THROTTLE = 16; // Min period needed for `playerEl.currentTime` to update
let stopPrevious: NoneToVoidFunction; let stopPrevious: NoneToVoidFunction;
const RoundVideo: FC<OwnProps> = ({ const RoundVideo: FC<OwnProps> = ({
@ -89,8 +94,19 @@ const RoundVideo: FC<OwnProps> = ({
transitionClassNames: spinnerClassNames, transitionClassNames: spinnerClassNames,
} = useShowTransition(isTransferring, undefined, wasLoadDisabled); } = useShowTransition(isTransferring, undefined, wasLoadDisabled);
const [isActivated, setIsActivated] = useState<boolean>(false); const [isActivated, setIsActivated] = useState(false);
const [progress, setProgress] = useState<number>(0);
const [getProgress, setProgress] = useSignal(0);
const getThrottledProgress = useThrottledSignal(getProgress, PROGRESS_THROTTLE);
useEffect(() => {
if (!isActivated) {
return;
}
const playerEl = playerRef.current!;
setProgress(playerEl.currentTime / playerEl.duration);
}, [setProgress, isActivated, getThrottledProgress]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!isActivated) { if (!isActivated) {
@ -100,9 +116,7 @@ const RoundVideo: FC<OwnProps> = ({
const svgCenter = ROUND_VIDEO_DIMENSIONS_PX / 2; const svgCenter = ROUND_VIDEO_DIMENSIONS_PX / 2;
const svgMargin = 6; const svgMargin = 6;
const circumference = (svgCenter - svgMargin) * 2 * Math.PI; const circumference = (svgCenter - svgMargin) * 2 * Math.PI;
const strokeDashOffset = circumference - progress * circumference; const strokeDashOffset = circumference - getThrottledProgress() * circumference;
const playerEl = playerRef.current!;
const playingProgressEl = playingProgressRef.current!; const playingProgressEl = playingProgressRef.current!;
const svgEl = playingProgressEl.firstElementChild; const svgEl = playingProgressEl.firstElementChild;
@ -118,9 +132,7 @@ const RoundVideo: FC<OwnProps> = ({
} else { } else {
(svgEl.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', strokeDashOffset.toString()); (svgEl.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', strokeDashOffset.toString());
} }
}, [isActivated, getThrottledProgress]);
setProgress(playerEl.currentTime / playerEl.duration);
}, [isActivated, progress]);
const shouldPlay = Boolean(mediaData && isIntersecting); const shouldPlay = Boolean(mediaData && isIntersecting);
@ -136,7 +148,7 @@ const RoundVideo: FC<OwnProps> = ({
requestMutation(() => { requestMutation(() => {
playingProgressRef.current!.innerHTML = ''; playingProgressRef.current!.innerHTML = '';
}); });
}, []); }, [setProgress]);
const capturePlaying = useCallback(() => { const capturePlaying = useCallback(() => {
stopPrevious?.(); stopPrevious?.();
@ -179,7 +191,7 @@ const RoundVideo: FC<OwnProps> = ({
const playerEl = e.currentTarget; const playerEl = e.currentTarget;
setProgress(playerEl.currentTime / playerEl.duration); setProgress(playerEl.currentTime / playerEl.duration);
}, []); }, [setProgress]);
return ( return (
<div <div

View File

@ -1,17 +1,41 @@
import type { Signal } from '../util/signals'; import type { Signal } from '../util/signals';
import type { Scheduler } from '../util/schedulers';
import useThrottledCallback from './useThrottledCallback'; import useThrottledCallback from './useThrottledCallback';
import useDebouncedCallback from './useDebouncedCallback'; import useDebouncedCallback from './useDebouncedCallback';
import useDerivedSignal from './useDerivedSignal'; import useDerivedSignal from './useDerivedSignal';
export function useThrottledResolver<T>(resolver: () => T, deps: any[], ms: number, noFirst = false) { export function useThrottledResolver<T>(
resolver: () => T,
deps: any[],
msOrSchedulerFn: number | Scheduler,
noFirst = false,
) {
return useThrottledCallback((setValue: (newValue: T) => void) => { return useThrottledCallback((setValue: (newValue: T) => void) => {
setValue(resolver()); setValue(resolver());
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, deps, ms, noFirst); }, deps, msOrSchedulerFn, noFirst);
} }
export function useDebouncedResolver<T>(resolver: () => T, deps: any[], ms: number, noFirst = false, noLast = false) { export function useThrottledSignal<T extends any>(
getValue: Signal<T>,
ms: number,
noFirst = false,
): Signal<T> {
const throttledResolver = useThrottledResolver(() => getValue(), [getValue], ms, noFirst);
return useDerivedSignal(
throttledResolver, [throttledResolver, getValue], true,
);
}
export function useDebouncedResolver<T>(
resolver: () => T,
deps: any[],
ms: number,
noFirst = false,
noLast = false,
) {
return useDebouncedCallback((setValue: (newValue: T) => void) => { return useDebouncedCallback((setValue: (newValue: T) => void) => {
setValue(resolver()); setValue(resolver());
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps