mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Video Stickers Improvements
This commit is contained in:
parent
b102311660
commit
47363cb2e3
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
23
submodules/SoftwareVideo/BUILD
Normal file
23
submodules/SoftwareVideo/BUILD
Normal 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",
|
||||
],
|
||||
)
|
22
submodules/SoftwareVideo/Info.plist
Normal file
22
submodules/SoftwareVideo/Info.plist
Normal 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>
|
@ -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
|
@ -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<SoftwareVideoSource?>(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 "<none>"
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
56
submodules/SoftwareVideo/Sources/VideoStickerNode.swift
Normal file
56
submodules/SoftwareVideo/Sources/VideoStickerNode.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -31,6 +31,7 @@ swift_library(
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -9,6 +9,7 @@ import AVFoundation
|
||||
import PhotoResources
|
||||
import AppBundle
|
||||
import ContextUI
|
||||
import SoftwareVideo
|
||||
|
||||
final class ChatContextResultPeekContent: PeekControllerContent {
|
||||
let account: Account
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import TelegramAnimatedStickerNode
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ShimmerEffect
|
||||
import SoftwareVideo
|
||||
|
||||
final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
|
||||
let account: Account
|
||||
|
@ -9,6 +9,7 @@ import AVFoundation
|
||||
import ContextUI
|
||||
import TelegramPresentationData
|
||||
import ShimmerEffect
|
||||
import SoftwareVideo
|
||||
|
||||
final class MultiplexedVideoPlaceholderNode: ASDisplayNode {
|
||||
private let effectNode: ShimmerEffectNode
|
||||
|
@ -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)
|
||||
|
@ -13,6 +13,7 @@ import GridMessageSelectionNode
|
||||
import UniversalMediaPlayer
|
||||
import ListMessageItem
|
||||
import ChatMessageInteractiveMediaBadge
|
||||
import SoftwareVideo
|
||||
|
||||
private final class FrameSequenceThumbnailNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
|
Loading…
x
Reference in New Issue
Block a user