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

View File

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

View File

@ -1,9 +1,9 @@
import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact';
import { fastRaf } from '../../../../util/schedulers';
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useHeavyAnimationCheck from '../../../../hooks/useHeavyAnimationCheck';
import usePlayPause from '../../../../hooks/usePlayPause';
import useBackgroundMode, { isBackgroundModeActive } from '../../../../hooks/useBackgroundMode';
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../../../hooks/useHeavyAnimationCheck';
import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../../../hooks/usePriorityPlaybackCheck';
export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement | null }, canPlay: boolean) {
const canPlayRef = useRef();
@ -11,18 +11,8 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
const { play, pause } = usePlayPause(playerRef);
const isFrozenRef = useRef();
const freezePlaying = useCallback(() => {
isFrozenRef.current = true;
pause();
}, [pause]);
const unfreezePlaying = useCallback(() => {
isFrozenRef.current = false;
if (canPlayRef.current) {
if (canPlayRef.current && !isFrozen()) {
play();
}
}, [play]);
@ -31,18 +21,19 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
fastRaf(unfreezePlaying);
}, [unfreezePlaying]);
useBackgroundMode(freezePlaying, unfreezePlayingOnRaf, !canPlay);
useHeavyAnimationCheck(freezePlaying, unfreezePlaying, !canPlay);
useBackgroundMode(pause, unfreezePlayingOnRaf, !canPlay);
useHeavyAnimationCheck(pause, unfreezePlaying, !canPlay);
usePriorityPlaybackCheck(pause, unfreezePlaying, !canPlay);
const handlePlaying = useCallback(() => {
if (!canPlayRef.current || isFrozenRef.current) {
if (!canPlayRef.current || isFrozen()) {
pause();
}
}, [pause]);
useEffect(() => {
if (canPlay) {
if (!isFrozenRef.current) {
if (!isFrozen()) {
play();
}
} else {
@ -52,3 +43,39 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
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(
onBlur?: AnyToVoidFunction,
onFocus?: AnyToVoidFunction,
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(() => {
if (isDisabled) {
return undefined;
}
if (onBlur && !document.hasFocus()) {
handleBlur();
if (!isFocused) {
onBlur?.();
}
if (onBlur) {
window.addEventListener('blur', handleBlur);
blurCallbacks.addCallback(onBlur);
}
if (onFocus) {
window.addEventListener('focus', handleFocus);
focusCallbacks.addCallback(onFocus);
}
return () => {
if (onFocus) {
window.removeEventListener('focus', handleFocus);
focusCallbacks.removeCallback(onFocus);
}
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) {
this.isWaiting = false;
this.frames = this.frames.map((frame, i) => {
if (i === this.prevFrameIndex) {
return frame;