diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index 92e7ccde1..ae47e02bd 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -1,5 +1,3 @@ -import { inflate } from 'pako/dist/pako_inflate'; - import { Api as GramJs, TelegramClient } from '../../../lib/gramjs'; import { ApiMediaFormat, ApiOnProgress, ApiParsedMedia, ApiPreparedMedia, @@ -20,7 +18,9 @@ type EntityType = ( 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | '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( { @@ -53,7 +53,7 @@ export default async function downloadMedia( 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; return { @@ -183,7 +183,7 @@ async function download( return { mimeType, data, fullSize }; } else if (entityType === 'stickerSet') { 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 }; } else { @@ -228,10 +228,8 @@ async function parseMedia( ): Promise { switch (mediaFormat) { case ApiMediaFormat.BlobUrl: - return new Blob([data], { type: mimeType }); case ApiMediaFormat.Lottie: { - const json = inflate(data, { to: 'string' }); - return JSON.parse(json); + return new Blob([data], { type: mimeType }); } case ApiMediaFormat.Progressive: { return data.buffer; @@ -241,7 +239,7 @@ async function parseMedia( return undefined; } -function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia { +function prepareMedia(mediaData: Exclude): ApiPreparedMedia { if (mediaData instanceof Blob) { return URL.createObjectURL(mediaData); } diff --git a/src/api/types/media.ts b/src/api/types/media.ts index b5fef6afa..2a19b9411 100644 --- a/src/api/types/media.ts +++ b/src/api/types/media.ts @@ -8,6 +8,5 @@ export enum ApiMediaFormat { Stream, } -export type ApiParsedMedia = string | Blob | AnyLiteral | ArrayBuffer; -export type ApiPreparedMedia = string | AnyLiteral; -export type ApiMediaFormatToPrepared = T extends ApiMediaFormat.Lottie ? AnyLiteral : string; +export type ApiParsedMedia = string | Blob | ArrayBuffer; +export type ApiPreparedMedia = string; diff --git a/src/components/common/AnimatedEmoji.tsx b/src/components/common/AnimatedEmoji.tsx index fb1dbb3db..ff2a8153b 100644 --- a/src/components/common/AnimatedEmoji.tsx +++ b/src/components/common/AnimatedEmoji.tsx @@ -89,7 +89,7 @@ const AnimatedEmoji: FC = ({ = ({ playSegment, color, }) => { - const [iconData, setIconData] = useState>(); + const [iconData, setIconData] = useState(); useEffect(() => { getAnimationData(name).then(setIconData); diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index 2319b6e1a..cf3271bbe 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -10,7 +10,7 @@ import useBackgroundMode from '../../hooks/useBackgroundMode'; type OwnProps = { className?: string; id: string; - animationData: AnyLiteral; + animationData?: string; play?: boolean | string; playSegment?: [number, number]; speed?: number; @@ -202,7 +202,7 @@ const AnimatedSticker: FC = ({ if (animation) { if (isFirstRender.current) { isFirstRender.current = false; - } else { + } else if (animationData) { animation.changeData(animationData); playAnimation(); } diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index 2d0131cb0..58e44b58d 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -3,7 +3,7 @@ import React, { } from '../../lib/teact/teact'; import { getDispatch } from '../../lib/teact/teactn'; -import { ApiMediaFormat, ApiMessage } from '../../api/types'; +import { ApiMessage } from '../../api/types'; import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo'; import { @@ -83,7 +83,7 @@ const Document: FC = ({ const shouldDownload = Boolean(isDownloading || (isLoadAllowed && wasIntersected)); const documentHash = getMessageMediaHash(message, 'download'); - const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress( + const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress( documentHash, !shouldDownload, undefined, undefined, undefined, true, ); const isLoaded = Boolean(mediaData); diff --git a/src/components/common/LocalAnimatedEmoji.tsx b/src/components/common/LocalAnimatedEmoji.tsx index 38926615d..081552963 100644 --- a/src/components/common/LocalAnimatedEmoji.tsx +++ b/src/components/common/LocalAnimatedEmoji.tsx @@ -49,7 +49,7 @@ const LocalAnimatedEmoji: FC = ({ const isIntersecting = useIsIntersecting(ref, observeIntersection); - const [localStickerAnimationData, setLocalStickerAnimationData] = useState(); + const [localStickerAnimationData, setLocalStickerAnimationData] = useState(); useEffect(() => { if (localSticker) { getAnimationData(localSticker as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => { diff --git a/src/components/common/PasswordMonkey.tsx b/src/components/common/PasswordMonkey.tsx index b50e2c3cf..a2ccc669a 100644 --- a/src/components/common/PasswordMonkey.tsx +++ b/src/components/common/PasswordMonkey.tsx @@ -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 PasswordMonkey: FC = ({ isPasswordVisible, isBig }) => { - const [closeMonkeyData, setCloseMonkeyData] = useState>(); - const [peekMonkeyData, setPeekMonkeyData] = useState>(); + const [closeMonkeyData, setCloseMonkeyData] = useState(); + const [peekMonkeyData, setPeekMonkeyData] = useState(); const [isFirstMonkeyLoaded, setIsFirstMonkeyLoaded] = useState(false); const [isPeekShown, setIsPeekShown] = useState(false); diff --git a/src/components/common/TrackingMonkey.tsx b/src/components/common/TrackingMonkey.tsx index 5ab562ad3..aee345229 100644 --- a/src/components/common/TrackingMonkey.tsx +++ b/src/components/common/TrackingMonkey.tsx @@ -29,8 +29,8 @@ const TrackingMonkey: FC = ({ isTracking, isBig, }) => { - const [idleMonkeyData, setIdleMonkeyData] = useState>(); - const [trackingMonkeyData, setTrackingMonkeyData] = useState>(); + const [idleMonkeyData, setIdleMonkeyData] = useState(); + const [trackingMonkeyData, setTrackingMonkeyData] = useState(); const [isFirstMonkeyLoaded, setIsFirstMonkeyLoaded] = useState(false); const TRACKING_FRAMES_PER_SYMBOL = (TRACKING_END_FRAME - TRACKING_START_FRAME) / codeLength; diff --git a/src/components/left/settings/folders/SettingsFoldersEdit.tsx b/src/components/left/settings/folders/SettingsFoldersEdit.tsx index c87ca4546..6940006d6 100644 --- a/src/components/left/settings/folders/SettingsFoldersEdit.tsx +++ b/src/components/left/settings/folders/SettingsFoldersEdit.tsx @@ -69,7 +69,7 @@ const SettingsFoldersEdit: FC = ({ loadMoreChats, } = getDispatch(); - const [animationData, setAnimationData] = useState>(); + const [animationData, setAnimationData] = useState(); const [isAnimationLoaded, setIsAnimationLoaded] = useState(false); const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []); diff --git a/src/components/left/settings/folders/SettingsFoldersMain.tsx b/src/components/left/settings/folders/SettingsFoldersMain.tsx index 18b49d1aa..2af0747eb 100644 --- a/src/components/left/settings/folders/SettingsFoldersMain.tsx +++ b/src/components/left/settings/folders/SettingsFoldersMain.tsx @@ -64,7 +64,7 @@ const SettingsFoldersMain: FC = ({ showDialog, } = getDispatch(); - const [animationData, setAnimationData] = useState>(); + const [animationData, setAnimationData] = useState(); const [isAnimationLoaded, setIsAnimationLoaded] = useState(false); const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []); diff --git a/src/components/main/DownloadManager.tsx b/src/components/main/DownloadManager.tsx index 4f0a3b364..0af0e3602 100644 --- a/src/components/main/DownloadManager.tsx +++ b/src/components/main/DownloadManager.tsx @@ -37,7 +37,7 @@ const DownloadManager: FC = ({ } if (!startedDownloads.has(downloadHash)) { - const mediaData = mediaLoader.getFromMemory(downloadHash); + const mediaData = mediaLoader.getFromMemory(downloadHash); if (mediaData) { startedDownloads.delete(downloadHash); download(mediaData, getMessageContentFilename(message)); diff --git a/src/components/middle/EmojiInteractionAnimation.tsx b/src/components/middle/EmojiInteractionAnimation.tsx index 65c1639e6..ecf0a9922 100644 --- a/src/components/middle/EmojiInteractionAnimation.tsx +++ b/src/components/middle/EmojiInteractionAnimation.tsx @@ -75,9 +75,9 @@ const EmojiInteractionAnimation: FC = ({ }, PLAYING_DURATION); }, [stop]); - const mediaDataEffect = useMedia(`sticker${effectAnimationId}`, !effectAnimationId, ApiMediaFormat.Lottie); + const effectAnimationData = useMedia(`sticker${effectAnimationId}`, !effectAnimationId, ApiMediaFormat.Lottie); - const [localEffectAnimationData, setLocalEffectAnimationData] = useState(); + const [localEffectAnimationData, setLocalEffectAnimationData] = useState(); useEffect(() => { if (localEffectAnimation) { getAnimationData(localEffectAnimation as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => { @@ -99,7 +99,7 @@ const EmojiInteractionAnimation: FC = ({ = ({ id={`reaction_emoji_${centerIconId}`} className={animationClassNames} size={CENTER_ICON_SIZE} - animationData={mediaDataCenterIcon as AnyLiteral} + animationData={mediaDataCenterIcon} play noLoop onLoad={markAnimationLoaded} @@ -78,7 +78,7 @@ const ReactionAnimatedEmoji: FC = ({ id={`reaction_effect_${effectId}`} className={buildClassName('effect', animationClassNames)} size={EFFECT_SIZE} - animationData={mediaDataEffect as AnyLiteral} + animationData={mediaDataEffect} play noLoop onEnded={handleEnded} diff --git a/src/components/middle/message/ReactionSelector.tsx b/src/components/middle/message/ReactionSelector.tsx index 6c799ca83..67b5ed93c 100644 --- a/src/components/middle/message/ReactionSelector.tsx +++ b/src/components/middle/message/ReactionSelector.tsx @@ -65,7 +65,7 @@ const AvailableReaction: FC<{ = ({ const mediaData = useMedia( mediaHash, !shouldLoad, - getMessageMediaFormat(message, 'inline', true), + getMessageMediaFormat(message, 'inline'), lastSyncTime, ); @@ -89,7 +89,7 @@ const Sticker: FC = ({ key={mediaHash} className={buildClassName('full-media', transitionClassNames)} id={mediaHash} - animationData={mediaData as AnyLiteral} + animationData={mediaData} size={width} play={shouldPlay} noLoop={!shouldLoop} diff --git a/src/components/right/management/ManageDiscussion.tsx b/src/components/right/management/ManageDiscussion.tsx index 0fe7c6faf..151fc3cae 100644 --- a/src/components/right/management/ManageDiscussion.tsx +++ b/src/components/right/management/ManageDiscussion.tsx @@ -55,7 +55,7 @@ const ManageDiscussion: FC = ({ } = getDispatch(); const [linkedGroupId, setLinkedGroupId] = useState(); - const [animationData, setAnimationData] = useState>(); + const [animationData, setAnimationData] = useState(); const [isAnimationLoaded, setIsAnimationLoaded] = useState(false); const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []); const [isConfirmUnlinkGroupDialogOpen, openConfirmUnlinkGroupDialog, closeConfirmUnlinkGroupDialog] = useFlag(); diff --git a/src/hooks/useMedia.ts b/src/hooks/useMedia.ts index 4fea31008..3865e2f11 100644 --- a/src/hooks/useMedia.ts +++ b/src/hooks/useMedia.ts @@ -13,7 +13,7 @@ export default ( cacheBuster?: number, delay?: number | false, ) => { - const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined; + const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined; const forceUpdate = useForceUpdate(); useEffect(() => { diff --git a/src/hooks/useMediaWithLoadProgress.ts b/src/hooks/useMediaWithLoadProgress.ts index 90866a2dc..30b752d39 100644 --- a/src/hooks/useMediaWithLoadProgress.ts +++ b/src/hooks/useMediaWithLoadProgress.ts @@ -22,7 +22,7 @@ export default function useMediaWithLoadProgress(mediaHash) : undefined; + const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined; const isStreaming = mediaFormat === ApiMediaFormat.Stream || ( IS_PROGRESSIVE_SUPPORTED && mediaFormat === ApiMediaFormat.Progressive ); diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index a3b23071c..7f3b26f23 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -22,8 +22,6 @@ const CHUNK_SIZE = 1; const MAX_WORKERS = 4; const HIGH_PRIORITY_QUALITY = IS_SINGLE_COLUMN_LAYOUT ? 0.75 : 1; 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 LOW_PRIORITY_CACHE_MODULO = 0; @@ -39,9 +37,9 @@ class RLottie { private key!: string; - private msPerFrame!: number; + private msPerFrame = 1000 / 60; - private reduceFactor!: number; + private reduceFactor = 1; private cacheModulo!: number; @@ -86,7 +84,7 @@ class RLottie { constructor( private id: string, private container: HTMLDivElement, - private animationData: AnyLiteral, + private animationData: string, private params: Params = {}, private onLoad?: () => void, private customColor?: [number, number, number], @@ -189,10 +187,6 @@ class RLottie { 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.chunkSize = CHUNK_SIZE; } @@ -201,17 +195,10 @@ class RLottie { 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) { this.customColor = newColor; + + // TODO Remove? if (this.customColor) { const imageData = this.ctx.getImageData(0, 0, this.imgSize, this.imgSize); 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() { this.workerIndex = cycleRestrict(MAX_WORKERS, ++lastWorkerIndex); @@ -251,7 +223,6 @@ class RLottie { this.animationData, this.imgSize, this.params.isLowPriority, - this.reduceFactor, 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.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() { if (!this.framesCount) { return; diff --git a/src/lib/rlottie/rlottie.worker.ts b/src/lib/rlottie/rlottie.worker.ts index a976bec39..46212f759 100644 --- a/src/lib/rlottie/rlottie.worker.ts +++ b/src/lib/rlottie/rlottie.worker.ts @@ -1,3 +1,4 @@ +import { inflate } from 'pako/dist/pako_inflate'; import createWorkerInterface from '../../util/createWorkerInterface'; import { CancellableCallback } from '../../util/WorkerConnector'; @@ -27,6 +28,9 @@ const rLottieApiPromise = new Promise((resolve) => { }; }); +const HIGH_PRIORITY_MAX_FPS = 60; +const LOW_PRIORITY_MAX_FPS = 30; + const renderers = new Map 0; } -export function getMessageMediaFormat( - message: ApiMessage, target: Target, -): Exclude; -export function getMessageMediaFormat(message: ApiMessage, target: Target, canBeLottie: true): ApiMediaFormat; export function getMessageMediaFormat( message: ApiMessage, target: Target, ): ApiMediaFormat { diff --git a/src/util/cacheApi.ts b/src/util/cacheApi.ts index 5ffb5ee7d..bc9703061 100644 --- a/src/util/cacheApi.ts +++ b/src/util/cacheApi.ts @@ -35,12 +35,6 @@ export async function fetch( } 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')); // iOS Safari fails to preserve `type` in cache let resolvedType = blob.type || contentType; diff --git a/src/util/mediaLoader.ts b/src/util/mediaLoader.ts index 4623db0ca..5b5befd83 100644 --- a/src/util/mediaLoader.ts +++ b/src/util/mediaLoader.ts @@ -1,6 +1,5 @@ import { ApiMediaFormat, - ApiMediaFormatToPrepared, ApiOnProgress, ApiParsedMedia, ApiPreparedMedia, @@ -18,7 +17,7 @@ import { webpToPng } from './webpToPng'; const asCacheApiType = { [ApiMediaFormat.BlobUrl]: cacheApi.Type.Blob, - [ApiMediaFormat.Lottie]: cacheApi.Type.Json, + [ApiMediaFormat.Lottie]: cacheApi.Type.Blob, [ApiMediaFormat.Progressive]: undefined, [ApiMediaFormat.Stream]: undefined, }; @@ -36,13 +35,13 @@ export function fetch( isHtmlAllowed = false, onProgress?: ApiOnProgress, callbackUniqueId?: string, -): Promise> { +): Promise { if (mediaFormat === ApiMediaFormat.Progressive) { return ( IS_PROGRESSIVE_SUPPORTED ? getProgressive(url) : fetch(url, ApiMediaFormat.BlobUrl, isHtmlAllowed, onProgress, callbackUniqueId) - ) as Promise>; + ) as Promise; } if (!fetchPromises.has(url)) { @@ -73,11 +72,11 @@ export function fetch( activeCallbacks.set(callbackUniqueId, onProgress); } - return fetchPromises.get(url) as Promise>; + return fetchPromises.get(url) as Promise; } -export function getFromMemory(url: string) { - return memoryCache.get(url) as ApiMediaFormatToPrepared; +export function getFromMemory(url: string) { + return memoryCache.get(url) as ApiPreparedMedia; } export function cancelProgress(progressCallback: ApiOnProgress) { @@ -214,7 +213,7 @@ function makeOnProgress(url: string, mediaSource?: MediaSource, sourceBuffer?: S return onProgress; } -function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia { +function prepareMedia(mediaData: Exclude): ApiPreparedMedia { if (mediaData instanceof Blob) { return URL.createObjectURL(mediaData); } diff --git a/src/util/notifications.ts b/src/util/notifications.ts index 8388286fe..db52d7146 100644 --- a/src/util/notifications.ts +++ b/src/util/notifications.ts @@ -308,10 +308,10 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) { async function getAvatar(chat: ApiChat) { const imageHash = getChatAvatarHash(chat); if (!imageHash) return undefined; - let mediaData = mediaLoader.getFromMemory(imageHash); + let mediaData = mediaLoader.getFromMemory(imageHash); if (!mediaData) { await mediaLoader.fetch(imageHash, ApiMediaFormat.BlobUrl); - mediaData = mediaLoader.getFromMemory(imageHash); + mediaData = mediaLoader.getFromMemory(imageHash); } return mediaData; }