[Perf] Media Viewer: Pause animated stickers and other video

This commit is contained in:
Alexander Zinchuk 2023-04-15 13:50:29 +02:00
parent ecf4ffb7ae
commit 26ac63d49f
8 changed files with 184 additions and 126 deletions

View File

@ -10,8 +10,9 @@ import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle'; import buildStyle from '../../util/buildStyle';
import generateIdFor from '../../util/generateIdFor'; import generateIdFor from '../../util/generateIdFor';
import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck'; import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
import useBackgroundMode from '../../hooks/useBackgroundMode'; import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck';
import useBackgroundMode, { isBackgroundModeActive } from '../../hooks/useBackgroundMode';
import useSyncEffect from '../../hooks/useSyncEffect'; import useSyncEffect from '../../hooks/useSyncEffect';
import { useStateRef } from '../../hooks/useStateRef'; import { useStateRef } from '../../hooks/useStateRef';
@ -91,8 +92,6 @@ const AnimatedSticker: FC<OwnProps> = ({
const [animation, setAnimation] = useState<RLottieInstance>(); const [animation, setAnimation] = useState<RLottieInstance>();
const animationRef = useRef<RLottieInstance>(); const animationRef = useRef<RLottieInstance>();
const wasPlaying = useRef(false);
const isFrozen = useRef(false);
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
const canPlay = play || playSegment; const canPlay = play || playSegment;
@ -182,50 +181,31 @@ const AnimatedSticker: FC<OwnProps> = ({
}, [viewId]); }, [viewId]);
const playAnimation = useCallback((shouldRestart = false) => { const playAnimation = useCallback((shouldRestart = false) => {
if (animation && (playRef.current || playSegmentRef.current)) { if (
if (playSegmentRef.current) { !animation
animation.playSegment(playSegmentRef.current); || !(playRef.current || playSegmentRef.current)
} else { || isFrozen()
animation.play(shouldRestart, viewId); ) {
} return;
}
if (playSegmentRef.current) {
animation.playSegment(playSegmentRef.current);
} else {
animation.play(shouldRestart, viewId);
} }
}, [animation, playRef, playSegmentRef, viewId]); }, [animation, playRef, playSegmentRef, viewId]);
const playAnimationOnRaf = useCallback(() => {
fastRaf(playAnimation);
}, [playAnimation]);
const pauseAnimation = useCallback(() => { const pauseAnimation = useCallback(() => {
if (!animation) { if (animation?.isPlaying()) {
return; animation.pause(viewId);
} }
animation.pause(viewId);
}, [animation, viewId]); }, [animation, viewId]);
const freezeAnimation = useCallback(() => {
isFrozen.current = true;
if (!animation) {
return;
}
if (!wasPlaying.current) {
wasPlaying.current = animation.isPlaying();
}
pauseAnimation();
}, [animation, pauseAnimation]);
const unfreezeAnimation = useCallback(() => {
if (wasPlaying.current) {
playAnimation(noLoop);
}
wasPlaying.current = false;
isFrozen.current = false;
}, [noLoop, playAnimation]);
const unfreezeAnimationOnRaf = useCallback(() => {
fastRaf(unfreezeAnimation);
}, [unfreezeAnimation]);
useSyncEffect(([prevNoLoop]) => { useSyncEffect(([prevNoLoop]) => {
if (prevNoLoop !== undefined && noLoop !== prevNoLoop) { if (prevNoLoop !== undefined && noLoop !== prevNoLoop) {
animation?.setNoLoop(noLoop); animation?.setNoLoop(noLoop);
@ -242,19 +222,13 @@ const AnimatedSticker: FC<OwnProps> = ({
if (!animation) { if (!animation) {
return; return;
} }
if (canPlay) { if (canPlay) {
if (isFrozen.current) { if (!isFrozen()) {
wasPlaying.current = true;
} else {
playAnimation(noLoop); playAnimation(noLoop);
} }
} else { } else {
// eslint-disable-next-line no-lonely-if pauseAnimation();
if (isFrozen.current) {
wasPlaying.current = false;
} else {
pauseAnimation();
}
} }
}, [animation, canPlay, noLoop, playAnimation, pauseAnimation]); }, [animation, canPlay, noLoop, playAnimation, pauseAnimation]);
@ -269,11 +243,12 @@ const AnimatedSticker: FC<OwnProps> = ({
} }
}, [playAnimation, animation, tgsUrl]); }, [playAnimation, animation, tgsUrl]);
useHeavyAnimationCheck(freezeAnimation, unfreezeAnimation, !canPlay || forceOnHeavyAnimation); useHeavyAnimationCheck(pauseAnimation, playAnimation, !canPlay || forceOnHeavyAnimation);
usePriorityPlaybackCheck(pauseAnimation, playAnimation, !canPlay);
// Pausing frame may not happen in background, // Pausing frame may not happen in background,
// so we need to make sure it happens right after focusing, // so we need to make sure it happens right after focusing,
// then we can play again. // then we can play again.
useBackgroundMode(freezeAnimation, unfreezeAnimationOnRaf, !canPlay); useBackgroundMode(pauseAnimation, playAnimationOnRaf, !canPlay);
if (sharedCanvas) { if (sharedCanvas) {
return undefined; return undefined;
@ -294,3 +269,7 @@ const AnimatedSticker: FC<OwnProps> = ({
}; };
export default memo(AnimatedSticker); export default memo(AnimatedSticker);
function isFrozen() {
return isHeavyAnimating() || isPriorityPlaybackActive() || isBackgroundModeActive();
}

View File

@ -32,6 +32,7 @@ import { renderMessageText } from '../common/helpers/renderMessageText';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../hooks/useForceUpdate';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck';
import { exitPictureInPictureIfNeeded } from '../../hooks/usePictureInPicture'; import { exitPictureInPictureIfNeeded } from '../../hooks/usePictureInPicture';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
@ -149,8 +150,12 @@ const MediaViewer: FC<StateProps> = ({
} }
disableDirectTextInput(); disableDirectTextInput();
const stopPriorityPlayback = dispatchPriorityPlaybackEvent();
return enableDirectTextInput; return () => {
stopPriorityPlayback();
enableDirectTextInput();
};
}, [isOpen]); }, [isOpen]);
useEffect(() => { useEffect(() => {

View File

@ -770,6 +770,10 @@
vertical-align: bottom; vertical-align: bottom;
} }
.MessageMeta {
z-index: 1;
}
.text-content { .text-content {
word-break: normal; word-break: normal;
line-height: var(--emoji-only-size); line-height: var(--emoji-only-size);

View File

@ -1,9 +1,9 @@
import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact'; import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact';
import { fastRaf } from '../../../../util/schedulers'; import { fastRaf } from '../../../../util/schedulers';
import useBackgroundMode from '../../../../hooks/useBackgroundMode'; import useBackgroundMode, { isBackgroundModeActive } from '../../../../hooks/useBackgroundMode';
import useHeavyAnimationCheck from '../../../../hooks/useHeavyAnimationCheck'; import useHeavyAnimationCheck, { isHeavyAnimating } from '../../../../hooks/useHeavyAnimationCheck';
import usePlayPause from '../../../../hooks/usePlayPause'; import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../../../hooks/usePriorityPlaybackCheck';
export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement | null }, canPlay: boolean) { export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement | null }, canPlay: boolean) {
const canPlayRef = useRef(); const canPlayRef = useRef();
@ -11,18 +11,8 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
const { play, pause } = usePlayPause(playerRef); const { play, pause } = usePlayPause(playerRef);
const isFrozenRef = useRef();
const freezePlaying = useCallback(() => {
isFrozenRef.current = true;
pause();
}, [pause]);
const unfreezePlaying = useCallback(() => { const unfreezePlaying = useCallback(() => {
isFrozenRef.current = false; if (canPlayRef.current && !isFrozen()) {
if (canPlayRef.current) {
play(); play();
} }
}, [play]); }, [play]);
@ -31,18 +21,19 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
fastRaf(unfreezePlaying); fastRaf(unfreezePlaying);
}, [unfreezePlaying]); }, [unfreezePlaying]);
useBackgroundMode(freezePlaying, unfreezePlayingOnRaf, !canPlay); useBackgroundMode(pause, unfreezePlayingOnRaf, !canPlay);
useHeavyAnimationCheck(freezePlaying, unfreezePlaying, !canPlay); useHeavyAnimationCheck(pause, unfreezePlaying, !canPlay);
usePriorityPlaybackCheck(pause, unfreezePlaying, !canPlay);
const handlePlaying = useCallback(() => { const handlePlaying = useCallback(() => {
if (!canPlayRef.current || isFrozenRef.current) { if (!canPlayRef.current || isFrozen()) {
pause(); pause();
} }
}, [pause]); }, [pause]);
useEffect(() => { useEffect(() => {
if (canPlay) { if (canPlay) {
if (!isFrozenRef.current) { if (!isFrozen()) {
play(); play();
} }
} else { } else {
@ -52,3 +43,39 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
return { handlePlaying }; return { handlePlaying };
} }
function usePlayPause(mediaRef: React.RefObject<HTMLMediaElement>) {
const shouldPauseRef = useRef(false);
const isLoadingPlayRef = useRef(false);
const play = useCallback(() => {
shouldPauseRef.current = false;
if (mediaRef.current && !isLoadingPlayRef.current && document.body.contains(mediaRef.current)) {
isLoadingPlayRef.current = true;
mediaRef.current.play().then(() => {
isLoadingPlayRef.current = false;
if (shouldPauseRef.current) {
mediaRef.current?.pause();
shouldPauseRef.current = false;
}
}).catch((e) => {
// eslint-disable-next-line no-console
console.warn(e);
});
}
}, [mediaRef]);
const pause = useCallback(() => {
if (isLoadingPlayRef.current) {
shouldPauseRef.current = true;
} else {
mediaRef.current?.pause();
}
}, [mediaRef]);
return { play, pause };
}
function isFrozen() {
return isHeavyAnimating() || isPriorityPlaybackActive() || isBackgroundModeActive();
}

View File

@ -1,49 +1,60 @@
import { useCallback, useEffect, useRef } from '../lib/teact/teact'; import { useEffect } from '../lib/teact/teact';
import { createCallbackManager } from '../util/callbacks';
const blurCallbacks = createCallbackManager();
const focusCallbacks = createCallbackManager();
let isFocused = document.hasFocus();
window.addEventListener('blur', () => {
if (!isFocused) {
return;
}
isFocused = false;
blurCallbacks.runCallbacks();
});
window.addEventListener('focus', () => {
isFocused = true;
focusCallbacks.runCallbacks();
});
export default function useBackgroundMode( export default function useBackgroundMode(
onBlur?: AnyToVoidFunction, onBlur?: AnyToVoidFunction,
onFocus?: AnyToVoidFunction, onFocus?: AnyToVoidFunction,
isDisabled = false, isDisabled = false,
) { ) {
const wasBlurred = useRef<boolean>(false);
const handleBlur = useCallback(() => {
if (wasBlurred.current) {
return;
}
onBlur?.();
wasBlurred.current = true;
}, [onBlur]);
const handleFocus = useCallback(() => {
onFocus?.();
wasBlurred.current = false;
}, [onFocus]);
useEffect(() => { useEffect(() => {
if (isDisabled) { if (isDisabled) {
return undefined; return undefined;
} }
if (onBlur && !document.hasFocus()) { if (!isFocused) {
handleBlur(); onBlur?.();
} }
if (onBlur) { if (onBlur) {
window.addEventListener('blur', handleBlur); blurCallbacks.addCallback(onBlur);
} }
if (onFocus) { if (onFocus) {
window.addEventListener('focus', handleFocus); focusCallbacks.addCallback(onFocus);
} }
return () => { return () => {
if (onFocus) { if (onFocus) {
window.removeEventListener('focus', handleFocus); focusCallbacks.removeCallback(onFocus);
} }
if (onBlur) { if (onBlur) {
window.removeEventListener('blur', handleBlur); blurCallbacks.removeCallback(onBlur);
} }
}; };
}, [handleBlur, handleFocus, isDisabled, onBlur, onFocus]); }, [isDisabled, onBlur, onFocus]);
}
export function isBackgroundModeActive() {
return !isFocused;
} }

View File

@ -1,33 +0,0 @@
import { useCallback, useRef } from '../lib/teact/teact';
export default function usePlayPause(mediaRef: React.RefObject<HTMLMediaElement>) {
const shouldPauseRef = useRef(false);
const isLoadingPlayRef = useRef(false);
const play = useCallback(() => {
shouldPauseRef.current = false;
if (mediaRef.current && !isLoadingPlayRef.current && document.body.contains(mediaRef.current)) {
isLoadingPlayRef.current = true;
mediaRef.current.play().then(() => {
isLoadingPlayRef.current = false;
if (shouldPauseRef.current) {
mediaRef.current?.pause();
shouldPauseRef.current = false;
}
}).catch((e) => {
// eslint-disable-next-line no-console
console.warn(e);
});
}
}, [mediaRef]);
const pause = useCallback(() => {
if (isLoadingPlayRef.current) {
shouldPauseRef.current = true;
} else {
mediaRef.current?.pause();
}
}, [mediaRef]);
return { play, pause };
}

View File

@ -0,0 +1,63 @@
import { useEffect } from '../lib/teact/teact';
import { createCallbackManager } from '../util/callbacks';
const startCallbacks = createCallbackManager();
const endCallbacks = createCallbackManager();
let timeout: number | undefined;
let isActive = false;
const usePriorityPlaybackCheck = (
handleAnimationStart: AnyToVoidFunction,
handleAnimationEnd: AnyToVoidFunction,
isDisabled = false,
) => {
useEffect(() => {
if (isDisabled) {
return undefined;
}
if (isActive) {
handleAnimationStart();
}
startCallbacks.addCallback(handleAnimationStart);
endCallbacks.addCallback(handleAnimationEnd);
return () => {
endCallbacks.removeCallback(handleAnimationEnd);
startCallbacks.removeCallback(handleAnimationStart);
};
}, [isDisabled, handleAnimationEnd, handleAnimationStart]);
};
export function isPriorityPlaybackActive() {
return isActive;
}
export function dispatchPriorityPlaybackEvent() {
if (!isActive) {
isActive = true;
startCallbacks.runCallbacks();
}
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
// Race condition may happen if another `dispatchPriorityPlaybackEvent` is called before `onEnd`
function onEnd() {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
isActive = false;
endCallbacks.runCallbacks();
}
return onEnd;
}
export default usePriorityPlaybackCheck;

View File

@ -181,6 +181,8 @@ class RLottie {
} }
if (!this.params.isLowPriority) { if (!this.params.isLowPriority) {
this.isWaiting = false;
this.frames = this.frames.map((frame, i) => { this.frames = this.frames.map((frame, i) => {
if (i === this.prevFrameIndex) { if (i === this.prevFrameIndex) {
return frame; return frame;