GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,641 @@
import Foundation
import Compression
import Display
import SwiftSignalKit
import MediaResources
import RLottieBinding
import GZip
import ManagedFile
import AnimationCompression
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
public extension EmojiFitzModifier {
var lottieFitzModifier: LottieFitzModifier {
switch self {
case .type12:
return .type12
case .type3:
return .type3
case .type4:
return .type4
case .type5:
return .type5
case .type6:
return .type6
}
}
}
public protocol AnimatedStickerFrameSource: AnyObject {
var frameRate: Int { get }
var frameCount: Int { get }
var frameIndex: Int { get }
func takeFrame(draw: Bool) -> AnimatedStickerFrame?
func skipToEnd()
func skipToFrameIndex(_ index: Int)
}
final class AnimatedStickerFrameSourceWrapper {
let value: AnimatedStickerFrameSource
init(_ value: AnimatedStickerFrameSource) {
self.value = value
}
}
public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource {
private let queue: Queue
private var data: Data
private var dataComplete: Bool
private let notifyUpdated: () -> Void
private var scratchBuffer: Data
let width: Int
let bytesPerRow: Int
let height: Int
public let frameRate: Int
public let frameCount: Int
public var frameIndex: Int
private let initialOffset: Int
private var offset: Int
var decodeBuffer: Data
var frameBuffer: Data
public init?(queue: Queue, data: Data, complete: Bool, notifyUpdated: @escaping () -> Void) {
self.queue = queue
self.data = data
self.dataComplete = complete
self.notifyUpdated = notifyUpdated
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({ buffer -> Bool in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return false
}
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 { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memset(bytes, 0, frameBufferLength)
}
}
deinit {
assert(self.queue.isCurrent())
}
public func takeFrame(draw: Bool) -> 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 { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
if self.offset + 4 > dataLength {
if self.dataComplete {
self.frameIndex = 0
self.offset = self.initialOffset
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memset(bytes, 0, frameBufferLength)
}
}
return
}
var frameLength: Int32 = 0
memcpy(&frameLength, bytes.advanced(by: self.offset), 4)
if self.offset + 4 + Int(frameLength) > dataLength {
return
}
self.offset += 4
if draw {
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.frameBuffer.withUnsafeMutableBytes { frameBuffer -> Void in
guard let frameBytes = frameBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
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 && self.dataComplete {
isLastFrame = true
self.frameIndex = 0
self.offset = self.initialOffset
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memset(bytes, 0, frameBufferLength)
}
}
}
if let frameData = frameData, draw {
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame, totalFrames: self.frameCount)
} else {
return nil
}
}
func updateData(data: Data, complete: Bool) {
self.data = data
self.dataComplete = complete
}
public func skipToEnd() {
}
public func skipToFrameIndex(_ index: Int) {
}
}
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
private final class AnimatedStickerDirectFrameSourceCache {
private enum FrameRangeResult {
case range(Range<Int>)
case notFound
case corruptedFile
}
private let queue: Queue
private let storeQueue: Queue
private let file: ManagedFile
private let frameCount: Int
private let width: Int
private let height: Int
private let useHardware: Bool
private var isStoringFrames = Set<Int>()
private var scratchBuffer: Data
private var decodeBuffer: Data
private var frameCompressor: AnimationCompressor?
init?(queue: Queue, pathPrefix: String, width: Int, height: Int, frameCount: Int, fitzModifier: EmojiFitzModifier?, useHardware: Bool) {
self.queue = queue
self.storeQueue = sharedStoreQueue
self.frameCount = frameCount
self.width = width// alignUp(size: width, align: 8)
self.height = height//alignUp(size: height, align: 8)
self.useHardware = useHardware
let suffix : String
if let fitzModifier = fitzModifier {
suffix = "_fitz\(fitzModifier.rawValue)"
} else {
suffix = ""
}
let path = "\(pathPrefix)_\(width):\(height)\(suffix).stickerframecachev3\(useHardware ? "-mtl" : "")"
var file = ManagedFile(queue: queue, path: path, mode: .readwrite)
if let file = file {
self.file = file
} else {
let _ = try? FileManager.default.removeItem(atPath: path)
file = ManagedFile(queue: queue, path: path, mode: .readwrite)
if let file = file {
self.file = file
} else {
return nil
}
}
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
self.decodeBuffer = Data(count: yuvaLength)
self.initializeFrameTable()
}
private func initializeFrameTable() {
if let size = self.file.getSize(), size >= self.frameCount * 4 * 2 {
} else {
self.file.truncate(count: 0)
for _ in 0 ..< self.frameCount {
var zero: Int32 = 0
let _ = self.file.write(&zero, count: 4)
let _ = self.file.write(&zero, count: 4)
}
}
}
private func readFrameRange(index: Int) -> FrameRangeResult {
if index < 0 || index >= self.frameCount {
return .notFound
}
let _ = self.file.seek(position: Int64(index * 4 * 2))
var offset: Int32 = 0
var length: Int32 = 0
if self.file.read(&offset, 4) != 4 {
return .corruptedFile
}
if self.file.read(&length, 4) != 4 {
return .corruptedFile
}
if length == 0 {
return .notFound
}
if length < 0 || offset < 0 {
return .corruptedFile
}
if Int64(offset) + Int64(length) > 200 * 1024 * 1024 {
return .corruptedFile
}
return .range(Int(offset) ..< Int(offset + length))
}
func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
if self.useHardware {
self.storeUncompressedRgbFrameMetal(index: index, rgbData: rgbData)
} else {
self.storeUncompressedRgbFrameSoft(index: index, rgbData: rgbData)
}
}
func storeUncompressedRgbFrameMetal(index: Int, rgbData: Data) {
if self.isStoringFrames.contains(index) {
return
}
self.isStoringFrames.insert(index)
if self.frameCompressor == nil {
self.frameCompressor = AnimationCompressor(sharedContext: AnimationCompressor.SharedContext.shared)
}
let queue = self.queue
let frameCompressor = self.frameCompressor
let width = self.width
let height = self.height
DispatchQueue.main.async { [weak self] in
frameCompressor?.compress(image: AnimationCompressor.ImageData(width: width, height: height, bytesPerRow: width * 4, data: rgbData), completion: { compressedData in
queue.async {
guard let strongSelf = self else {
return
}
guard let currentSize = strongSelf.file.getSize() else {
return
}
let _ = strongSelf.file.seek(position: Int64(index * 4 * 2))
var offset = Int32(currentSize)
var length = Int32(compressedData.data.count)
let _ = strongSelf.file.write(&offset, count: 4)
let _ = strongSelf.file.write(&length, count: 4)
let _ = strongSelf.file.seek(position: Int64(currentSize))
compressedData.data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
if let baseAddress = buffer.baseAddress {
let _ = strongSelf.file.write(baseAddress, count: Int(length))
}
}
}
})
}
}
func storeUncompressedRgbFrameSoft(index: Int, rgbData: Data) {
if index < 0 || index >= self.frameCount {
return
}
if self.isStoringFrames.contains(index) {
return
}
self.isStoringFrames.insert(index)
let width = self.width
let height = self.height
let queue = self.queue
self.storeQueue.async { [weak self] in
let compressedData = compressFrame(width: width, height: height, rgbData: rgbData, unpremultiply: true)
queue.async {
guard let strongSelf = self else {
return
}
guard let currentSize = strongSelf.file.getSize() else {
return
}
guard let compressedData = compressedData else {
return
}
let _ = strongSelf.file.seek(position: Int64(index * 4 * 2))
var offset = Int32(currentSize)
var length = Int32(compressedData.count)
let _ = strongSelf.file.write(&offset, count: 4)
let _ = strongSelf.file.write(&length, count: 4)
let _ = strongSelf.file.seek(position: Int64(currentSize))
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
if let baseAddress = buffer.baseAddress {
let _ = strongSelf.file.write(baseAddress, count: Int(length))
}
}
}
}
}
/*func readUncompressedYuvaFrameOld(index: Int) -> Data? {
if index < 0 || index >= self.frameCount {
return nil
}
let rangeResult = self.readFrameRange(index: index)
switch rangeResult {
case let .range(range):
self.file.seek(position: Int64(range.lowerBound))
let length = range.upperBound - range.lowerBound
let compressedData = self.file.readData(count: length)
if compressedData.count != length {
return nil
}
var frameData: Data?
let decodeBufferLength = self.decodeBuffer.count
compressedData.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
frameData = Data(bytes: decodeBytes, count: resultLength)
}
}
}
return frameData
case .notFound:
return nil
case .corruptedFile:
self.file.truncate(count: 0)
self.initializeFrameTable()
return nil
}
}*/
func readCompressedFrame(index: Int, totalFrames: Int) -> AnimatedStickerFrame? {
if index < 0 || index >= self.frameCount {
return nil
}
let rangeResult = self.readFrameRange(index: index)
switch rangeResult {
case let .range(range):
let _ = self.file.seek(position: Int64(range.lowerBound))
let length = range.upperBound - range.lowerBound
let compressedData = self.file.readData(count: length)
if compressedData.count != length {
return nil
}
if compressedData.count > 4 {
var magic: Int32 = 0
compressedData.withUnsafeBytes { bytes in
let _ = memcpy(&magic, bytes.baseAddress!, 4)
}
if magic == 0x543ee445 {
return AnimatedStickerFrame(data: compressedData, type: .dct, width: 0, height: 0, bytesPerRow: 0, index: index, isLastFrame: index == frameCount - 1, totalFrames: frameCount)
}
}
var frameData: Data?
let decodeBufferLength = self.decodeBuffer.count
compressedData.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
frameData = Data(bytes: decodeBytes, count: resultLength)
}
}
}
if let frameData = frameData {
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: index, isLastFrame: index == frameCount - 1, totalFrames: frameCount)
} else {
return nil
}
case .notFound:
return nil
case .corruptedFile:
self.file.truncate(count: 0)
self.initializeFrameTable()
return nil
}
}
}
public final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
private let queue: Queue
private let data: Data
private let width: Int
private let height: Int
private let cache: AnimatedStickerDirectFrameSourceCache?
private let bytesPerRow: Int
public let frameCount: Int
public let frameRate: Int
fileprivate var currentFrame: Int
private let animation: LottieInstance
public var frameIndex: Int {
return self.currentFrame % self.frameCount
}
public init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?, useMetalCache: Bool = false, fitzModifier: EmojiFitzModifier?) {
self.queue = queue
self.data = data
self.width = width
self.height = height
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width))
self.currentFrame = 0
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
guard let animation = LottieInstance(data: decompressedData, fitzModifier: fitzModifier?.lottieFitzModifier ?? .none, colorReplacements: nil, cacheKey: "") else {
print("Could not load sticker data")
return nil
}
self.animation = animation
let frameCount = max(1, Int(animation.frameCount))
self.frameCount = frameCount
self.frameRate = max(1, Int(animation.frameRate))
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount, fitzModifier: fitzModifier, useHardware: useMetalCache)
}
}
deinit {
assert(self.queue.isCurrent())
}
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
let frameIndex = self.currentFrame % self.frameCount
self.currentFrame += 1
if draw {
if let cache = self.cache, let compressedFrame = cache.readCompressedFrame(index: frameIndex, totalFrames: self.frameCount) {
return compressedFrame
} else {
var frameData = Data(count: self.bytesPerRow * self.height)
frameData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
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))
}
if let cache = self.cache {
cache.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
}
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
}
} else {
return nil
}
}
public func skipToEnd() {
self.currentFrame = self.frameCount - 1
}
public func skipToFrameIndex(_ index: Int) {
self.currentFrame = index
}
}
@@ -0,0 +1,848 @@
import Foundation
import SwiftSignalKit
import Compression
import Display
import AsyncDisplayKit
import YuvConversion
import MediaResources
import AnimationCompression
import UIKit
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()
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else {
return
}
if !strongSelf.isInHierarchy {
if strongSelf.value {
strongSelf.value = false
strongSelf.updated?(false)
}
}
}
}
}
public enum AnimatedStickerMode {
case cached
case direct(cachePathPrefix: String?)
}
public enum AnimatedStickerPlaybackPosition {
case start
case end
case timestamp(Double)
case frameIndex(Int)
}
public enum AnimatedStickerPlaybackMode {
case once
case count(Int)
case loop
case still(AnimatedStickerPlaybackPosition)
}
public final class AnimatedStickerFrame {
public let data: Data
public let type: AnimationRendererFrameType
public let width: Int
public let height: Int
public let bytesPerRow: Int
let index: Int
let isLastFrame: Bool
let totalFrames: Int
let multiplyAlpha: Bool
init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool, totalFrames: Int, multiplyAlpha: Bool = false) {
self.data = data
self.type = type
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
self.index = index
self.isLastFrame = isLastFrame
self.totalFrames = totalFrames
self.multiplyAlpha = multiplyAlpha
}
}
public final class AnimatedStickerFrameQueue {
private let queue: Queue
private let length: Int
private let source: AnimatedStickerFrameSource
private var frames: [AnimatedStickerFrame] = []
public init(queue: Queue, length: Int, source: AnimatedStickerFrameSource) {
self.queue = queue
self.length = length
self.source = source
}
deinit {
assert(self.queue.isCurrent())
}
public func take(draw: Bool) -> AnimatedStickerFrame? {
if self.frames.isEmpty {
if let frame = self.source.takeFrame(draw: draw) {
self.frames.append(frame)
}
}
if !self.frames.isEmpty {
let frame = self.frames.removeFirst()
return frame
} else {
return nil
}
}
public func generateFramesIfNeeded() {
if self.frames.isEmpty {
if let frame = self.source.takeFrame(draw: true) {
self.frames.append(frame)
}
}
}
}
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 protocol AnimatedStickerNodeSource {
var fitzModifier: EmojiFitzModifier? { get }
var isVideo: Bool { get }
func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError>
func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError>
}
public protocol AnimatedStickerNode: ASDisplayNode {
var automaticallyLoadFirstFrame: Bool { get set }
var automaticallyLoadLastFrame: Bool { get set }
var playToCompletionOnStop: Bool { get set }
var started: () -> Void { get set }
var completed: (Bool) -> Void { get set }
var frameUpdated: (Int, Int) -> Void { get set }
var currentFrameIndex: Int { get }
var currentFrameImage: UIImage? { get }
var currentFrameCount: Int { get }
var isPlaying: Bool { get }
var stopAtNearestLoop: Bool { get set }
var status: Signal<AnimatedStickerStatus, NoError> { get }
var autoplay: Bool { get set }
var visibility: Bool { get set }
var overrideVisibility: Bool { get set }
var isPlayingChanged: (Bool) -> Void { get set }
func cloneCurrentFrame(from otherNode: AnimatedStickerNode?)
func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode)
func reset()
func playOnce()
func playLoop()
func play(firstFrame: Bool, fromIndex: Int?)
func pause()
func stop()
func seekTo(_ position: AnimatedStickerPlaybackPosition)
func playIfNeeded() -> Bool
func updateLayout(size: CGSize)
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool)
}
public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedStickerNode {
private let queue: Queue
private let disposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private let eventsNode: AnimatedStickerNodeDisplayEvents
public var automaticallyLoadFirstFrame: Bool = false
public var automaticallyLoadLastFrame: Bool = false
public var playToCompletionOnStop: Bool = false
public var started: () -> Void = {}
private var reportedStarted = false
public var completed: (Bool) -> Void = { _ in }
public var frameUpdated: (Int, Int) -> Void = { _, _ in }
public private(set) var currentFrameIndex: Int = 0
public private(set) var currentFrameCount: Int = 0
public private(set) var currentFrameRate: Int = 0
private var playFromIndex: Int?
public var frameColorUpdated: ((UIColor) -> Void)?
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
private var directData: (Data, String, Int, Int, String?, EmojiFitzModifier?, Bool)?
private var cachedData: (Data, Bool, EmojiFitzModifier?)?
private let useMetalCache: Bool
private var renderer: AnimationRendererPool.Holder?
public var isPlaying: Bool = false
private var currentLoopCount: Int = 0
private var canDisplayFirstFrame: Bool = false
public var playbackMode: AnimatedStickerPlaybackMode = .loop
public var stopAtNearestLoop: Bool = false
private let playbackStatus = Promise<AnimatedStickerStatus>()
public var status: Signal<AnimatedStickerStatus, NoError> {
return self.playbackStatus.get()
}
public var autoplay = false
public var overrideVisibility: Bool = false
public var currentFrameImage: UIImage? {
return self.renderer?.renderer.currentFrameImage
}
public var visibility = false {
didSet {
if self.visibility != oldValue {
self.updateIsPlaying()
}
}
}
private var isDisplaying = false {
didSet {
if self.isDisplaying != oldValue {
self.updateIsPlaying()
}
}
}
public var isPlayingChanged: (Bool) -> Void = { _ in }
private var overlayColor: (UIColor?, Bool)? = nil
private var size: CGSize?
public var dynamicColor: UIColor? {
didSet {
if let renderer = self.renderer?.renderer as? SoftwareAnimationRenderer {
renderer.renderAsTemplateImage = self.dynamicColor != nil
}
self.renderer?.renderer.view.tintColor = self.dynamicColor
}
}
public var forceSynchronous = false
public init(useMetalCache: Bool = false) {
self.queue = sharedQueue
self.eventsNode = AnimatedStickerNodeDisplayEvents()
self.useMetalCache = useMetalCache
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()
}
private static let hardwareRendererPool = AnimationRendererPool(generate: {
if #available(iOS 10.0, *) {
return CompressedAnimationRenderer()
} else {
return SoftwareAnimationRenderer(templateImageSupport: true)
}
})
private static let softwareRendererPool = AnimationRendererPool(generate: {
return SoftwareAnimationRenderer(templateImageSupport: true)
})
private weak var nodeToCopyFrameFrom: DefaultAnimatedStickerNodeImpl?
override public func didLoad() {
super.didLoad()
if #available(iOS 10.0, *), (self.useMetalCache/* || "".isEmpty*/) {
self.renderer = DefaultAnimatedStickerNodeImpl.hardwareRendererPool.take()
} else {
self.renderer = DefaultAnimatedStickerNodeImpl.softwareRendererPool.take()
if let renderer = self.renderer?.renderer as? SoftwareAnimationRenderer {
renderer.renderAsTemplateImage = self.dynamicColor != nil
}
self.renderer?.renderer.view.tintColor = self.dynamicColor
if let contents = self.nodeToCopyFrameFrom?.renderer?.renderer.contents {
self.renderer?.renderer.contents = contents
}
}
self.renderer?.renderer.frame = CGRect(origin: CGPoint(), size: self.size ?? self.bounds.size)
if let (overlayColor, replace) = self.overlayColor {
self.renderer?.renderer.setOverlayColor(overlayColor, replace: replace, animated: false)
}
self.nodeToCopyFrameFrom = nil
self.addSubnode(self.renderer!.renderer)
}
public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) {
guard let otherNode = otherNode as? DefaultAnimatedStickerNodeImpl else {
self.nodeToCopyFrameFrom = nil
return
}
if let renderer = self.renderer?.renderer as? SoftwareAnimationRenderer, let otherRenderer = otherNode.renderer?.renderer as? SoftwareAnimationRenderer {
if let contents = otherRenderer.contents {
renderer.contents = contents
}
} else {
self.nodeToCopyFrameFrom = otherNode
}
}
public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) {
if width < 2 || height < 2 {
return
}
self.playbackMode = playbackMode
switch mode {
case let .direct(cachePathPrefix):
let f: (String) -> Void = { [weak self] path in
guard let strongSelf = self else {
return
}
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
strongSelf.directData = (directData, path, width, height, cachePathPrefix, source.fitzModifier, source.isVideo)
}
if case let .still(position) = strongSelf.playbackMode {
strongSelf.seekTo(position)
} else if strongSelf.isPlaying || strongSelf.autoplay {
if strongSelf.autoplay {
strongSelf.isSetUpForPlayback = false
strongSelf.isPlaying = true
}
let fromIndex = strongSelf.playFromIndex
strongSelf.playFromIndex = nil
strongSelf.play(fromIndex: fromIndex)
} else if strongSelf.canDisplayFirstFrame {
strongSelf.play(firstFrame: true)
}
}
self.disposable.set((source.directDataPath(attemptSynchronously: self.forceSynchronous)
|> filter { $0 != nil }
|> deliverOnMainQueue).startStrict(next: { path in
f(path!)
}))
case .cached:
self.disposable.set((source.cachedDataPath(width: width, height: height)
|> deliverOnMainQueue).startStrict(next: { [weak self] path, complete in
guard let strongSelf = self else {
return
}
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
if let (_, currentComplete, _) = strongSelf.cachedData {
if !currentComplete {
strongSelf.cachedData = (data, complete, source.fitzModifier)
strongSelf.frameSource.with { frameSource in
frameSource?.with { frameSource in
if let frameSource = frameSource.value as? AnimatedStickerCachedFrameSource {
frameSource.updateData(data: data, complete: complete)
}
}
}
}
} else {
strongSelf.cachedData = (data, complete, source.fitzModifier)
if strongSelf.isPlaying {
strongSelf.play()
} else if strongSelf.canDisplayFirstFrame {
strongSelf.play(firstFrame: true)
}
}
}
}))
}
}
public func reset() {
self.disposable.set(nil)
self.fetchDisposable.set(nil)
}
private func updateIsPlaying() {
if !self.autoplay {
let isPlaying = self.visibility && (self.isDisplaying || self.overrideVisibility)
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
if isPlaying {
self.play()
} else{
self.pause()
}
self.isPlayingChanged(isPlaying)
}
}
if self.automaticallyLoadLastFrame {
if self.isDisplaying {
self.seekTo(.end)
}
} else {
let canDisplayFirstFrame = self.automaticallyLoadFirstFrame && self.isDisplaying
if self.canDisplayFirstFrame != canDisplayFirstFrame {
self.canDisplayFirstFrame = canDisplayFirstFrame
if canDisplayFirstFrame {
self.play(firstFrame: true)
}
}
}
}
private var isSetUpForPlayback = false
public func playOnce() {
self.playbackMode = .once
self.play()
}
public func playLoop() {
self.playbackMode = .loop
self.play()
}
public func play(firstFrame: Bool = false, fromIndex: Int? = nil) {
if !firstFrame {
switch self.playbackMode {
case .once:
self.isPlaying = true
case .count:
self.currentLoopCount = 0
self.isPlaying = true
default:
break
}
}
if self.isSetUpForPlayback {
let directData = self.directData
let cachedData = self.cachedData
let queue = self.queue
let timerHolder = self.timer
let frameSourceHolder = self.frameSource
let useMetalCache = self.useMetalCache
self.queue.async { [weak self] in
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }.value
if maybeFrameSource == nil {
let notifyUpdated: (() -> Void)? = nil
if let directData = directData {
if directData.6 {
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
} else {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
}
} else if let (cachedData, cachedDataComplete, _) = cachedData {
if #available(iOS 9.0, *) {
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
notifyUpdated?()
})
}
}
let _ = frameSourceHolder.swap(maybeFrameSource.flatMap { maybeFrameSource in
return QueueLocalObject(queue: queue, generate: {
return AnimatedStickerFrameSourceWrapper(maybeFrameSource)
})
})
}
guard let frameSource = maybeFrameSource else {
return
}
if let fromIndex = fromIndex {
frameSource.skipToFrameIndex(fromIndex)
}
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 timerEvent: () -> Void = {
let frame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: true)
}
if let frame = frame {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
guard let strongSelf = self else {
return
}
if !strongSelf.reportedStarted {
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.frameUpdated(frame.index, frame.totalFrames)
strongSelf.currentFrameIndex = frame.index
strongSelf.currentFrameCount = frame.totalFrames
strongSelf.currentFrameRate = frameRate
if frame.isLastFrame {
var stopped = false
var stopNow = false
if case .still = strongSelf.playbackMode {
stopNow = true
} else if case .once = strongSelf.playbackMode {
stopNow = true
} else if case let .count(count) = strongSelf.playbackMode {
strongSelf.currentLoopCount += 1
if count <= strongSelf.currentLoopCount {
stopNow = true
}
} else if strongSelf.stopAtNearestLoop {
stopNow = true
}
if stopNow {
strongSelf.stop()
strongSelf.isPlaying = false
stopped = true
}
strongSelf.completed(stopped)
}
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()
}
}
let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
timerEvent()
}, queue: queue)
let _ = timerHolder.swap(timer)
timerEvent()
timer.start()
}
} else {
self.isSetUpForPlayback = true
let directData = self.directData
let cachedData = self.cachedData
if directData == nil && cachedData == nil {
self.playFromIndex = fromIndex
}
let queue = self.queue
let timerHolder = self.timer
let frameSourceHolder = self.frameSource
let useMetalCache = self.useMetalCache
self.queue.async { [weak self] in
var maybeFrameSource: AnimatedStickerFrameSource?
let notifyUpdated: (() -> Void)? = nil
if let directData = directData {
if directData.6 {
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
} else {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
}
} else if let (cachedData, cachedDataComplete, _) = cachedData {
if #available(iOS 9.0, *) {
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
notifyUpdated?()
})
}
}
let _ = frameSourceHolder.swap(maybeFrameSource.flatMap { maybeFrameSource in
return QueueLocalObject(queue: queue, generate: {
return AnimatedStickerFrameSourceWrapper(maybeFrameSource)
})
})
guard let frameSource = maybeFrameSource else {
return
}
if let fromIndex = fromIndex {
frameSource.skipToFrameIndex(fromIndex)
}
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 frame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: true)
}
if let frame = frame {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
guard let strongSelf = self else {
return
}
if !strongSelf.reportedStarted {
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.frameUpdated(frame.index, frame.totalFrames)
strongSelf.currentFrameIndex = frame.index
strongSelf.currentFrameCount = frame.totalFrames
strongSelf.currentFrameRate = frameRate
if frame.isLastFrame {
var stopped = false
var stopNow = false
if case .still = strongSelf.playbackMode {
stopNow = true
} else if case .once = strongSelf.playbackMode {
stopNow = true
} else if case let .count(count) = strongSelf.playbackMode {
strongSelf.currentLoopCount += 1
if count <= strongSelf.currentLoopCount {
stopNow = true
}
} else if strongSelf.stopAtNearestLoop {
stopNow = true
}
if stopNow {
strongSelf.stop()
strongSelf.isPlaying = false
stopped = true
}
strongSelf.completed(stopped)
}
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 pause() {
self.timer.swap(nil)?.invalidate()
}
public func stop() {
self.isSetUpForPlayback = false
self.reportedStarted = false
self.timer.swap(nil)?.invalidate()
if self.playToCompletionOnStop {
self.seekTo(.start)
}
}
public func seekTo(_ position: AnimatedStickerPlaybackPosition) {
self.isPlaying = false
let directData = self.directData
let cachedData = self.cachedData
let queue = self.queue
let frameSourceHolder = self.frameSource
let timerHolder = self.timer
let useMetalCache = self.useMetalCache
let action = { [weak self] in
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }.value
if case .timestamp = position {
} else {
if let directData = directData {
if directData.6 {
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
} else {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
}
if case .end = position {
maybeFrameSource?.skipToEnd()
}
} else if let (cachedData, cachedDataComplete, _) = cachedData {
if #available(iOS 9.0, *) {
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {})
}
}
}
guard let frameSource = maybeFrameSource else {
return
}
if frameSource.frameCount == 0 {
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
var maybeFrame: AnimatedStickerFrame??
if case let .timestamp(timestamp) = position {
var stickerTimestamp = timestamp
while stickerTimestamp > duration {
stickerTimestamp -= duration
}
let targetFrame = Int(stickerTimestamp / duration * Double(frameSource.frameCount))
if targetFrame == frameSource.frameIndex {
return
}
var delta = targetFrame - frameSource.frameIndex
if delta < 0 {
delta = frameSource.frameCount + delta
}
for i in 0 ..< delta {
maybeFrame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: i == delta - 1)
}
}
} else if case let .frameIndex(frameIndex) = position {
let targetFrame = frameIndex
if targetFrame == frameSource.frameIndex {
return
}
var delta = targetFrame - frameSource.frameIndex
if delta < 0 {
delta = frameSource.frameCount + delta
}
for i in 0 ..< delta {
maybeFrame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: i == delta - 1)
}
}
} else {
maybeFrame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: true)
}
}
if let maybeFrame = maybeFrame, let frame = maybeFrame {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
guard let strongSelf = self else {
return
}
if !strongSelf.reportedStarted {
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: false, duration: duration, timestamp: 0.0)))
}
}
frameQueue.with { frameQueue in
frameQueue.generateFramesIfNeeded()
}
}
if self.forceSynchronous {
action()
} else {
self.queue.async(action)
}
}
public func playIfNeeded() -> Bool {
if !self.isPlaying {
self.isPlaying = true
self.play()
return true
}
return false
}
public func updateLayout(size: CGSize) {
self.size = size
self.renderer?.renderer.frame = CGRect(origin: CGPoint(), size: size)
}
public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
self.overlayColor = (color, replace)
self.renderer?.renderer.setOverlayColor(color, replace: replace, animated: animated)
}
}
@@ -0,0 +1,58 @@
import Foundation
import SwiftSignalKit
import UIKit
import AsyncDisplayKit
public enum AnimationRendererFrameType {
case argb
case yuva
case dct
}
final class AnimationRendererPool {
final class Holder {
let pool: AnimationRendererPool
let renderer: AnimationRenderer
init(pool: AnimationRendererPool, renderer: AnimationRenderer) {
self.pool = pool
self.renderer = renderer
}
deinit {
self.renderer.removeFromSupernode()
self.pool.putBack(renderer: self.renderer)
}
}
private let generate: () -> AnimationRenderer
private var items: [AnimationRenderer] = []
init(generate: @escaping () -> AnimationRenderer) {
self.generate = generate
}
func take() -> Holder {
if !self.items.isEmpty {
let item = self.items.removeLast()
return Holder(pool: self, renderer: item)
} else {
return Holder(pool: self, renderer: self.generate())
}
}
private func putBack(renderer: AnimationRenderer) {
/*#if DEBUG
self.items.append(renderer)
#endif*/
}
}
protocol AnimationRenderer: ASDisplayNode {
var currentFrameImage: UIImage? { get }
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?)
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool)
}
@@ -0,0 +1,134 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import YuvConversion
import Accelerate
import AnimationCompression
import Metal
import MetalKit
import MetalImageView
@available(iOS 10.0, *)
final class CompressedAnimationRenderer: ASDisplayNode, AnimationRenderer {
private final class View: UIView {
static override var layerClass: AnyClass {
return MetalImageLayer.self
}
init(device: MTLDevice) {
super.init(frame: CGRect())
(self.layer as! MetalImageLayer).renderer.device = device
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
}
private var highlightedContentNode: ASDisplayNode?
private var highlightedColor: UIColor?
private var highlightReplacesContent = false
private let renderer: CompressedImageRenderer
var currentFrameImage: UIImage? {
return nil
}
override init() {
self.renderer = CompressedImageRenderer(sharedContext: AnimationCompressor.SharedContext.shared)!
super.init()
self.setViewBlock({
return View(device: AnimationCompressor.SharedContext.shared.device)
})
}
override func didLoad() {
super.didLoad()
self.layer.isOpaque = false
self.layer.backgroundColor = nil
}
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?) {
switch type {
case .dct:
self.renderer.renderIdct(layer: self.layer as! MetalImageLayer, compressedImage: AnimationCompressor.CompressedImageData(data: data), completion: { [weak self] in
self?.updateHighlightedContentNode()
completion()
})
case .argb:
self.renderer.renderRgb(layer: self.layer as! MetalImageLayer, width: width, height: height, bytesPerRow: bytesPerRow, data: data, completion: { [weak self] in
self?.updateHighlightedContentNode()
completion()
})
case .yuva:
self.renderer.renderYuva(layer: self.layer as! MetalImageLayer, width: width, height: height, data: data, completion: { [weak self] in
self?.updateHighlightedContentNode()
completion()
})
}
}
private func updateHighlightedContentNode() {
guard let highlightedContentNode = self.highlightedContentNode, let highlightedColor = self.highlightedColor else {
return
}
if let contents = self.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID {
(highlightedContentNode.view as! UIImageView).image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate)
}
highlightedContentNode.tintColor = highlightedColor
if self.highlightReplacesContent {
self.contents = nil
}
}
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
self.highlightReplacesContent = replace
var updated = false
if let current = self.highlightedColor, let color = color {
updated = !current.isEqual(color)
} else if (self.highlightedColor != nil) != (color != nil) {
updated = true
}
if !updated {
return
}
self.highlightedColor = color
if let _ = color {
if let highlightedContentNode = self.highlightedContentNode {
highlightedContentNode.alpha = 1.0
} else {
let highlightedContentNode = ASDisplayNode(viewBlock: {
return UIImageView()
}, didLoad: nil)
highlightedContentNode.displaysAsynchronously = false
self.highlightedContentNode = highlightedContentNode
highlightedContentNode.frame = self.bounds
self.addSubnode(highlightedContentNode)
}
self.updateHighlightedContentNode()
} else if let highlightedContentNode = self.highlightedContentNode {
highlightedContentNode.alpha = 0.0
highlightedContentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] completed in
guard let strongSelf = self, completed else {
return
}
strongSelf.highlightedContentNode?.removeFromSupernode()
strongSelf.highlightedContentNode = nil
})
}
}
}
@@ -0,0 +1,387 @@
import Foundation
import UIKit
import AsyncDisplayKit
import RLottieBinding
import SwiftSignalKit
import GZip
import Display
public final class DirectAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode {
private static let sharedQueue = Queue(name: "DirectAnimatedStickerNode", qos: .userInteractive)
private final class LoadFrameTask {
var isCancelled: Bool = false
}
public var automaticallyLoadFirstFrame: Bool = false
public var automaticallyLoadLastFrame: Bool = false
public var playToCompletionOnStop: Bool = false
private var didStart: Bool = false
public var started: () -> Void = {}
public var completed: (Bool) -> Void = { _ in }
private var didComplete: Bool = false
public var frameUpdated: (Int, Int) -> Void = { _, _ in }
public var currentFrameIndex: Int {
get {
return self.frameIndex
} set(value) {
}
}
public var currentFrameCount: Int {
get {
if let lottieInstance = self.lottieInstance {
return Int(lottieInstance.frameCount)
} else if let videoSource = self.videoSource {
return Int(videoSource.frameRate)
} else {
return 0
}
} set(value) {
}
}
public var currentFrameImage: UIImage? {
if let contents = self.layer.contents {
return UIImage(cgImage: contents as! CGImage)
} else {
return nil
}
}
public private(set) var isPlaying: Bool = false
public var stopAtNearestLoop: Bool = false
private let statusPromise = Promise<AnimatedStickerStatus>()
public var status: Signal<AnimatedStickerStatus, NoError> {
return self.statusPromise.get()
}
public var autoplay: Bool = true
public var visibility: Bool = false {
didSet {
self.updatePlayback()
}
}
public var overrideVisibility: Bool = false
public var isPlayingChanged: (Bool) -> Void = { _ in }
private var sourceDisposable: Disposable?
private var playbackSize: CGSize?
private var lottieInstance: LottieInstance?
private var videoSource: AnimatedStickerFrameSource?
private var frameIndex: Int = 0
private var playbackMode: AnimatedStickerPlaybackMode = .loop
private var frameImages: [Int: UIImage] = [:]
private var loadFrameTasks: [Int: LoadFrameTask] = [:]
private var nextFrameTimer: SwiftSignalKit.Timer?
override public init() {
super.init()
}
deinit {
self.sourceDisposable?.dispose()
self.nextFrameTimer?.invalidate()
}
public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) {
}
public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode) {
self.didStart = false
self.didComplete = false
self.sourceDisposable?.dispose()
self.playbackSize = CGSize(width: CGFloat(width), height: CGFloat(height))
self.playbackMode = playbackMode
self.sourceDisposable = (source.directDataPath(attemptSynchronously: false)
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] path in
guard let strongSelf = self, let path = path else {
return
}
if source.isVideo {
if let videoSource = makeVideoStickerDirectFrameSource(queue: DirectAnimatedStickerNode.sharedQueue, path: path, hintVP9: true, width: width, height: height, cachePathPrefix: nil, unpremultiplyAlpha: false) {
strongSelf.setupPlayback(videoSource: videoSource)
}
} else {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return
}
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
guard let lottieInstance = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
print("Could not load sticker data")
return
}
strongSelf.setupPlayback(lottieInstance: lottieInstance)
}
}).strict()
}
private func updatePlayback() {
let isPlaying = self.visibility && (self.lottieInstance != nil || self.videoSource != nil)
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
if self.isPlaying {
self.startNextFrameTimerIfNeeded()
self.updateLoadFrameTasks()
} else {
self.nextFrameTimer?.invalidate()
self.nextFrameTimer = nil
}
self.isPlayingChanged(self.isPlaying)
}
}
private func startNextFrameTimerIfNeeded() {
var frameRate: Double?
if let lottieInstance = self.lottieInstance {
frameRate = Double(lottieInstance.frameRate)
} else if let videoSource = self.videoSource {
frameRate = Double(videoSource.frameRate)
}
if self.nextFrameTimer == nil, let frameRate = frameRate, self.frameImages[self.frameIndex] != nil {
let nextFrameTimer = SwiftSignalKit.Timer(timeout: 1.0 / frameRate, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.nextFrameTimer = nil
strongSelf.advanceFrameIfPossible()
}, queue: .mainQueue())
self.nextFrameTimer = nextFrameTimer
nextFrameTimer.start()
}
}
private func advanceFrameIfPossible() {
var frameCount: Int?
if let lottieInstance = self.lottieInstance {
frameCount = Int(lottieInstance.frameCount)
} else if let videoSource = self.videoSource {
frameCount = Int(videoSource.frameCount)
}
guard let frameCount = frameCount else {
return
}
if self.frameIndex == frameCount - 1 {
switch self.playbackMode {
case .loop:
self.completed(false)
case let .count(count):
if count <= 1 {
if !self.didComplete {
self.didComplete = true
self.completed(true)
}
return
} else {
self.playbackMode = .count(count - 1)
self.completed(false)
}
case .once:
if !self.didComplete {
self.didComplete = true
self.completed(true)
}
return
case .still:
break
}
}
let nextFrameIndex = (self.frameIndex + 1) % frameCount
self.frameIndex = nextFrameIndex
self.updateFrameImageIfNeeded()
self.updateLoadFrameTasks()
}
private func updateFrameImageIfNeeded() {
var frameCount: Int?
if let lottieInstance = self.lottieInstance {
frameCount = Int(lottieInstance.frameCount)
} else if let videoSource = self.videoSource {
frameCount = Int(videoSource.frameCount)
}
guard let frameCount = frameCount else {
return
}
var allowedIndices: [Int] = []
for i in 0 ..< 2 {
let mappedIndex = (self.frameIndex + i) % frameCount
allowedIndices.append(mappedIndex)
}
var removeKeys: [Int] = []
for index in self.frameImages.keys {
if !allowedIndices.contains(index) {
removeKeys.append(index)
}
}
for index in removeKeys {
self.frameImages.removeValue(forKey: index)
}
for (index, task) in self.loadFrameTasks {
if !allowedIndices.contains(index) {
task.isCancelled = true
}
}
if let image = self.frameImages[self.frameIndex] {
self.layer.contents = image.cgImage
self.frameUpdated(self.frameIndex, frameCount)
if !self.didComplete {
self.startNextFrameTimerIfNeeded()
}
if !self.didStart {
self.didStart = true
self.started()
}
}
}
private func updateLoadFrameTasks() {
var frameCount: Int?
if let lottieInstance = self.lottieInstance {
frameCount = Int(lottieInstance.frameCount)
} else if let videoSource = self.videoSource {
frameCount = Int(videoSource.frameCount)
}
guard let frameCount = frameCount else {
return
}
let frameIndex = self.frameIndex % frameCount
if self.frameImages[frameIndex] == nil {
self.maybeStartLoadFrameTask(frameIndex: frameIndex)
} else {
self.maybeStartLoadFrameTask(frameIndex: (frameIndex + 1) % frameCount)
}
}
private func maybeStartLoadFrameTask(frameIndex: Int) {
guard self.lottieInstance != nil || self.videoSource != nil else {
return
}
guard let playbackSize = self.playbackSize else {
return
}
if self.loadFrameTasks[frameIndex] != nil {
return
}
let task = LoadFrameTask()
self.loadFrameTasks[frameIndex] = task
let lottieInstance = self.lottieInstance
let videoSource = self.videoSource
DirectAnimatedStickerNode.sharedQueue.async { [weak self] in
var image: UIImage?
if !task.isCancelled {
if let lottieInstance = lottieInstance {
if let drawingContext = DrawingContext(size: playbackSize, scale: 1.0, opaque: false, clear: false) {
lottieInstance.renderFrame(with: Int32(frameIndex), into: drawingContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(drawingContext.scaledSize.width), height: Int32(drawingContext.scaledSize.height), bytesPerRow: Int32(drawingContext.bytesPerRow))
image = drawingContext.generateImage()
}
} else if let videoSource = videoSource {
if let frame = videoSource.takeFrame(draw: true) {
if let drawingContext = DrawingContext(size: CGSize(width: frame.width, height: frame.height), scale: 1.0, opaque: false, clear: false, bytesPerRow: frame.bytesPerRow) {
frame.data.copyBytes(to: drawingContext.bytes.assumingMemoryBound(to: UInt8.self), from: 0 ..< min(frame.data.count, drawingContext.length))
image = drawingContext.generateImage()
}
}
}
}
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if let currentTask = strongSelf.loadFrameTasks[frameIndex], currentTask === task {
strongSelf.loadFrameTasks.removeValue(forKey: frameIndex)
}
if !task.isCancelled, let image = image {
strongSelf.frameImages[frameIndex] = image
strongSelf.updateFrameImageIfNeeded()
strongSelf.updateLoadFrameTasks()
}
}
}
}
private func setupPlayback(lottieInstance: LottieInstance) {
self.lottieInstance = lottieInstance
self.updatePlayback()
}
private func setupPlayback(videoSource: AnimatedStickerFrameSource) {
self.videoSource = videoSource
self.updatePlayback()
}
public func reset() {
}
public func playOnce() {
}
public func playLoop() {
}
public func play(firstFrame: Bool, fromIndex: Int?) {
if let fromIndex = fromIndex {
self.frameIndex = fromIndex
self.updateLoadFrameTasks()
}
}
public func pause() {
}
public func stop() {
}
public func seekTo(_ position: AnimatedStickerPlaybackPosition) {
}
public func playIfNeeded() -> Bool {
return false
}
public func updateLayout(size: CGSize) {
}
public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
}
}
@@ -0,0 +1,80 @@
import Foundation
import Compression
import YuvConversion
func compressFrame(width: Int, height: Int, rgbData: Data, unpremultiply: Bool) -> Data? {
let bytesPerRow = rgbData.count / height
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
assert(yuvaPixelsPerAlphaRow % 2 == 0)
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
let yuvaFrameData = malloc(yuvaLength)!
defer {
free(yuvaFrameData)
}
memset(yuvaFrameData, 0, yuvaLength)
var compressedFrameData = Data(count: yuvaLength)
let compressedFrameDataLength = compressedFrameData.count
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
defer {
free(scratchData)
}
var rgbData = rgbData
rgbData.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) -> Void in
if let baseAddress = buffer.baseAddress {
encodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), baseAddress.assumingMemoryBound(to: UInt8.self), Int32(width), Int32(height), Int32(bytesPerRow), true, unpremultiply)
}
}
var maybeResultSize: Int?
compressedFrameData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let length = compression_encode_buffer(bytes, compressedFrameDataLength, yuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
maybeResultSize = length
}
guard let resultSize = maybeResultSize else {
return nil
}
compressedFrameData.count = resultSize
return compressedFrameData
}
func compressFrame(width: Int, height: Int, yuvaData: Data) -> Data? {
let yuvaLength = yuvaData.count
var compressedFrameData = Data(count: yuvaLength)
let compressedFrameDataLength = compressedFrameData.count
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
defer {
free(scratchData)
}
var maybeResultSize: Int?
yuvaData.withUnsafeBytes { yuvaBuffer -> Void in
if let yuvaFrameData = yuvaBuffer.baseAddress {
compressedFrameData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let length = compression_encode_buffer(bytes, compressedFrameDataLength, yuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
maybeResultSize = length
}
}
}
guard let resultSize = maybeResultSize else {
return nil
}
compressedFrameData.count = resultSize
return compressedFrameData
}
@@ -0,0 +1,260 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import YuvConversion
import Accelerate
final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
private let templateImageSupport: Bool
private var highlightedContentNode: ASDisplayNode?
private var highlightedColor: UIColor?
private var highlightReplacesContent = false
public var renderAsTemplateImage: Bool = false
public private(set) var currentFrameImage: UIImage?
init(templateImageSupport: Bool) {
self.templateImageSupport = templateImageSupport
super.init()
if templateImageSupport {
self.setViewBlock({
return UIImageView()
})
}
}
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?) {
assert(bytesPerRow > 0)
let renderAsTemplateImage = self.renderAsTemplateImage
queue.async { [weak self] in
switch type {
case .argb:
let calculatedBytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width))
assert(bytesPerRow == calculatedBytesPerRow)
case .yuva:
break
case .dct:
break
}
var image: UIImage?
var averageColorValue: UIColor?
autoreleasepool {
image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, contextBytesPerRow in
switch type {
case .yuva:
data.withUnsafeBytes { bytes -> Void in
guard let baseAddress = bytes.baseAddress else {
return
}
if bytesPerRow <= 0 || height <= 0 || width <= 0 || bytesPerRow * height > bytes.count {
assert(false)
return
}
decodeYUVAToRGBA(baseAddress.assumingMemoryBound(to: UInt8.self), pixelData, Int32(width), Int32(height), Int32(contextBytesPerRow))
}
case .argb:
var data = data
data.withUnsafeMutableBytes { bytes -> Void in
guard let baseAddress = bytes.baseAddress else {
return
}
if mulAlpha {
var srcData = vImage_Buffer(data: baseAddress.assumingMemoryBound(to: UInt8.self), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow)
var destData = vImage_Buffer(data: pixelData, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow)
let permuteMap: [UInt8] = [3, 2, 1, 0]
vImagePermuteChannels_ARGB8888(&srcData, &destData, permuteMap, vImage_Flags(kvImageDoNotTile))
vImagePremultiplyData_ARGB8888(&destData, &destData, vImage_Flags(kvImageDoNotTile))
vImagePermuteChannels_ARGB8888(&destData, &destData, permuteMap, vImage_Flags(kvImageDoNotTile))
} else {
memcpy(pixelData, baseAddress.assumingMemoryBound(to: UInt8.self), bytes.count)
}
}
case .dct:
break
}
})
if renderAsTemplateImage {
image = image?.withRenderingMode(.alwaysTemplate)
}
if averageColor != nil {
let blurredWidth = 16
let blurredHeight = 16
let blurredBytesPerRow = blurredWidth * 4
guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else {
return
}
let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight))
if let image, let cgImage = image.cgImage {
context.withFlippedContext { c in
c.setFillColor(UIColor.white.cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8)))
}
}
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(blurredWidth)
destinationBuffer.height = UInt(blurredHeight)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = context.bytesPerRow
vImageBoxConvolve_ARGB8888(&destinationBuffer,
&destinationBuffer,
nil,
0, 0,
UInt32(15),
UInt32(15),
nil,
vImage_Flags(kvImageTruncateKernel))
let divisor: Int32 = 0x1000
let rwgt: CGFloat = 0.3086
let gwgt: CGFloat = 0.6094
let bwgt: CGFloat = 0.0820
let adjustSaturation: CGFloat = 1.7
let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation
let b = (1.0 - adjustSaturation) * rwgt
let c = (1.0 - adjustSaturation) * rwgt
let d = (1.0 - adjustSaturation) * gwgt
let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation
let f = (1.0 - adjustSaturation) * gwgt
let g = (1.0 - adjustSaturation) * bwgt
let h = (1.0 - adjustSaturation) * bwgt
let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation
let satMatrix: [CGFloat] = [
a, b, c, 0,
d, e, f, 0,
g, h, i, 0,
0, 0, 0, 1
]
var matrix: [Int16] = satMatrix.map { value in
return Int16(value * CGFloat(divisor))
}
vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
context.withFlippedContext { c in
c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
}
var sumR: UInt64 = 0
var sumG: UInt64 = 0
var sumB: UInt64 = 0
var sumA: UInt64 = 0
for y in 0 ..< blurredHeight {
let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow)
for x in 0 ..< blurredWidth {
let pixel = row.advanced(by: x * 4)
sumB += UInt64(pixel.advanced(by: 0).pointee)
sumG += UInt64(pixel.advanced(by: 1).pointee)
sumR += UInt64(pixel.advanced(by: 2).pointee)
sumA += UInt64(pixel.advanced(by: 3).pointee)
}
}
sumR /= UInt64(blurredWidth * blurredHeight)
sumG /= UInt64(blurredWidth * blurredHeight)
sumB /= UInt64(blurredWidth * blurredHeight)
sumA /= UInt64(blurredWidth * blurredHeight)
sumA = 255
averageColorValue = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0)
}
}
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.currentFrameImage = image
if strongSelf.templateImageSupport {
(strongSelf.view as? UIImageView)?.image = image
} else {
strongSelf.contents = image?.cgImage
}
strongSelf.updateHighlightedContentNode()
if strongSelf.highlightedContentNode?.frame != strongSelf.bounds {
strongSelf.highlightedContentNode?.frame = strongSelf.bounds
}
completion()
if let averageColor, let averageColorValue {
averageColor(averageColorValue)
}
}
}
}
private func updateHighlightedContentNode() {
guard let highlightedContentNode = self.highlightedContentNode, let highlightedColor = self.highlightedColor else {
return
}
(highlightedContentNode.view as! UIImageView).image = self.currentFrameImage?.withRenderingMode(.alwaysTemplate)
highlightedContentNode.tintColor = highlightedColor
if self.highlightReplacesContent {
if self.templateImageSupport {
(self.view as? UIImageView)?.image = nil
} else {
self.contents = nil
}
}
}
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
self.highlightReplacesContent = replace
var updated = false
if let current = self.highlightedColor, let color = color {
updated = !current.isEqual(color)
} else if (self.highlightedColor != nil) != (color != nil) {
updated = true
}
if !updated {
return
}
self.highlightedColor = color
if let _ = color {
if let highlightedContentNode = self.highlightedContentNode {
highlightedContentNode.alpha = 1.0
} else {
let highlightedContentNode = ASDisplayNode(viewBlock: {
return UIImageView()
}, didLoad: nil)
highlightedContentNode.displaysAsynchronously = false
self.highlightedContentNode = highlightedContentNode
highlightedContentNode.frame = self.bounds
self.addSubnode(highlightedContentNode)
}
self.updateHighlightedContentNode()
} else if let highlightedContentNode = self.highlightedContentNode {
highlightedContentNode.alpha = 0.0
highlightedContentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] completed in
guard let strongSelf = self, completed else {
return
}
strongSelf.highlightedContentNode?.removeFromSupernode()
strongSelf.highlightedContentNode = nil
})
}
}
}
@@ -0,0 +1,457 @@
import Foundation
import Compression
import Display
import SwiftSignalKit
import UniversalMediaPlayer
import CoreMedia
import ManagedFile
import Accelerate
import TelegramCore
import WebPBinding
import UIKit
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
private let maximumFrameCount = 30 * 10
private final class VideoStickerFrameSourceCache {
private enum FrameRangeResult {
case range(Range<Int>)
case notFound
case corruptedFile
}
private let queue: Queue
private let storeQueue: Queue
private let path: String
private let file: ManagedFile
private let width: Int
private let height: Int
public private(set) var frameRate: Int32 = 0
public private(set) var frameCount: Int32 = 0
private var isStoringFrames = Set<Int>()
var storedFrames: Int {
return self.isStoringFrames.count
}
private var scratchBuffer: Data
private var decodeBuffer: Data
init?(queue: Queue, pathPrefix: String, width: Int, height: Int) {
self.queue = queue
self.storeQueue = sharedStoreQueue
self.width = width
self.height = height
let version: Int = 3
self.path = "\(pathPrefix)_\(width)x\(height)-v\(version).vstickerframecache"
var file = ManagedFile(queue: queue, path: self.path, mode: .readwrite)
if let file = file {
self.file = file
} else {
let _ = try? FileManager.default.removeItem(atPath: self.path)
file = ManagedFile(queue: queue, path: self.path, mode: .readwrite)
if let file = file {
self.file = file
} else {
return nil
}
}
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
self.decodeBuffer = Data(count: yuvaLength)
self.initializeFrameTable()
}
deinit {
if self.frameCount == 0 {
let _ = try? FileManager.default.removeItem(atPath: self.path)
}
}
private func initializeFrameTable() {
var reset = true
if let size = self.file.getSize(), size >= maximumFrameCount {
if self.readFrameRate() {
reset = false
}
}
if reset {
self.file.truncate(count: 0)
var zero: Int32 = 0
let _ = self.file.write(&zero, count: 4)
let _ = self.file.write(&zero, count: 4)
for _ in 0 ..< maximumFrameCount {
let _ = self.file.write(&zero, count: 4)
let _ = self.file.write(&zero, count: 4)
}
}
}
private func readFrameRate() -> Bool {
guard self.frameCount == 0 else {
return true
}
let _ = self.file.seek(position: 0)
var frameRate: Int32 = 0
if self.file.read(&frameRate, 4) != 4 {
return false
}
if frameRate < 0 {
return false
}
if frameRate == 0 {
return false
}
self.frameRate = frameRate
let _ = self.file.seek(position: 4)
var frameCount: Int32 = 0
if self.file.read(&frameCount, 4) != 4 {
return false
}
if frameCount < 0 {
return false
}
if frameCount == 0 {
return false
}
self.frameCount = frameCount
return true
}
private func readFrameRange(index: Int) -> FrameRangeResult {
if index < 0 || index >= maximumFrameCount {
return .notFound
}
guard self.readFrameRate() else {
return .notFound
}
if index >= self.frameCount {
return .notFound
}
let _ = self.file.seek(position: Int64(8 + index * 4 * 2))
var offset: Int32 = 0
var length: Int32 = 0
if self.file.read(&offset, 4) != 4 {
return .corruptedFile
}
if self.file.read(&length, 4) != 4 {
return .corruptedFile
}
if length == 0 {
return .notFound
}
if length < 0 || offset < 0 {
return .corruptedFile
}
if Int64(offset) + Int64(length) > 100 * 1024 * 1024 {
return .corruptedFile
}
return .range(Int(offset) ..< Int(offset + length))
}
func storeFrameRateAndCount(frameRate: Int, frameCount: Int) {
let _ = self.file.seek(position: 0)
var frameRate = Int32(frameRate)
let _ = self.file.write(&frameRate, count: 4)
let _ = self.file.seek(position: 4)
var frameCount = Int32(frameCount)
let _ = self.file.write(&frameCount, count: 4)
}
func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
if index < 0 || index >= maximumFrameCount {
return
}
if self.isStoringFrames.contains(index) {
return
}
self.isStoringFrames.insert(index)
let width = self.width
let height = self.height
let queue = self.queue
self.storeQueue.async { [weak self] in
let compressedData = compressFrame(width: width, height: height, rgbData: rgbData, unpremultiply: false)
queue.async {
guard let strongSelf = self else {
return
}
guard let currentSize = strongSelf.file.getSize() else {
return
}
guard let compressedData = compressedData else {
return
}
let _ = strongSelf.file.seek(position: Int64(8 + index * 4 * 2))
var offset = Int32(currentSize)
var length = Int32(compressedData.count)
let _ = strongSelf.file.write(&offset, count: 4)
let _ = strongSelf.file.write(&length, count: 4)
let _ = strongSelf.file.seek(position: Int64(currentSize))
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
if let baseAddress = buffer.baseAddress {
let _ = strongSelf.file.write(baseAddress, count: Int(length))
}
}
}
}
}
func readUncompressedYuvaFrame(index: Int) -> Data? {
if index < 0 || index >= maximumFrameCount {
return nil
}
let rangeResult = self.readFrameRange(index: index)
switch rangeResult {
case let .range(range):
let _ = self.file.seek(position: Int64(range.lowerBound))
let length = range.upperBound - range.lowerBound
let compressedData = self.file.readData(count: length)
if compressedData.count != length {
return nil
}
var frameData: Data?
let decodeBufferLength = self.decodeBuffer.count
compressedData.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
frameData = Data(bytes: decodeBytes, count: resultLength)
}
}
}
return frameData
case .notFound:
return nil
case .corruptedFile:
self.file.truncate(count: 0)
self.initializeFrameTable()
return nil
}
}
}
private let useCache = true
public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, hintVP9: Bool, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool) -> AnimatedStickerFrameSource? {
return VideoStickerDirectFrameSource(queue: queue, path: path, isVP9: hintVP9, width: width, height: height, cachePathPrefix: cachePathPrefix, unpremultiplyAlpha: unpremultiplyAlpha)
}
public final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
private let queue: Queue
private let path: String
private let width: Int
private let height: Int
private let cache: VideoStickerFrameSourceCache?
private let image: UIImage?
private let bytesPerRow: Int
public var frameCount: Int
public let frameRate: Int
public var duration: Double
fileprivate var currentFrame: Int
private var source: FFMpegFileReader?
public var frameIndex: Int {
if self.frameCount == 0 {
return 0
} else {
return self.currentFrame % self.frameCount
}
}
public init?(queue: Queue, path: String, isVP9: Bool = true, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool = true) {
self.queue = queue
self.path = path
self.width = width
self.height = height
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(self.width))
self.currentFrame = 0
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
VideoStickerFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height)
}
if useCache, let cache = self.cache, cache.frameCount > 0 {
self.source = nil
self.image = nil
self.frameRate = Int(cache.frameRate)
self.frameCount = Int(cache.frameCount)
if self.frameRate > 0 {
self.duration = Double(self.frameCount) / Double(self.frameRate)
} else {
self.duration = 0.0
}
} else if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let image = WebP.convert(fromWebP: data) {
self.source = nil
self.image = image
self.frameRate = 1
self.frameCount = 1
self.duration = 0.0
} else {
let source = FFMpegFileReader(
source: .file(path),
passthroughDecoder: false,
useHardwareAcceleration: false,
selectedStream: .mediaType(.video),
seek: nil,
maxReadablePts: nil
)
if let source {
self.source = source
self.frameRate = min(30, source.frameRate())
self.duration = source.duration().seconds
} else {
self.source = nil
self.frameRate = 30
self.duration = 0.0
}
self.image = nil
self.frameCount = 0
}
}
deinit {
assert(self.queue.isCurrent())
}
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
let frameIndex: Int
if self.frameCount > 0 {
frameIndex = self.currentFrame % self.frameCount
} else {
frameIndex = self.currentFrame
}
self.currentFrame += 1
if draw {
if let image = self.image {
guard let context = DrawingContext(size: CGSize(width: self.width, height: self.height), scale: 1.0, opaque: false, clear: true, bytesPerRow: self.bytesPerRow) else {
return nil
}
context.withFlippedContext { c in
c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: context.size))
}
let frameData = Data(bytes: context.bytes, count: self.bytesPerRow * self.height)
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount, multiplyAlpha: true)
} else if useCache, let cache = self.cache, let yuvData = cache.readUncompressedYuvaFrame(index: frameIndex) {
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
} else if let source = self.source {
let frameAndLoop = source.readFrame(argb: true)
switch frameAndLoop {
case let .frame(frame):
var frameData = Data(count: self.bytesPerRow * self.height)
frameData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let imageBuffer = CMSampleBufferGetImageBuffer(frame.sampleBuffer)
CVPixelBufferLockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer!)
let width = CVPixelBufferGetWidth(imageBuffer!)
let height = CVPixelBufferGetHeight(imageBuffer!)
let srcData = CVPixelBufferGetBaseAddress(imageBuffer!)
var sourceBuffer = vImage_Buffer(data: srcData, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow)
var destBuffer = vImage_Buffer(data: bytes, height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: self.bytesPerRow)
let _ = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, vImage_Flags(kvImageDoNotTile))
CVPixelBufferUnlockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
}
self.cache?.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount, multiplyAlpha: true)
case .endOfStream:
if self.frameCount == 0 {
if let cache = self.cache {
if cache.storedFrames == frameIndex {
self.frameCount = frameIndex
cache.storeFrameRateAndCount(frameRate: self.frameRate, frameCount: self.frameCount)
} else {
Logger.shared.log("VideoSticker", "Missed a frame? \(frameIndex) \(cache.storedFrames)")
}
} else {
self.frameCount = frameIndex
}
}
self.currentFrame = 0
self.source = FFMpegFileReader(
source: .file(self.path),
passthroughDecoder: false,
useHardwareAcceleration: false,
selectedStream: .mediaType(.video),
seek: nil,
maxReadablePts: nil
)
if let cache = self.cache {
if let yuvData = cache.readUncompressedYuvaFrame(index: self.currentFrame) {
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
}
}
return nil
case .waitingForMoreData, .error:
return nil
}
} else {
return nil
}
} else {
return nil
}
}
public func skipToEnd() {
self.currentFrame = self.frameCount - 1
}
public func skipToFrameIndex(_ index: Int) {
self.currentFrame = index
}
}