mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Refactor wallet-related modules
This commit is contained in:
@@ -1,598 +0,0 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import Compression
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import RLottieBinding
|
||||
import GZip
|
||||
import Tuples
|
||||
import MediaResources
|
||||
import StickerResources
|
||||
|
||||
private let sharedQueue = Queue()
|
||||
|
||||
private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
||||
private var value: Bool = false
|
||||
var updated: ((Bool) -> Void)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.isLayerBacked = true
|
||||
}
|
||||
|
||||
override func didEnterHierarchy() {
|
||||
super.didEnterHierarchy()
|
||||
|
||||
if !self.value {
|
||||
self.value = true
|
||||
self.updated?(true)
|
||||
}
|
||||
}
|
||||
|
||||
override func didExitHierarchy() {
|
||||
super.didExitHierarchy()
|
||||
|
||||
if self.value {
|
||||
self.value = false
|
||||
self.updated?(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum AnimatedStickerMode {
|
||||
case cached
|
||||
case direct
|
||||
}
|
||||
|
||||
public enum AnimatedStickerPlaybackMode {
|
||||
case once
|
||||
case loop
|
||||
}
|
||||
|
||||
private final class AnimatedStickerFrame {
|
||||
let data: Data
|
||||
let type: AnimationRendererFrameType
|
||||
let width: Int
|
||||
let height: Int
|
||||
let bytesPerRow: Int
|
||||
let index: Int
|
||||
let isLastFrame: Bool
|
||||
|
||||
init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool) {
|
||||
self.data = data
|
||||
self.type = type
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = bytesPerRow
|
||||
self.index = index
|
||||
self.isLastFrame = isLastFrame
|
||||
}
|
||||
}
|
||||
|
||||
private protocol AnimatedStickerFrameSource: class {
|
||||
var frameRate: Int { get }
|
||||
var frameCount: Int { get }
|
||||
|
||||
func takeFrame() -> AnimatedStickerFrame
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let data: Data
|
||||
private var scratchBuffer: Data
|
||||
let width: Int
|
||||
let bytesPerRow: Int
|
||||
let height: Int
|
||||
let frameRate: Int
|
||||
let frameCount: Int
|
||||
private var frameIndex: Int
|
||||
private let initialOffset: Int
|
||||
private var offset: Int
|
||||
var decodeBuffer: Data
|
||||
var frameBuffer: Data
|
||||
|
||||
init?(queue: Queue, data: Data) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
|
||||
|
||||
var offset = 0
|
||||
var width = 0
|
||||
var height = 0
|
||||
var bytesPerRow = 0
|
||||
var frameRate = 0
|
||||
var frameCount = 0
|
||||
|
||||
if !self.data.withUnsafeBytes({ (bytes: UnsafePointer<UInt8>) -> Bool in
|
||||
var frameRateValue: Int32 = 0
|
||||
var frameCountValue: Int32 = 0
|
||||
var widthValue: Int32 = 0
|
||||
var heightValue: Int32 = 0
|
||||
var bytesPerRowValue: Int32 = 0
|
||||
memcpy(&frameRateValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&frameCountValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&widthValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&heightValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&bytesPerRowValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
frameRate = Int(frameRateValue)
|
||||
frameCount = Int(frameCountValue)
|
||||
width = Int(widthValue)
|
||||
height = Int(heightValue)
|
||||
bytesPerRow = Int(bytesPerRowValue)
|
||||
|
||||
return true
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.bytesPerRow = bytesPerRow
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.frameRate = frameRate
|
||||
self.frameCount = frameCount
|
||||
|
||||
self.frameIndex = 0
|
||||
self.initialOffset = offset
|
||||
self.offset = offset
|
||||
|
||||
self.decodeBuffer = Data(count: self.bytesPerRow * height)
|
||||
self.frameBuffer = Data(count: self.bytesPerRow * height)
|
||||
let frameBufferLength = self.frameBuffer.count
|
||||
self.frameBuffer.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
memset(bytes, 0, frameBufferLength)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func takeFrame() -> AnimatedStickerFrame {
|
||||
var frameData: Data?
|
||||
var isLastFrame = false
|
||||
|
||||
let dataLength = self.data.count
|
||||
let decodeBufferLength = self.decodeBuffer.count
|
||||
let frameBufferLength = self.frameBuffer.count
|
||||
|
||||
let frameIndex = self.frameIndex
|
||||
|
||||
self.data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
|
||||
var frameLength: Int32 = 0
|
||||
memcpy(&frameLength, bytes.advanced(by: self.offset), 4)
|
||||
self.offset += 4
|
||||
|
||||
self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
self.frameBuffer.withUnsafeMutableBytes { (frameBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self)
|
||||
var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self)
|
||||
for _ in 0 ..< decodeBufferLength / 8 {
|
||||
lhs.pointee = lhs.pointee ^ rhs.pointee
|
||||
lhs = lhs.advanced(by: 1)
|
||||
rhs = rhs.advanced(by: 1)
|
||||
}
|
||||
var lhsRest = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
var rhsRest = UnsafeMutableRawPointer(decodeBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
for _ in (decodeBufferLength / 8) * 8 ..< decodeBufferLength {
|
||||
lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee
|
||||
lhsRest = lhsRest.advanced(by: 1)
|
||||
rhsRest = rhsRest.advanced(by: 1)
|
||||
}
|
||||
|
||||
frameData = Data(bytes: frameBytes, count: decodeBufferLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.frameIndex += 1
|
||||
self.offset += Int(frameLength)
|
||||
if self.offset == dataLength {
|
||||
isLastFrame = true
|
||||
self.frameIndex = 0
|
||||
self.offset = self.initialOffset
|
||||
self.frameBuffer.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
memset(bytes, 0, frameBufferLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AnimatedStickerFrame(data: frameData!, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame)
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let data: Data
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
private let bytesPerRow: Int
|
||||
let frameCount: Int
|
||||
let frameRate: Int
|
||||
private var currentFrame: Int
|
||||
private let animation: LottieInstance
|
||||
|
||||
init?(queue: Queue, data: Data, width: Int, height: Int) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = (4 * Int(width) + 15) & (~15)
|
||||
self.currentFrame = 0
|
||||
guard let rawData = TGGUnzipData(data, 8 * 1024 * 1024) else {
|
||||
return nil
|
||||
}
|
||||
guard let animation = LottieInstance(data: rawData, cacheKey: "") else {
|
||||
return nil
|
||||
}
|
||||
self.animation = animation
|
||||
self.frameCount = Int(animation.frameCount)
|
||||
self.frameRate = Int(animation.frameRate)
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func takeFrame() -> AnimatedStickerFrame {
|
||||
let frameIndex = self.currentFrame % self.frameCount
|
||||
self.currentFrame += 1
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
memset(bytes, 0, self.bytesPerRow * self.height)
|
||||
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow))
|
||||
}
|
||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatedStickerFrameQueue {
|
||||
private let queue: Queue
|
||||
private let length: Int
|
||||
private let source: AnimatedStickerFrameSource
|
||||
private var frames: [AnimatedStickerFrame] = []
|
||||
|
||||
init(queue: Queue, length: Int, source: AnimatedStickerFrameSource) {
|
||||
self.queue = queue
|
||||
self.length = length
|
||||
self.source = source
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func take() -> AnimatedStickerFrame? {
|
||||
if self.frames.isEmpty {
|
||||
self.frames.append(self.source.takeFrame())
|
||||
}
|
||||
let frame = self.frames.removeFirst()
|
||||
return frame
|
||||
}
|
||||
|
||||
func generateFramesIfNeeded() {
|
||||
if self.frames.isEmpty {
|
||||
self.frames.append(self.source.takeFrame())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct AnimatedStickerStatus: Equatable {
|
||||
public let playing: Bool
|
||||
public let duration: Double
|
||||
public let timestamp: Double
|
||||
|
||||
public init(playing: Bool, duration: Double, timestamp: Double) {
|
||||
self.playing = playing
|
||||
self.duration = duration
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
|
||||
public enum AnimatedStickerNodeResource {
|
||||
case resource(Account, MediaResource)
|
||||
case localFile(String)
|
||||
}
|
||||
|
||||
public final class AnimatedStickerNode: ASDisplayNode {
|
||||
private let queue: Queue
|
||||
private var fileReference: FileMediaReference?
|
||||
private let disposable = MetaDisposable()
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
private let eventsNode: AnimatedStickerNodeDisplayEvents
|
||||
|
||||
public var automaticallyLoadFirstFrame: Bool = false
|
||||
public var playToCompletionOnStop: Bool = false
|
||||
|
||||
public var started: () -> Void = {}
|
||||
private var reportedStarted = false
|
||||
|
||||
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
|
||||
|
||||
private var directData: Tuple4<Data, String, Int, Int>?
|
||||
private var cachedData: Data?
|
||||
|
||||
private var renderer: (AnimationRenderer & ASDisplayNode)?
|
||||
|
||||
private var isPlaying: Bool = false
|
||||
private var canDisplayFirstFrame: Bool = false
|
||||
private var playbackMode: AnimatedStickerPlaybackMode = .loop
|
||||
|
||||
private let playbackStatus = Promise<AnimatedStickerStatus>()
|
||||
public var status: Signal<AnimatedStickerStatus, NoError> {
|
||||
return self.playbackStatus.get()
|
||||
}
|
||||
|
||||
public var visibility = false {
|
||||
didSet {
|
||||
if self.visibility != oldValue {
|
||||
self.updateIsPlaying()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isDisplaying = false {
|
||||
didSet {
|
||||
if self.isDisplaying != oldValue {
|
||||
self.updateIsPlaying()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public init() {
|
||||
self.queue = sharedQueue
|
||||
self.eventsNode = AnimatedStickerNodeDisplayEvents()
|
||||
|
||||
super.init()
|
||||
|
||||
self.eventsNode.updated = { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isDisplaying = value
|
||||
}
|
||||
self.addSubnode(self.eventsNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
self.fetchDisposable.dispose()
|
||||
self.timer.swap(nil)?.invalidate()
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
self.renderer = SoftwareAnimationRenderer()
|
||||
#else
|
||||
self.renderer = SoftwareAnimationRenderer()
|
||||
//self.renderer = MetalAnimationRenderer()
|
||||
#endif
|
||||
self.renderer?.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
||||
self.addSubnode(self.renderer!)
|
||||
}
|
||||
|
||||
public func setup(resource: AnimatedStickerNodeResource, fitzModifier: EmojiFitzModifier? = nil, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) {
|
||||
if width < 2 || height < 2 {
|
||||
return
|
||||
}
|
||||
self.playbackMode = playbackMode
|
||||
switch mode {
|
||||
case .direct:
|
||||
let f: (MediaResourceData) -> Void = { [weak self] data in
|
||||
guard let strongSelf = self, data.complete else {
|
||||
return
|
||||
}
|
||||
if let directData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead]) {
|
||||
strongSelf.directData = Tuple(directData, data.path, width, height)
|
||||
}
|
||||
if strongSelf.isPlaying {
|
||||
strongSelf.play()
|
||||
} else if strongSelf.canDisplayFirstFrame {
|
||||
strongSelf.play(firstFrame: true)
|
||||
}
|
||||
}
|
||||
switch resource {
|
||||
case let .resource(account, resource):
|
||||
self.disposable.set((account.postbox.mediaBox.resourceData(resource)
|
||||
|> deliverOnMainQueue).start(next: { data in
|
||||
f(data)
|
||||
}))
|
||||
case let .localFile(path):
|
||||
f(MediaResourceData(path: path, offset: 0, size: Int(Int32.max - 1), complete: true))
|
||||
}
|
||||
case .cached:
|
||||
switch resource {
|
||||
case let .resource(account, resource):
|
||||
self.disposable.set((chatMessageAnimationData(postbox: account.postbox, resource: resource, fitzModifier: fitzModifier, width: width, height: height, synchronousLoad: false)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
if let strongSelf = self, data.complete {
|
||||
strongSelf.cachedData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead])
|
||||
if strongSelf.isPlaying {
|
||||
strongSelf.play()
|
||||
} else if strongSelf.canDisplayFirstFrame {
|
||||
strongSelf.play(firstFrame: true)
|
||||
}
|
||||
}
|
||||
}))
|
||||
case .localFile:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
self.disposable.set(nil)
|
||||
self.fetchDisposable.set(nil)
|
||||
}
|
||||
|
||||
private func updateIsPlaying() {
|
||||
let isPlaying = self.visibility && self.isDisplaying
|
||||
if self.isPlaying != isPlaying {
|
||||
self.isPlaying = isPlaying
|
||||
if isPlaying {
|
||||
self.play()
|
||||
} else{
|
||||
self.stop()
|
||||
}
|
||||
}
|
||||
let canDisplayFirstFrame = self.automaticallyLoadFirstFrame && self.isDisplaying
|
||||
if self.canDisplayFirstFrame != canDisplayFirstFrame {
|
||||
self.canDisplayFirstFrame = canDisplayFirstFrame
|
||||
if canDisplayFirstFrame {
|
||||
self.play(firstFrame: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func play(firstFrame: Bool = false) {
|
||||
let directData = self.directData
|
||||
let cachedData = self.cachedData
|
||||
let queue = self.queue
|
||||
let timerHolder = self.timer
|
||||
self.queue.async { [weak self] in
|
||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData._0, width: directData._2, height: directData._3)
|
||||
} else if let cachedData = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData)
|
||||
}
|
||||
}
|
||||
guard let frameSource = maybeFrameSource else {
|
||||
return
|
||||
}
|
||||
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
|
||||
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
|
||||
})
|
||||
timerHolder.swap(nil)?.invalidate()
|
||||
|
||||
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
|
||||
let frameRate = frameSource.frameRate
|
||||
|
||||
let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
|
||||
let maybeFrame = frameQueue.syncWith { frameQueue in
|
||||
return frameQueue.take()
|
||||
}
|
||||
if let maybeFrame = maybeFrame, let frame = maybeFrame {
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.renderer?.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, completion: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.reportedStarted {
|
||||
strongSelf.reportedStarted = true
|
||||
strongSelf.started()
|
||||
}
|
||||
})
|
||||
|
||||
if case .once = strongSelf.playbackMode, frame.isLastFrame {
|
||||
strongSelf.stop()
|
||||
strongSelf.isPlaying = false
|
||||
}
|
||||
|
||||
let timestamp: Double = frameRate > 0 ? Double(frame.index) / Double(frameRate) : 0
|
||||
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: strongSelf.isPlaying, duration: duration, timestamp: timestamp)))
|
||||
}
|
||||
}
|
||||
frameQueue.with { frameQueue in
|
||||
frameQueue.generateFramesIfNeeded()
|
||||
}
|
||||
}, queue: queue)
|
||||
let _ = timerHolder.swap(timer)
|
||||
timer.start()
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
self.reportedStarted = false
|
||||
self.timer.swap(nil)?.invalidate()
|
||||
if self.playToCompletionOnStop {
|
||||
self.seekToStart()
|
||||
}
|
||||
}
|
||||
|
||||
public func seekToStart() {
|
||||
self.isPlaying = false
|
||||
|
||||
let directData = self.directData
|
||||
let cachedData = self.cachedData
|
||||
let queue = self.queue
|
||||
let timerHolder = self.timer
|
||||
self.queue.async { [weak self] in
|
||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData._0, width: directData._2, height: directData._3)
|
||||
} else if let cachedData = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData)
|
||||
}
|
||||
}
|
||||
guard let frameSource = maybeFrameSource else {
|
||||
return
|
||||
}
|
||||
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
|
||||
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
|
||||
})
|
||||
timerHolder.swap(nil)?.invalidate()
|
||||
|
||||
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
|
||||
|
||||
let maybeFrame = frameQueue.syncWith { frameQueue in
|
||||
return frameQueue.take()
|
||||
}
|
||||
if let maybeFrame = maybeFrame, let frame = maybeFrame {
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.renderer?.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, completion: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.reportedStarted {
|
||||
strongSelf.reportedStarted = true
|
||||
strongSelf.started()
|
||||
}
|
||||
})
|
||||
|
||||
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: false, duration: duration, timestamp: 0.0)))
|
||||
}
|
||||
}
|
||||
frameQueue.with { frameQueue in
|
||||
frameQueue.generateFramesIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func playIfNeeded() -> Bool {
|
||||
if !self.isPlaying {
|
||||
self.isPlaying = true
|
||||
self.play()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize) {
|
||||
self.renderer?.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import Display
|
||||
import TelegramCore
|
||||
import Compression
|
||||
import GZip
|
||||
import RLottieBinding
|
||||
import MediaResources
|
||||
import MobileCoreServices
|
||||
import MediaResources
|
||||
import YuvConversion
|
||||
|
||||
let colorKeyRegex = try? NSRegularExpression(pattern: "\"k\":\\[[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\]")
|
||||
|
||||
private func transformedWithFitzModifier(data: Data, fitzModifier: EmojiFitzModifier?) -> Data {
|
||||
if let fitzModifier = fitzModifier, var string = String(data: data, encoding: .utf8) {
|
||||
var colors: [UIColor] = [0xf77e41, 0xffb139, 0xffd140, 0xffdf79].map { UIColor(rgb: $0) }
|
||||
let replacementColors: [UIColor]
|
||||
switch fitzModifier {
|
||||
case .type12:
|
||||
replacementColors = [0xca907a, 0xedc5a5, 0xf7e3c3, 0xfbefd6].map { UIColor(rgb: $0) }
|
||||
case .type3:
|
||||
replacementColors = [0xaa7c60, 0xc8a987, 0xddc89f, 0xe6d6b2].map { UIColor(rgb: $0) }
|
||||
case .type4:
|
||||
replacementColors = [0x8c6148, 0xad8562, 0xc49e76, 0xd4b188].map { UIColor(rgb: $0) }
|
||||
case .type5:
|
||||
replacementColors = [0x6e3c2c, 0x925a34, 0xa16e46, 0xac7a52].map { UIColor(rgb: $0) }
|
||||
case .type6:
|
||||
replacementColors = [0x291c12, 0x472a22, 0x573b30, 0x68493c].map { UIColor(rgb: $0) }
|
||||
}
|
||||
|
||||
func colorToString(_ color: UIColor) -> String {
|
||||
var r: CGFloat = 0.0
|
||||
var g: CGFloat = 0.0
|
||||
var b: CGFloat = 0.0
|
||||
if color.getRed(&r, green: &g, blue: &b, alpha: nil) {
|
||||
return "\"k\":[\(r),\(g),\(b),1]"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func match(_ a: Double, _ b: Double, eps: Double) -> Bool {
|
||||
return abs(a - b) < eps
|
||||
}
|
||||
|
||||
var replacements: [(NSTextCheckingResult, String)] = []
|
||||
|
||||
if let colorKeyRegex = colorKeyRegex {
|
||||
let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string))
|
||||
for result in results.reversed() {
|
||||
if let range = Range(result.range, in: string) {
|
||||
let substring = String(string[range])
|
||||
let color = substring[substring.index(string.startIndex, offsetBy: "\"k\":[".count) ..< substring.index(before: substring.endIndex)]
|
||||
let components = color.split(separator: ",")
|
||||
if components.count == 4, let r = Double(components[0]), let g = Double(components[1]), let b = Double(components[2]), let a = Double(components[3]) {
|
||||
if match(a, 1.0, eps: 0.01) {
|
||||
for i in 0 ..< colors.count {
|
||||
let color = colors[i]
|
||||
var cr: CGFloat = 0.0
|
||||
var cg: CGFloat = 0.0
|
||||
var cb: CGFloat = 0.0
|
||||
if color.getRed(&cr, green: &cg, blue: &cb, alpha: nil) {
|
||||
if match(r, Double(cr), eps: 0.01) && match(g, Double(cg), eps: 0.01) && match(b, Double(cb), eps: 0.01) {
|
||||
replacements.append((result, colorToString(replacementColors[i])))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (result, text) in replacements {
|
||||
if let range = Range(result.range, in: string) {
|
||||
string = string.replacingCharacters(in: range, with: text)
|
||||
}
|
||||
}
|
||||
|
||||
return string.data(using: .utf8) ?? data
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchCompressedLottieFirstFrameAJpeg(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal<TempBoxFile, NoError> {
|
||||
return Signal({ subscriber in
|
||||
let queue = Queue()
|
||||
|
||||
let cancelled = Atomic<Bool>(value: false)
|
||||
|
||||
queue.async {
|
||||
if cancelled.with({ $0 }) {
|
||||
return
|
||||
}
|
||||
|
||||
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024)
|
||||
if let decompressedData = decompressedData {
|
||||
let decompressedData = transformedWithFitzModifier(data: decompressedData, fitzModifier: fitzModifier)
|
||||
if let player = LottieInstance(data: decompressedData, cacheKey: cacheKey) {
|
||||
if cancelled.with({ $0 }) {
|
||||
return
|
||||
}
|
||||
|
||||
let context = DrawingContext(size: size, scale: 1.0, clear: true)
|
||||
player.renderFrame(with: 0, into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(size.width), height: Int32(size.height), bytesPerRow: Int32(context.bytesPerRow))
|
||||
|
||||
let yuvaPixelsPerAlphaRow = (Int(size.width) + 1) & (~1)
|
||||
assert(yuvaPixelsPerAlphaRow % 2 == 0)
|
||||
|
||||
let yuvaLength = Int(size.width) * Int(size.height) * 2 + yuvaPixelsPerAlphaRow * Int(size.height) / 2
|
||||
var yuvaFrameData = malloc(yuvaLength)!
|
||||
memset(yuvaFrameData, 0, yuvaLength)
|
||||
|
||||
defer {
|
||||
free(yuvaFrameData)
|
||||
}
|
||||
|
||||
encodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), context.bytes.assumingMemoryBound(to: UInt8.self), Int32(size.width), Int32(size.height), Int32(context.bytesPerRow))
|
||||
decodeYUVAToRGBA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), context.bytes.assumingMemoryBound(to: UInt8.self), Int32(size.width), Int32(size.height), Int32(context.bytesPerRow))
|
||||
|
||||
if let colorSourceImage = context.generateImage(), let alphaImage = generateGrayscaleAlphaMaskImage(image: colorSourceImage) {
|
||||
let colorContext = DrawingContext(size: size, scale: 1.0, clear: false)
|
||||
colorContext.withFlippedContext { c in
|
||||
c.setFillColor(UIColor.black.cgColor)
|
||||
c.fill(CGRect(origin: CGPoint(), size: size))
|
||||
c.draw(colorSourceImage.cgImage!, in: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
guard let colorImage = colorContext.generateImage() else {
|
||||
return
|
||||
}
|
||||
|
||||
let colorData = NSMutableData()
|
||||
let alphaData = NSMutableData()
|
||||
|
||||
if let colorDestination = CGImageDestinationCreateWithData(colorData as CFMutableData, kUTTypeJPEG, 1, nil), let alphaDestination = CGImageDestinationCreateWithData(alphaData as CFMutableData, kUTTypeJPEG, 1, nil) {
|
||||
CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary)
|
||||
CGImageDestinationSetProperties(alphaDestination, [:] as CFDictionary)
|
||||
|
||||
let colorQuality: Float
|
||||
let alphaQuality: Float
|
||||
colorQuality = 0.5
|
||||
alphaQuality = 0.4
|
||||
|
||||
let options = NSMutableDictionary()
|
||||
options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString)
|
||||
|
||||
let optionsAlpha = NSMutableDictionary()
|
||||
optionsAlpha.setObject(alphaQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString)
|
||||
|
||||
CGImageDestinationAddImage(colorDestination, colorImage.cgImage!, options as CFDictionary)
|
||||
CGImageDestinationAddImage(alphaDestination, alphaImage.cgImage!, optionsAlpha as CFDictionary)
|
||||
if CGImageDestinationFinalize(colorDestination) && CGImageDestinationFinalize(alphaDestination) {
|
||||
let finalData = NSMutableData()
|
||||
var colorSize: Int32 = Int32(colorData.length)
|
||||
finalData.append(&colorSize, length: 4)
|
||||
finalData.append(colorData as Data)
|
||||
var alphaSize: Int32 = Int32(alphaData.length)
|
||||
finalData.append(&alphaSize, length: 4)
|
||||
finalData.append(alphaData as Data)
|
||||
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "image.ajpg")
|
||||
let _ = try? finalData.write(to: URL(fileURLWithPath: tempFile.path), options: [])
|
||||
subscriber.putNext(tempFile)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ActionDisposable {
|
||||
let _ = cancelled.swap(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private let threadPool: ThreadPool = {
|
||||
return ThreadPool(threadCount: 3, threadPriority: 0.5)
|
||||
}()
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal<String, NoError> {
|
||||
return Signal({ subscriber in
|
||||
let cancelled = Atomic<Bool>(value: false)
|
||||
|
||||
threadPool.addTask(ThreadPoolTask({ _ in
|
||||
if cancelled.with({ $0 }) {
|
||||
//print("cancelled 1")
|
||||
return
|
||||
}
|
||||
|
||||
let startTime = CACurrentMediaTime()
|
||||
var drawingTime: Double = 0
|
||||
var appendingTime: Double = 0
|
||||
var deltaTime: Double = 0
|
||||
var compressionTime: Double = 0
|
||||
|
||||
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024)
|
||||
if let decompressedData = decompressedData {
|
||||
let decompressedData = transformedWithFitzModifier(data: decompressedData, fitzModifier: fitzModifier)
|
||||
if let player = LottieInstance(data: decompressedData, cacheKey: cacheKey) {
|
||||
let endFrame = Int(player.frameCount)
|
||||
|
||||
if cancelled.with({ $0 }) {
|
||||
//print("cancelled 2")
|
||||
return
|
||||
}
|
||||
|
||||
let path = NSTemporaryDirectory() + "\(arc4random64()).lz4v"
|
||||
guard let fileContext = ManagedFile(queue: nil, path: path, mode: .readwrite) else {
|
||||
return
|
||||
}
|
||||
|
||||
let bytesPerRow = (4 * Int(size.width) + 15) & (~15)
|
||||
|
||||
var currentFrame: Int32 = 0
|
||||
|
||||
var fps: Int32 = player.frameRate
|
||||
var frameCount: Int32 = player.frameCount
|
||||
let _ = fileContext.write(&fps, count: 4)
|
||||
let _ = fileContext.write(&frameCount, count: 4)
|
||||
var widthValue: Int32 = Int32(size.width)
|
||||
var heightValue: Int32 = Int32(size.height)
|
||||
var bytesPerRowValue: Int32 = Int32(bytesPerRow)
|
||||
let _ = fileContext.write(&widthValue, count: 4)
|
||||
let _ = fileContext.write(&heightValue, count: 4)
|
||||
let _ = fileContext.write(&bytesPerRowValue, count: 4)
|
||||
|
||||
let frameLength = bytesPerRow * Int(size.height)
|
||||
assert(frameLength % 16 == 0)
|
||||
|
||||
let currentFrameData = malloc(frameLength)!
|
||||
memset(currentFrameData, 0, frameLength)
|
||||
|
||||
let yuvaPixelsPerAlphaRow = (Int(size.width) + 1) & (~1)
|
||||
assert(yuvaPixelsPerAlphaRow % 2 == 0)
|
||||
|
||||
let yuvaLength = Int(size.width) * Int(size.height) * 2 + yuvaPixelsPerAlphaRow * Int(size.height) / 2
|
||||
var yuvaFrameData = malloc(yuvaLength)!
|
||||
memset(yuvaFrameData, 0, yuvaLength)
|
||||
|
||||
var previousYuvaFrameData = malloc(yuvaLength)!
|
||||
memset(previousYuvaFrameData, 0, yuvaLength)
|
||||
|
||||
defer {
|
||||
free(currentFrameData)
|
||||
free(previousYuvaFrameData)
|
||||
free(yuvaFrameData)
|
||||
}
|
||||
|
||||
var compressedFrameData = Data(count: frameLength)
|
||||
let compressedFrameDataLength = compressedFrameData.count
|
||||
|
||||
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
|
||||
defer {
|
||||
free(scratchData)
|
||||
}
|
||||
|
||||
while currentFrame < endFrame {
|
||||
if cancelled.with({ $0 }) {
|
||||
//print("cancelled 3")
|
||||
return
|
||||
}
|
||||
|
||||
let drawStartTime = CACurrentMediaTime()
|
||||
memset(currentFrameData, 0, frameLength)
|
||||
player.renderFrame(with: Int32(currentFrame), into: currentFrameData.assumingMemoryBound(to: UInt8.self), width: Int32(size.width), height: Int32(size.height), bytesPerRow: Int32(bytesPerRow))
|
||||
drawingTime += CACurrentMediaTime() - drawStartTime
|
||||
|
||||
let appendStartTime = CACurrentMediaTime()
|
||||
|
||||
encodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), currentFrameData.assumingMemoryBound(to: UInt8.self), Int32(size.width), Int32(size.height), Int32(bytesPerRow))
|
||||
|
||||
appendingTime += CACurrentMediaTime() - appendStartTime
|
||||
|
||||
let deltaStartTime = CACurrentMediaTime()
|
||||
var lhs = previousYuvaFrameData.assumingMemoryBound(to: UInt64.self)
|
||||
var rhs = yuvaFrameData.assumingMemoryBound(to: UInt64.self)
|
||||
for _ in 0 ..< yuvaLength / 8 {
|
||||
lhs.pointee = rhs.pointee ^ lhs.pointee
|
||||
lhs = lhs.advanced(by: 1)
|
||||
rhs = rhs.advanced(by: 1)
|
||||
}
|
||||
var lhsRest = previousYuvaFrameData.assumingMemoryBound(to: UInt8.self).advanced(by: (yuvaLength / 8) * 8)
|
||||
var rhsRest = yuvaFrameData.assumingMemoryBound(to: UInt8.self).advanced(by: (yuvaLength / 8) * 8)
|
||||
for _ in (yuvaLength / 8) * 8 ..< yuvaLength {
|
||||
lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee
|
||||
lhsRest = lhsRest.advanced(by: 1)
|
||||
rhsRest = rhsRest.advanced(by: 1)
|
||||
}
|
||||
deltaTime += CACurrentMediaTime() - deltaStartTime
|
||||
|
||||
let compressionStartTime = CACurrentMediaTime()
|
||||
compressedFrameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
let length = compression_encode_buffer(bytes, compressedFrameDataLength, previousYuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
|
||||
var frameLengthValue: Int32 = Int32(length)
|
||||
let _ = fileContext.write(&frameLengthValue, count: 4)
|
||||
let _ = fileContext.write(bytes, count: length)
|
||||
}
|
||||
|
||||
let tmp = previousYuvaFrameData
|
||||
previousYuvaFrameData = yuvaFrameData
|
||||
yuvaFrameData = tmp
|
||||
|
||||
compressionTime += CACurrentMediaTime() - compressionStartTime
|
||||
|
||||
currentFrame += 1
|
||||
}
|
||||
|
||||
subscriber.putNext(path)
|
||||
subscriber.putCompletion()
|
||||
print("animation render time \(CACurrentMediaTime() - startTime)")
|
||||
print("of which drawing time \(drawingTime)")
|
||||
print("of which appending time \(appendingTime)")
|
||||
print("of which delta time \(deltaTime)")
|
||||
|
||||
print("of which compression time \(compressionTime)")
|
||||
}
|
||||
}
|
||||
}))
|
||||
return ActionDisposable {
|
||||
let _ = cancelled.swap(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
enum AnimationRendererFrameType {
|
||||
case argb
|
||||
case yuva
|
||||
}
|
||||
|
||||
protocol AnimationRenderer {
|
||||
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, completion: @escaping () -> Void)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// AnimationUI.h
|
||||
// AnimationUI
|
||||
//
|
||||
// Created by Peter on 8/1/19.
|
||||
// Copyright © 2019 Telegram Messenger LLP. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for AnimationUI.
|
||||
FOUNDATION_EXPORT double AnimationUIVersionNumber;
|
||||
|
||||
//! Project version string for AnimationUI.
|
||||
FOUNDATION_EXPORT const unsigned char AnimationUIVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <AnimationUI/PublicHeader.h>
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import YuvConversion
|
||||
|
||||
final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, completion: @escaping () -> Void) {
|
||||
queue.async { [weak self] in
|
||||
let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15)
|
||||
assert(bytesPerRow == calculatedBytesPerRow)
|
||||
|
||||
let image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, bytesPerRow in
|
||||
switch type {
|
||||
case .yuva:
|
||||
data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
|
||||
decodeYUVAToRGBA(bytes, pixelData, Int32(width), Int32(height), Int32(bytesPerRow))
|
||||
}
|
||||
case .argb:
|
||||
data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
|
||||
memcpy(pixelData, bytes, data.count)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Queue.mainQueue().async {
|
||||
self?.contents = image?.cgImage
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user