[Perf] Media Viewer: Pause animated stickers and other video
This commit is contained in:
parent
ecf4ffb7ae
commit
26ac63d49f
@ -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();
|
||||
}
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -770,6 +770,10 @@
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.MessageMeta {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
word-break: normal;
|
||||
line-height: var(--emoji-only-size);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
63
src/hooks/usePriorityPlaybackCheck.ts
Normal file
63
src/hooks/usePriorityPlaybackCheck.ts
Normal 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;
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user