[Perf] RLottie: Avoid redundant JSON data (de)serialization
This commit is contained in:
parent
b6c80b5236
commit
29d5ef3ef7
@ -1,5 +1,3 @@
|
|||||||
import { inflate } from 'pako/dist/pako_inflate';
|
|
||||||
|
|
||||||
import { Api as GramJs, TelegramClient } from '../../../lib/gramjs';
|
import { Api as GramJs, TelegramClient } from '../../../lib/gramjs';
|
||||||
import {
|
import {
|
||||||
ApiMediaFormat, ApiOnProgress, ApiParsedMedia, ApiPreparedMedia,
|
ApiMediaFormat, ApiOnProgress, ApiParsedMedia, ApiPreparedMedia,
|
||||||
@ -20,7 +18,9 @@ type EntityType = (
|
|||||||
'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' |
|
'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' |
|
||||||
'document'
|
'document'
|
||||||
);
|
);
|
||||||
|
|
||||||
const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document']);
|
const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document']);
|
||||||
|
const TGS_MIME_TYPE = 'application/x-tgsticker';
|
||||||
|
|
||||||
export default async function downloadMedia(
|
export default async function downloadMedia(
|
||||||
{
|
{
|
||||||
@ -53,7 +53,7 @@ export default async function downloadMedia(
|
|||||||
void cacheApi.save(cacheName, url, parsed);
|
void cacheApi.save(cacheName, url, parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prepared = mediaFormat === ApiMediaFormat.Progressive ? '' : prepareMedia(parsed);
|
const prepared = mediaFormat === ApiMediaFormat.Progressive ? '' : prepareMedia(parsed as string | Blob);
|
||||||
const arrayBuffer = mediaFormat === ApiMediaFormat.Progressive ? parsed as ArrayBuffer : undefined;
|
const arrayBuffer = mediaFormat === ApiMediaFormat.Progressive ? parsed as ArrayBuffer : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -183,7 +183,7 @@ async function download(
|
|||||||
return { mimeType, data, fullSize };
|
return { mimeType, data, fullSize };
|
||||||
} else if (entityType === 'stickerSet') {
|
} else if (entityType === 'stickerSet') {
|
||||||
const data = await client.downloadStickerSetThumb(entity);
|
const data = await client.downloadStickerSetThumb(entity);
|
||||||
const mimeType = mediaFormat === ApiMediaFormat.Lottie ? 'application/json' : getMimeType(data);
|
const mimeType = mediaFormat === ApiMediaFormat.Lottie ? TGS_MIME_TYPE : getMimeType(data);
|
||||||
|
|
||||||
return { mimeType, data };
|
return { mimeType, data };
|
||||||
} else {
|
} else {
|
||||||
@ -228,10 +228,8 @@ async function parseMedia(
|
|||||||
): Promise<ApiParsedMedia | undefined> {
|
): Promise<ApiParsedMedia | undefined> {
|
||||||
switch (mediaFormat) {
|
switch (mediaFormat) {
|
||||||
case ApiMediaFormat.BlobUrl:
|
case ApiMediaFormat.BlobUrl:
|
||||||
return new Blob([data], { type: mimeType });
|
|
||||||
case ApiMediaFormat.Lottie: {
|
case ApiMediaFormat.Lottie: {
|
||||||
const json = inflate(data, { to: 'string' });
|
return new Blob([data], { type: mimeType });
|
||||||
return JSON.parse(json);
|
|
||||||
}
|
}
|
||||||
case ApiMediaFormat.Progressive: {
|
case ApiMediaFormat.Progressive: {
|
||||||
return data.buffer;
|
return data.buffer;
|
||||||
@ -241,7 +239,7 @@ async function parseMedia(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia {
|
function prepareMedia(mediaData: Exclude<ApiParsedMedia, ArrayBuffer>): ApiPreparedMedia {
|
||||||
if (mediaData instanceof Blob) {
|
if (mediaData instanceof Blob) {
|
||||||
return URL.createObjectURL(mediaData);
|
return URL.createObjectURL(mediaData);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,5 @@ export enum ApiMediaFormat {
|
|||||||
Stream,
|
Stream,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiParsedMedia = string | Blob | AnyLiteral | ArrayBuffer;
|
export type ApiParsedMedia = string | Blob | ArrayBuffer;
|
||||||
export type ApiPreparedMedia = string | AnyLiteral;
|
export type ApiPreparedMedia = string;
|
||||||
export type ApiMediaFormatToPrepared<T> = T extends ApiMediaFormat.Lottie ? AnyLiteral : string;
|
|
||||||
|
|||||||
@ -89,7 +89,7 @@ const AnimatedEmoji: FC<OwnProps> = ({
|
|||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
key={localMediaHash}
|
key={localMediaHash}
|
||||||
id={localMediaHash}
|
id={localMediaHash}
|
||||||
animationData={mediaData as AnyLiteral}
|
animationData={mediaData!}
|
||||||
size={width}
|
size={width}
|
||||||
quality={QUALITY}
|
quality={QUALITY}
|
||||||
play={isIntersecting && playKey}
|
play={isIntersecting && playKey}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const AnimatedIcon: FC<OwnProps> = ({
|
|||||||
playSegment,
|
playSegment,
|
||||||
color,
|
color,
|
||||||
}) => {
|
}) => {
|
||||||
const [iconData, setIconData] = useState<Record<string, any>>();
|
const [iconData, setIconData] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAnimationData(name).then(setIconData);
|
getAnimationData(name).then(setIconData);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import useBackgroundMode from '../../hooks/useBackgroundMode';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
id: string;
|
id: string;
|
||||||
animationData: AnyLiteral;
|
animationData?: string;
|
||||||
play?: boolean | string;
|
play?: boolean | string;
|
||||||
playSegment?: [number, number];
|
playSegment?: [number, number];
|
||||||
speed?: number;
|
speed?: number;
|
||||||
@ -202,7 +202,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
if (animation) {
|
if (animation) {
|
||||||
if (isFirstRender.current) {
|
if (isFirstRender.current) {
|
||||||
isFirstRender.current = false;
|
isFirstRender.current = false;
|
||||||
} else {
|
} else if (animationData) {
|
||||||
animation.changeData(animationData);
|
animation.changeData(animationData);
|
||||||
playAnimation();
|
playAnimation();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React, {
|
|||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getDispatch } from '../../lib/teact/teactn';
|
import { getDispatch } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiMediaFormat, ApiMessage } from '../../api/types';
|
import { ApiMessage } from '../../api/types';
|
||||||
|
|
||||||
import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo';
|
import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo';
|
||||||
import {
|
import {
|
||||||
@ -83,7 +83,7 @@ const Document: FC<OwnProps> = ({
|
|||||||
const shouldDownload = Boolean(isDownloading || (isLoadAllowed && wasIntersected));
|
const shouldDownload = Boolean(isDownloading || (isLoadAllowed && wasIntersected));
|
||||||
|
|
||||||
const documentHash = getMessageMediaHash(message, 'download');
|
const documentHash = getMessageMediaHash(message, 'download');
|
||||||
const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress<ApiMediaFormat.BlobUrl>(
|
const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress(
|
||||||
documentHash, !shouldDownload, undefined, undefined, undefined, true,
|
documentHash, !shouldDownload, undefined, undefined, undefined, true,
|
||||||
);
|
);
|
||||||
const isLoaded = Boolean(mediaData);
|
const isLoaded = Boolean(mediaData);
|
||||||
|
|||||||
@ -49,7 +49,7 @@ const LocalAnimatedEmoji: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
const [localStickerAnimationData, setLocalStickerAnimationData] = useState<AnyLiteral>();
|
const [localStickerAnimationData, setLocalStickerAnimationData] = useState<string>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localSticker) {
|
if (localSticker) {
|
||||||
getAnimationData(localSticker as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => {
|
getAnimationData(localSticker as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => {
|
||||||
|
|||||||
@ -22,8 +22,8 @@ const SEGMENT_COVER_EYE: [number, number] = [20, 0];
|
|||||||
const STICKER_SIZE = IS_SINGLE_COLUMN_LAYOUT ? STICKER_SIZE_AUTH_MOBILE : STICKER_SIZE_AUTH;
|
const STICKER_SIZE = IS_SINGLE_COLUMN_LAYOUT ? STICKER_SIZE_AUTH_MOBILE : STICKER_SIZE_AUTH;
|
||||||
|
|
||||||
const PasswordMonkey: FC<OwnProps> = ({ isPasswordVisible, isBig }) => {
|
const PasswordMonkey: FC<OwnProps> = ({ isPasswordVisible, isBig }) => {
|
||||||
const [closeMonkeyData, setCloseMonkeyData] = useState<Record<string, any>>();
|
const [closeMonkeyData, setCloseMonkeyData] = useState<string>();
|
||||||
const [peekMonkeyData, setPeekMonkeyData] = useState<Record<string, any>>();
|
const [peekMonkeyData, setPeekMonkeyData] = useState<string>();
|
||||||
const [isFirstMonkeyLoaded, setIsFirstMonkeyLoaded] = useState(false);
|
const [isFirstMonkeyLoaded, setIsFirstMonkeyLoaded] = useState(false);
|
||||||
const [isPeekShown, setIsPeekShown] = useState(false);
|
const [isPeekShown, setIsPeekShown] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@ -29,8 +29,8 @@ const TrackingMonkey: FC<OwnProps> = ({
|
|||||||
isTracking,
|
isTracking,
|
||||||
isBig,
|
isBig,
|
||||||
}) => {
|
}) => {
|
||||||
const [idleMonkeyData, setIdleMonkeyData] = useState<Record<string, any>>();
|
const [idleMonkeyData, setIdleMonkeyData] = useState<string>();
|
||||||
const [trackingMonkeyData, setTrackingMonkeyData] = useState<Record<string, any>>();
|
const [trackingMonkeyData, setTrackingMonkeyData] = useState<string>();
|
||||||
const [isFirstMonkeyLoaded, setIsFirstMonkeyLoaded] = useState(false);
|
const [isFirstMonkeyLoaded, setIsFirstMonkeyLoaded] = useState(false);
|
||||||
const TRACKING_FRAMES_PER_SYMBOL = (TRACKING_END_FRAME - TRACKING_START_FRAME) / codeLength;
|
const TRACKING_FRAMES_PER_SYMBOL = (TRACKING_END_FRAME - TRACKING_START_FRAME) / codeLength;
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
loadMoreChats,
|
loadMoreChats,
|
||||||
} = getDispatch();
|
} = getDispatch();
|
||||||
|
|
||||||
const [animationData, setAnimationData] = useState<Record<string, any>>();
|
const [animationData, setAnimationData] = useState<string>();
|
||||||
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
|
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
|
||||||
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
|
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
showDialog,
|
showDialog,
|
||||||
} = getDispatch();
|
} = getDispatch();
|
||||||
|
|
||||||
const [animationData, setAnimationData] = useState<Record<string, any>>();
|
const [animationData, setAnimationData] = useState<string>();
|
||||||
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
|
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
|
||||||
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
|
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const DownloadManager: FC<StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!startedDownloads.has(downloadHash)) {
|
if (!startedDownloads.has(downloadHash)) {
|
||||||
const mediaData = mediaLoader.getFromMemory<ApiMediaFormat.BlobUrl>(downloadHash);
|
const mediaData = mediaLoader.getFromMemory(downloadHash);
|
||||||
if (mediaData) {
|
if (mediaData) {
|
||||||
startedDownloads.delete(downloadHash);
|
startedDownloads.delete(downloadHash);
|
||||||
download(mediaData, getMessageContentFilename(message));
|
download(mediaData, getMessageContentFilename(message));
|
||||||
|
|||||||
@ -75,9 +75,9 @@ const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
|
|||||||
}, PLAYING_DURATION);
|
}, PLAYING_DURATION);
|
||||||
}, [stop]);
|
}, [stop]);
|
||||||
|
|
||||||
const mediaDataEffect = useMedia(`sticker${effectAnimationId}`, !effectAnimationId, ApiMediaFormat.Lottie);
|
const effectAnimationData = useMedia(`sticker${effectAnimationId}`, !effectAnimationId, ApiMediaFormat.Lottie);
|
||||||
|
|
||||||
const [localEffectAnimationData, setLocalEffectAnimationData] = useState<AnyLiteral>();
|
const [localEffectAnimationData, setLocalEffectAnimationData] = useState<string | undefined>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localEffectAnimation) {
|
if (localEffectAnimation) {
|
||||||
getAnimationData(localEffectAnimation as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => {
|
getAnimationData(localEffectAnimation as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => {
|
||||||
@ -99,7 +99,7 @@ const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
|
|||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
id={`effect_${effectAnimationId}`}
|
id={`effect_${effectAnimationId}`}
|
||||||
size={EFFECT_SIZE}
|
size={EFFECT_SIZE}
|
||||||
animationData={(localEffectAnimationData || mediaDataEffect) as AnyLiteral}
|
animationData={localEffectAnimationData || effectAnimationData}
|
||||||
play={isPlaying}
|
play={isPlaying}
|
||||||
isLowPriority={IS_ANDROID}
|
isLowPriority={IS_ANDROID}
|
||||||
forceOnHeavyAnimation
|
forceOnHeavyAnimation
|
||||||
|
|||||||
@ -67,7 +67,7 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
|||||||
id={`reaction_emoji_${centerIconId}`}
|
id={`reaction_emoji_${centerIconId}`}
|
||||||
className={animationClassNames}
|
className={animationClassNames}
|
||||||
size={CENTER_ICON_SIZE}
|
size={CENTER_ICON_SIZE}
|
||||||
animationData={mediaDataCenterIcon as AnyLiteral}
|
animationData={mediaDataCenterIcon}
|
||||||
play
|
play
|
||||||
noLoop
|
noLoop
|
||||||
onLoad={markAnimationLoaded}
|
onLoad={markAnimationLoaded}
|
||||||
@ -78,7 +78,7 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
|||||||
id={`reaction_effect_${effectId}`}
|
id={`reaction_effect_${effectId}`}
|
||||||
className={buildClassName('effect', animationClassNames)}
|
className={buildClassName('effect', animationClassNames)}
|
||||||
size={EFFECT_SIZE}
|
size={EFFECT_SIZE}
|
||||||
animationData={mediaDataEffect as AnyLiteral}
|
animationData={mediaDataEffect}
|
||||||
play
|
play
|
||||||
noLoop
|
noLoop
|
||||||
onEnded={handleEnded}
|
onEnded={handleEnded}
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const AvailableReaction: FC<{
|
|||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
id={`select_${reaction.reaction}`}
|
id={`select_${reaction.reaction}`}
|
||||||
className={animatedClassNames}
|
className={animatedClassNames}
|
||||||
animationData={mediaData as AnyLiteral}
|
animationData={mediaData}
|
||||||
play={isActivated}
|
play={isActivated}
|
||||||
noLoop
|
noLoop
|
||||||
size={REACTION_SIZE}
|
size={REACTION_SIZE}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
const mediaData = useMedia(
|
const mediaData = useMedia(
|
||||||
mediaHash,
|
mediaHash,
|
||||||
!shouldLoad,
|
!shouldLoad,
|
||||||
getMessageMediaFormat(message, 'inline', true),
|
getMessageMediaFormat(message, 'inline'),
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
key={mediaHash}
|
key={mediaHash}
|
||||||
className={buildClassName('full-media', transitionClassNames)}
|
className={buildClassName('full-media', transitionClassNames)}
|
||||||
id={mediaHash}
|
id={mediaHash}
|
||||||
animationData={mediaData as AnyLiteral}
|
animationData={mediaData}
|
||||||
size={width}
|
size={width}
|
||||||
play={shouldPlay}
|
play={shouldPlay}
|
||||||
noLoop={!shouldLoop}
|
noLoop={!shouldLoop}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
|
|||||||
} = getDispatch();
|
} = getDispatch();
|
||||||
|
|
||||||
const [linkedGroupId, setLinkedGroupId] = useState<string>();
|
const [linkedGroupId, setLinkedGroupId] = useState<string>();
|
||||||
const [animationData, setAnimationData] = useState<Record<string, any>>();
|
const [animationData, setAnimationData] = useState<string>();
|
||||||
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
|
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
|
||||||
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
|
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
|
||||||
const [isConfirmUnlinkGroupDialogOpen, openConfirmUnlinkGroupDialog, closeConfirmUnlinkGroupDialog] = useFlag();
|
const [isConfirmUnlinkGroupDialogOpen, openConfirmUnlinkGroupDialog, closeConfirmUnlinkGroupDialog] = useFlag();
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
|||||||
cacheBuster?: number,
|
cacheBuster?: number,
|
||||||
delay?: number | false,
|
delay?: number | false,
|
||||||
) => {
|
) => {
|
||||||
const mediaData = mediaHash ? mediaLoader.getFromMemory<T>(mediaHash) : undefined;
|
const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined;
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export default function useMediaWithLoadProgress<T extends ApiMediaFormat = ApiM
|
|||||||
delay?: number | false,
|
delay?: number | false,
|
||||||
isHtmlAllowed = false,
|
isHtmlAllowed = false,
|
||||||
) {
|
) {
|
||||||
const mediaData = mediaHash ? mediaLoader.getFromMemory<T>(mediaHash) : undefined;
|
const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined;
|
||||||
const isStreaming = mediaFormat === ApiMediaFormat.Stream || (
|
const isStreaming = mediaFormat === ApiMediaFormat.Stream || (
|
||||||
IS_PROGRESSIVE_SUPPORTED && mediaFormat === ApiMediaFormat.Progressive
|
IS_PROGRESSIVE_SUPPORTED && mediaFormat === ApiMediaFormat.Progressive
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,8 +22,6 @@ const CHUNK_SIZE = 1;
|
|||||||
const MAX_WORKERS = 4;
|
const MAX_WORKERS = 4;
|
||||||
const HIGH_PRIORITY_QUALITY = IS_SINGLE_COLUMN_LAYOUT ? 0.75 : 1;
|
const HIGH_PRIORITY_QUALITY = IS_SINGLE_COLUMN_LAYOUT ? 0.75 : 1;
|
||||||
const LOW_PRIORITY_QUALITY = 0.75;
|
const LOW_PRIORITY_QUALITY = 0.75;
|
||||||
const HIGH_PRIORITY_MAX_FPS = 60;
|
|
||||||
const LOW_PRIORITY_MAX_FPS = 30;
|
|
||||||
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
|
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
|
||||||
const LOW_PRIORITY_CACHE_MODULO = 0;
|
const LOW_PRIORITY_CACHE_MODULO = 0;
|
||||||
|
|
||||||
@ -39,9 +37,9 @@ class RLottie {
|
|||||||
|
|
||||||
private key!: string;
|
private key!: string;
|
||||||
|
|
||||||
private msPerFrame!: number;
|
private msPerFrame = 1000 / 60;
|
||||||
|
|
||||||
private reduceFactor!: number;
|
private reduceFactor = 1;
|
||||||
|
|
||||||
private cacheModulo!: number;
|
private cacheModulo!: number;
|
||||||
|
|
||||||
@ -86,7 +84,7 @@ class RLottie {
|
|||||||
constructor(
|
constructor(
|
||||||
private id: string,
|
private id: string,
|
||||||
private container: HTMLDivElement,
|
private container: HTMLDivElement,
|
||||||
private animationData: AnyLiteral,
|
private animationData: string,
|
||||||
private params: Params = {},
|
private params: Params = {},
|
||||||
private onLoad?: () => void,
|
private onLoad?: () => void,
|
||||||
private customColor?: [number, number, number],
|
private customColor?: [number, number, number],
|
||||||
@ -189,10 +187,6 @@ class RLottie {
|
|||||||
|
|
||||||
const { isLowPriority } = this.params;
|
const { isLowPriority } = this.params;
|
||||||
|
|
||||||
const maxFps = isLowPriority ? LOW_PRIORITY_MAX_FPS : HIGH_PRIORITY_MAX_FPS;
|
|
||||||
const sourceFps = this.animationData.fr || maxFps;
|
|
||||||
this.reduceFactor = sourceFps % maxFps === 0 ? sourceFps / maxFps : 1;
|
|
||||||
this.msPerFrame = 1000 / (sourceFps / this.reduceFactor);
|
|
||||||
this.cacheModulo = isLowPriority ? LOW_PRIORITY_CACHE_MODULO : HIGH_PRIORITY_CACHE_MODULO;
|
this.cacheModulo = isLowPriority ? LOW_PRIORITY_CACHE_MODULO : HIGH_PRIORITY_CACHE_MODULO;
|
||||||
this.chunkSize = CHUNK_SIZE;
|
this.chunkSize = CHUNK_SIZE;
|
||||||
}
|
}
|
||||||
@ -201,17 +195,10 @@ class RLottie {
|
|||||||
this.canvas.remove();
|
this.canvas.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onChangeData(framesCount: number) {
|
|
||||||
this.isWaiting = false;
|
|
||||||
this.framesCount = framesCount;
|
|
||||||
this.chunksCount = Math.ceil(framesCount / this.chunkSize);
|
|
||||||
this.isAnimating = false;
|
|
||||||
|
|
||||||
this.doPlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
setColor(newColor: [number, number, number] | undefined) {
|
setColor(newColor: [number, number, number] | undefined) {
|
||||||
this.customColor = newColor;
|
this.customColor = newColor;
|
||||||
|
|
||||||
|
// TODO Remove?
|
||||||
if (this.customColor) {
|
if (this.customColor) {
|
||||||
const imageData = this.ctx.getImageData(0, 0, this.imgSize, this.imgSize);
|
const imageData = this.ctx.getImageData(0, 0, this.imgSize, this.imgSize);
|
||||||
const arr = imageData.data;
|
const arr = imageData.data;
|
||||||
@ -226,21 +213,6 @@ class RLottie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeData(animationData: AnyLiteral) {
|
|
||||||
this.pause();
|
|
||||||
this.animationData = animationData;
|
|
||||||
this.initConfig();
|
|
||||||
|
|
||||||
workers[this.workerIndex].request({
|
|
||||||
name: 'changeData',
|
|
||||||
args: [
|
|
||||||
this.key,
|
|
||||||
this.animationData,
|
|
||||||
this.onChangeData.bind(this),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private initRenderer() {
|
private initRenderer() {
|
||||||
this.workerIndex = cycleRestrict(MAX_WORKERS, ++lastWorkerIndex);
|
this.workerIndex = cycleRestrict(MAX_WORKERS, ++lastWorkerIndex);
|
||||||
|
|
||||||
@ -251,7 +223,6 @@ class RLottie {
|
|||||||
this.animationData,
|
this.animationData,
|
||||||
this.imgSize,
|
this.imgSize,
|
||||||
this.params.isLowPriority,
|
this.params.isLowPriority,
|
||||||
this.reduceFactor,
|
|
||||||
this.onRendererInit.bind(this),
|
this.onRendererInit.bind(this),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -264,7 +235,9 @@ class RLottie {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRendererInit(framesCount: number) {
|
private onRendererInit(reduceFactor: number, msPerFrame: number, framesCount: number) {
|
||||||
|
this.reduceFactor = reduceFactor;
|
||||||
|
this.msPerFrame = msPerFrame;
|
||||||
this.framesCount = framesCount;
|
this.framesCount = framesCount;
|
||||||
this.chunksCount = Math.ceil(framesCount / this.chunkSize);
|
this.chunksCount = Math.ceil(framesCount / this.chunkSize);
|
||||||
|
|
||||||
@ -273,6 +246,33 @@ class RLottie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeData(animationData: string) {
|
||||||
|
this.pause();
|
||||||
|
this.animationData = animationData;
|
||||||
|
this.initConfig();
|
||||||
|
|
||||||
|
workers[this.workerIndex].request({
|
||||||
|
name: 'changeData',
|
||||||
|
args: [
|
||||||
|
this.key,
|
||||||
|
this.animationData,
|
||||||
|
this.params.isLowPriority,
|
||||||
|
this.onChangeData.bind(this),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onChangeData(reduceFactor: number, msPerFrame: number, framesCount: number) {
|
||||||
|
this.reduceFactor = reduceFactor;
|
||||||
|
this.msPerFrame = msPerFrame;
|
||||||
|
this.framesCount = framesCount;
|
||||||
|
this.chunksCount = Math.ceil(framesCount / this.chunkSize);
|
||||||
|
this.isWaiting = false;
|
||||||
|
this.isAnimating = false;
|
||||||
|
|
||||||
|
this.doPlay();
|
||||||
|
}
|
||||||
|
|
||||||
private doPlay() {
|
private doPlay() {
|
||||||
if (!this.framesCount) {
|
if (!this.framesCount) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { inflate } from 'pako/dist/pako_inflate';
|
||||||
import createWorkerInterface from '../../util/createWorkerInterface';
|
import createWorkerInterface from '../../util/createWorkerInterface';
|
||||||
import { CancellableCallback } from '../../util/WorkerConnector';
|
import { CancellableCallback } from '../../util/WorkerConnector';
|
||||||
|
|
||||||
@ -27,6 +28,9 @@ const rLottieApiPromise = new Promise<void>((resolve) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const HIGH_PRIORITY_MAX_FPS = 60;
|
||||||
|
const LOW_PRIORITY_MAX_FPS = 30;
|
||||||
|
|
||||||
const renderers = new Map<string, {
|
const renderers = new Map<string, {
|
||||||
imgSize: number;
|
imgSize: number;
|
||||||
reduceFactor: number;
|
reduceFactor: number;
|
||||||
@ -35,42 +39,70 @@ const renderers = new Map<string, {
|
|||||||
|
|
||||||
async function init(
|
async function init(
|
||||||
key: string,
|
key: string,
|
||||||
animationData: AnyLiteral,
|
animationData: string,
|
||||||
imgSize: number,
|
imgSize: number,
|
||||||
isLowPriority: boolean,
|
isLowPriority: boolean,
|
||||||
reduceFactor: number,
|
|
||||||
onInit: CancellableCallback,
|
onInit: CancellableCallback,
|
||||||
) {
|
) {
|
||||||
if (!rLottieApi) {
|
if (!rLottieApi) {
|
||||||
await rLottieApiPromise;
|
await rLottieApiPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = JSON.stringify(animationData);
|
const json = await extractJson(animationData);
|
||||||
const stringOnWasmHeap = allocate(intArrayFromString(json), 'i8', 0);
|
const stringOnWasmHeap = allocate(intArrayFromString(json), 'i8', 0);
|
||||||
const handle = rLottieApi.init();
|
const handle = rLottieApi.init();
|
||||||
const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap);
|
const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap);
|
||||||
rLottieApi.resize(handle, imgSize, imgSize);
|
rLottieApi.resize(handle, imgSize, imgSize);
|
||||||
|
|
||||||
renderers.set(key, { imgSize, reduceFactor, handle });
|
const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount);
|
||||||
|
|
||||||
onInit(Math.ceil(framesCount / reduceFactor));
|
renderers.set(key, { imgSize, reduceFactor, handle });
|
||||||
|
onInit(reduceFactor, msPerFrame, reducedFramesCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeData(
|
async function changeData(
|
||||||
key: string,
|
key: string,
|
||||||
animationData: AnyLiteral,
|
animationData: string,
|
||||||
|
isLowPriority: boolean,
|
||||||
onInit: CancellableCallback,
|
onInit: CancellableCallback,
|
||||||
) {
|
) {
|
||||||
if (!rLottieApi) {
|
if (!rLottieApi) {
|
||||||
await rLottieApiPromise;
|
await rLottieApiPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = JSON.stringify(animationData);
|
const json = await extractJson(animationData);
|
||||||
const { reduceFactor, handle } = renderers.get(key)!;
|
|
||||||
|
|
||||||
const stringOnWasmHeap = allocate(intArrayFromString(json), 'i8', 0);
|
const stringOnWasmHeap = allocate(intArrayFromString(json), 'i8', 0);
|
||||||
|
const { handle } = renderers.get(key)!;
|
||||||
const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap);
|
const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap);
|
||||||
onInit(Math.ceil(framesCount / reduceFactor));
|
|
||||||
|
const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount);
|
||||||
|
onInit(reduceFactor, msPerFrame, reducedFramesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractJson(animationData: string) {
|
||||||
|
const response = await fetch(animationData);
|
||||||
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
|
||||||
|
// Support deprecated JSON format cached locally
|
||||||
|
if (contentType?.startsWith('text/')) {
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
return inflate(arrayBuffer, { to: 'string' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcParams(json: string, isLowPriority: boolean, framesCount: number) {
|
||||||
|
const animationData = JSON.parse(json);
|
||||||
|
const maxFps = isLowPriority ? LOW_PRIORITY_MAX_FPS : HIGH_PRIORITY_MAX_FPS;
|
||||||
|
const sourceFps = animationData.fr || maxFps;
|
||||||
|
const reduceFactor = sourceFps % maxFps === 0 ? sourceFps / maxFps : 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
reduceFactor,
|
||||||
|
msPerFrame: 1000 / (sourceFps / reduceFactor),
|
||||||
|
reducedFramesCount: Math.ceil(framesCount / reduceFactor),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderFrames(
|
async function renderFrames(
|
||||||
|
|||||||
@ -243,10 +243,6 @@ export function getAudioHasCover(media: ApiAudio) {
|
|||||||
return media.thumbnailSizes && media.thumbnailSizes.length > 0;
|
return media.thumbnailSizes && media.thumbnailSizes.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMessageMediaFormat(
|
|
||||||
message: ApiMessage, target: Target,
|
|
||||||
): Exclude<ApiMediaFormat, ApiMediaFormat.Lottie>;
|
|
||||||
export function getMessageMediaFormat(message: ApiMessage, target: Target, canBeLottie: true): ApiMediaFormat;
|
|
||||||
export function getMessageMediaFormat(
|
export function getMessageMediaFormat(
|
||||||
message: ApiMessage, target: Target,
|
message: ApiMessage, target: Target,
|
||||||
): ApiMediaFormat {
|
): ApiMediaFormat {
|
||||||
|
|||||||
@ -35,12 +35,6 @@ export async function fetch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
|
|
||||||
// Safari does not return correct Content-Type header for webp images.
|
|
||||||
if (key.startsWith('sticker')) {
|
|
||||||
return new Blob([blob], { type: 'image/webp' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldRecreate = !blob.type || (!isHtmlAllowed && blob.type.includes('html'));
|
const shouldRecreate = !blob.type || (!isHtmlAllowed && blob.type.includes('html'));
|
||||||
// iOS Safari fails to preserve `type` in cache
|
// iOS Safari fails to preserve `type` in cache
|
||||||
let resolvedType = blob.type || contentType;
|
let resolvedType = blob.type || contentType;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
ApiMediaFormat,
|
ApiMediaFormat,
|
||||||
ApiMediaFormatToPrepared,
|
|
||||||
ApiOnProgress,
|
ApiOnProgress,
|
||||||
ApiParsedMedia,
|
ApiParsedMedia,
|
||||||
ApiPreparedMedia,
|
ApiPreparedMedia,
|
||||||
@ -18,7 +17,7 @@ import { webpToPng } from './webpToPng';
|
|||||||
|
|
||||||
const asCacheApiType = {
|
const asCacheApiType = {
|
||||||
[ApiMediaFormat.BlobUrl]: cacheApi.Type.Blob,
|
[ApiMediaFormat.BlobUrl]: cacheApi.Type.Blob,
|
||||||
[ApiMediaFormat.Lottie]: cacheApi.Type.Json,
|
[ApiMediaFormat.Lottie]: cacheApi.Type.Blob,
|
||||||
[ApiMediaFormat.Progressive]: undefined,
|
[ApiMediaFormat.Progressive]: undefined,
|
||||||
[ApiMediaFormat.Stream]: undefined,
|
[ApiMediaFormat.Stream]: undefined,
|
||||||
};
|
};
|
||||||
@ -36,13 +35,13 @@ export function fetch<T extends ApiMediaFormat>(
|
|||||||
isHtmlAllowed = false,
|
isHtmlAllowed = false,
|
||||||
onProgress?: ApiOnProgress,
|
onProgress?: ApiOnProgress,
|
||||||
callbackUniqueId?: string,
|
callbackUniqueId?: string,
|
||||||
): Promise<ApiMediaFormatToPrepared<T>> {
|
): Promise<ApiPreparedMedia> {
|
||||||
if (mediaFormat === ApiMediaFormat.Progressive) {
|
if (mediaFormat === ApiMediaFormat.Progressive) {
|
||||||
return (
|
return (
|
||||||
IS_PROGRESSIVE_SUPPORTED
|
IS_PROGRESSIVE_SUPPORTED
|
||||||
? getProgressive(url)
|
? getProgressive(url)
|
||||||
: fetch(url, ApiMediaFormat.BlobUrl, isHtmlAllowed, onProgress, callbackUniqueId)
|
: fetch(url, ApiMediaFormat.BlobUrl, isHtmlAllowed, onProgress, callbackUniqueId)
|
||||||
) as Promise<ApiMediaFormatToPrepared<T>>;
|
) as Promise<ApiPreparedMedia>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchPromises.has(url)) {
|
if (!fetchPromises.has(url)) {
|
||||||
@ -73,11 +72,11 @@ export function fetch<T extends ApiMediaFormat>(
|
|||||||
activeCallbacks.set(callbackUniqueId, onProgress);
|
activeCallbacks.set(callbackUniqueId, onProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchPromises.get(url) as Promise<ApiMediaFormatToPrepared<T>>;
|
return fetchPromises.get(url) as Promise<ApiPreparedMedia>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFromMemory<T extends ApiMediaFormat>(url: string) {
|
export function getFromMemory(url: string) {
|
||||||
return memoryCache.get(url) as ApiMediaFormatToPrepared<T>;
|
return memoryCache.get(url) as ApiPreparedMedia;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cancelProgress(progressCallback: ApiOnProgress) {
|
export function cancelProgress(progressCallback: ApiOnProgress) {
|
||||||
@ -214,7 +213,7 @@ function makeOnProgress(url: string, mediaSource?: MediaSource, sourceBuffer?: S
|
|||||||
return onProgress;
|
return onProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia {
|
function prepareMedia(mediaData: Exclude<ApiParsedMedia, ArrayBuffer>): ApiPreparedMedia {
|
||||||
if (mediaData instanceof Blob) {
|
if (mediaData instanceof Blob) {
|
||||||
return URL.createObjectURL(mediaData);
|
return URL.createObjectURL(mediaData);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -308,10 +308,10 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
|
|||||||
async function getAvatar(chat: ApiChat) {
|
async function getAvatar(chat: ApiChat) {
|
||||||
const imageHash = getChatAvatarHash(chat);
|
const imageHash = getChatAvatarHash(chat);
|
||||||
if (!imageHash) return undefined;
|
if (!imageHash) return undefined;
|
||||||
let mediaData = mediaLoader.getFromMemory<ApiMediaFormat.BlobUrl>(imageHash);
|
let mediaData = mediaLoader.getFromMemory(imageHash);
|
||||||
if (!mediaData) {
|
if (!mediaData) {
|
||||||
await mediaLoader.fetch(imageHash, ApiMediaFormat.BlobUrl);
|
await mediaLoader.fetch(imageHash, ApiMediaFormat.BlobUrl);
|
||||||
mediaData = mediaLoader.getFromMemory<ApiMediaFormat.BlobUrl>(imageHash);
|
mediaData = mediaLoader.getFromMemory(imageHash);
|
||||||
}
|
}
|
||||||
return mediaData;
|
return mediaData;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user