From 6f731d3b6307c8459ad5b14f7c796ead2c0d2024 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 28 Jul 2020 02:46:04 +0400 Subject: [PATCH] Streaming sticker caching --- .../Sources/AnimatedStickerNode.swift | 352 +++++++++++++++++- .../Sources/SoftwareAnimationRenderer.swift | 9 +- .../Sources/ChatListEmptyNode.swift | 2 +- .../ChatListFilterSettingsHeaderItem.swift | 2 +- .../Sources/LegacyPaintStickerView.swift | 2 +- .../TwoFactorAuthDataInputScreen.swift | 4 +- .../Sources/TwoFactorAuthSplashScreen.swift | 4 +- .../Sources/PeersNearbyHeaderItem.swift | 2 +- submodules/Postbox/Sources/MediaBox.swift | 5 + .../StatisticsUI/Sources/StatsEmptyItem.swift | 2 +- .../Sources/StickerPreviewPeekContent.swift | 2 +- .../ChatMessageAnimatedStickerItemNode.swift | 10 +- .../Sources/NotificationContentContext.swift | 2 +- .../TooltipUI/Sources/TooltipScreen.swift | 4 +- .../Sources/UndoOverlayControllerNode.swift | 4 +- 15 files changed, 369 insertions(+), 37 deletions(-) diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 887a0cee0a..ebbf1292e8 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -5,8 +5,10 @@ import Display import AsyncDisplayKit import RLottieBinding import GZip +import YuvConversion private let sharedQueue = Queue() +private let sharedStoreQueue = Queue.concurrentDefaultQueue() private class AnimatedStickerNodeDisplayEvents: ASDisplayNode { private var value: Bool = false @@ -46,7 +48,7 @@ private class AnimatedStickerNodeDisplayEvents: ASDisplayNode { public enum AnimatedStickerMode { case cached - case direct + case direct(cachePathPrefix: String?) } public enum AnimatedStickerPlaybackPosition { @@ -267,11 +269,321 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource } } +private func wrappedWrite(_ fd: Int32, _ data: UnsafeRawPointer, _ count: Int) -> Int { + return write(fd, data, count) +} + +private func wrappedRead(_ fd: Int32, _ data: UnsafeMutableRawPointer, _ count: Int) -> Int { + return read(fd, data, count) +} + +//TODO: separate ManagedFile into its own module +private final class ManagedFileImpl { + enum Mode { + case read + case readwrite + case append + } + + private let queue: Queue? + private let fd: Int32 + private let mode: Mode + + init?(queue: Queue?, path: String, mode: Mode) { + if let queue = queue { + assert(queue.isCurrent()) + } + self.queue = queue + self.mode = mode + let fileMode: Int32 + let accessMode: UInt16 + switch mode { + case .read: + fileMode = O_RDONLY + accessMode = S_IRUSR + case .readwrite: + fileMode = O_RDWR | O_CREAT + accessMode = S_IRUSR | S_IWUSR + case .append: + fileMode = O_WRONLY | O_CREAT | O_APPEND + accessMode = S_IRUSR | S_IWUSR + } + let fd = open(path, fileMode, accessMode) + if fd >= 0 { + self.fd = fd + } else { + return nil + } + } + + deinit { + if let queue = self.queue { + assert(queue.isCurrent()) + } + close(self.fd) + } + + public func write(_ data: UnsafeRawPointer, count: Int) -> Int { + if let queue = self.queue { + assert(queue.isCurrent()) + } + return wrappedWrite(self.fd, data, count) + } + + public func read(_ data: UnsafeMutableRawPointer, _ count: Int) -> Int { + if let queue = self.queue { + assert(queue.isCurrent()) + } + return wrappedRead(self.fd, data, count) + } + + public func readData(count: Int) -> Data { + if let queue = self.queue { + assert(queue.isCurrent()) + } + var result = Data(count: count) + result.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + let readCount = self.read(bytes, count) + assert(readCount == count) + } + return result + } + + public func seek(position: Int64) { + if let queue = self.queue { + assert(queue.isCurrent()) + } + lseek(self.fd, position, SEEK_SET) + } + + public func truncate(count: Int64) { + if let queue = self.queue { + assert(queue.isCurrent()) + } + ftruncate(self.fd, count) + } + + public func getSize() -> Int? { + if let queue = self.queue { + assert(queue.isCurrent()) + } + var value = stat() + if fstat(self.fd, &value) == 0 { + return Int(value.st_size) + } else { + return nil + } + } + + public func sync() { + if let queue = self.queue { + assert(queue.isCurrent()) + } + fsync(self.fd) + } +} + +private func compressFrame(width: Int, height: Int, rgbData: Data) -> Data? { + let bytesPerRow = rgbData.count / height + + let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1) + assert(yuvaPixelsPerAlphaRow % 2 == 0) + + let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2 + var yuvaFrameData = malloc(yuvaLength)! + defer { + free(yuvaFrameData) + } + memset(yuvaFrameData, 0, yuvaLength) + + var compressedFrameData = Data(count: yuvaLength) + let compressedFrameDataLength = compressedFrameData.count + + let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))! + defer { + free(scratchData) + } + + var rgbData = rgbData + rgbData.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) -> Void in + if let baseAddress = buffer.baseAddress { + encodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), baseAddress.assumingMemoryBound(to: UInt8.self), Int32(width), Int32(height), Int32(bytesPerRow)) + } + } + + var maybeResultSize: Int? + + compressedFrameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + let length = compression_encode_buffer(bytes, compressedFrameDataLength, yuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE) + maybeResultSize = length + } + + guard let resultSize = maybeResultSize else { + return nil + } + compressedFrameData.count = resultSize + return compressedFrameData +} + +private final class AnimatedStickerDirectFrameSourceCache { + private let queue: Queue + private let storeQueue: Queue + private let file: ManagedFileImpl + private let frameCount: Int + private let width: Int + private let height: Int + + private var isStoringFrames = Set() + + private var scratchBuffer: Data + private var decodeBuffer: Data + + init?(queue: Queue, pathPrefix: String, width: Int, height: Int, frameCount: Int) { + self.queue = queue + self.storeQueue = sharedStoreQueue + + self.frameCount = frameCount + self.width = width + self.height = height + + let path = "\(pathPrefix)_\(width):\(height).stickerframecache" + var file = ManagedFileImpl(queue: queue, path: path, mode: .readwrite) + if let file = file { + self.file = file + } else { + let _ = try? FileManager.default.removeItem(atPath: path) + file = ManagedFileImpl(queue: queue, path: path, mode: .readwrite) + if let file = file { + self.file = file + } else { + return nil + } + } + + self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE)) + + let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1) + let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2 + self.decodeBuffer = Data(count: yuvaLength) + + self.initializeFrameTable() + } + + private func initializeFrameTable() { + if let size = self.file.getSize(), size >= self.frameCount * 4 * 2 { + } else { + self.file.truncate(count: 0) + for _ in 0 ..< self.frameCount { + var zero: Int32 = 0 + let _ = self.file.write(&zero, count: 4) + let _ = self.file.write(&zero, count: 4) + } + } + } + + private func readFrameRange(index: Int) -> Range? { + if index < 0 || index >= self.frameCount { + return nil + } + + self.file.seek(position: Int64(index * 4 * 2)) + var offset: Int32 = 0 + var length: Int32 = 0 + if self.file.read(&offset, 4) != 4 { + return nil + } + if self.file.read(&length, 4) != 4 { + return nil + } + if length == 0 { + return nil + } + if length < 0 || offset < 0 { + return nil + } + return (Int(offset) ..< Int(offset + length)) + } + + func storeUncompressedRgbFrame(index: Int, rgbData: Data) { + if index < 0 || index >= self.frameCount { + return + } + if self.isStoringFrames.contains(index) { + return + } + self.isStoringFrames.insert(index) + + let width = self.width + let height = self.height + + let queue = self.queue + self.storeQueue.async { [weak self] in + let compressedData = compressFrame(width: width, height: height, rgbData: rgbData) + + queue.async { + guard let strongSelf = self else { + return + } + guard let currentSize = strongSelf.file.getSize() else { + return + } + guard let compressedData = compressedData else { + return + } + + strongSelf.file.seek(position: Int64(index * 4 * 2)) + var offset = Int32(currentSize) + var length = Int32(compressedData.count) + let _ = strongSelf.file.write(&offset, count: 4) + let _ = strongSelf.file.write(&length, count: 4) + strongSelf.file.seek(position: Int64(currentSize)) + compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in + if let baseAddress = buffer.baseAddress { + let _ = strongSelf.file.write(baseAddress, count: Int(length)) + } + } + } + } + } + + func readUncompressedYuvFrame(index: Int) -> Data? { + if index < 0 || index >= self.frameCount { + return nil + } + guard let range = self.readFrameRange(index: index) else { + return nil + } + self.file.seek(position: Int64(range.lowerBound)) + let length = range.upperBound - range.lowerBound + let compressedData = self.file.readData(count: length) + if compressedData.count != length { + return nil + } + + var frameData: Data? + + let decodeBufferLength = self.decodeBuffer.count + + compressedData.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer) -> Void in + self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer) -> Void in + let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE) + + frameData = Data(bytes: decodeBytes, count: resultLength) + } + } + } + + return frameData + } +} + private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource { private let queue: Queue private let data: Data private let width: Int private let height: Int + private let cache: AnimatedStickerDirectFrameSourceCache? private let bytesPerRow: Int let frameCount: Int let frameRate: Int @@ -282,7 +594,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource return self.currentFrame % self.frameCount } - init?(queue: Queue, data: Data, width: Int, height: Int) { + init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?) { self.queue = queue self.data = data self.width = width @@ -294,8 +606,13 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource return nil } self.animation = animation - self.frameCount = Int(animation.frameCount) + let frameCount = Int(animation.frameCount) + self.frameCount = frameCount self.frameRate = Int(animation.frameRate) + + self.cache = cachePathPrefix.flatMap { cachePathPrefix in + AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount) + } } deinit { @@ -306,12 +623,19 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource let frameIndex = self.currentFrame % self.frameCount self.currentFrame += 1 if draw { - var frameData = Data(count: self.bytesPerRow * self.height) - frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in - memset(bytes, 0, self.bytesPerRow * self.height) - self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow)) + if let cache = self.cache, let yuvData = cache.readUncompressedYuvFrame(index: frameIndex) { + return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: 0, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1) + } else { + var frameData = Data(count: self.bytesPerRow * self.height) + frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + memset(bytes, 0, self.bytesPerRow * self.height) + self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow)) + } + if let cache = self.cache { + cache.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData) + } + return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1) } - return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1) } else { return nil } @@ -409,7 +733,7 @@ public final class AnimatedStickerNode: ASDisplayNode { private let timer = Atomic(value: nil) private let frameSource = Atomic?>(value: nil) - private var directData: (Data, String, Int, Int)? + private var directData: (Data, String, Int, Int, String?)? private var cachedData: (Data, Bool)? private var renderer: (AnimationRenderer & ASDisplayNode)? @@ -479,13 +803,13 @@ public final class AnimatedStickerNode: ASDisplayNode { } self.playbackMode = playbackMode switch mode { - case .direct: + case let .direct(cachePathPrefix): let f: (String) -> Void = { [weak self] path in guard let strongSelf = self else { return } if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { - strongSelf.directData = (directData, path, width, height) + strongSelf.directData = (directData, path, width, height, cachePathPrefix) } if case let .still(position) = playbackMode { strongSelf.seekTo(position) @@ -568,7 +892,7 @@ public final class AnimatedStickerNode: ASDisplayNode { if maybeFrameSource == nil { let notifyUpdated: (() -> Void)? = nil if let directData = directData { - maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3) + maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4) } else if let (cachedData, cachedDataComplete) = cachedData { if #available(iOS 9.0, *) { maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: { @@ -640,7 +964,7 @@ public final class AnimatedStickerNode: ASDisplayNode { var maybeFrameSource: AnimatedStickerFrameSource? let notifyUpdated: (() -> Void)? = nil if let directData = directData { - maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3) + maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4) } else if let (cachedData, cachedDataComplete) = cachedData { if #available(iOS 9.0, *) { maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: { @@ -730,7 +1054,7 @@ public final class AnimatedStickerNode: ASDisplayNode { } else { var maybeFrameSource: AnimatedStickerFrameSource? if let directData = directData { - maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3) + maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4) if case .end = position { maybeFrameSource?.skipToEnd() } diff --git a/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift b/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift index 252cd89ae3..234698994d 100644 --- a/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift +++ b/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift @@ -8,8 +8,13 @@ import YuvConversion final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer { func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, completion: @escaping () -> Void) { queue.async { [weak self] in - let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15) - assert(bytesPerRow == calculatedBytesPerRow) + switch type { + case .argb: + let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15) + assert(bytesPerRow == calculatedBytesPerRow) + case .yuva: + break + } let image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, bytesPerRow in switch type { diff --git a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift index 46a9794511..8cb64b4e0f 100644 --- a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift +++ b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift @@ -68,7 +68,7 @@ final class ChatListEmptyNode: ASDisplayNode { animationName = "ChatListEmpty" } if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") { - self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct) + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animationSize = CGSize(width: 124.0, height: 124.0) self.animationNode.visibility = true } diff --git a/submodules/ChatListUI/Sources/ChatListFilterSettingsHeaderItem.swift b/submodules/ChatListUI/Sources/ChatListFilterSettingsHeaderItem.swift index b80cceed46..d322c174fa 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterSettingsHeaderItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterSettingsHeaderItem.swift @@ -126,7 +126,7 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode { animationName = "ChatListNewFolder" } if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") { - strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct) + strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) strongSelf.animationNode.visibility = true } } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift index 595dea2b24..36bdadbdb2 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift @@ -117,7 +117,7 @@ class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView { let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource) - self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct) + self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) |> deliverOn(Queue.concurrentDefaultQueue())).start()) diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index eadc36ff4a..a4f3dcc72a 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -700,14 +700,14 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS case .emailConfirmation: if let path = getAppBundle().path(forResource: "TwoFactorSetupMail", ofType: "tgs") { let animatedStickerNode = AnimatedStickerNode() - animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct) + animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) animatedStickerNode.visibility = true self.animatedStickerNode = animatedStickerNode } case .passwordHint: if let path = getAppBundle().path(forResource: "TwoFactorSetupHint", ofType: "tgs") { let animatedStickerNode = AnimatedStickerNode() - animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct) + animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) animatedStickerNode.visibility = true self.animatedStickerNode = animatedStickerNode } diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift index 48428f078e..af6154e147 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift @@ -112,7 +112,7 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { buttonText = self.presentationData.strings.TwoFactorSetup_Intro_Action if let path = getAppBundle().path(forResource: "TwoFactorSetupIntro", ofType: "tgs") { - self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct) + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animationSize = CGSize(width: 124.0, height: 124.0) self.animationNode.visibility = true } @@ -122,7 +122,7 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { buttonText = self.presentationData.strings.TwoFactorSetup_Done_Action if let path = getAppBundle().path(forResource: "TwoFactorSetupDone", ofType: "tgs") { - self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, mode: .direct) + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, mode: .direct(cachePathPrefix: nil)) self.animationSize = CGSize(width: 124.0, height: 124.0) self.animationNode.visibility = true } diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift index ed5a84dc13..223449a8ff 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift @@ -73,7 +73,7 @@ class PeersNearbyHeaderItemNode: ListViewItemNode { self.animationNode = AnimatedStickerNode() if let path = getAppBundle().path(forResource: "Compass", ofType: "tgs") { - self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct) + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animationNode.visibility = true } diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 3a3ff6f109..e9f267e3bf 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -239,6 +239,11 @@ public final class MediaBox { return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)" } + public func shortLivedResourceCachePathPrefix(_ id: MediaResourceId) -> String { + let cacheString = "short-cache" + return "\(self.basePath)/\(cacheString)/\(fileNameForId(id))" + } + public func storeResourceData(_ id: MediaResourceId, data: Data, synchronous: Bool = false) { let begin = { let paths = self.storePathsForId(id) diff --git a/submodules/StatisticsUI/Sources/StatsEmptyItem.swift b/submodules/StatisticsUI/Sources/StatsEmptyItem.swift index 93974e5e49..5ffd086023 100644 --- a/submodules/StatisticsUI/Sources/StatsEmptyItem.swift +++ b/submodules/StatisticsUI/Sources/StatsEmptyItem.swift @@ -55,7 +55,7 @@ final class StatsEmptyStateItemNode: ItemListControllerEmptyStateItemNode { self.animationNode = AnimatedStickerNode() if let path = getAppBundle().path(forResource: "Charts", ofType: "tgs") { - self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct) + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) self.animationNode.visibility = true } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index f758b215c9..6af3b952f5 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -93,7 +93,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0)) - self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct) + self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) self.animationNode?.visibility = true self.animationNode?.addSubnode(self.textNode) } else { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index ea24c4f4fa..521e6f598e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -377,12 +377,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let file = file { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) - let mode: AnimatedStickerMode - if file.resource is LocalFileReferenceMediaResource { - mode = .direct - } else { - mode = .cached - } + + let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let mode: AnimatedStickerMode = .direct(cachePathPrefix: pathPrefix) + animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode) } } diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 50b8133924..9c45f18203 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -327,7 +327,7 @@ public final class NotificationViewControllerImpl { let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0)) strongSelf.imageNode.setSignal(chatMessageAnimatedSticker(postbox: accountAndImage.0.postbox, file: fileReference.media, small: false, size: fittedDimensions)) - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: accountAndImage.0, resource: fileReference.media.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: accountAndImage.0, resource: fileReference.media.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) animatedStickerNode.visibility = true accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true)) diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index f84bbbd491..b9ec084763 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -89,12 +89,12 @@ private final class TooltipScreenNode: ViewControllerTracingNode { break case .chatListPress: if let path = getAppBundle().path(forResource: "ChatListFoldersTooltip", ofType: "json") { - self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct) + self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animatedStickerNode.automaticallyLoadFirstFrame = true } case .info: if let path = getAppBundle().path(forResource: "anim_infotip", ofType: "json") { - self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct) + self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animatedStickerNode.automaticallyLoadFirstFrame = true } } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 2ce5060ba9..3591ae9963 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -206,7 +206,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animationNode = nil self.animatedStickerNode = AnimatedStickerNode() self.animatedStickerNode?.visibility = true - self.animatedStickerNode?.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 100, height: 100, playbackMode: .once, mode: .direct) + self.animatedStickerNode?.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 100, height: 100, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) @@ -349,7 +349,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { case let .result(_, items, _): let item = items[Int(value)] if let item = item as? StickerPackItem { - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: 120, height: 120, playbackMode: .once, mode: .direct) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: 120, height: 120, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) } default: break