From 47363cb2e37d2edbb14f8578b4690577e79a4c88 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 14 Jan 2022 22:32:45 +0300 Subject: [PATCH] Video Stickers Improvements --- .../Sources/Node/ChatListItem.swift | 2 +- .../FFMpegMediaVideoFrameDecoder.swift | 8 +- submodules/SoftwareVideo/BUILD | 23 ++++++ submodules/SoftwareVideo/Info.plist | 22 ++++++ .../Sources/SampleBufferPool.swift | 12 +-- .../SoftwareVideoLayerFrameManager.swift | 18 ++++- .../Sources/VideoStickerNode.swift | 56 ++++++++++++++ submodules/StickerPackPreviewUI/BUILD | 1 + .../Sources/StickerPackPreviewGridItem.swift | 33 +++++++- .../Sources/StickerPreviewPeekContent.swift | 27 +++++-- .../SyncCore/SyncCore_TelegramMediaFile.swift | 20 +++++ submodules/TelegramUI/BUILD | 1 + .../ChatContextResultPeekContentNode.swift | 1 + .../ChatMediaInputStickerGridItem.swift | 45 +++++++++-- .../ChatMediaInputStickerPackItem.swift | 5 ++ .../ChatMessageAnimatedStickerItemNode.swift | 77 +++++++++---------- .../TelegramUI/Sources/ChatMessageItem.swift | 2 +- ...ListContextResultsChatInputPanelItem.swift | 1 + .../Sources/MultiplexedVideoNode.swift | 1 + .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 7 +- .../Sources/PeerInfoGifPaneNode.swift | 1 + 21 files changed, 283 insertions(+), 80 deletions(-) create mode 100644 submodules/SoftwareVideo/BUILD create mode 100644 submodules/SoftwareVideo/Info.plist rename submodules/{TelegramUI => SoftwareVideo}/Sources/SampleBufferPool.swift (86%) rename submodules/{TelegramUI => SoftwareVideo}/Sources/SoftwareVideoLayerFrameManager.swift (94%) create mode 100644 submodules/SoftwareVideo/Sources/VideoStickerNode.swift diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 9795c13e04..bbf9f243f3 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1146,7 +1146,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } break inner } else if let file = media as? TelegramMediaFile { - if file.isVideo, !file.isInstantVideo, let _ = file.dimensions { + if file.isVideo, !file.isInstantVideo && !file.isVideoSticker, let _ = file.dimensions { let fitSize = contentImageSize contentImageSpecs.append((message, .file(file), fitSize)) } diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index 0d95e9e7c9..56b89852b6 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -277,7 +277,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { options as CFDictionary, &pixelBufferRef) } - + guard let pixelBuffer = pixelBufferRef else { return nil } @@ -399,11 +399,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { return nil } } - - func decodeImage() { - - } - + public func reset() { self.codecContext.flushBuffers() self.resetDecoderOnNextFrame = true diff --git a/submodules/SoftwareVideo/BUILD b/submodules/SoftwareVideo/BUILD new file mode 100644 index 0000000000..0d0da4f9db --- /dev/null +++ b/submodules/SoftwareVideo/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SoftwareVideo", + module_name = "SoftwareVideo", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/MediaPlayer:UniversalMediaPlayer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SoftwareVideo/Info.plist b/submodules/SoftwareVideo/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/submodules/SoftwareVideo/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/submodules/TelegramUI/Sources/SampleBufferPool.swift b/submodules/SoftwareVideo/Sources/SampleBufferPool.swift similarity index 86% rename from submodules/TelegramUI/Sources/SampleBufferPool.swift rename to submodules/SoftwareVideo/Sources/SampleBufferPool.swift index 1e67917933..1bc235682e 100644 --- a/submodules/TelegramUI/Sources/SampleBufferPool.swift +++ b/submodules/SoftwareVideo/Sources/SampleBufferPool.swift @@ -14,19 +14,19 @@ private final class SampleBufferLayerImpl: AVSampleBufferDisplayLayer { } } -final class SampleBufferLayer { - let layer: AVSampleBufferDisplayLayer +public final class SampleBufferLayer { + public let layer: AVSampleBufferDisplayLayer private let enqueue: (AVSampleBufferDisplayLayer) -> Void - var isFreed: Bool = false + public var isFreed: Bool = false fileprivate init(layer: AVSampleBufferDisplayLayer, enqueue: @escaping (AVSampleBufferDisplayLayer) -> Void) { self.layer = layer self.enqueue = enqueue } deinit { - if !isFreed { + if !self.isFreed { self.enqueue(self.layer) } } @@ -34,11 +34,11 @@ final class SampleBufferLayer { private let pool = Atomic<[AVSampleBufferDisplayLayer]>(value: []) -func clearSampleBufferLayerPoll() { +public func clearSampleBufferLayerPoll() { let _ = pool.modify { _ in return [] } } -func takeSampleBufferLayer() -> SampleBufferLayer { +public func takeSampleBufferLayer() -> SampleBufferLayer { var layer: AVSampleBufferDisplayLayer? let _ = pool.modify { list in var list = list diff --git a/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift similarity index 94% rename from submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift rename to submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift index 859da17ac8..70d00e8aba 100644 --- a/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift @@ -10,7 +10,7 @@ private let applyQueue = Queue() private let workers = ThreadPool(threadCount: 3, threadPriority: 0.2) private var nextWorker = 0 -final class SoftwareVideoLayerFrameManager { +public final class SoftwareVideoLayerFrameManager { private let fetchDisposable: Disposable private var dataDisposable = MetaDisposable() private let source = Atomic(value: nil) @@ -32,7 +32,10 @@ final class SoftwareVideoLayerFrameManager { private var layerRotationAngleAndAspect: (CGFloat, CGFloat)? - init(account: Account, fileReference: FileMediaReference, layerHolder: SampleBufferLayer, hintVP9: Bool = false) { + private var didStart = false + var started: () -> Void = { } + + public init(account: Account, fileReference: FileMediaReference, layerHolder: SampleBufferLayer, hintVP9: Bool = false) { var resource = fileReference.media.resource var secondaryResource: MediaResource? for attribute in fileReference.media.attributes { @@ -61,7 +64,7 @@ final class SoftwareVideoLayerFrameManager { self.dataDisposable.dispose() } - func start() { + public func start() { func stringForResource(_ resource: MediaResource?) -> String { guard let resource = resource else { return "" @@ -115,7 +118,7 @@ final class SoftwareVideoLayerFrameManager { })) } - func tick(timestamp: Double) { + public func tick(timestamp: Double) { applyQueue.async { if self.baseTimestamp == nil && !self.frames.isEmpty { self.baseTimestamp = timestamp @@ -148,6 +151,13 @@ final class SoftwareVideoLayerFrameManager { self.layerHolder.layer.setAffineTransform(transform) }*/ self.layerHolder.layer.enqueue(frame.sampleBuffer) + + if !self.didStart { + self.didStart = true + Queue.mainQueue().async { + self.started() + } + } } } diff --git a/submodules/SoftwareVideo/Sources/VideoStickerNode.swift b/submodules/SoftwareVideo/Sources/VideoStickerNode.swift new file mode 100644 index 0000000000..dad2e31a65 --- /dev/null +++ b/submodules/SoftwareVideo/Sources/VideoStickerNode.swift @@ -0,0 +1,56 @@ +import Foundation +import AVFoundation +import AsyncDisplayKit +import Display +import TelegramCore + +public class VideoStickerNode: ASDisplayNode { + private var layerHolder: SampleBufferLayer? + private var manager: SoftwareVideoLayerFrameManager? + + private var displayLink: ConstantDisplayLinkAnimator? + private var displayLinkTimestamp: Double = 0.0 + + public var started: () -> Void = {} + + private var validLayout: CGSize? + + public func update(isPlaying: Bool) { + let displayLink: ConstantDisplayLinkAnimator + if let current = self.displayLink { + displayLink = current + } else { + displayLink = ConstantDisplayLinkAnimator { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.manager?.tick(timestamp: strongSelf.displayLinkTimestamp) + strongSelf.displayLinkTimestamp += 1.0 / 30.0 + } + displayLink.frameInterval = 2 + self.displayLink = displayLink + } + self.displayLink?.isPaused = !isPlaying + } + + public func update(account: Account, fileReference: FileMediaReference) { + let layerHolder = takeSampleBufferLayer() + layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill + if let size = self.validLayout { + layerHolder.layer.frame = CGRect(origin: CGPoint(), size: size) + } + self.layer.addSublayer(layerHolder.layer) + self.layerHolder = layerHolder + + let manager = SoftwareVideoLayerFrameManager(account: account, fileReference: fileReference, layerHolder: layerHolder, hintVP9: true) + manager.started = self.started + self.manager = manager + manager.start() + } + + public func updateLayout(size: CGSize) { + self.validLayout = size + + self.layerHolder?.layer.frame = CGRect(origin: CGPoint(), size: size) + } +} diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index 548a88134f..be58773907 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/UndoUI:UndoUI", "//submodules/ContextUI:ContextUI", + "//submodules/SoftwareVideo:SoftwareVideo", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index 5f40225493..a613cd7879 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -11,6 +11,7 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import TelegramPresentationData import ShimmerEffect +import SoftwareVideo final class StickerPackPreviewInteraction { var previewedItem: StickerPreviewPeekItem? @@ -60,13 +61,19 @@ final class StickerPackPreviewGridItemNode: GridItemNode { private var isEmpty: Bool? private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? + private var videoNode: VideoStickerNode? private var placeholderNode: StickerShimmerEffectNode? private var theme: PresentationTheme? override var isVisibleInGrid: Bool { didSet { - self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true + let visibility = self.isVisibleInGrid && (self.interaction?.playAnimatedStickers ?? true) + if let videoNode = self.videoNode { + videoNode.update(isPlaying: visibility) + } else if let animationNode = self.animationNode { + animationNode.visibility = visibility + } } } @@ -139,7 +146,20 @@ final class StickerPackPreviewGridItemNode: GridItemNode { if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem || self.isEmpty != isEmpty { if let stickerItem = stickerItem { - if stickerItem.file.isAnimatedSticker { + if stickerItem.file.isVideoSticker { + if self.videoNode == nil { + let videoNode = VideoStickerNode() + self.videoNode = videoNode + self.addSubnode(videoNode) + videoNode.started = { [weak self] in + self?.imageNode.isHidden = true + } + } + self.videoNode?.update(account: account, fileReference: stickerPackFileReference(stickerItem.file)) + + self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) + } else if stickerItem.file.isAnimatedSticker { let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))) @@ -202,10 +222,15 @@ final class StickerPackPreviewGridItemNode: GridItemNode { if let (_, item) = self.currentState { if let item = item, let dimensions = item.file.dimensions?.cgSize { let imageSize = dimensions.aspectFitted(boundingSize) + let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + self.imageNode.frame = imageFrame + if let videoNode = self.videoNode { + videoNode.frame = imageFrame + videoNode.updateLayout(size: imageSize) + } if let animationNode = self.animationNode { - animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.frame = imageFrame animationNode.updateLayout(size: imageSize) } } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 50f5d7fe0f..83e1821310 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -9,6 +9,7 @@ import StickerResources import AnimatedStickerNode import TelegramAnimatedStickerNode import ContextUI +import SoftwareVideo public enum StickerPreviewPeekItem: Equatable { case pack(StickerPackItem) @@ -71,6 +72,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC private var textNode: ASTextNode public var imageNode: TransformImageNode public var animationNode: AnimatedStickerNode? + public var videoNode: VideoStickerNode? private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -86,16 +88,24 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC break } - if item.file.isAnimatedSticker { + if item.file.isVideoSticker { + let videoNode = VideoStickerNode() + self.videoNode = videoNode + + videoNode.update(account: self.account, fileReference: .standalone(media: item.file)) + videoNode.update(isPlaying: true) + + videoNode.addSubnode(self.textNode) + } else if item.file.isAnimatedSticker { let animationNode = AnimatedStickerNode() self.animationNode = animationNode 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(cachePathPrefix: nil)) - self.animationNode?.visibility = true - self.animationNode?.addSubnode(self.textNode) + animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) + animationNode.visibility = true + animationNode.addSubnode(self.textNode) } else { self.imageNode.addSubnode(self.textNode) self.animationNode = nil @@ -107,7 +117,9 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC self.isUserInteractionEnabled = false - if let animationNode = self.animationNode { + if let videoNode = self.videoNode { + self.addSubnode(videoNode) + } else if let animationNode = self.animationNode { self.addSubnode(animationNode) } else { self.addSubnode(self.imageNode) @@ -125,7 +137,10 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() let imageFrame = CGRect(origin: CGPoint(x: 0.0, y: textSize.height + textSpacing), size: imageSize) self.imageNode.frame = imageFrame - if let animationNode = self.animationNode { + if let videoNode = self.videoNode { + videoNode.frame = imageFrame + videoNode.updateLayout(size: imageSize) + } else if let animationNode = self.animationNode { animationNode.frame = imageFrame animationNode.updateLayout(size: imageSize) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index cb91d3f12f..186f715353 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -478,6 +478,26 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + public var isVideoSticker: Bool { + if let fileName = self.fileName, fileName.hasSuffix(".webm") { + return true + } + if let _ = self.fileName, self.mimeType == "video/webm" { + var hasSticker = false + var hasAnimated = false + for attribute in self.attributes { + if case .Sticker = attribute { + hasSticker = true + } + if case .Animated = attribute { + hasAnimated = true + } + } + return hasSticker && hasAnimated + } + return false + } + public var hasLinkedStickers: Bool { for attribute in self.attributes { if case .HasLinkedStickers = attribute { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index df390590b1..66ea767ea2 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -254,6 +254,7 @@ swift_library( "//submodules/Components/ReactionImageComponent:ReactionImageComponent", "//submodules/Translate:Translate", "//submodules/TabBarUI:TabBarUI", + "//submodules/SoftwareVideo:SoftwareVideo", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift index d863811fcb..0190387cc0 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift @@ -9,6 +9,7 @@ import AVFoundation import PhotoResources import AppBundle import ContextUI +import SoftwareVideo final class ChatContextResultPeekContent: PeekControllerContent { let account: Account diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index 9254a99e15..45e2644df8 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -11,6 +11,7 @@ import AccountContext import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect +import SoftwareVideo enum ChatMediaInputStickerGridSectionAccessory { case none @@ -174,6 +175,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { private var currentSize: CGSize? let imageNode: TransformImageNode private(set) var animationNode: AnimatedStickerNode? + private(set) var videoNode: VideoStickerNode? private(set) var placeholderNode: StickerShimmerEffectNode? private var didSetUpAnimationNode = false private var item: ChatMediaInputStickerGridItem? @@ -265,7 +267,23 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { } if let dimensions = item.stickerItem.file.dimensions { - if item.stickerItem.file.isAnimatedSticker { + if item.stickerItem.file.isVideoSticker { + if self.videoNode == nil { + let videoNode = VideoStickerNode() + videoNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) + self.videoNode = videoNode + videoNode.started = { [weak self] in + self?.imageNode.isHidden = true + } + if let placeholderNode = self.placeholderNode { + self.insertSubnode(videoNode, belowSubnode: placeholderNode) + } else { + self.addSubnode(videoNode) + } + } + self.imageNode.setSignal(chatMessageSticker(account: item.account, file: item.stickerItem.file, small: !item.large, synchronousLoad: synchronousLoads && isVisible)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(item.stickerItem.file), resource: item.stickerItem.file.resource).start()) + } else if item.stickerItem.file.isAnimatedSticker { if self.animationNode == nil { let animationNode = AnimatedStickerNode() animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) @@ -306,16 +324,23 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) + let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() if self.imageNode.supernode === self { - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + self.imageNode.frame = imageFrame } if let animationNode = self.animationNode { if animationNode.supernode === self { - animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.frame = imageFrame } animationNode.updateLayout(size: imageSize) } + if let videoNode = self.videoNode { + if videoNode.supernode === self { + videoNode.frame = imageFrame + } + videoNode.updateLayout(size: imageSize) + } } } @@ -365,12 +390,18 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if self.isPlaying != isPlaying { self.isPlaying = isPlaying self.animationNode?.visibility = isPlaying + self.videoNode?.update(isPlaying: isPlaying) if let item = self.item, isPlaying, !self.didSetUpAnimationNode { self.didSetUpAnimationNode = true - let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fitSize = item.large ? CGSize(width: 384.0, height: 384.0) : CGSize(width: 160.0, height: 160.0) - let fittedDimensions = dimensions.cgSize.aspectFitted(fitSize) - self.animationNode?.setup(source: AnimatedStickerResourceSource(account: item.account, resource: item.stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + + if let videoNode = self.videoNode { + videoNode.update(account: item.account, fileReference: stickerPackFileReference(item.stickerItem.file)) + } else if let animationNode = self.animationNode { + let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fitSize = item.large ? CGSize(width: 384.0, height: 384.0) : CGSize(width: 160.0, height: 160.0) + let fittedDimensions = dimensions.cgSize.aspectFitted(fitSize) + animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: item.stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + } } } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift index f1f4ecd228..a43ce4dcf5 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift @@ -11,6 +11,7 @@ import ItemListStickerPackItem import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect +import SoftwareVideo final class ChatMediaInputStickerPackItem: ListViewItem { let account: Account @@ -83,6 +84,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { private let scalingNode: ASDisplayNode private let imageNode: TransformImageNode private var animatedStickerNode: AnimatedStickerNode? + private var videoStickerNode: VideoStickerNode? private var placeholderNode: StickerShimmerEffectNode? private let highlightNode: ASImageNode private let titleNode: ImmediateTextNode @@ -287,6 +289,9 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { animatedStickerNode.frame = self.imageNode.frame animatedStickerNode.updateLayout(size: self.imageNode.frame.size) } + if let videoNode = self.videoStickerNode { + videoNode.frame = self.imageNode.frame + } if let placeholderNode = self.placeholderNode { placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize) placeholderNode.position = self.imageNode.position diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index b995fa3a3d..c750fbbfbd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -26,6 +26,7 @@ import WallpaperBackgroundNode import LocalMediaResources import AppBundle import LottieMeshSwift +import SoftwareVideo private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -54,26 +55,11 @@ extension SlotMachineAnimationNode: GenericAnimatedStickerNode { } } -private class VideoStickerNode: ASDisplayNode, GenericAnimatedStickerNode { - private var layerHolder: SampleBufferLayer? - var manager: SoftwareVideoLayerFrameManager? - +extension VideoStickerNode: GenericAnimatedStickerNode { func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) { } - func update(context: AccountContext, fileReference: FileMediaReference, size: CGSize) { - let layerHolder = takeSampleBufferLayer() - layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill - layerHolder.layer.frame = CGRect(origin: CGPoint(), size: size) - self.layer.addSublayer(layerHolder.layer) - self.layerHolder = layerHolder - - let manager = SoftwareVideoLayerFrameManager(account: context.account, fileReference: fileReference, layerHolder: layerHolder, hintVP9: true) - self.manager = manager - manager.start() - } - var currentFrameIndex: Int { return 0 } @@ -201,9 +187,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var didSetUpAnimationNode = false private var isPlaying = false - private var displayLink: ConstantDisplayLinkAnimator? - private var displayLinkTimestamp: Double = 0.0 - private var additionalAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = [] private var overlayMeshAnimationNode: ChatMessageTransitionNode.DecorationItemNode? private var enqueuedAdditionalAnimations: [(Int, Double)] = [] @@ -436,8 +419,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if self.visibilityStatus != oldValue { self.updateVisibility() self.haptic?.enabled = self.visibilityStatus - - } } } @@ -470,9 +451,24 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } self.animationNode = animationNode } - } else if let telegramFile = self.telegramFile, let fileName = telegramFile.fileName, fileName.hasSuffix(".webm") { + } else if let telegramFile = self.telegramFile, telegramFile.mimeType == "video/webm" { let videoNode = VideoStickerNode() - videoNode.update(context: item.context, fileReference: .standalone(media: telegramFile), size: CGSize(width: 184.0, height: 184.0)) + videoNode.started = { [weak self] in + if let strongSelf = self { + strongSelf.imageNode.alpha = 0.0 + if !strongSelf.enableSynchronousImageApply { + let current = CACurrentMediaTime() + if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 { + if !strongSelf.placeholderNode.alpha.isZero { + strongSelf.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.removePlaceholder(animated: true) + } + } else { + strongSelf.removePlaceholder(animated: false) + } + } + } + } self.animationNode = videoNode } else { let animationNode = AnimatedStickerNode() @@ -591,25 +587,21 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let isPlaying = self.visibilityStatus && !self.forceStopAnimations - if let _ = self.animationNode as? VideoStickerNode { - let displayLink: ConstantDisplayLinkAnimator - if let current = self.displayLink { - displayLink = current - } else { - displayLink = ConstantDisplayLinkAnimator { [weak self] in - guard let strongSelf = self, let animationNode = strongSelf.animationNode as? VideoStickerNode else { - return + if let videoNode = self.animationNode as? VideoStickerNode { + if self.isPlaying != isPlaying { + self.isPlaying = isPlaying + + if self.isPlaying && !self.didSetUpAnimationNode { + self.didSetUpAnimationNode = true + + if let file = self.telegramFile { + videoNode.update(account: item.context.account, fileReference: .standalone(media: file)) } - animationNode.manager?.tick(timestamp: strongSelf.displayLinkTimestamp) - strongSelf.displayLinkTimestamp += 1.0 / 30.0 } - displayLink.frameInterval = 2 - self.displayLink = displayLink + + videoNode.update(isPlaying: isPlaying) } - self.displayLink?.isPaused = !isPlaying } else if let animationNode = self.animationNode as? AnimatedStickerNode { - let isPlaying = self.visibilityStatus && !self.forceStopAnimations - if !isPlaying { for decorationNode in self.additionalAnimationNodes { if let transitionNode = item.controllerInteraction.getMessageTransitionNode() { @@ -1203,9 +1195,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode { strongSelf.animationNode?.frame = animationNodeFrame - } - if let animationNode = strongSelf.animationNode as? AnimatedStickerNode, strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode { - animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size) + if let videoNode = strongSelf.animationNode as? VideoStickerNode { + videoNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size) + } + if let animationNode = strongSelf.animationNode as? AnimatedStickerNode { + animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size) + } } strongSelf.enableSynchronousImageApply = true diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Sources/ChatMessageItem.swift index bcca9e881e..456a774b11 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItem.swift @@ -380,7 +380,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { loop: for media in self.message.media { if let telegramFile = media as? TelegramMediaFile { - if let fileName = telegramFile.fileName, fileName.hasSuffix(".webm") { + if telegramFile.isVideoSticker { viewClassName = ChatMessageAnimatedStickerItemNode.self break loop } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index 30b75a0e3b..99cbcde95e 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -14,6 +14,7 @@ import TelegramAnimatedStickerNode import TelegramPresentationData import AccountContext import ShimmerEffect +import SoftwareVideo final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { let account: Account diff --git a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift index 70084b7a3f..fb63510fe1 100644 --- a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift @@ -9,6 +9,7 @@ import AVFoundation import ContextUI import TelegramPresentationData import ShimmerEffect +import SoftwareVideo final class MultiplexedVideoPlaceholderNode: ASDisplayNode { private let effectNode: ShimmerEffectNode diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 712a3c61b5..4c3f25c74d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -125,13 +125,14 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { let previousIcon = self.icon + let themeUpdated = self.theme != presentationData.theme let iconUpdated = self.icon != icon let isActiveUpdated = self.isActive != isActive self.isActive = isActive let iconSize = CGSize(width: 40.0, height: 40.0) - if self.theme != presentationData.theme || self.icon != icon { + if themeUpdated || iconUpdated { self.theme = presentationData.theme self.icon = icon @@ -194,9 +195,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { let animationNode: AnimationNode if let current = self.animationNode { animationNode = current - if iconUpdated { - animationNode.setAnimation(name: animationName, colors: colors) - } + animationNode.setAnimation(name: animationName, colors: colors) } else { animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) self.referenceNode.addSubnode(animationNode) diff --git a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift index d16b75a67f..16367af592 100644 --- a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift @@ -13,6 +13,7 @@ import GridMessageSelectionNode import UniversalMediaPlayer import ListMessageItem import ChatMessageInteractiveMediaBadge +import SoftwareVideo private final class FrameSequenceThumbnailNode: ASDisplayNode { private let context: AccountContext