import Foundation 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 func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) } 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 final var shouldBeAnimating: Bool = false { didSet { if self.shouldBeAnimating != oldValue { for f in self.updateStateCallbacks.copyItems() { f() } } } } public var blurredRepresentationBackgroundColor: UIColor? public var blurredRepresentationTarget: CALayer? { didSet { if self.blurredRepresentationTarget !== oldValue { for f in self.updateStateCallbacks.copyItems() { f() } } } } 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() } } } } } } } } }