Video Stickers Improvements

This commit is contained in:
Ilya Laktyushin 2022-01-14 22:32:45 +03:00
parent b102311660
commit 47363cb2e3
21 changed files with 283 additions and 80 deletions

View File

@ -1146,7 +1146,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
break inner break inner
} else if let file = media as? TelegramMediaFile { } 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 let fitSize = contentImageSize
contentImageSpecs.append((message, .file(file), fitSize)) contentImageSpecs.append((message, .file(file), fitSize))
} }

View File

@ -277,7 +277,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
options as CFDictionary, options as CFDictionary,
&pixelBufferRef) &pixelBufferRef)
} }
guard let pixelBuffer = pixelBufferRef else { guard let pixelBuffer = pixelBufferRef else {
return nil return nil
} }
@ -399,11 +399,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
return nil return nil
} }
} }
func decodeImage() {
}
public func reset() { public func reset() {
self.codecContext.flushBuffers() self.codecContext.flushBuffers()
self.resetDecoderOnNextFrame = true self.resetDecoderOnNextFrame = true

View File

@ -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",
],
)

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -14,19 +14,19 @@ private final class SampleBufferLayerImpl: AVSampleBufferDisplayLayer {
} }
} }
final class SampleBufferLayer { public final class SampleBufferLayer {
let layer: AVSampleBufferDisplayLayer public let layer: AVSampleBufferDisplayLayer
private let enqueue: (AVSampleBufferDisplayLayer) -> Void private let enqueue: (AVSampleBufferDisplayLayer) -> Void
var isFreed: Bool = false public var isFreed: Bool = false
fileprivate init(layer: AVSampleBufferDisplayLayer, enqueue: @escaping (AVSampleBufferDisplayLayer) -> Void) { fileprivate init(layer: AVSampleBufferDisplayLayer, enqueue: @escaping (AVSampleBufferDisplayLayer) -> Void) {
self.layer = layer self.layer = layer
self.enqueue = enqueue self.enqueue = enqueue
} }
deinit { deinit {
if !isFreed { if !self.isFreed {
self.enqueue(self.layer) self.enqueue(self.layer)
} }
} }
@ -34,11 +34,11 @@ final class SampleBufferLayer {
private let pool = Atomic<[AVSampleBufferDisplayLayer]>(value: []) private let pool = Atomic<[AVSampleBufferDisplayLayer]>(value: [])
func clearSampleBufferLayerPoll() { public func clearSampleBufferLayerPoll() {
let _ = pool.modify { _ in return [] } let _ = pool.modify { _ in return [] }
} }
func takeSampleBufferLayer() -> SampleBufferLayer { public func takeSampleBufferLayer() -> SampleBufferLayer {
var layer: AVSampleBufferDisplayLayer? var layer: AVSampleBufferDisplayLayer?
let _ = pool.modify { list in let _ = pool.modify { list in
var list = list var list = list

View File

@ -10,7 +10,7 @@ private let applyQueue = Queue()
private let workers = ThreadPool(threadCount: 3, threadPriority: 0.2) private let workers = ThreadPool(threadCount: 3, threadPriority: 0.2)
private var nextWorker = 0 private var nextWorker = 0
final class SoftwareVideoLayerFrameManager { public final class SoftwareVideoLayerFrameManager {
private let fetchDisposable: Disposable private let fetchDisposable: Disposable
private var dataDisposable = MetaDisposable() private var dataDisposable = MetaDisposable()
private let source = Atomic<SoftwareVideoSource?>(value: nil) private let source = Atomic<SoftwareVideoSource?>(value: nil)
@ -32,7 +32,10 @@ final class SoftwareVideoLayerFrameManager {
private var layerRotationAngleAndAspect: (CGFloat, CGFloat)? 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 resource = fileReference.media.resource
var secondaryResource: MediaResource? var secondaryResource: MediaResource?
for attribute in fileReference.media.attributes { for attribute in fileReference.media.attributes {
@ -61,7 +64,7 @@ final class SoftwareVideoLayerFrameManager {
self.dataDisposable.dispose() self.dataDisposable.dispose()
} }
func start() { public func start() {
func stringForResource(_ resource: MediaResource?) -> String { func stringForResource(_ resource: MediaResource?) -> String {
guard let resource = resource else { guard let resource = resource else {
return "<none>" return "<none>"
@ -115,7 +118,7 @@ final class SoftwareVideoLayerFrameManager {
})) }))
} }
func tick(timestamp: Double) { public func tick(timestamp: Double) {
applyQueue.async { applyQueue.async {
if self.baseTimestamp == nil && !self.frames.isEmpty { if self.baseTimestamp == nil && !self.frames.isEmpty {
self.baseTimestamp = timestamp self.baseTimestamp = timestamp
@ -148,6 +151,13 @@ final class SoftwareVideoLayerFrameManager {
self.layerHolder.layer.setAffineTransform(transform) self.layerHolder.layer.setAffineTransform(transform)
}*/ }*/
self.layerHolder.layer.enqueue(frame.sampleBuffer) self.layerHolder.layer.enqueue(frame.sampleBuffer)
if !self.didStart {
self.didStart = true
Queue.mainQueue().async {
self.started()
}
}
} }
} }

View File

@ -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)
}
}

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/UndoUI:UndoUI", "//submodules/UndoUI:UndoUI",
"//submodules/ContextUI:ContextUI", "//submodules/ContextUI:ContextUI",
"//submodules/SoftwareVideo:SoftwareVideo",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -11,6 +11,7 @@ import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import TelegramPresentationData import TelegramPresentationData
import ShimmerEffect import ShimmerEffect
import SoftwareVideo
final class StickerPackPreviewInteraction { final class StickerPackPreviewInteraction {
var previewedItem: StickerPreviewPeekItem? var previewedItem: StickerPreviewPeekItem?
@ -60,13 +61,19 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
private var isEmpty: Bool? private var isEmpty: Bool?
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode? private var animationNode: AnimatedStickerNode?
private var videoNode: VideoStickerNode?
private var placeholderNode: StickerShimmerEffectNode? private var placeholderNode: StickerShimmerEffectNode?
private var theme: PresentationTheme? private var theme: PresentationTheme?
override var isVisibleInGrid: Bool { override var isVisibleInGrid: Bool {
didSet { 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 self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem || self.isEmpty != isEmpty {
if let stickerItem = stickerItem { 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) 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)))) 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) = self.currentState {
if let item = item, let dimensions = item.file.dimensions?.cgSize { if let item = item, let dimensions = item.file.dimensions?.cgSize {
let imageSize = dimensions.aspectFitted(boundingSize) 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.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 { 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) animationNode.updateLayout(size: imageSize)
} }
} }

View File

@ -9,6 +9,7 @@ import StickerResources
import AnimatedStickerNode import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import ContextUI import ContextUI
import SoftwareVideo
public enum StickerPreviewPeekItem: Equatable { public enum StickerPreviewPeekItem: Equatable {
case pack(StickerPackItem) case pack(StickerPackItem)
@ -71,6 +72,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
private var textNode: ASTextNode private var textNode: ASTextNode
public var imageNode: TransformImageNode public var imageNode: TransformImageNode
public var animationNode: AnimatedStickerNode? public var animationNode: AnimatedStickerNode?
public var videoNode: VideoStickerNode?
private var containerLayout: (ContainerViewLayout, CGFloat)? private var containerLayout: (ContainerViewLayout, CGFloat)?
@ -86,16 +88,24 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
break 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() let animationNode = AnimatedStickerNode()
self.animationNode = animationNode self.animationNode = animationNode
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0)) 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)) 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 animationNode.visibility = true
self.animationNode?.addSubnode(self.textNode) animationNode.addSubnode(self.textNode)
} else { } else {
self.imageNode.addSubnode(self.textNode) self.imageNode.addSubnode(self.textNode)
self.animationNode = nil self.animationNode = nil
@ -107,7 +117,9 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
self.isUserInteractionEnabled = false 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) self.addSubnode(animationNode)
} else { } else {
self.addSubnode(self.imageNode) 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()))() 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) let imageFrame = CGRect(origin: CGPoint(x: 0.0, y: textSize.height + textSpacing), size: imageSize)
self.imageNode.frame = imageFrame 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.frame = imageFrame
animationNode.updateLayout(size: imageSize) animationNode.updateLayout(size: imageSize)
} }

