diff --git a/Tests/AnimationCacheTest/BUILD b/Tests/AnimationCacheTest/BUILD index b42fca4697..43d5b08ae6 100644 --- a/Tests/AnimationCacheTest/BUILD +++ b/Tests/AnimationCacheTest/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/rlottie:RLottieBinding", diff --git a/Tests/AnimationCacheTest/Sources/ViewController.swift b/Tests/AnimationCacheTest/Sources/ViewController.swift index bfd2798873..f0ae8edcdc 100644 --- a/Tests/AnimationCacheTest/Sources/ViewController.swift +++ b/Tests/AnimationCacheTest/Sources/ViewController.swift @@ -3,6 +3,7 @@ import UIKit import Display import AnimationCache +import DCTAnimationCacheImpl import SwiftSignalKit import VideoAnimationCache import LottieAnimationCache @@ -50,7 +51,7 @@ public final class ViewController: UIViewController { let basePath = NSTemporaryDirectory() + "/animation-cache" let _ = try? FileManager.default.removeItem(atPath: basePath) let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: basePath), withIntermediateDirectories: true) - self.cache = AnimationCacheImpl(basePath: basePath, allocateTempFile: { + self.cache = DCTAnimationCacheImpl(basePath: basePath, allocateTempFile: { return basePath + "/\(Int64.random(in: 0 ... Int64.max))" }) diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index cb3ce76c7c..623c6ad991 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/TextFormat:TextFormat", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 45045f3107..c31f36300b 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -20,6 +20,7 @@ import EntityKeyboard import ComponentDisplayAdapters import AnimationCache import MultiAnimationRenderer +import DCTMultiAnimationRendererImpl import EmojiTextAttachmentView import TextFormat import GZip @@ -504,8 +505,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.reactionsLocked = reactionsLocked self.animationCache = animationCache - self.animationRenderer = MultiAnimationRendererImpl() - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = context.sharedContext.immediateExperimentalUISettings.compressedEmojiCache + self.animationRenderer = DCTMultiAnimationRendererImpl() + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = context.sharedContext.immediateExperimentalUISettings.compressedEmojiCache self.backgroundMaskNode = ASDisplayNode() var backgroundGlassParams: ReactionContextBackgroundNode.GlassParams? @@ -3341,7 +3342,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let currentItemNode = currentItemNode { itemNode = currentItemNode } else { - let animationRenderer = MultiAnimationRendererImpl() + let animationRenderer = DCTMultiAnimationRendererImpl() itemNode = ReactionNode(context: context, theme: theme, item: reaction, icon: .none, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false) } self.itemNode = itemNode diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index c53cfdcae3..96e730748f 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -283,6 +283,8 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent", diff --git a/submodules/TelegramUI/Components/AnimationCache/BUILD b/submodules/TelegramUI/Components/AnimationCache/BUILD index f751e8899f..3d182a866a 100644 --- a/submodules/TelegramUI/Components/AnimationCache/BUILD +++ b/submodules/TelegramUI/Components/AnimationCache/BUILD @@ -11,10 +11,6 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/CryptoUtils:CryptoUtils", - "//submodules/ManagedFile:ManagedFile", - "//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT", - "//third-party/subcodec:SubcodecObjC", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 0c99aada38..6c6391bb73 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -1,51 +1,19 @@ import Foundation import UIKit import SwiftSignalKit -import CryptoUtils -import ManagedFile -import Compression - -private let algorithm: compression_algorithm = COMPRESSION_LZFSE - -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 func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { - if useTotalFileAllocatedSize { - let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { - if values.isRegularFile ?? false { - if let fileSize = values.fileAllocatedSize { - return Int64(fileSize) - } - } - } - } - - var value = stat() - if stat(path, &value) == 0 { - return value.st_size - } else { - return nil - } -} public final class AnimationCacheItemFrame { public enum RequestedFormat { case rgba case yuva(rowAlignment: Int) } - + public final class Plane { public let data: Data public let width: Int public let height: Int public let bytesPerRow: Int - + public init(data: Data, width: Int, height: Int, bytesPerRow: Int) { self.data = data self.width = width @@ -53,15 +21,15 @@ public final class AnimationCacheItemFrame { self.bytesPerRow = bytesPerRow } } - + public enum Format { case rgba(data: Data, width: Int, height: Int, bytesPerRow: Int) case yuva(y: Plane, u: Plane, v: Plane, a: Plane) } - + public let format: Format public let duration: Double - + public init(format: Format, duration: Double) { self.format = format self.duration = duration @@ -73,31 +41,31 @@ public final class AnimationCacheItem { case duration(Double) case frames(Int) } - + public struct AdvanceResult { public let frame: AnimationCacheItemFrame public let didLoop: Bool - + public init(frame: AnimationCacheItemFrame, didLoop: Bool) { self.frame = frame self.didLoop = didLoop } } - + public let numFrames: Int private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult? private let resetImpl: () -> Void - + public init(numFrames: Int, advanceImpl: @escaping (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult?, resetImpl: @escaping () -> Void) { self.numFrames = numFrames self.advanceImpl = advanceImpl self.resetImpl = resetImpl } - + public func advance(advance: Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult? { return self.advanceImpl(advance, requestedFormat) } - + public func reset() { self.resetImpl() } @@ -109,7 +77,7 @@ public struct AnimationCacheItemDrawingSurface { public let height: Int public let bytesPerRow: Int public let length: Int - + public init( argb: UnsafeMutablePointer, width: Int, @@ -128,7 +96,7 @@ public struct AnimationCacheItemDrawingSurface { public protocol AnimationCacheItemWriter: AnyObject { var queue: Queue { get } var isCancelled: Bool { get } - + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) func finish() } @@ -136,7 +104,7 @@ public protocol AnimationCacheItemWriter: AnyObject { public final class AnimationCacheItemResult { public let item: AnimationCacheItem? public let isFinal: Bool - + public init(item: AnimationCacheItem?, isFinal: Bool) { self.item = item self.isFinal = isFinal @@ -147,7 +115,7 @@ public struct AnimationCacheFetchOptions { public let size: CGSize public let writer: AnimationCacheItemWriter public let firstFrameOnly: Bool - + public init( size: CGSize, writer: AnimationCacheItemWriter, @@ -164,1525 +132,3 @@ public protocol AnimationCache: AnyObject { func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable } - -private func md5Hash(_ string: String) -> String { - let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in - return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) - } - return hashData.withUnsafeBytes { bytes -> String in - let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) - return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15]) - } -} - -private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { - assert(hashString.count == 32) - var directory = "" - - for i in 0 ..< 1 { - if !directory.isEmpty { - directory.append("/") - } - directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)])) - } - - return (directory, "\(hashString)_\(width)x\(height)") -} - -private func roundUp(_ numToRound: Int, multiple: Int) -> Int { - if multiple == 0 { - return numToRound - } - - let remainder = numToRound % multiple - if remainder == 0 { - return numToRound; - } - - return numToRound + multiple - remainder -} - -private func compressData(data: Data, addSizeHeader: Bool = false) -> Data? { - let scratchData = malloc(compression_encode_scratch_buffer_size(algorithm))! - defer { - free(scratchData) - } - - let headerSize = addSizeHeader ? 4 : 0 - var compressedData = Data(count: headerSize + data.count + 16 * 1024) - let resultSize = compressedData.withUnsafeMutableBytes { buffer -> Int in - guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return 0 - } - - if addSizeHeader { - var decompressedSize: UInt32 = UInt32(data.count) - memcpy(bytes, &decompressedSize, 4) - } - - return data.withUnsafeBytes { sourceBuffer -> Int in - return compression_encode_buffer(bytes.advanced(by: headerSize), buffer.count - headerSize, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), sourceBuffer.count, scratchData, algorithm) - } - } - - if resultSize <= 0 { - return nil - } - compressedData.count = headerSize + resultSize - return compressedData -} - -private func decompressData(data: Data, range: Range, decompressedSize: Int) -> Data? { - let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))! - defer { - free(scratchData) - } - - var decompressedFrameData = Data(count: decompressedSize) - let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in - guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return 0 - } - return data.withUnsafeBytes { sourceBuffer -> Int in - return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm) - } - } - - if resultSize <= 0 { - return nil - } - if decompressedFrameData.count != resultSize { - decompressedFrameData.count = resultSize - } - return decompressedFrameData -} - -private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { - enum WriteError: Error { - case generic - } - - struct CompressedResult { - var animationPath: String - } - - private struct FrameMetadata { - var duration: Double - } - - var queue: Queue { - return self.innerQueue - } - let innerQueue: Queue - var isCancelled: Bool = false - - private let compressedPath: String - private var file: ManagedFile? - private var compressedWriter: CompressedFileWriter? - private let completion: (CompressedResult?) -> Void - - - private var currentSurface: ImageARGB? - private var currentYUVASurface: ImageYUVA420? - private var currentFrameFloat: FloatCoefficientsYUVA420? - private var previousFrameCoefficients: DctCoefficientsYUVA420? - private var deltaFrameFloat: FloatCoefficientsYUVA420? - private var previousYUVASurface: ImageYUVA420? - private var currentDctData: DctData? - private var differenceCoefficients: DctCoefficientsYUVA420? - private var currentDctCoefficients: DctCoefficientsYUVA420? - private var contentLengthOffset: Int? - private var isFailed: Bool = false - private var isFinished: Bool = false - - private var frames: [FrameMetadata] = [] - - private let dctQualityLuma: Int - private let dctQualityChroma: Int - private let dctQualityDelta: Int - - private let lock = Lock() - - init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { - self.dctQualityLuma = 70 - self.dctQualityChroma = 88 - self.dctQualityDelta = 22 - - self.innerQueue = queue - self.compressedPath = allocateTempFile() - - guard let file = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { - return nil - } - self.file = file - self.compressedWriter = CompressedFileWriter(file: file) - self.completion = completion - } - - func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) { - do { - try self.lock.throwingLocked { - let width = roundUp(proposedWidth, multiple: 16) - let height = roundUp(proposedHeight, multiple: 16) - - let surface: ImageARGB - if let current = self.currentSurface { - if current.argbPlane.width == width && current.argbPlane.height == height { - surface = current - surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in - memset(bytes.baseAddress!, 0, bytes.count) - } - } else { - self.isFailed = true - return - } - } else { - surface = ImageARGB(width: width, height: height, rowAlignment: 32) - self.currentSurface = surface - } - - let duration = surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Double? in - return drawingBlock(AnimationCacheItemDrawingSurface( - argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), - width: width, - height: height, - bytesPerRow: surface.argbPlane.bytesPerRow, - length: bytes.count - )) - } - - guard let duration = duration else { - return - } - - try addInternal(with: { yuvaSurface in - surface.toYUVA420(target: yuvaSurface) - - return duration - }, width: width, height: height, insertKeyframe: insertKeyframe) - } - } catch { - } - } - - func addYUV(with drawingBlock: (ImageYUVA420) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) throws { - let width = roundUp(proposedWidth, multiple: 16) - let height = roundUp(proposedHeight, multiple: 16) - - do { - try self.lock.throwingLocked { - try addInternal(with: { yuvaSurface in - return drawingBlock(yuvaSurface) - }, width: width, height: height, insertKeyframe: insertKeyframe) - } - } catch { - } - } - - func addInternal(with drawingBlock: (ImageYUVA420) -> Double?, width: Int, height: Int, insertKeyframe: Bool) throws { - if width == 0 || height == 0 { - self.isFailed = true - throw WriteError.generic - } - if self.isFailed || self.isFinished { - throw WriteError.generic - } - - guard !self.isFailed, !self.isFinished, let file = self.file, let compressedWriter = self.compressedWriter else { - throw WriteError.generic - } - - var isFirstFrame = false - - let yuvaSurface: ImageYUVA420 - if let current = self.currentYUVASurface { - if current.yPlane.width == width && current.yPlane.height == height { - yuvaSurface = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - isFirstFrame = true - - yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil) - self.currentYUVASurface = yuvaSurface - } - - let currentFrameFloat: FloatCoefficientsYUVA420 - if let current = self.currentFrameFloat { - if current.yPlane.width == width && current.yPlane.height == height { - currentFrameFloat = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - currentFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) - self.currentFrameFloat = currentFrameFloat - } - - let previousFrameCoefficients: DctCoefficientsYUVA420 - if let current = self.previousFrameCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - previousFrameCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - previousFrameCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.previousFrameCoefficients = previousFrameCoefficients - } - - let deltaFrameFloat: FloatCoefficientsYUVA420 - if let current = self.deltaFrameFloat { - if current.yPlane.width == width && current.yPlane.height == height { - deltaFrameFloat = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - deltaFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) - self.deltaFrameFloat = deltaFrameFloat - } - - let dctData: DctData - if let current = self.currentDctData { - dctData = current - } else { - dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma, delta: self.dctQualityDelta) - self.currentDctData = dctData - } - - let duration = drawingBlock(yuvaSurface) - - guard let duration = duration else { - return - } - - let dctCoefficients: DctCoefficientsYUVA420 - if let current = self.currentDctCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - dctCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.currentDctCoefficients = dctCoefficients - } - - let differenceCoefficients: DctCoefficientsYUVA420 - if let current = self.differenceCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - differenceCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - differenceCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.differenceCoefficients = differenceCoefficients - } - - #if !arch(arm64) - var insertKeyframe = insertKeyframe - insertKeyframe = true - #endif - - let previousYUVASurface: ImageYUVA420 - if let current = self.previousYUVASurface { - previousYUVASurface = current - } else { - previousYUVASurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) - self.previousYUVASurface = previousYUVASurface - } - - let isKeyframe: Bool - if !isFirstFrame && !insertKeyframe { - isKeyframe = false - - //previous + delta = current - //delta = current - previous - yuvaSurface.toCoefficients(target: differenceCoefficients) - differenceCoefficients.subtract(other: previousFrameCoefficients) - differenceCoefficients.dct4x4(dctData: dctData, target: dctCoefficients) - - //previous + delta = current - dctCoefficients.idct4x4Add(dctData: dctData, target: previousFrameCoefficients) - //previousFrameCoefficients.add(other: differenceCoefficients) - } else { - isKeyframe = true - - yuvaSurface.dct8x8(dctData: dctData, target: dctCoefficients) - - dctCoefficients.idct8x8(dctData: dctData, target: yuvaSurface) - yuvaSurface.toCoefficients(target: previousFrameCoefficients) - } - - if isFirstFrame { - file.write(6 as UInt32) - - file.write(UInt32(dctCoefficients.yPlane.width)) - file.write(UInt32(dctCoefficients.yPlane.height)) - - let lumaDctTable = dctData.lumaTable.serializedData() - file.write(UInt32(lumaDctTable.count)) - let _ = file.write(lumaDctTable) - - let chromaDctTable = dctData.chromaTable.serializedData() - file.write(UInt32(chromaDctTable.count)) - let _ = file.write(chromaDctTable) - - let deltaDctTable = dctData.deltaTable.serializedData() - file.write(UInt32(deltaDctTable.count)) - let _ = file.write(deltaDctTable) - - self.contentLengthOffset = Int(file.position()) - file.write(0 as UInt32) - } - - do { - let frameLength = dctCoefficients.yPlane.data.count + dctCoefficients.uPlane.data.count + dctCoefficients.vPlane.data.count + dctCoefficients.aPlane.data.count - try compressedWriter.writeUInt32(UInt32(frameLength)) - - try compressedWriter.writeUInt32(isKeyframe ? 1 : 0) - - for i in 0 ..< 4 { - let dctPlane: DctCoefficientPlane - switch i { - case 0: - dctPlane = dctCoefficients.yPlane - case 1: - dctPlane = dctCoefficients.uPlane - case 2: - dctPlane = dctCoefficients.vPlane - case 3: - dctPlane = dctCoefficients.aPlane - default: - preconditionFailure() - } - - try compressedWriter.writeUInt32(UInt32(dctPlane.data.count)) - try dctPlane.data.withUnsafeBytes { bytes in - try compressedWriter.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - } - - self.frames.append(FrameMetadata(duration: duration)) - } catch { - self.isFailed = true - throw WriteError.generic - } - } - - func finish() { - do { - let result = try self.finishInternal() - self.completion(result) - } catch { - } - } - - func finishInternal() throws -> CompressedResult? { - var shouldComplete = false - self.lock.locked { - if !self.isFinished { - self.isFinished = true - shouldComplete = true - - guard let contentLengthOffset = self.contentLengthOffset, let file = self.file, let compressedWriter = self.compressedWriter else { - self.isFailed = true - return - } - assert(contentLengthOffset >= 0) - - do { - try compressedWriter.flush() - - let metadataPosition = file.position() - let contentLength = Int(metadataPosition) - contentLengthOffset - 4 - let _ = file.seek(position: Int64(contentLengthOffset)) - file.write(UInt32(contentLength)) - - let _ = file.seek(position: metadataPosition) - file.write(UInt32(self.frames.count)) - for frame in self.frames { - file.write(Float32(frame.duration)) - } - - if !self.isFailed { - self.compressedWriter = nil - self.file = nil - - file._unsafeClose() - } - } catch { - self.isFailed = true - } - } - } - - if shouldComplete { - if !self.isFailed { - return CompressedResult(animationPath: self.compressedPath) - } else { - let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) - return nil - } - } else { - return nil - } - } -} - -private final class AnimationCacheItemAccessor { - private enum ReadError: Error { - case generic - } - - final class CurrentFrame { - let index: Int - var remainingDuration: Double - let duration: Double - let yuva: ImageYUVA420 - - init(index: Int, duration: Double, yuva: ImageYUVA420) { - self.index = index - self.duration = duration - self.remainingDuration = duration - self.yuva = yuva - } - } - - struct FrameInfo { - let duration: Double - } - - private let data: Data - private var compressedDataReader: DecompressedData? - private let range: Range - private let frameMapping: [Int: FrameInfo] - private let width: Int - private let height: Int - private let durationMapping: [Double] - - private var currentFrame: CurrentFrame? - - private var currentYUVASurface: ImageYUVA420? - private var currentCoefficients: DctCoefficientsYUVA420? - private let currentDctData: DctData - private var sharedDctCoefficients: DctCoefficientsYUVA420? - private var deltaCoefficients: DctCoefficientsYUVA420? - - init(data: Data, range: Range, frameMapping: [FrameInfo], width: Int, height: Int, dctData: DctData) { - self.data = data - self.range = range - self.width = width - self.height = height - - var resultFrameMapping: [Int: FrameInfo] = [:] - var durationMapping: [Double] = [] - - for i in 0 ..< frameMapping.count { - let frame = frameMapping[i] - resultFrameMapping[i] = frame - durationMapping.append(frame.duration) - } - - self.frameMapping = resultFrameMapping - self.durationMapping = durationMapping - - self.currentDctData = dctData - } - - private func loadNextFrame() -> Bool { - var didLoop = false - let index: Int - if let currentFrame = self.currentFrame { - if currentFrame.index + 1 >= self.durationMapping.count { - index = 0 - self.compressedDataReader = nil - didLoop = true - } else { - index = currentFrame.index + 1 - } - } else { - index = 0 - self.compressedDataReader = nil - } - - if self.compressedDataReader == nil { - self.compressedDataReader = DecompressedData(compressedData: self.data, dataRange: self.range) - } - - guard let compressedDataReader = self.compressedDataReader else { - self.currentFrame = nil - return didLoop - } - - do { - let frameLength = Int(try compressedDataReader.readUInt32()) - - let frameType = Int(try compressedDataReader.readUInt32()) - - let dctCoefficients: DctCoefficientsYUVA420 - if let sharedDctCoefficients = self.sharedDctCoefficients, sharedDctCoefficients.yPlane.width == self.width, sharedDctCoefficients.yPlane.height == self.height, !"".isEmpty { - dctCoefficients = sharedDctCoefficients - } else { - dctCoefficients = DctCoefficientsYUVA420(width: self.width, height: self.height) - self.sharedDctCoefficients = dctCoefficients - } - - var frameOffset = 0 - for i in 0 ..< 4 { - let planeLength = Int(try compressedDataReader.readUInt32()) - if planeLength < 0 || planeLength > 20 * 1024 * 1024 { - throw ReadError.generic - } - - let plane: DctCoefficientPlane - switch i { - case 0: - plane = dctCoefficients.yPlane - case 1: - plane = dctCoefficients.uPlane - case 2: - plane = dctCoefficients.vPlane - case 3: - plane = dctCoefficients.aPlane - default: - throw ReadError.generic - } - - if planeLength != plane.data.count { - throw ReadError.generic - } - - if frameOffset + plane.data.count > frameLength { - throw ReadError.generic - } - - try plane.data.withUnsafeMutableBytes { bytes in - try compressedDataReader.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - frameOffset += plane.data.count - } - - let yuvaSurface: ImageYUVA420 - if let currentYUVASurface = self.currentYUVASurface { - yuvaSurface = currentYUVASurface - } else { - yuvaSurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) - } - - let currentCoefficients: DctCoefficientsYUVA420 - if let current = self.currentCoefficients { - currentCoefficients = current - } else { - currentCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) - self.currentCoefficients = currentCoefficients - } - - /*let deltaCoefficients: DctCoefficientsYUVA420 - if let current = self.deltaCoefficients { - deltaCoefficients = current - } else { - deltaCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) - self.deltaCoefficients = deltaCoefficients - }*/ - - switch frameType { - case 1: - dctCoefficients.idct8x8(dctData: self.currentDctData, target: yuvaSurface) - yuvaSurface.toCoefficients(target: currentCoefficients) - default: - dctCoefficients.idct4x4Add(dctData: self.currentDctData, target: currentCoefficients) - //currentCoefficients.add(other: deltaCoefficients) - - currentCoefficients.toYUVA420(target: yuvaSurface) - } - - self.currentFrame = CurrentFrame(index: index, duration: self.durationMapping[index], yuva: yuvaSurface) - } catch { - self.currentFrame = nil - self.compressedDataReader = nil - } - - return didLoop - } - - func reset() { - self.currentFrame = nil - } - - func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItem.AdvanceResult? { - var didLoop = false - switch advance { - case let .frames(count): - for _ in 0 ..< count { - if self.loadNextFrame() { - didLoop = true - } - } - case let .duration(duration): - var durationOverflow = duration - while true { - if let currentFrame = self.currentFrame { - currentFrame.remainingDuration -= durationOverflow - if currentFrame.remainingDuration <= 0.0 { - durationOverflow = -currentFrame.remainingDuration - if self.loadNextFrame() { - didLoop = true - } - } else { - break - } - } else { - if self.loadNextFrame() { - didLoop = true - } - break - } - } - } - - guard let currentFrame = self.currentFrame else { - return nil - } - - switch requestedFormat { - case .rgba: - let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32) - currentFrame.yuva.toARGB(target: currentSurface) - - return AnimationCacheItem.AdvanceResult( - frame: AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration), - didLoop: didLoop - ) - case .yuva: - return AnimationCacheItem.AdvanceResult( - frame: AnimationCacheItemFrame( - format: .yuva( - y: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.yPlane.data, - width: currentFrame.yuva.yPlane.width, - height: currentFrame.yuva.yPlane.height, - bytesPerRow: currentFrame.yuva.yPlane.bytesPerRow - ), - u: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.uPlane.data, - width: currentFrame.yuva.uPlane.width, - height: currentFrame.yuva.uPlane.height, - bytesPerRow: currentFrame.yuva.uPlane.bytesPerRow - ), - v: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.vPlane.data, - width: currentFrame.yuva.vPlane.width, - height: currentFrame.yuva.vPlane.height, - bytesPerRow: currentFrame.yuva.vPlane.bytesPerRow - ), - a: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.aPlane.data, - width: currentFrame.yuva.aPlane.width, - height: currentFrame.yuva.aPlane.height, - bytesPerRow: currentFrame.yuva.aPlane.bytesPerRow - ) - ), - duration: currentFrame.duration - ), - didLoop: didLoop - ) - } - } -} - -private func readData(data: Data, offset: Int, count: Int) -> Data { - var result = Data(count: count) - result.withUnsafeMutableBytes { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), count) - } - } - return result -} - -private func readUInt32(data: Data, offset: Int) -> UInt32 { - var value: UInt32 = 0 - withUnsafeMutableBytes(of: &value, { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) - } - }) - - return value -} - -private func readFloat32(data: Data, offset: Int) -> Float32 { - var value: Float32 = 0 - withUnsafeMutableBytes(of: &value, { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) - } - }) - - return value -} - -private func writeUInt32(data: inout Data, value: UInt32) { - var value: UInt32 = value - withUnsafeBytes(of: &value, { bytes -> Void in - data.count += 4 - data.withUnsafeMutableBytes { dataBytes -> Void in - memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) - } - }) -} - -private func writeFloat32(data: inout Data, value: Float32) { - var value: Float32 = value - withUnsafeBytes(of: &value, { bytes -> Void in - data.count += 4 - data.withUnsafeMutableBytes { dataBytes -> Void in - memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) - } - }) -} - -private final class CompressedFileWriter { - enum WriteError: Error { - case generic - } - - private let file: ManagedFile - private let stream: UnsafeMutablePointer - - private let tempOutputBufferSize: Int = 64 * 1024 - private let tempOutputBuffer: UnsafeMutablePointer - private let tempInputBufferCapacity: Int = 64 * 1024 - private let tempInputBuffer: UnsafeMutablePointer - private var tempInputBufferSize: Int = 0 - - private var didFail: Bool = false - - init?(file: ManagedFile) { - self.file = file - - self.stream = UnsafeMutablePointer.allocate(capacity: 1) - guard compression_stream_init(self.stream, COMPRESSION_STREAM_ENCODE, algorithm) != COMPRESSION_STATUS_ERROR else { - self.stream.deallocate() - return nil - } - - self.tempOutputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempOutputBufferSize) - self.tempInputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempInputBufferCapacity) - } - - deinit { - compression_stream_destroy(self.stream) - self.stream.deallocate() - self.tempOutputBuffer.deallocate() - self.tempInputBuffer.deallocate() - } - - private func flushBuffer() throws { - if self.didFail { - throw WriteError.generic - } - - if self.tempInputBufferSize <= 0 { - return - } - - self.stream.pointee.src_ptr = UnsafePointer(self.tempInputBuffer) - self.stream.pointee.src_size = self.tempInputBufferSize - - while true { - self.stream.pointee.dst_ptr = self.tempOutputBuffer - self.stream.pointee.dst_size = self.tempOutputBufferSize - - let status = compression_stream_process(self.stream, 0) - if status == COMPRESSION_STATUS_ERROR { - self.didFail = true - throw WriteError.generic - } - - let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size - if writtenBytes > 0 { - let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) - } - - if status == COMPRESSION_STATUS_END { - break - } else { - if self.stream.pointee.src_size == 0 { - break - } - } - } - - self.tempInputBufferSize = 0 - } - - func write(bytes: UnsafePointer, count: Int) throws { - var writtenBytes = 0 - while writtenBytes < count { - let availableBytes = self.tempInputBufferCapacity - self.tempInputBufferSize - if availableBytes == 0 { - try flushBuffer() - } else { - let writeCount = min(availableBytes, count - writtenBytes) - - memcpy(self.tempInputBuffer.advanced(by: self.tempInputBufferSize), bytes.advanced(by: writtenBytes), writeCount) - self.tempInputBufferSize += writeCount - writtenBytes += writeCount - } - } - } - - func flush() throws { - if self.didFail { - throw WriteError.generic - } - - try self.flushBuffer() - - while true { - self.stream.pointee.dst_ptr = self.tempOutputBuffer - self.stream.pointee.dst_size = self.tempOutputBufferSize - - let status = compression_stream_process(self.stream, Int32(COMPRESSION_STREAM_FINALIZE.rawValue)) - if status == COMPRESSION_STATUS_ERROR { - self.didFail = true - throw WriteError.generic - } - - let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size - if writtenBytes > 0 { - let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) - } - - if status == COMPRESSION_STATUS_END { - break - } - } - } - - func writeUInt32(_ value: UInt32) throws { - var value: UInt32 = value - try withUnsafeBytes(of: &value, { bytes -> Void in - try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - } - - func writeFloat32(_ value: Float32) throws { - var value: Float32 = value - try withUnsafeBytes(of: &value, { bytes -> Void in - try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - } -} - -private final class DecompressedData { - enum ReadError: Error { - case didReadToEnd - } - - private let compressedData: Data - private let dataRange: Range - private let stream: UnsafeMutablePointer - private var isComplete = false - - init?(compressedData: Data, dataRange: Range) { - self.compressedData = compressedData - self.dataRange = dataRange - - self.stream = UnsafeMutablePointer.allocate(capacity: 1) - guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, algorithm) != COMPRESSION_STATUS_ERROR else { - self.stream.deallocate() - return nil - } - - self.compressedData.withUnsafeBytes { bytes in - self.stream.pointee.src_ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dataRange.lowerBound) - self.stream.pointee.src_size = dataRange.upperBound - dataRange.lowerBound - } - } - - deinit { - compression_stream_destroy(self.stream) - self.stream.deallocate() - } - - func read(bytes: UnsafeMutablePointer, count: Int) throws { - if self.isComplete { - throw ReadError.didReadToEnd - } - - self.stream.pointee.dst_ptr = bytes - self.stream.pointee.dst_size = count - - let status = compression_stream_process(self.stream, 0) - - if status == COMPRESSION_STATUS_ERROR { - self.isComplete = true - throw ReadError.didReadToEnd - } else if status == COMPRESSION_STATUS_END { - if self.stream.pointee.src_size == 0 { - self.isComplete = true - } - } - - if self.stream.pointee.dst_size != 0 { - throw ReadError.didReadToEnd - } - } - - func readUInt32() throws -> UInt32 { - var value: UInt32 = 0 - try withUnsafeMutableBytes(of: &value, { bytes -> Void in - try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - return value - } - - func readFloat32() throws -> Float32 { - var value: Float32 = 0 - try withUnsafeMutableBytes(of: &value, { bytes -> Void in - try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - return value - } -} - -private enum LoadItemError: Error { - case dataError -} - -private func loadItem(path: String) throws -> AnimationCacheItem { - guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { - throw LoadItemError.dataError - } - - var offset: Int = 0 - let dataLength = compressedData.count - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let formatVersion = readUInt32(data: compressedData, offset: offset) - offset += 4 - if formatVersion != 6 { - throw LoadItemError.dataError - } - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let width = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let height = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctLumaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctLumaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctLumaData = readData(data: compressedData, offset: offset, count: Int(dctLumaTableLength)) - offset += Int(dctLumaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctChromaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctChromaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctChromaData = readData(data: compressedData, offset: offset, count: Int(dctChromaTableLength)) - offset += Int(dctChromaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctDeltaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctDeltaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctDeltaData = readData(data: compressedData, offset: offset, count: Int(dctDeltaTableLength)) - offset += Int(dctDeltaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let contentLength = Int(readUInt32(data: compressedData, offset: offset)) - offset += 4 - - let compressedFrameDataRange = offset ..< (offset + contentLength) - offset += contentLength - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let frameCount = Int(readUInt32(data: compressedData, offset: offset)) - offset += 4 - - var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] - for _ in 0 ..< frameCount { - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let frameDuration = readFloat32(data: compressedData, offset: offset) - offset += 4 - - frameMapping.append(AnimationCacheItemAccessor.FrameInfo(duration: Double(frameDuration))) - } - - guard let dctData = DctData(lumaTable: dctLumaData, chromaTable: dctChromaData, deltaTable: dctDeltaData) else { - throw LoadItemError.dataError - } - - let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctData: dctData) - - return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in - return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat) - }, resetImpl: { - itemAccessor.reset() - }) -} - -private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { - guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else { - return nil - } - guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { - _ in - }) else { - return nil - } - - do { - for _ in 0 ..< higherResolutionItem.numFrames { - try writer.addYUV(with: { yuva in - guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { - return nil - } - switch frame.frame.format { - case .rgba: - return nil - case let .yuva(y, u, v, a): - yuva.yPlane.copyScaled(fromPlane: y) - yuva.uPlane.copyScaled(fromPlane: u) - yuva.vPlane.copyScaled(fromPlane: v) - yuva.aPlane.copyScaled(fromPlane: a) - } - - return frame.frame.duration - }, proposedWidth: width, proposedHeight: height, insertKeyframe: true) - } - - guard let result = try writer.finishInternal() else { - return nil - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - return nil - } - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return nil - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - - guard let item = try? loadItem(path: itemPath) else { - return nil - } - return item - } catch { - return nil - } -} - -private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> Bool { - guard let animationItem = try? loadItem(path: animationItemPath) else { - return false - } - guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { _ in - }) else { - return false - } - - do { - for _ in 0 ..< min(1, animationItem.numFrames) { - guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else { - return false - } - switch frame.frame.format { - case .rgba: - return false - case let .yuva(y, u, v, a): - try writer.addYUV(with: { yuva in - assert(yuva.yPlane.bytesPerRow == y.bytesPerRow) - assert(yuva.uPlane.bytesPerRow == u.bytesPerRow) - assert(yuva.vPlane.bytesPerRow == v.bytesPerRow) - assert(yuva.aPlane.bytesPerRow == a.bytesPerRow) - - yuva.yPlane.copyScaled(fromPlane: y) - yuva.uPlane.copyScaled(fromPlane: u) - yuva.vPlane.copyScaled(fromPlane: v) - yuva.aPlane.copyScaled(fromPlane: a) - - return frame.frame.duration - }, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true) - } - } - - guard let result = try writer.finishInternal() else { - return false - } - - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return false - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - return true - } catch { - return false - } -} - -private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? { - var candidates: [(path: String, width: Int, height: Int)] = [] - if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) { - for url in enumerator { - guard let url = url as? URL else { - continue - } - let fileName = url.lastPathComponent - if fileName.hasPrefix(baseName) { - let scanner = Scanner(string: fileName) - guard scanner.scanString(baseName) != nil else { - continue - } - guard let itemWidth = scanner.scanInt() else { - continue - } - guard scanner.scanString("x") != nil else { - continue - } - guard let itemHeight = scanner.scanInt() else { - continue - } - if !baseSuffix.isEmpty { - guard scanner.scanString(baseSuffix) != nil else { - continue - } - } - guard scanner.isAtEnd else { - continue - } - if itemWidth > width && itemHeight > height { - candidates.append((url.path, itemWidth, itemHeight)) - } - } - } - } - if !candidates.isEmpty { - candidates.sort(by: { $0.width < $1.width }) - return candidates[0].path - } - return nil -} - -public final class AnimationCacheImpl: AnimationCache { - private final class Impl { - private struct ItemKey: Hashable { - var id: String - var width: Int - var height: Int - } - - private final class ItemContext { - let subscribers = Bag<(AnimationCacheItemResult) -> Void>() - let disposable = MetaDisposable() - - deinit { - self.disposable.dispose() - } - } - - private let queue: Queue - private let basePath: String - private let allocateTempFile: () -> String - private let updateStorageStats: (String, Int64) -> Void - - private let fetchQueues: [Queue] - private var nextFetchQueueIndex: Int = 0 - - private var itemContexts: [ItemKey: ItemContext] = [:] - - init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { - self.queue = queue - - let fetchQueueCount: Int - if ProcessInfo.processInfo.processorCount > 2 { - fetchQueueCount = 3 - } else { - fetchQueueCount = 2 - } - - self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "AnimationCacheImpl-Fetch\(i)", qos: .default) } - self.basePath = basePath - self.allocateTempFile = allocateTempFile - self.updateStorageStats = updateStorageStats - } - - deinit { - } - - func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" - let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemPath), let item = try? loadItem(path: itemPath) { - updateResult(AnimationCacheItemResult(item: item, isFinal: true)) - - return EmptyDisposable - } - let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height)) - - let itemContext: ItemContext - var beginFetch = false - if let current = self.itemContexts[key] { - itemContext = current - } else { - itemContext = ItemContext() - self.itemContexts[key] = itemContext - beginFetch = true - } - - let queue = self.queue - let index = itemContext.subscribers.add(updateResult) - - updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) - - if beginFetch { - let fetchQueueIndex = self.nextFetchQueueIndex - self.nextFetchQueueIndex += 1 - let allocateTempFile = self.allocateTempFile - let updateStorageStats = self.updateStorageStats - guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in - queue.async { - guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { - return - } - - strongSelf.itemContexts.removeValue(forKey: key) - - guard let result = result else { - return - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - return - } - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - - let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) - - for f in itemContext.subscribers.copyItems() { - guard let item = try? loadItem(path: itemPath) else { - continue - } - f(AnimationCacheItemResult(item: item, isFinal: true)) - } - } - }) else { - return EmptyDisposable - } - - let fetchDisposable = MetaDisposable() - fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false))) - - itemContext.disposable.set(ActionDisposable { [weak writer] in - if let writer = writer { - writer.isCancelled = true - } - - fetchDisposable.dispose() - }) - } - - return ActionDisposable { [weak self, weak itemContext] in - queue.async { - guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { - return - } - itemContext.subscribers.remove(index) - if itemContext.subscribers.isEmpty { - itemContext.disposable.dispose() - strongSelf.itemContexts.removeValue(forKey: key) - } - } - } - } - - static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { - let hashString = md5Hash(sourceId) - let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemFirstFramePath) { - if let item = try? loadItem(path: itemFirstFramePath) { - return item - } - } - - if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { - if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { - return adaptedItem - } - } - - return nil - } - - static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let hashString = md5Hash(sourceId) - let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = try? loadItem(path: itemFirstFramePath) { - completion(AnimationCacheItemResult(item: item, isFinal: true)) - return EmptyDisposable - } - - if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { - if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { - completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true)) - return EmptyDisposable - } - } - - if let fetch = fetch { - completion(AnimationCacheItemResult(item: nil, isFinal: false)) - - guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in - queue.async { - guard let result = result else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemFirstFramePath) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - if let size = fileSize(itemFirstFramePath) { - updateStorageStats(itemFirstFramePath, size) - } - guard let item = try? loadItem(path: itemFirstFramePath) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - - completion(AnimationCacheItemResult(item: item, isFinal: true)) - } - }) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return EmptyDisposable - } - - let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true)) - return fetchDisposable - } else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return EmptyDisposable - } - } - } - - private let queue: Queue - private let basePath: String - private let impl: QueueLocalObject - private let allocateTempFile: () -> String - private let updateStorageStats: (String, Int64) -> Void - - public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { - let queue = Queue() - self.queue = queue - self.basePath = basePath - self.allocateTempFile = allocateTempFile - self.updateStorageStats = updateStorageStats - self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) - }) - } - - public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal { - return Signal { subscriber in - let disposable = MetaDisposable() - - self.impl.with { impl in - disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in - subscriber.putNext(result) - if result.isFinal { - subscriber.putCompletion() - } - })) - } - - return disposable - } - |> runOn(self.queue) - } - - public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { - return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile, updateStorageStats: self.updateStorageStats) - } - - public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let disposable = MetaDisposable() - - let basePath = self.basePath - let allocateTempFile = self.allocateTempFile - let updateStorageStats = self.updateStorageStats - queue.async { - disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats, fetch: fetch, completion: completion)) - } - - return disposable - } -} diff --git a/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD new file mode 100644 index 0000000000..273ab8aead --- /dev/null +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DCTAnimationCacheImpl", + module_name = "DCTAnimationCacheImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/CryptoUtils:CryptoUtils", + "//submodules/ManagedFile:ManagedFile", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT", + "//third-party/subcodec:SubcodecObjC", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift new file mode 100644 index 0000000000..cd2c8f6e37 --- /dev/null +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift @@ -0,0 +1,1558 @@ +import Foundation +import UIKit +import SwiftSignalKit +import CryptoUtils +import ManagedFile +import Compression +import AnimationCache + +private let algorithm: compression_algorithm = COMPRESSION_LZFSE + +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 func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { + if useTotalFileAllocatedSize { + let url = URL(fileURLWithPath: path) + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { + if values.isRegularFile ?? false { + if let fileSize = values.fileAllocatedSize { + return Int64(fileSize) + } + } + } + } + + var value = stat() + if stat(path, &value) == 0 { + return value.st_size + } else { + return nil + } +} + +private func md5Hash(_ string: String) -> String { + let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in + return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) + } + return hashData.withUnsafeBytes { bytes -> String in + let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15]) + } +} + +private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { + assert(hashString.count == 32) + var directory = "" + + for i in 0 ..< 1 { + if !directory.isEmpty { + directory.append("/") + } + directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)])) + } + + return (directory, "\(hashString)_\(width)x\(height)") +} + +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { + return numToRound + } + + let remainder = numToRound % multiple + if remainder == 0 { + return numToRound; + } + + return numToRound + multiple - remainder +} + +private func compressData(data: Data, addSizeHeader: Bool = false) -> Data? { + let scratchData = malloc(compression_encode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + let headerSize = addSizeHeader ? 4 : 0 + var compressedData = Data(count: headerSize + data.count + 16 * 1024) + let resultSize = compressedData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + + if addSizeHeader { + var decompressedSize: UInt32 = UInt32(data.count) + memcpy(bytes, &decompressedSize, 4) + } + + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_encode_buffer(bytes.advanced(by: headerSize), buffer.count - headerSize, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), sourceBuffer.count, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + compressedData.count = headerSize + resultSize + return compressedData +} + +private func decompressData(data: Data, range: Range, decompressedSize: Int) -> Data? { + let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + var decompressedFrameData = Data(count: decompressedSize) + let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + if decompressedFrameData.count != resultSize { + decompressedFrameData.count = resultSize + } + return decompressedFrameData +} + +private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { + enum WriteError: Error { + case generic + } + + struct CompressedResult { + var animationPath: String + } + + private struct FrameMetadata { + var duration: Double + } + + var queue: Queue { + return self.innerQueue + } + let innerQueue: Queue + var isCancelled: Bool = false + + private let compressedPath: String + private var file: ManagedFile? + private var compressedWriter: CompressedFileWriter? + private let completion: (CompressedResult?) -> Void + + + private var currentSurface: ImageARGB? + private var currentYUVASurface: ImageYUVA420? + private var currentFrameFloat: FloatCoefficientsYUVA420? + private var previousFrameCoefficients: DctCoefficientsYUVA420? + private var deltaFrameFloat: FloatCoefficientsYUVA420? + private var previousYUVASurface: ImageYUVA420? + private var currentDctData: DctData? + private var differenceCoefficients: DctCoefficientsYUVA420? + private var currentDctCoefficients: DctCoefficientsYUVA420? + private var contentLengthOffset: Int? + private var isFailed: Bool = false + private var isFinished: Bool = false + + private var frames: [FrameMetadata] = [] + + private let dctQualityLuma: Int + private let dctQualityChroma: Int + private let dctQualityDelta: Int + + private let lock = Lock() + + init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { + self.dctQualityLuma = 70 + self.dctQualityChroma = 88 + self.dctQualityDelta = 22 + + self.innerQueue = queue + self.compressedPath = allocateTempFile() + + guard let file = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { + return nil + } + self.file = file + self.compressedWriter = CompressedFileWriter(file: file) + self.completion = completion + } + + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) { + do { + try self.lock.throwingLocked { + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedHeight, multiple: 16) + + let surface: ImageARGB + if let current = self.currentSurface { + if current.argbPlane.width == width && current.argbPlane.height == height { + surface = current + surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in + memset(bytes.baseAddress!, 0, bytes.count) + } + } else { + self.isFailed = true + return + } + } else { + surface = ImageARGB(width: width, height: height, rowAlignment: 32) + self.currentSurface = surface + } + + let duration = surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Double? in + return drawingBlock(AnimationCacheItemDrawingSurface( + argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), + width: width, + height: height, + bytesPerRow: surface.argbPlane.bytesPerRow, + length: bytes.count + )) + } + + guard let duration = duration else { + return + } + + try addInternal(with: { yuvaSurface in + surface.toYUVA420(target: yuvaSurface) + + return duration + }, width: width, height: height, insertKeyframe: insertKeyframe) + } + } catch { + } + } + + func addYUV(with drawingBlock: (ImageYUVA420) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) throws { + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedHeight, multiple: 16) + + do { + try self.lock.throwingLocked { + try addInternal(with: { yuvaSurface in + return drawingBlock(yuvaSurface) + }, width: width, height: height, insertKeyframe: insertKeyframe) + } + } catch { + } + } + + func addInternal(with drawingBlock: (ImageYUVA420) -> Double?, width: Int, height: Int, insertKeyframe: Bool) throws { + if width == 0 || height == 0 { + self.isFailed = true + throw WriteError.generic + } + if self.isFailed || self.isFinished { + throw WriteError.generic + } + + guard !self.isFailed, !self.isFinished, let file = self.file, let compressedWriter = self.compressedWriter else { + throw WriteError.generic + } + + var isFirstFrame = false + + let yuvaSurface: ImageYUVA420 + if let current = self.currentYUVASurface { + if current.yPlane.width == width && current.yPlane.height == height { + yuvaSurface = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + isFirstFrame = true + + yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil) + self.currentYUVASurface = yuvaSurface + } + + let currentFrameFloat: FloatCoefficientsYUVA420 + if let current = self.currentFrameFloat { + if current.yPlane.width == width && current.yPlane.height == height { + currentFrameFloat = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + currentFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) + self.currentFrameFloat = currentFrameFloat + } + + let previousFrameCoefficients: DctCoefficientsYUVA420 + if let current = self.previousFrameCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + previousFrameCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + previousFrameCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.previousFrameCoefficients = previousFrameCoefficients + } + + let deltaFrameFloat: FloatCoefficientsYUVA420 + if let current = self.deltaFrameFloat { + if current.yPlane.width == width && current.yPlane.height == height { + deltaFrameFloat = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + deltaFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) + self.deltaFrameFloat = deltaFrameFloat + } + + let dctData: DctData + if let current = self.currentDctData { + dctData = current + } else { + dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma, delta: self.dctQualityDelta) + self.currentDctData = dctData + } + + let duration = drawingBlock(yuvaSurface) + + guard let duration = duration else { + return + } + + let dctCoefficients: DctCoefficientsYUVA420 + if let current = self.currentDctCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + dctCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.currentDctCoefficients = dctCoefficients + } + + let differenceCoefficients: DctCoefficientsYUVA420 + if let current = self.differenceCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + differenceCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + differenceCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.differenceCoefficients = differenceCoefficients + } + + #if !arch(arm64) + var insertKeyframe = insertKeyframe + insertKeyframe = true + #endif + + let previousYUVASurface: ImageYUVA420 + if let current = self.previousYUVASurface { + previousYUVASurface = current + } else { + previousYUVASurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) + self.previousYUVASurface = previousYUVASurface + } + + let isKeyframe: Bool + if !isFirstFrame && !insertKeyframe { + isKeyframe = false + + //previous + delta = current + //delta = current - previous + yuvaSurface.toCoefficients(target: differenceCoefficients) + differenceCoefficients.subtract(other: previousFrameCoefficients) + differenceCoefficients.dct4x4(dctData: dctData, target: dctCoefficients) + + //previous + delta = current + dctCoefficients.idct4x4Add(dctData: dctData, target: previousFrameCoefficients) + //previousFrameCoefficients.add(other: differenceCoefficients) + } else { + isKeyframe = true + + yuvaSurface.dct8x8(dctData: dctData, target: dctCoefficients) + + dctCoefficients.idct8x8(dctData: dctData, target: yuvaSurface) + yuvaSurface.toCoefficients(target: previousFrameCoefficients) + } + + if isFirstFrame { + file.write(6 as UInt32) + + file.write(UInt32(dctCoefficients.yPlane.width)) + file.write(UInt32(dctCoefficients.yPlane.height)) + + let lumaDctTable = dctData.lumaTable.serializedData() + file.write(UInt32(lumaDctTable.count)) + let _ = file.write(lumaDctTable) + + let chromaDctTable = dctData.chromaTable.serializedData() + file.write(UInt32(chromaDctTable.count)) + let _ = file.write(chromaDctTable) + + let deltaDctTable = dctData.deltaTable.serializedData() + file.write(UInt32(deltaDctTable.count)) + let _ = file.write(deltaDctTable) + + self.contentLengthOffset = Int(file.position()) + file.write(0 as UInt32) + } + + do { + let frameLength = dctCoefficients.yPlane.data.count + dctCoefficients.uPlane.data.count + dctCoefficients.vPlane.data.count + dctCoefficients.aPlane.data.count + try compressedWriter.writeUInt32(UInt32(frameLength)) + + try compressedWriter.writeUInt32(isKeyframe ? 1 : 0) + + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = dctCoefficients.yPlane + case 1: + dctPlane = dctCoefficients.uPlane + case 2: + dctPlane = dctCoefficients.vPlane + case 3: + dctPlane = dctCoefficients.aPlane + default: + preconditionFailure() + } + + try compressedWriter.writeUInt32(UInt32(dctPlane.data.count)) + try dctPlane.data.withUnsafeBytes { bytes in + try compressedWriter.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + } + + self.frames.append(FrameMetadata(duration: duration)) + } catch { + self.isFailed = true + throw WriteError.generic + } + } + + func finish() { + do { + let result = try self.finishInternal() + self.completion(result) + } catch { + } + } + + func finishInternal() throws -> CompressedResult? { + var shouldComplete = false + self.lock.locked { + if !self.isFinished { + self.isFinished = true + shouldComplete = true + + guard let contentLengthOffset = self.contentLengthOffset, let file = self.file, let compressedWriter = self.compressedWriter else { + self.isFailed = true + return + } + assert(contentLengthOffset >= 0) + + do { + try compressedWriter.flush() + + let metadataPosition = file.position() + let contentLength = Int(metadataPosition) - contentLengthOffset - 4 + let _ = file.seek(position: Int64(contentLengthOffset)) + file.write(UInt32(contentLength)) + + let _ = file.seek(position: metadataPosition) + file.write(UInt32(self.frames.count)) + for frame in self.frames { + file.write(Float32(frame.duration)) + } + + if !self.isFailed { + self.compressedWriter = nil + self.file = nil + + file._unsafeClose() + } + } catch { + self.isFailed = true + } + } + } + + if shouldComplete { + if !self.isFailed { + return CompressedResult(animationPath: self.compressedPath) + } else { + let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) + return nil + } + } else { + return nil + } + } +} + +private final class AnimationCacheItemAccessor { + private enum ReadError: Error { + case generic + } + + final class CurrentFrame { + let index: Int + var remainingDuration: Double + let duration: Double + let yuva: ImageYUVA420 + + init(index: Int, duration: Double, yuva: ImageYUVA420) { + self.index = index + self.duration = duration + self.remainingDuration = duration + self.yuva = yuva + } + } + + struct FrameInfo { + let duration: Double + } + + private let data: Data + private var compressedDataReader: DecompressedData? + private let range: Range + private let frameMapping: [Int: FrameInfo] + private let width: Int + private let height: Int + private let durationMapping: [Double] + + private var currentFrame: CurrentFrame? + + private var currentYUVASurface: ImageYUVA420? + private var currentCoefficients: DctCoefficientsYUVA420? + private let currentDctData: DctData + private var sharedDctCoefficients: DctCoefficientsYUVA420? + private var deltaCoefficients: DctCoefficientsYUVA420? + + init(data: Data, range: Range, frameMapping: [FrameInfo], width: Int, height: Int, dctData: DctData) { + self.data = data + self.range = range + self.width = width + self.height = height + + var resultFrameMapping: [Int: FrameInfo] = [:] + var durationMapping: [Double] = [] + + for i in 0 ..< frameMapping.count { + let frame = frameMapping[i] + resultFrameMapping[i] = frame + durationMapping.append(frame.duration) + } + + self.frameMapping = resultFrameMapping + self.durationMapping = durationMapping + + self.currentDctData = dctData + } + + private func loadNextFrame() -> Bool { + var didLoop = false + let index: Int + if let currentFrame = self.currentFrame { + if currentFrame.index + 1 >= self.durationMapping.count { + index = 0 + self.compressedDataReader = nil + didLoop = true + } else { + index = currentFrame.index + 1 + } + } else { + index = 0 + self.compressedDataReader = nil + } + + if self.compressedDataReader == nil { + self.compressedDataReader = DecompressedData(compressedData: self.data, dataRange: self.range) + } + + guard let compressedDataReader = self.compressedDataReader else { + self.currentFrame = nil + return didLoop + } + + do { + let frameLength = Int(try compressedDataReader.readUInt32()) + + let frameType = Int(try compressedDataReader.readUInt32()) + + let dctCoefficients: DctCoefficientsYUVA420 + if let sharedDctCoefficients = self.sharedDctCoefficients, sharedDctCoefficients.yPlane.width == self.width, sharedDctCoefficients.yPlane.height == self.height, !"".isEmpty { + dctCoefficients = sharedDctCoefficients + } else { + dctCoefficients = DctCoefficientsYUVA420(width: self.width, height: self.height) + self.sharedDctCoefficients = dctCoefficients + } + + var frameOffset = 0 + for i in 0 ..< 4 { + let planeLength = Int(try compressedDataReader.readUInt32()) + if planeLength < 0 || planeLength > 20 * 1024 * 1024 { + throw ReadError.generic + } + + let plane: DctCoefficientPlane + switch i { + case 0: + plane = dctCoefficients.yPlane + case 1: + plane = dctCoefficients.uPlane + case 2: + plane = dctCoefficients.vPlane + case 3: + plane = dctCoefficients.aPlane + default: + throw ReadError.generic + } + + if planeLength != plane.data.count { + throw ReadError.generic + } + + if frameOffset + plane.data.count > frameLength { + throw ReadError.generic + } + + try plane.data.withUnsafeMutableBytes { bytes in + try compressedDataReader.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + frameOffset += plane.data.count + } + + let yuvaSurface: ImageYUVA420 + if let currentYUVASurface = self.currentYUVASurface { + yuvaSurface = currentYUVASurface + } else { + yuvaSurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) + } + + let currentCoefficients: DctCoefficientsYUVA420 + if let current = self.currentCoefficients { + currentCoefficients = current + } else { + currentCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) + self.currentCoefficients = currentCoefficients + } + + /*let deltaCoefficients: DctCoefficientsYUVA420 + if let current = self.deltaCoefficients { + deltaCoefficients = current + } else { + deltaCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) + self.deltaCoefficients = deltaCoefficients + }*/ + + switch frameType { + case 1: + dctCoefficients.idct8x8(dctData: self.currentDctData, target: yuvaSurface) + yuvaSurface.toCoefficients(target: currentCoefficients) + default: + dctCoefficients.idct4x4Add(dctData: self.currentDctData, target: currentCoefficients) + //currentCoefficients.add(other: deltaCoefficients) + + currentCoefficients.toYUVA420(target: yuvaSurface) + } + + self.currentFrame = CurrentFrame(index: index, duration: self.durationMapping[index], yuva: yuvaSurface) + } catch { + self.currentFrame = nil + self.compressedDataReader = nil + } + + return didLoop + } + + func reset() { + self.currentFrame = nil + } + + func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItem.AdvanceResult? { + var didLoop = false + switch advance { + case let .frames(count): + for _ in 0 ..< count { + if self.loadNextFrame() { + didLoop = true + } + } + case let .duration(duration): + var durationOverflow = duration + while true { + if let currentFrame = self.currentFrame { + currentFrame.remainingDuration -= durationOverflow + if currentFrame.remainingDuration <= 0.0 { + durationOverflow = -currentFrame.remainingDuration + if self.loadNextFrame() { + didLoop = true + } + } else { + break + } + } else { + if self.loadNextFrame() { + didLoop = true + } + break + } + } + } + + guard let currentFrame = self.currentFrame else { + return nil + } + + switch requestedFormat { + case .rgba: + let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32) + currentFrame.yuva.toARGB(target: currentSurface) + + return AnimationCacheItem.AdvanceResult( + frame: AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration), + didLoop: didLoop + ) + case .yuva: + return AnimationCacheItem.AdvanceResult( + frame: AnimationCacheItemFrame( + format: .yuva( + y: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.yPlane.data, + width: currentFrame.yuva.yPlane.width, + height: currentFrame.yuva.yPlane.height, + bytesPerRow: currentFrame.yuva.yPlane.bytesPerRow + ), + u: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.uPlane.data, + width: currentFrame.yuva.uPlane.width, + height: currentFrame.yuva.uPlane.height, + bytesPerRow: currentFrame.yuva.uPlane.bytesPerRow + ), + v: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.vPlane.data, + width: currentFrame.yuva.vPlane.width, + height: currentFrame.yuva.vPlane.height, + bytesPerRow: currentFrame.yuva.vPlane.bytesPerRow + ), + a: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.aPlane.data, + width: currentFrame.yuva.aPlane.width, + height: currentFrame.yuva.aPlane.height, + bytesPerRow: currentFrame.yuva.aPlane.bytesPerRow + ) + ), + duration: currentFrame.duration + ), + didLoop: didLoop + ) + } + } +} + +private func readData(data: Data, offset: Int, count: Int) -> Data { + var result = Data(count: count) + result.withUnsafeMutableBytes { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), count) + } + } + return result +} + +private func readUInt32(data: Data, offset: Int) -> UInt32 { + var value: UInt32 = 0 + withUnsafeMutableBytes(of: &value, { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) + } + }) + + return value +} + +private func readFloat32(data: Data, offset: Int) -> Float32 { + var value: Float32 = 0 + withUnsafeMutableBytes(of: &value, { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) + } + }) + + return value +} + +private func writeUInt32(data: inout Data, value: UInt32) { + var value: UInt32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + +private func writeFloat32(data: inout Data, value: Float32) { + var value: Float32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + +private final class CompressedFileWriter { + enum WriteError: Error { + case generic + } + + private let file: ManagedFile + private let stream: UnsafeMutablePointer + + private let tempOutputBufferSize: Int = 64 * 1024 + private let tempOutputBuffer: UnsafeMutablePointer + private let tempInputBufferCapacity: Int = 64 * 1024 + private let tempInputBuffer: UnsafeMutablePointer + private var tempInputBufferSize: Int = 0 + + private var didFail: Bool = false + + init?(file: ManagedFile) { + self.file = file + + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + guard compression_stream_init(self.stream, COMPRESSION_STREAM_ENCODE, algorithm) != COMPRESSION_STATUS_ERROR else { + self.stream.deallocate() + return nil + } + + self.tempOutputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempOutputBufferSize) + self.tempInputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempInputBufferCapacity) + } + + deinit { + compression_stream_destroy(self.stream) + self.stream.deallocate() + self.tempOutputBuffer.deallocate() + self.tempInputBuffer.deallocate() + } + + private func flushBuffer() throws { + if self.didFail { + throw WriteError.generic + } + + if self.tempInputBufferSize <= 0 { + return + } + + self.stream.pointee.src_ptr = UnsafePointer(self.tempInputBuffer) + self.stream.pointee.src_size = self.tempInputBufferSize + + while true { + self.stream.pointee.dst_ptr = self.tempOutputBuffer + self.stream.pointee.dst_size = self.tempOutputBufferSize + + let status = compression_stream_process(self.stream, 0) + if status == COMPRESSION_STATUS_ERROR { + self.didFail = true + throw WriteError.generic + } + + let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size + if writtenBytes > 0 { + let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) + } + + if status == COMPRESSION_STATUS_END { + break + } else { + if self.stream.pointee.src_size == 0 { + break + } + } + } + + self.tempInputBufferSize = 0 + } + + func write(bytes: UnsafePointer, count: Int) throws { + var writtenBytes = 0 + while writtenBytes < count { + let availableBytes = self.tempInputBufferCapacity - self.tempInputBufferSize + if availableBytes == 0 { + try flushBuffer() + } else { + let writeCount = min(availableBytes, count - writtenBytes) + + memcpy(self.tempInputBuffer.advanced(by: self.tempInputBufferSize), bytes.advanced(by: writtenBytes), writeCount) + self.tempInputBufferSize += writeCount + writtenBytes += writeCount + } + } + } + + func flush() throws { + if self.didFail { + throw WriteError.generic + } + + try self.flushBuffer() + + while true { + self.stream.pointee.dst_ptr = self.tempOutputBuffer + self.stream.pointee.dst_size = self.tempOutputBufferSize + + let status = compression_stream_process(self.stream, Int32(COMPRESSION_STREAM_FINALIZE.rawValue)) + if status == COMPRESSION_STATUS_ERROR { + self.didFail = true + throw WriteError.generic + } + + let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size + if writtenBytes > 0 { + let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) + } + + if status == COMPRESSION_STATUS_END { + break + } + } + } + + func writeUInt32(_ value: UInt32) throws { + var value: UInt32 = value + try withUnsafeBytes(of: &value, { bytes -> Void in + try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + } + + func writeFloat32(_ value: Float32) throws { + var value: Float32 = value + try withUnsafeBytes(of: &value, { bytes -> Void in + try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + } +} + +private final class DecompressedData { + enum ReadError: Error { + case didReadToEnd + } + + private let compressedData: Data + private let dataRange: Range + private let stream: UnsafeMutablePointer + private var isComplete = false + + init?(compressedData: Data, dataRange: Range) { + self.compressedData = compressedData + self.dataRange = dataRange + + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, algorithm) != COMPRESSION_STATUS_ERROR else { + self.stream.deallocate() + return nil + } + + self.compressedData.withUnsafeBytes { bytes in + self.stream.pointee.src_ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dataRange.lowerBound) + self.stream.pointee.src_size = dataRange.upperBound - dataRange.lowerBound + } + } + + deinit { + compression_stream_destroy(self.stream) + self.stream.deallocate() + } + + func read(bytes: UnsafeMutablePointer, count: Int) throws { + if self.isComplete { + throw ReadError.didReadToEnd + } + + self.stream.pointee.dst_ptr = bytes + self.stream.pointee.dst_size = count + + let status = compression_stream_process(self.stream, 0) + + if status == COMPRESSION_STATUS_ERROR { + self.isComplete = true + throw ReadError.didReadToEnd + } else if status == COMPRESSION_STATUS_END { + if self.stream.pointee.src_size == 0 { + self.isComplete = true + } + } + + if self.stream.pointee.dst_size != 0 { + throw ReadError.didReadToEnd + } + } + + func readUInt32() throws -> UInt32 { + var value: UInt32 = 0 + try withUnsafeMutableBytes(of: &value, { bytes -> Void in + try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + return value + } + + func readFloat32() throws -> Float32 { + var value: Float32 = 0 + try withUnsafeMutableBytes(of: &value, { bytes -> Void in + try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + return value + } +} + +private enum LoadItemError: Error { + case dataError +} + +private func loadItem(path: String) throws -> AnimationCacheItem { + guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { + throw LoadItemError.dataError + } + + var offset: Int = 0 + let dataLength = compressedData.count + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let formatVersion = readUInt32(data: compressedData, offset: offset) + offset += 4 + if formatVersion != 6 { + throw LoadItemError.dataError + } + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let width = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let height = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctLumaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctLumaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctLumaData = readData(data: compressedData, offset: offset, count: Int(dctLumaTableLength)) + offset += Int(dctLumaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctChromaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctChromaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctChromaData = readData(data: compressedData, offset: offset, count: Int(dctChromaTableLength)) + offset += Int(dctChromaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctDeltaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctDeltaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctDeltaData = readData(data: compressedData, offset: offset, count: Int(dctDeltaTableLength)) + offset += Int(dctDeltaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let contentLength = Int(readUInt32(data: compressedData, offset: offset)) + offset += 4 + + let compressedFrameDataRange = offset ..< (offset + contentLength) + offset += contentLength + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let frameCount = Int(readUInt32(data: compressedData, offset: offset)) + offset += 4 + + var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] + for _ in 0 ..< frameCount { + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let frameDuration = readFloat32(data: compressedData, offset: offset) + offset += 4 + + frameMapping.append(AnimationCacheItemAccessor.FrameInfo(duration: Double(frameDuration))) + } + + guard let dctData = DctData(lumaTable: dctLumaData, chromaTable: dctChromaData, deltaTable: dctDeltaData) else { + throw LoadItemError.dataError + } + + let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctData: dctData) + + return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in + return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat) + }, resetImpl: { + itemAccessor.reset() + }) +} + +private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { + guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else { + return nil + } + guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { + _ in + }) else { + return nil + } + + do { + for _ in 0 ..< higherResolutionItem.numFrames { + try writer.addYUV(with: { yuva in + guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { + return nil + } + switch frame.frame.format { + case .rgba: + return nil + case let .yuva(y, u, v, a): + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + } + + return frame.frame.duration + }, proposedWidth: width, proposedHeight: height, insertKeyframe: true) + } + + guard let result = try writer.finishInternal() else { + return nil + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return nil + } + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return nil + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + + guard let item = try? loadItem(path: itemPath) else { + return nil + } + return item + } catch { + return nil + } +} + +private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> Bool { + guard let animationItem = try? loadItem(path: animationItemPath) else { + return false + } + guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { _ in + }) else { + return false + } + + do { + for _ in 0 ..< min(1, animationItem.numFrames) { + guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else { + return false + } + switch frame.frame.format { + case .rgba: + return false + case let .yuva(y, u, v, a): + try writer.addYUV(with: { yuva in + assert(yuva.yPlane.bytesPerRow == y.bytesPerRow) + assert(yuva.uPlane.bytesPerRow == u.bytesPerRow) + assert(yuva.vPlane.bytesPerRow == v.bytesPerRow) + assert(yuva.aPlane.bytesPerRow == a.bytesPerRow) + + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + + return frame.frame.duration + }, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true) + } + } + + guard let result = try writer.finishInternal() else { + return false + } + + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return false + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + return true + } catch { + return false + } +} + +private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? { + var candidates: [(path: String, width: Int, height: Int)] = [] + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) { + for url in enumerator { + guard let url = url as? URL else { + continue + } + let fileName = url.lastPathComponent + if fileName.hasPrefix(baseName) { + let scanner = Scanner(string: fileName) + guard scanner.scanString(baseName) != nil else { + continue + } + guard let itemWidth = scanner.scanInt() else { + continue + } + guard scanner.scanString("x") != nil else { + continue + } + guard let itemHeight = scanner.scanInt() else { + continue + } + if !baseSuffix.isEmpty { + guard scanner.scanString(baseSuffix) != nil else { + continue + } + } + guard scanner.isAtEnd else { + continue + } + if itemWidth > width && itemHeight > height { + candidates.append((url.path, itemWidth, itemHeight)) + } + } + } + } + if !candidates.isEmpty { + candidates.sort(by: { $0.width < $1.width }) + return candidates[0].path + } + return nil +} + +public final class DCTAnimationCacheImpl: AnimationCache { + private final class Impl { + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + } + + private final class ItemContext { + let subscribers = Bag<(AnimationCacheItemResult) -> Void>() + let disposable = MetaDisposable() + + deinit { + self.disposable.dispose() + } + } + + private let queue: Queue + private let basePath: String + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + private let fetchQueues: [Queue] + private var nextFetchQueueIndex: Int = 0 + + private var itemContexts: [ItemKey: ItemContext] = [:] + + init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + self.queue = queue + + let fetchQueueCount: Int + if ProcessInfo.processInfo.processorCount > 2 { + fetchQueueCount = 3 + } else { + fetchQueueCount = 2 + } + + self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "DCTAnimationCacheImpl-Fetch\(i)", qos: .default) } + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + } + + deinit { + } + + func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemPath), let item = try? loadItem(path: itemPath) { + updateResult(AnimationCacheItemResult(item: item, isFinal: true)) + + return EmptyDisposable + } + let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height)) + + let itemContext: ItemContext + var beginFetch = false + if let current = self.itemContexts[key] { + itemContext = current + } else { + itemContext = ItemContext() + self.itemContexts[key] = itemContext + beginFetch = true + } + + let queue = self.queue + let index = itemContext.subscribers.add(updateResult) + + updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) + + if beginFetch { + let fetchQueueIndex = self.nextFetchQueueIndex + self.nextFetchQueueIndex += 1 + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + + strongSelf.itemContexts.removeValue(forKey: key) + + guard let result = result else { + return + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return + } + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + + let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) + + for f in itemContext.subscribers.copyItems() { + guard let item = try? loadItem(path: itemPath) else { + continue + } + f(AnimationCacheItemResult(item: item, isFinal: true)) + } + } + }) else { + return EmptyDisposable + } + + let fetchDisposable = MetaDisposable() + fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false))) + + itemContext.disposable.set(ActionDisposable { [weak writer] in + if let writer = writer { + writer.isCancelled = true + } + + fetchDisposable.dispose() + }) + } + + return ActionDisposable { [weak self, weak itemContext] in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + itemContext.subscribers.remove(index) + if itemContext.subscribers.isEmpty { + itemContext.disposable.dispose() + strongSelf.itemContexts.removeValue(forKey: key) + } + } + } + } + + static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { + let hashString = md5Hash(sourceId) + let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemFirstFramePath) { + if let item = try? loadItem(path: itemFirstFramePath) { + return item + } + } + + if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { + if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { + return adaptedItem + } + } + + return nil + } + + static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let hashString = md5Hash(sourceId) + let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = try? loadItem(path: itemFirstFramePath) { + completion(AnimationCacheItemResult(item: item, isFinal: true)) + return EmptyDisposable + } + + if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { + if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { + completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true)) + return EmptyDisposable + } + } + + if let fetch = fetch { + completion(AnimationCacheItemResult(item: nil, isFinal: false)) + + guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in + queue.async { + guard let result = result else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemFirstFramePath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + if let size = fileSize(itemFirstFramePath) { + updateStorageStats(itemFirstFramePath, size) + } + guard let item = try? loadItem(path: itemFirstFramePath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + + completion(AnimationCacheItemResult(item: item, isFinal: true)) + } + }) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return EmptyDisposable + } + + let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true)) + return fetchDisposable + } else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return EmptyDisposable + } + } + } + + private let queue: Queue + private let basePath: String + private let impl: QueueLocalObject + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + let queue = Queue() + self.queue = queue + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) + }) + } + + public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in + subscriber.putNext(result) + if result.isFinal { + subscriber.putCompletion() + } + })) + } + + return disposable + } + |> runOn(self.queue) + } + + public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { + return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile, updateStorageStats: self.updateStorageStats) + } + + public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let disposable = MetaDisposable() + + let basePath = self.basePath + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + queue.async { + disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats, fetch: fetch, completion: completion)) + } + + return disposable + } +} diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift similarity index 99% rename from submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift rename to submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift index 66c853510f..7dfaecfc0b 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import AnimationCache import ImageDCT import Accelerate diff --git a/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD new file mode 100644 index 0000000000..6c27e9c86b --- /dev/null +++ b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DCTMultiAnimationRendererImpl", + module_name = "DCTMultiAnimationRendererImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift new file mode 100644 index 0000000000..8ab7c6f9d0 --- /dev/null +++ b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift @@ -0,0 +1,1078 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AnimationCache +import MultiAnimationRenderer +import Accelerate +import IOSurface + +private final class LoadFrameGroupTask { + let task: () -> () -> Void + let queueAffinity: Int + + init(task: @escaping () -> () -> Void, queueAffinity: Int) { + self.task = task + self.queueAffinity = queueAffinity + } +} + +private var yuvToRgbConversion: vImage_YpCbCrToARGB = { + var info = vImage_YpCbCrToARGB() + var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 16, CbCr_bias: 128, YpRangeMax: 235, CbCrRangeMax: 240, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0) + vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0) + return info +}() + +private final class ItemAnimationContext { + fileprivate final class Frame { + let frame: AnimationCacheItemFrame + let duration: Double + + let contentsAsImage: UIImage? + let contentsAsCVPixelBuffer: CVPixelBuffer? + + let size: CGSize + + var remainingDuration: Double + + private var blurredRepresentationValue: UIImage? + + init?(frame: AnimationCacheItemFrame) { + self.frame = frame + self.duration = frame.duration + self.remainingDuration = frame.duration + + switch frame.format { + case let .rgba(data, width, height, bytesPerRow): + guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { + return nil + } + + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + + guard let image = context.generateImage() else { + return nil + } + + self.contentsAsImage = image + self.contentsAsCVPixelBuffer = nil + self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) + case let .yuva(y, u, v, a): + var pixelBuffer: CVPixelBuffer? = nil + let _ = CVPixelBufferCreate(kCFAllocatorDefault, y.width, y.height, kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar, [ + kCVPixelBufferIOSurfacePropertiesKey: NSDictionary() + ] as CFDictionary, &pixelBuffer) + guard let pixelBuffer else { + return nil + } + + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + } + guard let baseAddressY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { + return nil + } + guard let baseAddressCbCr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { + return nil + } + guard let baseAddressA = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2) else { + return nil + } + + let dstBufferY = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressY), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)) + let dstBufferCbCr = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressCbCr), height: vImagePixelCount(y.height / 2), width: vImagePixelCount(y.width / 2), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)) + let dstBufferA = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressA), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)) + + y.data.withUnsafeBytes { (yBytes: UnsafeRawBufferPointer) -> Void in + if dstBufferY.rowBytes == y.bytesPerRow { + memcpy(dstBufferY.data, yBytes.baseAddress!, yBytes.count) + } else { + for i in 0 ..< y.height { + memcpy(dstBufferY.data.advanced(by: dstBufferY.rowBytes * i), yBytes.baseAddress!.advanced(by: y.bytesPerRow * i), y.bytesPerRow) + } + } + } + + a.data.withUnsafeBytes { (aBytes: UnsafeRawBufferPointer) -> Void in + if dstBufferA.rowBytes == a.bytesPerRow { + memcpy(dstBufferA.data, aBytes.baseAddress!, aBytes.count) + } else { + for i in 0 ..< y.height { + memcpy(dstBufferA.data.advanced(by: dstBufferA.rowBytes * i), aBytes.baseAddress!.advanced(by: a.bytesPerRow * i), a.bytesPerRow) + } + } + } + + u.data.withUnsafeBytes { (uBytes: UnsafeRawBufferPointer) -> Void in + v.data.withUnsafeBytes { (vBytes: UnsafeRawBufferPointer) -> Void in + let sourceU = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: uBytes.baseAddress!), + height: vImagePixelCount(u.height), + width: vImagePixelCount(u.width), + rowBytes: u.bytesPerRow + ) + let sourceV = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: vBytes.baseAddress!), + height: vImagePixelCount(v.height), + width: vImagePixelCount(v.width), + rowBytes: v.bytesPerRow + ) + + withUnsafePointer(to: sourceU, { sourceU in + withUnsafePointer(to: sourceV, { sourceV in + var srcPlanarBuffers: [ + UnsafePointer? + ] = [sourceU, sourceV] + var destChannels: [UnsafeMutableRawPointer?] = [ + dstBufferCbCr.data.advanced(by: 1), + dstBufferCbCr.data + ] + + let channelCount = 2 + + vImageConvert_PlanarToChunky8( + &srcPlanarBuffers, + &destChannels, + UInt32(channelCount), + MemoryLayout.stride * channelCount, + vImagePixelCount(u.width), + vImagePixelCount(u.height), + dstBufferCbCr.rowBytes, + vImage_Flags(kvImageDoNotTile) + ) + }) + }) + } + } + + self.contentsAsImage = nil + self.contentsAsCVPixelBuffer = pixelBuffer + self.size = CGSize(width: CGFloat(y.width), height: CGFloat(y.height)) + } + } + + func blurredRepresentation(color: UIColor?) -> UIImage? { + if let blurredRepresentationValue = self.blurredRepresentationValue { + return blurredRepresentationValue + } + + switch frame.format { + case let .rgba(data, width, height, bytesPerRow): + let blurredWidth = 12 + let blurredHeight = 12 + guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: bytesPerRow) else { + return nil + } + + let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) + + data.withUnsafeBytes { bytes -> Void in + if let dataProvider = CGDataProvider(dataInfo: nil, data: bytes.baseAddress!, size: bytes.count, releaseData: { _, _, _ in }) { + let image = CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: bytesPerRow, + space: DeviceGraphicsContextSettings.shared.colorSpace, + bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, + provider: dataProvider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) + if let image = image { + context.withFlippedContext { c in + c.setFillColor((color ?? .white).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + c.draw(image, 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((color ?? .white).withMultipliedAlpha(0.6).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } + } + + self.blurredRepresentationValue = context.generateImage() + return self.blurredRepresentationValue + case let .yuva(y, u, v, a): + let blurredWidth = 12 + let blurredHeight = 12 + let size = CGSize(width: blurredWidth, height: blurredHeight) + + var sourceY = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: y.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(y.height), + width: vImagePixelCount(y.width), + rowBytes: y.bytesPerRow + ) + + var sourceU = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: u.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(u.height), + width: vImagePixelCount(u.width), + rowBytes: u.bytesPerRow + ) + + var sourceV = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: v.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(v.height), + width: vImagePixelCount(v.width), + rowBytes: v.bytesPerRow + ) + + var sourceA = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: a.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(a.height), + width: vImagePixelCount(a.width), + rowBytes: a.bytesPerRow + ) + + let scaledYData = malloc(blurredWidth * blurredHeight)! + defer { + free(scaledYData) + } + + let scaledUData = malloc(blurredWidth * blurredHeight / 4)! + defer { + free(scaledUData) + } + + let scaledVData = malloc(blurredWidth * blurredHeight / 4)! + defer { + free(scaledVData) + } + + let scaledAData = malloc(blurredWidth * blurredHeight)! + defer { + free(scaledAData) + } + + var scaledY = vImage_Buffer( + data: scaledYData, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: blurredWidth + ) + + var scaledU = vImage_Buffer( + data: scaledUData, + height: vImagePixelCount(blurredHeight / 2), + width: vImagePixelCount(blurredWidth / 2), + rowBytes: blurredWidth / 2 + ) + + var scaledV = vImage_Buffer( + data: scaledVData, + height: vImagePixelCount(blurredHeight / 2), + width: vImagePixelCount(blurredWidth / 2), + rowBytes: blurredWidth / 2 + ) + + var scaledA = vImage_Buffer( + data: scaledAData, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: blurredWidth + ) + + vImageScale_Planar8(&sourceY, &scaledY, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceU, &scaledU, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceV, &scaledV, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceA, &scaledA, nil, vImage_Flags(kvImageHighQualityResampling)) + + guard let context = DrawingContext(size: size, scale: 1.0, clear: true) else { + return nil + } + + var destinationBuffer = vImage_Buffer( + data: context.bytes, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: context.bytesPerRow + ) + + var result = kvImageNoError + + var permuteMap: [UInt8] = [1, 2, 3, 0] + result = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&scaledY, &scaledU, &scaledV, &destinationBuffer, &yuvToRgbConversion, &permuteMap, 255, vImage_Flags(kvImageDoNotTile)) + if result != kvImageNoError { + return nil + } + + result = vImageOverwriteChannels_ARGB8888(&scaledA, &destinationBuffer, &destinationBuffer, 1 << 0, vImage_Flags(kvImageDoNotTile)); + if result != kvImageNoError { + return nil + } + + 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((color ?? .white).withMultipliedAlpha(0.6).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } + + self.blurredRepresentationValue = context.generateImage() + return self.blurredRepresentationValue + } + } + } + + static let queue0 = Queue(name: "ItemAnimationContext-0", qos: .default) + static let queue1 = Queue(name: "ItemAnimationContext-1", qos: .default) + + private let useYuvA: Bool + + private let cache: AnimationCache + let queueAffinity: Int + private let stateUpdated: () -> Void + + private var disposable: Disposable? + private var displayLink: ConstantDisplayLinkAnimator? + private var item: Atomic? + private var itemPlaceholderAndFrameIndex: (UIImage, Int)? + + private var currentFrame: Frame? + private var loadingFrameTaskId: Int? + private var nextLoadingFrameTaskId: Int = 0 + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + let targets = Bag>() + + init(cache: AnimationCache, queueAffinity: Int, itemId: String, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, stateUpdated: @escaping () -> Void) { + self.cache = cache + self.queueAffinity = queueAffinity + self.useYuvA = useYuvA + self.stateUpdated = stateUpdated + + self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let item = result.item { + strongSelf.item = Atomic(value: item) + } + if let (placeholder, index) = strongSelf.itemPlaceholderAndFrameIndex { + strongSelf.itemPlaceholderAndFrameIndex = nil + strongSelf.setFrameIndex(index: index, placeholder: placeholder) + } + strongSelf.updateIsPlaying() + } + }) + } + + deinit { + self.disposable?.dispose() + self.displayLink?.invalidate() + } + + func setFrameIndex(index: Int, placeholder: UIImage) { + if let item = self.item { + let nextFrame = item.with { item -> AnimationCacheItemFrame? in + item.reset() + for i in 0 ... index { + let result = item.advance(advance: .frames(1), requestedFormat: .rgba) + if i == index { + return result?.frame + } + } + return nil + } + + self.loadingFrameTaskId = nil + + if let nextFrame = nextFrame, let currentFrame = Frame(frame: nextFrame) { + self.currentFrame = currentFrame + + for target in self.targets.copyItems() { + if let target = target.value { + if let image = currentFrame.contentsAsImage { + target.transitionToContents(image.cgImage!, didLoop: false) + } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: false) + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + } + } else { + for target in self.targets.copyItems() { + if let target = target.value { + target.transitionToContents(placeholder.cgImage!, didLoop: false) + } + } + + self.itemPlaceholderAndFrameIndex = (placeholder, index) + } + } + + func updateAddedTarget(target: MultiAnimationRenderTarget) { + if let currentFrame = self.currentFrame { + if let cgImage = currentFrame.contentsAsImage?.cgImage { + target.transitionToContents(cgImage, didLoop: false) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: false) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + + self.updateIsPlaying() + } + + func updateIsPlaying() { + var isPlaying = true + if self.item == nil { + isPlaying = false + } + + var shouldBeAnimating = false + for target in self.targets.copyItems() { + if let target = target.value { + if target.shouldBeAnimating { + shouldBeAnimating = true + break + } + } + } + if !shouldBeAnimating { + isPlaying = false + } + + self.isPlaying = isPlaying + } + + func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? { + return self.update(advanceTimestamp: advanceTimestamp) + } + + private func update(advanceTimestamp: Double) -> LoadFrameGroupTask? { + guard let item = self.item else { + return nil + } + + var frameAdvance: AnimationCacheItem.Advance? + if self.loadingFrameTaskId == nil { + if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { + let divisionFactor = advanceTimestamp / currentFrame.remainingDuration + let wholeFactor = round(divisionFactor) + if abs(wholeFactor - divisionFactor) < 0.005 { + currentFrame.remainingDuration = 0.0 + frameAdvance = .frames(Int(wholeFactor)) + } else { + currentFrame.remainingDuration -= advanceTimestamp + if currentFrame.remainingDuration <= 0.0 { + frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) + } + } + } else if self.currentFrame == nil { + frameAdvance = .frames(1) + } + } + + if let frameAdvance = frameAdvance, self.loadingFrameTaskId == nil { + let taskId = self.nextLoadingFrameTaskId + self.nextLoadingFrameTaskId += 1 + + self.loadingFrameTaskId = taskId + let useYuvA = self.useYuvA + + return LoadFrameGroupTask(task: { [weak self] in + let currentFrame: (frame: Frame, didLoop: Bool)? + do { + if let (frame, didLoop) = try item.tryWith({ item -> (AnimationCacheItemFrame, Bool)? in + let defaultFormat: AnimationCacheItemFrame.RequestedFormat + if useYuvA { + defaultFormat = .yuva(rowAlignment: 1) + } else { + defaultFormat = .rgba + } + + if let result = item.advance(advance: frameAdvance, requestedFormat: defaultFormat) { + return (result.frame, result.didLoop) + } else { + return nil + } + }), let mappedFrame = Frame(frame: frame) { + currentFrame = (mappedFrame, didLoop) + } else { + currentFrame = nil + } + } catch { + assertionFailure() + currentFrame = nil + } + + return { + guard let strongSelf = self else { + return + } + + if strongSelf.loadingFrameTaskId != taskId { + return + } + + strongSelf.loadingFrameTaskId = nil + + if let currentFrame = currentFrame { + strongSelf.currentFrame = currentFrame.frame + for target in strongSelf.targets.copyItems() { + if let target = target.value { + if let image = currentFrame.frame.contentsAsImage { + target.transitionToContents(image.cgImage!, didLoop: currentFrame.didLoop) + } else if let pixelBuffer = currentFrame.frame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: currentFrame.didLoop) + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.frame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + } + } + }, queueAffinity: self.queueAffinity) + } + + if let _ = self.currentFrame { + for target in self.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: false) + } + } + } + + return nil + } +} + +public final class DCTMultiAnimationRendererImpl: MultiAnimationRenderer { + private final class GroupContext { + private let firstFrameQueue: Queue + private let stateUpdated: () -> Void + + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + var uniqueId: Int + } + + private var itemContexts: [ItemKey: ItemAnimationContext] = [:] + private var nextQueueAffinity: Int = 0 + private var nextUniqueId: Int = 1 + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) { + self.firstFrameQueue = firstFrameQueue + self.stateUpdated = stateUpdated + } + + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { + var uniqueId = 0 + if unique { + uniqueId = self.nextUniqueId + self.nextUniqueId += 1 + } + + let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: uniqueId) + let itemContext: ItemAnimationContext + if let current = self.itemContexts[itemKey] { + itemContext = current + } else { + let queueAffinity = self.nextQueueAffinity + self.nextQueueAffinity += 1 + itemContext = ItemAnimationContext(cache: cache, queueAffinity: queueAffinity, itemId: itemId, size: size, useYuvA: useYuvA, fetch: fetch, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.itemContexts[itemKey] = itemContext + } + + let index = itemContext.targets.add(Weak(target)) + itemContext.updateAddedTarget(target: target) + + let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in + Queue.mainQueue().async { + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { + return + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemKey) + } + } + } + + let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in + guard let itemContext = itemContext else { + return + } + itemContext.updateIsPlaying() + } + + return ActionDisposable { [weak self, weak itemContext, weak target] in + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { + return + } + if let target = target { + target.deinitCallbacks.remove(deinitIndex) + target.updateStateCallbacks.remove(updateStateIndex) + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemKey) + } + }.strict() + } + + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { + guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { + return false + } + guard let loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) else { + return false + } + + if let image = loadedFrame.contentsAsImage { + target.contents = image.cgImage + } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { + target.contents = pixelBuffer + } + target.numFrames = item.numFrames + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + + return true + } else { + return false + } + } + + func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { + var hadIntermediateUpdate = false + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] item in + guard let item = item.item else { + let isFinal = item.isFinal + hadIntermediateUpdate = true + Queue.mainQueue().async { + completion(false, isFinal) + } + return + } + + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) + } else { + loadedFrame = nil + } + + Queue.mainQueue().async { + guard let target = target else { + completion(false, true) + return + } + target.numFrames = item.numFrames + if let loadedFrame = loadedFrame { + if let cgImage = loadedFrame.contentsAsImage?.cgImage { + if hadIntermediateUpdate { + target.transitionToContents(cgImage, didLoop: false) + } else { + target.contents = cgImage + } + } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { + if hadIntermediateUpdate { + target.transitionToContents(pixelBuffer, didLoop: false) + } else { + target.contents = pixelBuffer + } + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + + completion(true, true) + } else { + completion(false, true) + } + } + }).strict() + } + + func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { item in + guard let item = item.item else { + Queue.mainQueue().async { + completion(nil) + } + return + } + + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) + } else { + loadedFrame = nil + } + + Queue.mainQueue().async { + if let loadedFrame = loadedFrame { + if let cgImage = loadedFrame.contentsAsImage?.cgImage { + completion(cgImage) + } else { + completion(nil) + } + } else { + completion(nil) + } + } + }).strict() + } + + func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] { + itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) + } + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] { + var tasks: [LoadFrameGroupTask] = [] + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) { + tasks.append(task) + } + } + } + + return tasks + } + } + + public static let firstFrameQueue = Queue(name: "DCTMultiAnimationRenderer-FirstFrame", qos: .userInteractive) + + public var useYuvA: Bool = false + private var groupContext: GroupContext? + private var frameSkip: Int + private var displayTimer: Foundation.Timer? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + if self.displayTimer == nil { + final class TimerTarget: NSObject { + private let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + } + + @objc func timerEvent() { + self.f() + } + } + let frameInterval = Double(self.frameSkip) / 60.0 + let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animationTick(frameInterval: frameInterval) + }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) + self.displayTimer = displayTimer + RunLoop.main.add(displayTimer, forMode: .common) + } + } else { + if let displayTimer = self.displayTimer { + self.displayTimer = nil + displayTimer.invalidate() + } + } + } + } + } + + public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } + } + + public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, unique: unique, size: size, useYuvA: self.useYuvA, fetch: fetch) + + return ActionDisposable { + disposable.dispose() + }.strict() + } + + public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) + } + + public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() + } + + public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() + } + + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + if let groupContext = self.groupContext { + groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) + } + } + + private func updateIsPlaying() { + var isPlaying = false + if let groupContext = self.groupContext { + if groupContext.isPlaying { + isPlaying = true + } + } + + self.isPlaying = isPlaying + } + + private func animationTick(frameInterval: Double) { + let secondsPerFrame = frameInterval + + var tasks: [LoadFrameGroupTask] = [] + if let groupContext = self.groupContext { + if groupContext.isPlaying { + tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame)) + } + } + + if !tasks.isEmpty { + let tasks0 = tasks.filter { $0.queueAffinity % 2 == 0 } + let tasks1 = tasks.filter { $0.queueAffinity % 2 == 1 } + let allTasks = [tasks0, tasks1] + + let taskCompletions = Atomic<[Int: [() -> Void]]>(value: [:]) + let queues: [Queue] = [ItemAnimationContext.queue0, ItemAnimationContext.queue1] + + for i in 0 ..< 2 { + let partTasks = allTasks[i] + let id = i + queues[i].async { + var completions: [() -> Void] = [] + for task in partTasks { + let complete = task.task() + completions.append(complete) + } + + var complete = false + let _ = taskCompletions.modify { current in + var current = current + current[id] = completions + if current.count == 2 { + complete = true + } + return current + } + + if complete { + Queue.mainQueue().async { + let allCompletions = taskCompletions.with { $0 } + for (_, fs) in allCompletions { + for f in fs { + f() + } + } + } + } + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD index 294e001b6f..cdfd5f6774 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/TelegramUIPreferences", "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 344e3fc428..b4ac019707 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -13,6 +13,7 @@ import AnimationCache import LottieAnimationCache import VideoAnimationCache import MultiAnimationRenderer +import DCTMultiAnimationRendererImpl import ShimmerEffect import TextFormat import TelegramUIPreferences @@ -817,7 +818,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { let isThumbnailCancelled = Atomic(value: false) self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { - MultiAnimationRendererImpl.firstFrameQueue.async { + DCTMultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 0ca5c0ae13..1c0a044379 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", "//submodules/TelegramUI/Components/LottieComponent", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift index 7de681f13f..114955ef56 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift @@ -8,6 +8,7 @@ import Postbox import SwiftSignalKit import MultiAnimationRenderer import AnimationCache +import DCTMultiAnimationRendererImpl import AccountContext import TelegramUIPreferences import GenerateStickerPlaceholderImage @@ -277,7 +278,7 @@ public final class InlineFileIconLayer: MultiAnimationRenderTarget { size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { - MultiAnimationRendererImpl.firstFrameQueue.async { + DCTMultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD index 4f5a66267d..e5a853e167 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD @@ -1,44 +1,4 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -load( - "@build_bazel_rules_apple//apple:resources.bzl", - "apple_resource_bundle", - "apple_resource_group", -) -load("//build-system/bazel-utils:plist_fragment.bzl", - "plist_fragment", -) - -filegroup( - name = "MultiAnimationRendererMetalResources", - srcs = glob([ - "Resources/**/*.metal", - ]), - visibility = ["//visibility:public"], -) - -plist_fragment( - name = "WallpaperBackgroundNodeBundleInfoPlist", - extension = "plist", - template = - """ - CFBundleIdentifier - org.telegram.MultiAnimationRenderer - CFBundleDevelopmentRegion - en - CFBundleName - MultiAnimationRenderer - """ -) - -apple_resource_bundle( - name = "MultiAnimationRendererBundle", - infoplists = [ - ":WallpaperBackgroundNodeBundleInfoPlist", - ], - resources = [ - ":MultiAnimationRendererMetalResources", - ], -) swift_library( name = "MultiAnimationRenderer", @@ -46,9 +6,6 @@ swift_library( srcs = glob([ "Sources/**/*.swift", ]), - data = [ - ":MultiAnimationRendererBundle", - ], copts = [ "-warnings-as-errors", ], diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal b/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal deleted file mode 100644 index 8343f753f0..0000000000 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal +++ /dev/null @@ -1,46 +0,0 @@ -#include -using namespace metal; - -typedef struct { - packed_float2 position; - packed_float2 texCoord; -} Vertex; - -typedef struct { - float4 position[[position]]; - float2 texCoord; -} Varyings; - -vertex Varyings multiAnimationVertex( - unsigned int vid[[vertex_id]], - constant Vertex *verticies[[buffer(0)]], - constant uint2 &resolution[[buffer(1)]], - constant uint2 &slotSize[[buffer(2)]], - constant uint2 &slotPosition[[buffer(3)]] -) { - Varyings out; - constant Vertex &v = verticies[vid]; - - out.position = float4(float2(v.position), 0.0, 1.0); - out.texCoord = v.texCoord; - - return out; -} - -fragment half4 multiAnimationFragment( - Varyings in[[stage_in]], - texture2d textureY[[texture(0)]], - texture2d textureU[[texture(1)]], - texture2d textureV[[texture(2)]], - texture2d textureA[[texture(3)]] -) { - constexpr sampler s(address::clamp_to_edge, filter::linear); - - half y = textureY.sample(s, in.texCoord).r; - half u = textureU.sample(s, in.texCoord).r - 0.5; - half v = textureV.sample(s, in.texCoord).r - 0.5; - half a = textureA.sample(s, in.texCoord).r; - - half4 out = half4(1.5748 * v + y, -0.1873 * v + y, 1.8556 * u + y, a); - return half4(out.b, out.g, out.r, out.a); -} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift deleted file mode 100644 index 8b13789179..0000000000 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index e0671fd539..6bf0029d65 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -3,8 +3,6 @@ import UIKit import SwiftSignalKit import Display import AnimationCache -import Accelerate -import IOSurface public protocol MultiAnimationRenderer: AnyObject { func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable @@ -19,10 +17,10 @@ private var nextRenderTargetId: Int64 = 1 open class MultiAnimationRenderTarget: SimpleLayer { public let id: Int64 public var numFrames: Int? - - let deinitCallbacks = Bag<() -> Void>() - let updateStateCallbacks = Bag<() -> Void>() - + + public let deinitCallbacks = Bag<() -> Void>() + public let updateStateCallbacks = Bag<() -> Void>() + public final var shouldBeAnimating: Bool = false { didSet { if self.shouldBeAnimating != oldValue { @@ -32,7 +30,7 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } } - + public var blurredRepresentationBackgroundColor: UIColor? public var blurredRepresentationTarget: CALayer? { didSet { @@ -43,1109 +41,39 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } } - + public override init() { assert(Thread.isMainThread) - + self.id = nextRenderTargetId nextRenderTargetId += 1 - + super.init() } - + public override init(layer: Any) { guard let layer = layer as? MultiAnimationRenderTarget else { preconditionFailure() } - + self.id = layer.id - + super.init(layer: layer) } - + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { for f in self.deinitCallbacks.copyItems() { f() } } - + open func updateDisplayPlaceholder(displayPlaceholder: Bool) { } - + open func transitionToContents(_ contents: AnyObject, didLoop: Bool) { } } - -private final class LoadFrameGroupTask { - let task: () -> () -> Void - let queueAffinity: Int - - init(task: @escaping () -> () -> Void, queueAffinity: Int) { - self.task = task - self.queueAffinity = queueAffinity - } -} - -private var yuvToRgbConversion: vImage_YpCbCrToARGB = { - var info = vImage_YpCbCrToARGB() - var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 16, CbCr_bias: 128, YpRangeMax: 235, CbCrRangeMax: 240, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0) - vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0) - return info -}() - -private final class ItemAnimationContext { - fileprivate final class Frame { - let frame: AnimationCacheItemFrame - let duration: Double - - let contentsAsImage: UIImage? - let contentsAsCVPixelBuffer: CVPixelBuffer? - - let size: CGSize - - var remainingDuration: Double - - private var blurredRepresentationValue: UIImage? - - init?(frame: AnimationCacheItemFrame) { - self.frame = frame - self.duration = frame.duration - self.remainingDuration = frame.duration - - switch frame.format { - case let .rgba(data, width, height, bytesPerRow): - guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { - return nil - } - - data.withUnsafeBytes { bytes -> Void in - memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) - } - - guard let image = context.generateImage() else { - return nil - } - - self.contentsAsImage = image - self.contentsAsCVPixelBuffer = nil - self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) - case let .yuva(y, u, v, a): - var pixelBuffer: CVPixelBuffer? = nil - let _ = CVPixelBufferCreate(kCFAllocatorDefault, y.width, y.height, kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar, [ - kCVPixelBufferIOSurfacePropertiesKey: NSDictionary() - ] as CFDictionary, &pixelBuffer) - guard let pixelBuffer else { - return nil - } - - CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - defer { - CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - } - guard let baseAddressY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { - return nil - } - guard let baseAddressCbCr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { - return nil - } - guard let baseAddressA = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2) else { - return nil - } - - let dstBufferY = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressY), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)) - let dstBufferCbCr = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressCbCr), height: vImagePixelCount(y.height / 2), width: vImagePixelCount(y.width / 2), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)) - let dstBufferA = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressA), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)) - - y.data.withUnsafeBytes { (yBytes: UnsafeRawBufferPointer) -> Void in - if dstBufferY.rowBytes == y.bytesPerRow { - memcpy(dstBufferY.data, yBytes.baseAddress!, yBytes.count) - } else { - for i in 0 ..< y.height { - memcpy(dstBufferY.data.advanced(by: dstBufferY.rowBytes * i), yBytes.baseAddress!.advanced(by: y.bytesPerRow * i), y.bytesPerRow) - } - } - } - - a.data.withUnsafeBytes { (aBytes: UnsafeRawBufferPointer) -> Void in - if dstBufferA.rowBytes == a.bytesPerRow { - memcpy(dstBufferA.data, aBytes.baseAddress!, aBytes.count) - } else { - for i in 0 ..< y.height { - memcpy(dstBufferA.data.advanced(by: dstBufferA.rowBytes * i), aBytes.baseAddress!.advanced(by: a.bytesPerRow * i), a.bytesPerRow) - } - } - } - - u.data.withUnsafeBytes { (uBytes: UnsafeRawBufferPointer) -> Void in - v.data.withUnsafeBytes { (vBytes: UnsafeRawBufferPointer) -> Void in - let sourceU = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: uBytes.baseAddress!), - height: vImagePixelCount(u.height), - width: vImagePixelCount(u.width), - rowBytes: u.bytesPerRow - ) - let sourceV = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: vBytes.baseAddress!), - height: vImagePixelCount(v.height), - width: vImagePixelCount(v.width), - rowBytes: v.bytesPerRow - ) - - withUnsafePointer(to: sourceU, { sourceU in - withUnsafePointer(to: sourceV, { sourceV in - var srcPlanarBuffers: [ - UnsafePointer? - ] = [sourceU, sourceV] - var destChannels: [UnsafeMutableRawPointer?] = [ - dstBufferCbCr.data.advanced(by: 1), - dstBufferCbCr.data - ] - - let channelCount = 2 - - vImageConvert_PlanarToChunky8( - &srcPlanarBuffers, - &destChannels, - UInt32(channelCount), - MemoryLayout.stride * channelCount, - vImagePixelCount(u.width), - vImagePixelCount(u.height), - dstBufferCbCr.rowBytes, - vImage_Flags(kvImageDoNotTile) - ) - }) - }) - } - } - - self.contentsAsImage = nil - self.contentsAsCVPixelBuffer = pixelBuffer - self.size = CGSize(width: CGFloat(y.width), height: CGFloat(y.height)) - } - } - - func blurredRepresentation(color: UIColor?) -> UIImage? { - if let blurredRepresentationValue = self.blurredRepresentationValue { - return blurredRepresentationValue - } - - switch frame.format { - case let .rgba(data, width, height, bytesPerRow): - let blurredWidth = 12 - let blurredHeight = 12 - guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: bytesPerRow) else { - return nil - } - - let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) - - data.withUnsafeBytes { bytes -> Void in - if let dataProvider = CGDataProvider(dataInfo: nil, data: bytes.baseAddress!, size: bytes.count, releaseData: { _, _, _ in }) { - let image = CGImage( - width: width, - height: height, - bitsPerComponent: 8, - bitsPerPixel: 32, - bytesPerRow: bytesPerRow, - space: DeviceGraphicsContextSettings.shared.colorSpace, - bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, - provider: dataProvider, - decode: nil, - shouldInterpolate: true, - intent: .defaultIntent - ) - if let image = image { - context.withFlippedContext { c in - c.setFillColor((color ?? .white).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - c.draw(image, 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((color ?? .white).withMultipliedAlpha(0.6).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - } - } - - self.blurredRepresentationValue = context.generateImage() - return self.blurredRepresentationValue - case let .yuva(y, u, v, a): - let blurredWidth = 12 - let blurredHeight = 12 - let size = CGSize(width: blurredWidth, height: blurredHeight) - - var sourceY = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: y.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(y.height), - width: vImagePixelCount(y.width), - rowBytes: y.bytesPerRow - ) - - var sourceU = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: u.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(u.height), - width: vImagePixelCount(u.width), - rowBytes: u.bytesPerRow - ) - - var sourceV = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: v.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(v.height), - width: vImagePixelCount(v.width), - rowBytes: v.bytesPerRow - ) - - var sourceA = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: a.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(a.height), - width: vImagePixelCount(a.width), - rowBytes: a.bytesPerRow - ) - - let scaledYData = malloc(blurredWidth * blurredHeight)! - defer { - free(scaledYData) - } - - let scaledUData = malloc(blurredWidth * blurredHeight / 4)! - defer { - free(scaledUData) - } - - let scaledVData = malloc(blurredWidth * blurredHeight / 4)! - defer { - free(scaledVData) - } - - let scaledAData = malloc(blurredWidth * blurredHeight)! - defer { - free(scaledAData) - } - - var scaledY = vImage_Buffer( - data: scaledYData, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: blurredWidth - ) - - var scaledU = vImage_Buffer( - data: scaledUData, - height: vImagePixelCount(blurredHeight / 2), - width: vImagePixelCount(blurredWidth / 2), - rowBytes: blurredWidth / 2 - ) - - var scaledV = vImage_Buffer( - data: scaledVData, - height: vImagePixelCount(blurredHeight / 2), - width: vImagePixelCount(blurredWidth / 2), - rowBytes: blurredWidth / 2 - ) - - var scaledA = vImage_Buffer( - data: scaledAData, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: blurredWidth - ) - - vImageScale_Planar8(&sourceY, &scaledY, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceU, &scaledU, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceV, &scaledV, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceA, &scaledA, nil, vImage_Flags(kvImageHighQualityResampling)) - - guard let context = DrawingContext(size: size, scale: 1.0, clear: true) else { - return nil - } - - var destinationBuffer = vImage_Buffer( - data: context.bytes, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: context.bytesPerRow - ) - - var result = kvImageNoError - - var permuteMap: [UInt8] = [1, 2, 3, 0] - result = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&scaledY, &scaledU, &scaledV, &destinationBuffer, &yuvToRgbConversion, &permuteMap, 255, vImage_Flags(kvImageDoNotTile)) - if result != kvImageNoError { - return nil - } - - result = vImageOverwriteChannels_ARGB8888(&scaledA, &destinationBuffer, &destinationBuffer, 1 << 0, vImage_Flags(kvImageDoNotTile)); - if result != kvImageNoError { - return nil - } - - 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((color ?? .white).withMultipliedAlpha(0.6).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - } - - self.blurredRepresentationValue = context.generateImage() - return self.blurredRepresentationValue - } - } - } - - static let queue0 = Queue(name: "ItemAnimationContext-0", qos: .default) - static let queue1 = Queue(name: "ItemAnimationContext-1", qos: .default) - - private let useYuvA: Bool - - private let cache: AnimationCache - let queueAffinity: Int - private let stateUpdated: () -> Void - - private var disposable: Disposable? - private var displayLink: ConstantDisplayLinkAnimator? - private var item: Atomic? - private var itemPlaceholderAndFrameIndex: (UIImage, Int)? - - private var currentFrame: Frame? - private var loadingFrameTaskId: Int? - private var nextLoadingFrameTaskId: Int = 0 - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - let targets = Bag>() - - init(cache: AnimationCache, queueAffinity: Int, itemId: String, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, stateUpdated: @escaping () -> Void) { - self.cache = cache - self.queueAffinity = queueAffinity - self.useYuvA = useYuvA - self.stateUpdated = stateUpdated - - self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - if let item = result.item { - strongSelf.item = Atomic(value: item) - } - if let (placeholder, index) = strongSelf.itemPlaceholderAndFrameIndex { - strongSelf.itemPlaceholderAndFrameIndex = nil - strongSelf.setFrameIndex(index: index, placeholder: placeholder) - } - strongSelf.updateIsPlaying() - } - }) - } - - deinit { - self.disposable?.dispose() - self.displayLink?.invalidate() - } - - func setFrameIndex(index: Int, placeholder: UIImage) { - if let item = self.item { - let nextFrame = item.with { item -> AnimationCacheItemFrame? in - item.reset() - for i in 0 ... index { - let result = item.advance(advance: .frames(1), requestedFormat: .rgba) - if i == index { - return result?.frame - } - } - return nil - } - - self.loadingFrameTaskId = nil - - if let nextFrame = nextFrame, let currentFrame = Frame(frame: nextFrame) { - self.currentFrame = currentFrame - - for target in self.targets.copyItems() { - if let target = target.value { - if let image = currentFrame.contentsAsImage { - target.transitionToContents(image.cgImage!, didLoop: false) - } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: false) - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - } - } else { - for target in self.targets.copyItems() { - if let target = target.value { - target.transitionToContents(placeholder.cgImage!, didLoop: false) - } - } - - self.itemPlaceholderAndFrameIndex = (placeholder, index) - } - } - - func updateAddedTarget(target: MultiAnimationRenderTarget) { - if let currentFrame = self.currentFrame { - if let cgImage = currentFrame.contentsAsImage?.cgImage { - target.transitionToContents(cgImage, didLoop: false) - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: false) - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - - self.updateIsPlaying() - } - - func updateIsPlaying() { - var isPlaying = true - if self.item == nil { - isPlaying = false - } - - var shouldBeAnimating = false - for target in self.targets.copyItems() { - if let target = target.value { - if target.shouldBeAnimating { - shouldBeAnimating = true - break - } - } - } - if !shouldBeAnimating { - isPlaying = false - } - - self.isPlaying = isPlaying - } - - func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? { - return self.update(advanceTimestamp: advanceTimestamp) - } - - private func update(advanceTimestamp: Double) -> LoadFrameGroupTask? { - guard let item = self.item else { - return nil - } - - var frameAdvance: AnimationCacheItem.Advance? - if self.loadingFrameTaskId == nil { - if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { - let divisionFactor = advanceTimestamp / currentFrame.remainingDuration - let wholeFactor = round(divisionFactor) - if abs(wholeFactor - divisionFactor) < 0.005 { - currentFrame.remainingDuration = 0.0 - frameAdvance = .frames(Int(wholeFactor)) - } else { - currentFrame.remainingDuration -= advanceTimestamp - if currentFrame.remainingDuration <= 0.0 { - frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) - } - } - } else if self.currentFrame == nil { - frameAdvance = .frames(1) - } - } - - if let frameAdvance = frameAdvance, self.loadingFrameTaskId == nil { - let taskId = self.nextLoadingFrameTaskId - self.nextLoadingFrameTaskId += 1 - - self.loadingFrameTaskId = taskId - let useYuvA = self.useYuvA - - return LoadFrameGroupTask(task: { [weak self] in - let currentFrame: (frame: Frame, didLoop: Bool)? - do { - if let (frame, didLoop) = try item.tryWith({ item -> (AnimationCacheItemFrame, Bool)? in - let defaultFormat: AnimationCacheItemFrame.RequestedFormat - if useYuvA { - defaultFormat = .yuva(rowAlignment: 1) - } else { - defaultFormat = .rgba - } - - if let result = item.advance(advance: frameAdvance, requestedFormat: defaultFormat) { - return (result.frame, result.didLoop) - } else { - return nil - } - }), let mappedFrame = Frame(frame: frame) { - currentFrame = (mappedFrame, didLoop) - } else { - currentFrame = nil - } - } catch { - assertionFailure() - currentFrame = nil - } - - return { - guard let strongSelf = self else { - return - } - - if strongSelf.loadingFrameTaskId != taskId { - return - } - - strongSelf.loadingFrameTaskId = nil - - if let currentFrame = currentFrame { - strongSelf.currentFrame = currentFrame.frame - for target in strongSelf.targets.copyItems() { - if let target = target.value { - if let image = currentFrame.frame.contentsAsImage { - target.transitionToContents(image.cgImage!, didLoop: currentFrame.didLoop) - } else if let pixelBuffer = currentFrame.frame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: currentFrame.didLoop) - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.frame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - } - } - }, queueAffinity: self.queueAffinity) - } - - if let _ = self.currentFrame { - for target in self.targets.copyItems() { - if let target = target.value { - target.updateDisplayPlaceholder(displayPlaceholder: false) - } - } - } - - return nil - } -} - -public final class MultiAnimationRendererImpl: MultiAnimationRenderer { - private final class GroupContext { - private let firstFrameQueue: Queue - private let stateUpdated: () -> Void - - private struct ItemKey: Hashable { - var id: String - var width: Int - var height: Int - var uniqueId: Int - } - - private var itemContexts: [ItemKey: ItemAnimationContext] = [:] - private var nextQueueAffinity: Int = 0 - private var nextUniqueId: Int = 1 - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) { - self.firstFrameQueue = firstFrameQueue - self.stateUpdated = stateUpdated - } - - func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { - var uniqueId = 0 - if unique { - uniqueId = self.nextUniqueId - self.nextUniqueId += 1 - } - - let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: uniqueId) - let itemContext: ItemAnimationContext - if let current = self.itemContexts[itemKey] { - itemContext = current - } else { - let queueAffinity = self.nextQueueAffinity - self.nextQueueAffinity += 1 - itemContext = ItemAnimationContext(cache: cache, queueAffinity: queueAffinity, itemId: itemId, size: size, useYuvA: useYuvA, fetch: fetch, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.itemContexts[itemKey] = itemContext - } - - let index = itemContext.targets.add(Weak(target)) - itemContext.updateAddedTarget(target: target) - - let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in - Queue.mainQueue().async { - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { - return - } - itemContext.targets.remove(index) - if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemKey) - } - } - } - - let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in - guard let itemContext = itemContext else { - return - } - itemContext.updateIsPlaying() - } - - return ActionDisposable { [weak self, weak itemContext, weak target] in - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { - return - } - if let target = target { - target.deinitCallbacks.remove(deinitIndex) - target.updateStateCallbacks.remove(updateStateIndex) - } - itemContext.targets.remove(index) - if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemKey) - } - }.strict() - } - - func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { - if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { - guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { - return false - } - guard let loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) else { - return false - } - - if let image = loadedFrame.contentsAsImage { - target.contents = image.cgImage - } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { - target.contents = pixelBuffer - } - target.numFrames = item.numFrames - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - - return true - } else { - return false - } - } - - func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { - var hadIntermediateUpdate = false - return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] item in - guard let item = item.item else { - let isFinal = item.isFinal - hadIntermediateUpdate = true - Queue.mainQueue().async { - completion(false, isFinal) - } - return - } - - let loadedFrame: ItemAnimationContext.Frame? - if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { - loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) - } else { - loadedFrame = nil - } - - Queue.mainQueue().async { - guard let target = target else { - completion(false, true) - return - } - target.numFrames = item.numFrames - if let loadedFrame = loadedFrame { - if let cgImage = loadedFrame.contentsAsImage?.cgImage { - if hadIntermediateUpdate { - target.transitionToContents(cgImage, didLoop: false) - } else { - target.contents = cgImage - } - } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { - if hadIntermediateUpdate { - target.transitionToContents(pixelBuffer, didLoop: false) - } else { - target.contents = pixelBuffer - } - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - - completion(true, true) - } else { - completion(false, true) - } - } - }).strict() - } - - func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { - return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { item in - guard let item = item.item else { - Queue.mainQueue().async { - completion(nil) - } - return - } - - let loadedFrame: ItemAnimationContext.Frame? - if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { - loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) - } else { - loadedFrame = nil - } - - Queue.mainQueue().async { - if let loadedFrame = loadedFrame { - if let cgImage = loadedFrame.contentsAsImage?.cgImage { - completion(cgImage) - } else { - completion(nil) - } - } else { - completion(nil) - } - } - }).strict() - } - - func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { - if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] { - itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) - } - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] { - var tasks: [LoadFrameGroupTask] = [] - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) { - tasks.append(task) - } - } - } - - return tasks - } - } - - public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive) - - public var useYuvA: Bool = false - private var groupContext: GroupContext? - private var frameSkip: Int - private var displayTimer: Foundation.Timer? - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - if self.isPlaying { - if self.displayTimer == nil { - final class TimerTarget: NSObject { - private let f: () -> Void - - init(_ f: @escaping () -> Void) { - self.f = f - } - - @objc func timerEvent() { - self.f() - } - } - let frameInterval = Double(self.frameSkip) / 60.0 - let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.animationTick(frameInterval: frameInterval) - }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) - self.displayTimer = displayTimer - RunLoop.main.add(displayTimer, forMode: .common) - } - } else { - if let displayTimer = self.displayTimer { - self.displayTimer = nil - displayTimer.invalidate() - } - } - } - } - } - - public init() { - if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { - self.frameSkip = 1 - } else { - self.frameSkip = 2 - } - } - - public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, unique: unique, size: size, useYuvA: self.useYuvA, fetch: fetch) - - return ActionDisposable { - disposable.dispose() - }.strict() - } - - public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) - } - - public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() - } - - public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() - } - - public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { - if let groupContext = self.groupContext { - groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) - } - } - - private func updateIsPlaying() { - var isPlaying = false - if let groupContext = self.groupContext { - if groupContext.isPlaying { - isPlaying = true - } - } - - self.isPlaying = isPlaying - } - - private func animationTick(frameInterval: Double) { - let secondsPerFrame = frameInterval - - var tasks: [LoadFrameGroupTask] = [] - if let groupContext = self.groupContext { - if groupContext.isPlaying { - tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame)) - } - } - - if !tasks.isEmpty { - let tasks0 = tasks.filter { $0.queueAffinity % 2 == 0 } - let tasks1 = tasks.filter { $0.queueAffinity % 2 == 1 } - let allTasks = [tasks0, tasks1] - - let taskCompletions = Atomic<[Int: [() -> Void]]>(value: [:]) - let queues: [Queue] = [ItemAnimationContext.queue0, ItemAnimationContext.queue1] - - for i in 0 ..< 2 { - let partTasks = allTasks[i] - let id = i - queues[i].async { - var completions: [() -> Void] = [] - for task in partTasks { - let complete = task.task() - completions.append(complete) - } - - var complete = false - let _ = taskCompletions.modify { current in - var current = current - current[id] = completions - if current.count == 2 { - complete = true - } - return current - } - - if complete { - Queue.mainQueue().async { - let allCompletions = taskCompletions.with { $0 } - for (_, fs) in allCompletions { - for f in fs { - f() - } - } - } - } - } - } - } - } -} diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD index becdf07725..47e6c76e66 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD +++ b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD @@ -36,6 +36,8 @@ swift_library( "//submodules/TelegramUI/Components/TelegramUIDeclareEncodables", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods", "//submodules/TelegramUI/Components/PeerSelectionController", "//submodules/TelegramUI/Components/ContextMenuScreen", diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift index 9d236de495..c3b242c393 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift @@ -27,6 +27,8 @@ import ManagedFile import TelegramUIDeclareEncodables import AnimationCache import MultiAnimationRenderer +import DCTAnimationCacheImpl +import DCTMultiAnimationRendererImpl import TelegramUIDeclareEncodables import TelegramAccountAuxiliaryMethods import PeerSelectionController @@ -105,14 +107,14 @@ private final class ShareControllerAccountContextExtension: ShareControllerAccou self.stateManager = stateManager self.engineData = TelegramEngine.EngineData(accountPeerId: stateManager.accountPeerId, postbox: stateManager.postbox) let cacheStorageBox = stateManager.postbox.mediaBox.cacheStorageBox - self.animationCache = AnimationCacheImpl(basePath: stateManager.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + self.animationCache = DCTAnimationCacheImpl(basePath: stateManager.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }, updateStorageStats: { path, size in if let pathData = path.data(using: .utf8) { cacheStorageBox.update(id: pathData, size: size) } }) - self.animationRenderer = MultiAnimationRendererImpl() + self.animationRenderer = DCTMultiAnimationRendererImpl() self.contentSettings = contentSettings self.appConfiguration = appConfiguration } diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index eca7ccd52f..419d1a1e27 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -20,6 +20,8 @@ import FetchManagerImpl import InAppPurchaseManager import AnimationCache import MultiAnimationRenderer +import DCTAnimationCacheImpl +import DCTMultiAnimationRendererImpl import AppBundle import DirectMediaImageCache @@ -317,15 +319,15 @@ public final class AccountContextImpl: AccountContext { self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl() let cacheStorageBox = self.account.postbox.mediaBox.cacheStorageBox - self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + self.animationCache = DCTAnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }, updateStorageStats: { path, size in if let pathData = path.data(using: .utf8) { cacheStorageBox.update(id: pathData, size: size) } }) - self.animationRenderer = MultiAnimationRendererImpl() - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = sharedContext.immediateExperimentalUISettings.compressedEmojiCache + self.animationRenderer = DCTMultiAnimationRendererImpl() + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = sharedContext.immediateExperimentalUISettings.compressedEmojiCache let updatedLimitsConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration]) |> map { preferences -> LimitsConfiguration in @@ -496,7 +498,7 @@ public final class AccountContextImpl: AccountContext { guard let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) else { return } - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = settings.compressedEmojiCache + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = settings.compressedEmojiCache }) }