2023-04-11 23:11:54 +04:00

1691 lines
65 KiB
Swift

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, .totalFileAllocatedSizeKey]))) {
if values.isRegularFile ?? false {
if let fileSize = values.totalFileAllocatedSize {
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
self.height = height
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
}
}
public final class AnimationCacheItem {
public enum Advance {
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()
}
}
public struct AnimationCacheItemDrawingSurface {
public let argb: UnsafeMutablePointer<UInt8>
public let width: Int
public let height: Int
public let bytesPerRow: Int
public let length: Int
public init(
argb: UnsafeMutablePointer<UInt8>,
width: Int,
height: Int,
bytesPerRow: Int,
length: Int
) {
self.argb = argb
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
self.length = length
}
}
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()
}
public final class AnimationCacheItemResult {
public let item: AnimationCacheItem?
public let isFinal: Bool
public init(item: AnimationCacheItem?, isFinal: Bool) {
self.item = item
self.isFinal = isFinal
}
}
public struct AnimationCacheFetchOptions {
public let size: CGSize
public let writer: AnimationCacheItemWriter
public let firstFrameOnly: Bool
public init(
size: CGSize,
writer: AnimationCacheItemWriter,
firstFrameOnly: Bool
) {
self.size = size
self.writer = writer
self.firstFrameOnly = firstFrameOnly
}
}
public protocol AnimationCache: AnyObject {
func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal<AnimationCacheItemResult, NoError>
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<Int>, 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<Int>
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<Int>, 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<compression_stream>
private let tempOutputBufferSize: Int = 64 * 1024
private let tempOutputBuffer: UnsafeMutablePointer<UInt8>
private let tempInputBufferCapacity: Int = 64 * 1024
private let tempInputBuffer: UnsafeMutablePointer<UInt8>
private var tempInputBufferSize: Int = 0
private var didFail: Bool = false
init?(file: ManagedFile) {
self.file = file
self.stream = UnsafeMutablePointer<compression_stream>.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<UInt8>.allocate(capacity: self.tempOutputBufferSize)
self.tempInputBuffer = UnsafeMutablePointer<UInt8>.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<UInt8>, 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<Int>
private let stream: UnsafeMutablePointer<compression_stream>
private var isComplete = false
init?(compressedData: Data, dataRange: Range<Int>) {
self.compressedData = compressedData
self.dataRange = dataRange
self.stream = UnsafeMutablePointer<compression_stream>.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<UInt8>, 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, into: nil) else {
continue
}
var itemWidth: Int = 0
guard scanner.scanInt(&itemWidth) else {
continue
}
guard scanner.scanString("x", into: nil) else {
continue
}
var itemHeight: Int = 0
guard scanner.scanInt(&itemHeight) else {
continue
}
if !baseSuffix.isEmpty {
guard scanner.scanString(baseSuffix, into: 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<Impl>
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<AnimationCacheItemResult, NoError> {
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
}
}