View File

@ -478,6 +478,26 @@ public final class TelegramMediaFile: Media, Equatable, Codable {
return false 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 { public var hasLinkedStickers: Bool {
for attribute in self.attributes { for attribute in self.attributes {
if case .HasLinkedStickers = attribute { if case .HasLinkedStickers = attribute {

View File

@ -254,6 +254,7 @@ swift_library(
"//submodules/Components/ReactionImageComponent:ReactionImageComponent", "//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/Translate:Translate", "//submodules/Translate:Translate",
"//submodules/TabBarUI:TabBarUI", "//submodules/TabBarUI:TabBarUI",
"//submodules/SoftwareVideo:SoftwareVideo",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -9,6 +9,7 @@ import AVFoundation
import PhotoResources import PhotoResources
import AppBundle import AppBundle
import ContextUI import ContextUI
import SoftwareVideo
final class ChatContextResultPeekContent: PeekControllerContent { final class ChatContextResultPeekContent: PeekControllerContent {
let account: Account let account: Account

View File

@ -11,6 +11,7 @@ import AccountContext
import AnimatedStickerNode import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import ShimmerEffect import ShimmerEffect
import SoftwareVideo
enum ChatMediaInputStickerGridSectionAccessory { enum ChatMediaInputStickerGridSectionAccessory {
case none case none
@ -174,6 +175,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
private var currentSize: CGSize? private var currentSize: CGSize?
let imageNode: TransformImageNode let imageNode: TransformImageNode
private(set) var animationNode: AnimatedStickerNode? private(set) var animationNode: AnimatedStickerNode?
private(set) var videoNode: VideoStickerNode?
private(set) var placeholderNode: StickerShimmerEffectNode? private(set) var placeholderNode: StickerShimmerEffectNode?
private var didSetUpAnimationNode = false private var didSetUpAnimationNode = false
private var item: ChatMediaInputStickerGridItem? private var item: ChatMediaInputStickerGridItem?
@ -265,7 +267,23 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
} }
if let dimensions = item.stickerItem.file.dimensions { 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 { if self.animationNode == nil {
let animationNode = AnimatedStickerNode() let animationNode = AnimatedStickerNode()
animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
@ -306,16 +324,23 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
if let (_, _, mediaDimensions) = self.currentState { if let (_, _, mediaDimensions) = self.currentState {
let imageSize = mediaDimensions.aspectFitted(boundingSize) 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()))() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
if self.imageNode.supernode === self { 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 let animationNode = self.animationNode {
if animationNode.supernode === self { 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) 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 { if self.isPlaying != isPlaying {
self.isPlaying = isPlaying self.isPlaying = isPlaying
self.animationNode?.visibility = isPlaying self.animationNode?.visibility = isPlaying
self.videoNode?.update(isPlaying: isPlaying)
if let item = self.item, isPlaying, !self.didSetUpAnimationNode { if let item = self.item, isPlaying, !self.didSetUpAnimationNode {
self.didSetUpAnimationNode = true 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) if let videoNode = self.videoNode {
let fittedDimensions = dimensions.cgSize.aspectFitted(fitSize) videoNode.update(account: item.account, fileReference: stickerPackFileReference(item.stickerItem.file))
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: item.account, resource: item.stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) } 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)
}
} }
} }
} }

View File

@ -11,6 +11,7 @@ import ItemListStickerPackItem
import AnimatedStickerNode import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import ShimmerEffect import ShimmerEffect
import SoftwareVideo
final class ChatMediaInputStickerPackItem: ListViewItem { final class ChatMediaInputStickerPackItem: ListViewItem {
let account: Account let account: Account
@ -83,6 +84,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
private let scalingNode: ASDisplayNode private let scalingNode: ASDisplayNode
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
private var animatedStickerNode: AnimatedStickerNode? private var animatedStickerNode: AnimatedStickerNode?
private var videoStickerNode: VideoStickerNode?
private var placeholderNode: StickerShimmerEffectNode? private var placeholderNode: StickerShimmerEffectNode?
private let highlightNode: ASImageNode private let highlightNode: ASImageNode
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
@ -287,6 +289,9 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
animatedStickerNode.frame = self.imageNode.frame animatedStickerNode.frame = self.imageNode.frame
animatedStickerNode.updateLayout(size: self.imageNode.frame.size) animatedStickerNode.updateLayout(size: self.imageNode.frame.size)
} }
if let videoNode = self.videoStickerNode {
videoNode.frame = self.imageNode.frame
}
if let placeholderNode = self.placeholderNode { if let placeholderNode = self.placeholderNode {
placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize) placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize)
placeholderNode.position = self.imageNode.position placeholderNode.position = self.imageNode.position

View File

@ -26,6 +26,7 @@ import WallpaperBackgroundNode
import LocalMediaResources import LocalMediaResources
import AppBundle import AppBundle
import LottieMeshSwift import LottieMeshSwift
import SoftwareVideo
private let nameFont = Font.medium(14.0) private let nameFont = Font.medium(14.0)
private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotPrefixFont = Font.regular(14.0)
@ -54,26 +55,11 @@ extension SlotMachineAnimationNode: GenericAnimatedStickerNode {
} }
} }
private class VideoStickerNode: ASDisplayNode, GenericAnimatedStickerNode { extension VideoStickerNode: GenericAnimatedStickerNode {
private var layerHolder: SampleBufferLayer?
var manager: SoftwareVideoLayerFrameManager?
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) { 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 { var currentFrameIndex: Int {
return 0 return 0
} }
@ -201,9 +187,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var didSetUpAnimationNode = false private var didSetUpAnimationNode = false
private var isPlaying = false private var isPlaying = false
private var displayLink: ConstantDisplayLinkAnimator?
private var displayLinkTimestamp: Double = 0.0
private var additionalAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = [] private var additionalAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = []
private var overlayMeshAnimationNode: ChatMessageTransitionNode.DecorationItemNode? private var overlayMeshAnimationNode: ChatMessageTransitionNode.DecorationItemNode?
private var enqueuedAdditionalAnimations: [(Int, Double)] = [] private var enqueuedAdditionalAnimations: [(Int, Double)] = []
@ -436,8 +419,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if self.visibilityStatus != oldValue { if self.visibilityStatus != oldValue {
self.updateVisibility() self.updateVisibility()
self.haptic?.enabled = self.visibilityStatus self.haptic?.enabled = self.visibilityStatus
} }
} }
} }
@ -470,9 +451,24 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
self.animationNode = animationNode 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() 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 self.animationNode = videoNode
} else { } else {
let animationNode = AnimatedStickerNode() let animationNode = AnimatedStickerNode()
@ -591,25 +587,21 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
let isPlaying = self.visibilityStatus && !self.forceStopAnimations let isPlaying = self.visibilityStatus && !self.forceStopAnimations
if let _ = self.animationNode as? VideoStickerNode { if let videoNode = self.animationNode as? VideoStickerNode {
let displayLink: ConstantDisplayLinkAnimator if self.isPlaying != isPlaying {
if let current = self.displayLink { self.isPlaying = isPlaying
displayLink = current
} else { if self.isPlaying && !self.didSetUpAnimationNode {
displayLink = ConstantDisplayLinkAnimator { [weak self] in self.didSetUpAnimationNode = true
guard let strongSelf = self, let animationNode = strongSelf.animationNode as? VideoStickerNode else {
return 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 { } else if let animationNode = self.animationNode as? AnimatedStickerNode {
let isPlaying = self.visibilityStatus && !self.forceStopAnimations
if !isPlaying { if !isPlaying {
for decorationNode in self.additionalAnimationNodes { for decorationNode in self.additionalAnimationNodes {
if let transitionNode = item.controllerInteraction.getMessageTransitionNode() { if let transitionNode = item.controllerInteraction.getMessageTransitionNode() {
@ -1203,9 +1195,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode { if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode {
strongSelf.animationNode?.frame = animationNodeFrame strongSelf.animationNode?.frame = animationNodeFrame
} if let videoNode = strongSelf.animationNode as? VideoStickerNode {
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode, strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode { videoNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
animationNode.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 strongSelf.enableSynchronousImageApply = true

View File

@ -380,7 +380,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
loop: for media in self.message.media { loop: for media in self.message.media {
if let telegramFile = media as? TelegramMediaFile { if let telegramFile = media as? TelegramMediaFile {
if let fileName = telegramFile.fileName, fileName.hasSuffix(".webm") { if telegramFile.isVideoSticker {
viewClassName = ChatMessageAnimatedStickerItemNode.self viewClassName = ChatMessageAnimatedStickerItemNode.self
break loop break loop
} }

View File

@ -14,6 +14,7 @@ import TelegramAnimatedStickerNode
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext
import ShimmerEffect import ShimmerEffect
import SoftwareVideo
final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
let account: Account let account: Account

View File

@ -9,6 +9,7 @@ import AVFoundation
import ContextUI import ContextUI
import TelegramPresentationData import TelegramPresentationData
import ShimmerEffect import ShimmerEffect
import SoftwareVideo
final class MultiplexedVideoPlaceholderNode: ASDisplayNode { final class MultiplexedVideoPlaceholderNode: ASDisplayNode {
private let effectNode: ShimmerEffectNode private let effectNode: ShimmerEffectNode

View File

@ -125,13 +125,14 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
let previousIcon = self.icon let previousIcon = self.icon
let themeUpdated = self.theme != presentationData.theme
let iconUpdated = self.icon != icon let iconUpdated = self.icon != icon
let isActiveUpdated = self.isActive != isActive let isActiveUpdated = self.isActive != isActive
self.isActive = isActive self.isActive = isActive
let iconSize = CGSize(width: 40.0, height: 40.0) 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.theme = presentationData.theme
self.icon = icon self.icon = icon
@ -194,9 +195,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
let animationNode: AnimationNode let animationNode: AnimationNode
if let current = self.animationNode { if let current = self.animationNode {
animationNode = current animationNode = current
if iconUpdated { animationNode.setAnimation(name: animationName, colors: colors)
animationNode.setAnimation(name: animationName, colors: colors)
}
} else { } else {
animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0)
self.referenceNode.addSubnode(animationNode) self.referenceNode.addSubnode(animationNode)

View File

@ -13,6 +13,7 @@ import GridMessageSelectionNode
import UniversalMediaPlayer import UniversalMediaPlayer
import ListMessageItem import ListMessageItem
import ChatMessageInteractiveMediaBadge import ChatMessageInteractiveMediaBadge
import SoftwareVideo
private final class FrameSequenceThumbnailNode: ASDisplayNode { private final class FrameSequenceThumbnailNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext