diff --git a/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift new file mode 100644 index 0000000000..0e4c88ffe6 --- /dev/null +++ b/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift @@ -0,0 +1,302 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import RLottieBinding +import SwiftSignalKit +import GZip +import Display + +public final class DirectAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode { + private static let sharedQueue = Queue(name: "DirectAnimatedStickerNode", qos: .userInteractive) + + private final class LoadFrameTask { + } + + public var automaticallyLoadFirstFrame: Bool = false + public var automaticallyLoadLastFrame: Bool = false + public var playToCompletionOnStop: Bool = false + + private var didStart: Bool = false + public var started: () -> Void = {} + + public var completed: (Bool) -> Void = { _ in } + private var didComplete: Bool = false + + public var frameUpdated: (Int, Int) -> Void = { _, _ in } + public var currentFrameIndex: Int { + get { + return self.frameIndex + } set(value) { + } + } + public var currentFrameCount: Int { + get { + guard let lottieInstance = self.lottieInstance else { + return 0 + } + return Int(lottieInstance.frameCount) + } set(value) { + } + } + + public private(set) var isPlaying: Bool = false + public var stopAtNearestLoop: Bool = false + + private let statusPromise = Promise() + public var status: Signal { + return self.statusPromise.get() + } + + public var autoplay: Bool = true + + public var visibility: Bool = false { + didSet { + self.updatePlayback() + } + } + + public var isPlayingChanged: (Bool) -> Void = { _ in } + + private var sourceDisposable: Disposable? + private var playbackSize: CGSize? + + private var lottieInstance: LottieInstance? + private var frameIndex: Int = 0 + private var playbackMode: AnimatedStickerPlaybackMode = .loop + + private var frameImages: [Int: UIImage] = [:] + private var loadFrameTasks: [Int: LoadFrameTask] = [:] + private var nextFrameTimer: SwiftSignalKit.Timer? + + override public init() { + super.init() + } + + deinit { + self.sourceDisposable?.dispose() + self.nextFrameTimer?.invalidate() + } + + public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) { + } + + public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode) { + self.didStart = false + self.didComplete = false + + self.sourceDisposable?.dispose() + + self.playbackSize = CGSize(width: CGFloat(width), height: CGFloat(height)) + self.playbackMode = playbackMode + + self.sourceDisposable = (source.directDataPath(attemptSynchronously: false) + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] path in + guard let strongSelf = self, let path = path else { + return + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { + return + } + + let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data + + guard let lottieInstance = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else { + print("Could not load sticker data") + return + } + + strongSelf.setupPlayback(lottieInstance: lottieInstance) + }) + } + + private func updatePlayback() { + let isPlaying = self.visibility && self.lottieInstance != nil + if self.isPlaying != isPlaying { + self.isPlaying = isPlaying + + if self.isPlaying { + self.startNextFrameTimerIfNeeded() + self.updateLoadFrameTasks() + } else { + self.nextFrameTimer?.invalidate() + self.nextFrameTimer = nil + } + + self.isPlayingChanged(self.isPlaying) + } + } + + private func startNextFrameTimerIfNeeded() { + if self.nextFrameTimer == nil, let lottieInstance = self.lottieInstance { + let nextFrameTimer = SwiftSignalKit.Timer(timeout: 1.0 / Double(lottieInstance.frameRate), repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.nextFrameTimer = nil + strongSelf.advanceFrameIfPossible() + }, queue: .mainQueue()) + self.nextFrameTimer = nextFrameTimer + nextFrameTimer.start() + } + } + + private func advanceFrameIfPossible() { + guard let lottieInstance = self.lottieInstance else { + return + } + + if self.frameIndex == Int(lottieInstance.frameCount) - 1 { + switch self.playbackMode { + case .loop: + self.completed(false) + case let .count(count): + if count <= 1 { + if !self.didComplete { + self.didComplete = true + self.completed(true) + } + return + } else { + self.playbackMode = .count(count - 1) + self.completed(false) + } + case .once: + if !self.didComplete { + self.didComplete = true + self.completed(true) + } + return + case .still: + break + } + } + + let nextFrameIndex = (self.frameIndex + 1) % Int(lottieInstance.frameCount) + self.frameIndex = nextFrameIndex + + self.updateFrameImageIfNeeded() + self.updateLoadFrameTasks() + } + + private func updateFrameImageIfNeeded() { + guard let lottieInstance = self.lottieInstance else { + return + } + + var allowedIndices: [Int] = [] + for i in 0 ..< 2 { + let mappedIndex = (self.frameIndex + i) % Int(lottieInstance.frameCount) + allowedIndices.append(mappedIndex) + } + + var removeKeys: [Int] = [] + for index in self.frameImages.keys { + if !allowedIndices.contains(index) { + removeKeys.append(index) + } + } + for index in removeKeys { + self.frameImages.removeValue(forKey: index) + } + + if let image = self.frameImages[self.frameIndex] { + self.layer.contents = image.cgImage + + self.frameUpdated(self.frameIndex, Int(lottieInstance.frameCount)) + + if !self.didComplete { + self.startNextFrameTimerIfNeeded() + } + + if !self.didStart { + self.didStart = true + self.started() + } + } + } + + private func updateLoadFrameTasks() { + guard let lottieInstance = self.lottieInstance else { + return + } + + let frameIndex = self.frameIndex % Int(lottieInstance.frameCount) + if self.frameImages[frameIndex] == nil { + self.maybeStartLoadFrameTask(frameIndex: frameIndex) + } else { + self.maybeStartLoadFrameTask(frameIndex: (frameIndex + 1) % Int(lottieInstance.frameCount)) + } + } + + private func maybeStartLoadFrameTask(frameIndex: Int) { + guard let lottieInstance = self.lottieInstance, let playbackSize = self.playbackSize else { + return + } + if self.loadFrameTasks[frameIndex] != nil { + return + } + + self.loadFrameTasks[frameIndex] = LoadFrameTask() + + DirectAnimatedStickerNode.sharedQueue.async { [weak self] in + let drawingContext = DrawingContext(size: playbackSize, scale: 1.0, opaque: false, clear: false) + lottieInstance.renderFrame(with: Int32(frameIndex), into: drawingContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(drawingContext.scaledSize.width), height: Int32(drawingContext.scaledSize.height), bytesPerRow: Int32(drawingContext.bytesPerRow)) + + let image = drawingContext.generateImage() + + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + strongSelf.loadFrameTasks.removeValue(forKey: frameIndex) + + if let image = image { + strongSelf.frameImages[frameIndex] = image + strongSelf.updateFrameImageIfNeeded() + strongSelf.updateLoadFrameTasks() + } + } + } + } + + private func setupPlayback(lottieInstance: LottieInstance) { + self.lottieInstance = lottieInstance + + self.updatePlayback() + } + + public func reset() { + } + + public func playOnce() { + } + + public func play(firstFrame: Bool, fromIndex: Int?) { + if let fromIndex = fromIndex { + self.frameIndex = fromIndex + self.updateLoadFrameTasks() + } + } + + public func pause() { + } + + public func stop() { + } + + public func seekTo(_ position: AnimatedStickerPlaybackPosition) { + } + + public func playIfNeeded() -> Bool { + return false + } + + public func updateLayout(size: CGSize) { + } + + public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) { + } +} diff --git a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift index 947e700882..cbc118f748 100644 --- a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift @@ -349,7 +349,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { largeListAnimation: reaction.activateAnimation, applicationAnimation: aroundAnimation, largeApplicationAnimation: reaction.effectAnimation - ), hasAppearAnimation: false) + ), hasAppearAnimation: false, useDirectRendering: false) containerNode.isUserInteractionEnabled = false containerNode.addSubnode(itemNode) self.addSubnode(containerNode) @@ -395,7 +395,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { targetContainerNode.view.superview?.bringSubviewToFront(targetContainerNode.view) - let standaloneReactionAnimation = StandaloneReactionAnimation() + let standaloneReactionAnimation = StandaloneReactionAnimation(useDirectRendering: true) self.standaloneReactionAnimation = standaloneReactionAnimation targetContainerNode.addSubnode(standaloneReactionAnimation) diff --git a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift index b1233d1be9..01dab1d38d 100644 --- a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift @@ -105,6 +105,7 @@ private class StickerNode: ASDisplayNode { if file.isPremiumSticker || forceIsPremium { let animationNode = DefaultAnimatedStickerNodeImpl() + //let animationNode = DirectAnimatedStickerNode() animationNode.automaticallyLoadFirstFrame = true self.animationNode = animationNode @@ -122,13 +123,16 @@ private class StickerNode: ASDisplayNode { self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file), resource: effect.resource).start()) let source = AnimatedStickerResourceSource(account: self.context.account, resource: effect.resource, fitzModifier: nil) - let additionalAnimationNode = DefaultAnimatedStickerNodeImpl() + + let additionalAnimationNode: AnimatedStickerNode + + additionalAnimationNode = DirectAnimatedStickerNode() var pathPrefix: String? pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(effect.resource.id) pathPrefix = nil - additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 1.33), height: Int(fittedDimensions.height * 1.33), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) + additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 1.5), height: Int(fittedDimensions.height * 1.5), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) self.additionalAnimationNode = additionalAnimationNode } } else { @@ -188,7 +192,6 @@ private class StickerNode: ASDisplayNode { self.effectDisposable.dispose() } - private func removePlaceholder(animated: Bool) { if !animated { self.placeholderNode.removeFromSupernode() @@ -216,7 +219,9 @@ private class StickerNode: ASDisplayNode { } func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { - self.placeholderNode.updateAbsoluteRect(rect, within: containerSize) + if self.placeholderNode.supernode != nil { + self.placeholderNode.updateAbsoluteRect(rect, within: containerSize) + } } private func updatePlayback() { @@ -260,10 +265,12 @@ private class StickerNode: ASDisplayNode { } } - let placeholderFrame = CGRect(origin: CGPoint(x: -10.0, y: 0.0), size: imageSize) - let thumbnailDimensions = PixelDimensions(width: 512, height: 512) - self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: self.file.immediateThumbnailData, size: placeholderFrame.size, imageSize: thumbnailDimensions.cgSize) - self.placeholderNode.frame = placeholderFrame + if self.placeholderNode.supernode != nil { + let placeholderFrame = CGRect(origin: CGPoint(x: -10.0, y: 0.0), size: imageSize) + let thumbnailDimensions = PixelDimensions(width: 512, height: 512) + self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: self.file.immediateThumbnailData, size: placeholderFrame.size, imageSize: thumbnailDimensions.cgSize) + self.placeholderNode.frame = placeholderFrame + } } } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 9e54484787..af13929f5a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -963,6 +963,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } public final class StandaloneReactionAnimation: ASDisplayNode { + private let useDirectRendering: Bool private var itemNode: ReactionNode? = nil private var itemNodeIsEmbedded: Bool = false private let hapticFeedback = HapticFeedback() @@ -970,9 +971,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { private weak var targetView: UIView? - //private var colorCallbacks: [LOTColorValueCallback] = [] - - override public init() { + public init(useDirectRendering: Bool = false) { + self.useDirectRendering = useDirectRendering + super.init() self.isUserInteractionEnabled = false @@ -1064,7 +1065,12 @@ public final class StandaloneReactionAnimation: ASDisplayNode { itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: isLarge, isPreviewing: false, transition: .immediate) - let additionalAnimationNode = DefaultAnimatedStickerNodeImpl() + let additionalAnimationNode: AnimatedStickerNode + if self.useDirectRendering { + additionalAnimationNode = DirectAnimatedStickerNode() + } else { + additionalAnimationNode = DefaultAnimatedStickerNodeImpl() + } let additionalAnimation: TelegramMediaFile if isLarge && !forceSmallEffectAnimation { @@ -1139,7 +1145,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } var didBeginDismissAnimation = false - let beginDismissAnimation: () -> Void = { [weak self] in + let beginDismissAnimation: () -> Void = { [weak self, weak additionalAnimationNode] in if !didBeginDismissAnimation { didBeginDismissAnimation = true @@ -1150,9 +1156,11 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } if forceSmallEffectAnimation { - additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak additionalAnimationNode] _ in - additionalAnimationNode?.removeFromSupernode() - }) + if let additionalAnimationNode = additionalAnimationNode { + additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak additionalAnimationNode] _ in + additionalAnimationNode?.removeFromSupernode() + }) + } mainAnimationCompleted = true intermediateCompletion() diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 62ef7d5b8b..a660add59c 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -48,6 +48,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { let context: AccountContext let item: ReactionItem private let hasAppearAnimation: Bool + private let useDirectRendering: Bool private var animateInAnimationNode: AnimatedStickerNode? private let staticAnimationNode: AnimatedStickerNode @@ -67,16 +68,17 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { var expandedAnimationDidBegin: (() -> Void)? - public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, hasAppearAnimation: Bool = true) { + public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) { self.context = context self.item = item self.hasAppearAnimation = hasAppearAnimation + self.useDirectRendering = useDirectRendering - self.staticAnimationNode = DefaultAnimatedStickerNodeImpl() + self.staticAnimationNode = self.useDirectRendering ? DirectAnimatedStickerNode() : DefaultAnimatedStickerNodeImpl() if hasAppearAnimation { self.staticAnimationNode.isHidden = true - self.animateInAnimationNode = DefaultAnimatedStickerNodeImpl() + self.animateInAnimationNode = self.useDirectRendering ? DirectAnimatedStickerNode() : DefaultAnimatedStickerNodeImpl() } super.init() @@ -146,7 +148,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } self.staticAnimationNode.play(firstFrame: false, fromIndex: 0) } else if isExpanded, self.animationNode == nil { - let animationNode = DefaultAnimatedStickerNodeImpl() + let animationNode: AnimatedStickerNode = self.useDirectRendering ? DirectAnimatedStickerNode() : DefaultAnimatedStickerNodeImpl() animationNode.automaticallyLoadFirstFrame = true self.animationNode = animationNode self.addSubnode(animationNode) @@ -235,7 +237,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { if self.animationNode == nil { if isPreviewing { if self.stillAnimationNode == nil { - let stillAnimationNode = DefaultAnimatedStickerNodeImpl() + let stillAnimationNode: AnimatedStickerNode = self.useDirectRendering ? DirectAnimatedStickerNode() : DefaultAnimatedStickerNodeImpl() self.stillAnimationNode = stillAnimationNode self.addSubnode(stillAnimationNode) diff --git a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift index 29eef5bcd7..16c1c22e64 100644 --- a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift +++ b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift @@ -4,9 +4,18 @@ import Postbox import SwiftSignalKit import MtProtoKit -enum MultiplexedRequestTarget: Equatable, Hashable { +enum MultiplexedRequestTarget: Equatable, Hashable, CustomStringConvertible { case main(Int) case cdn(Int) + + var description: String { + switch self { + case let .main(id): + return "dc\(id)" + case let .cdn(id): + return "cdn\(id)" + } + } } private struct MultiplexedRequestTargetKey: Equatable, Hashable { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 7d91c4e971..291367eac0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -756,7 +756,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let statusLayoutInput: ChatMessageDateAndStatusNode.LayoutInput if let _ = textString { - statusLayoutInput = .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: reactionSettings) + statusLayoutInput = .trailingContent(contentWidth: textLayout.hasRTL ? 1000.0 : textLayout.trailingLineWidth, reactionSettings: reactionSettings) } else { statusLayoutInput = .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: reactionSettings) }