[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 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();
|
||||||
|
}
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user