import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import AnimatedStickerNode import MetalEngine import LottieCpp import GZip import MetalKit import HierarchyTrackingLayer private final class BundleMarker: NSObject { } private var metalLibraryValue: MTLLibrary? func metalLibrary(device: MTLDevice) -> MTLLibrary? { if let metalLibraryValue { return metalLibraryValue } let mainBundle = Bundle(for: BundleMarker.self) guard let path = mainBundle.path(forResource: "LottieMetalSourcesBundle", ofType: "bundle") else { return nil } guard let bundle = Bundle(path: path) else { return nil } guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { return nil } metalLibraryValue = library return library } private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount: Int) -> MTLTexture { let textureDescriptor = MTLTextureDescriptor() textureDescriptor.sampleCount = msaaSampleCount if msaaSampleCount == 1 { textureDescriptor.textureType = .type2D } else { textureDescriptor.textureType = .type2DMultisample } textureDescriptor.width = sideSize textureDescriptor.height = sideSize textureDescriptor.pixelFormat = .bgra8Unorm //textureDescriptor.storageMode = .memoryless textureDescriptor.storageMode = .private textureDescriptor.usage = [.renderTarget, .shaderRead] return device.makeTexture(descriptor: textureDescriptor)! } final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject { private var animationContainer: LottieAnimationContainer? var frameIndex: Int = 0 var internalData: MetalEngineSubjectInternalData? private var renderBufferHeap: MTLHeap? private var offscreenHeap: MTLHeap? private var multisampleTextureQueue: [MTLTexture] = [] private let currentBezierIndicesBuffer = PathRenderBuffer() private let currentBuffer = PathRenderBuffer() final class PrepareState: ComputeState { let pathRenderContext: PathRenderContext init?(device: MTLDevice) { guard let pathRenderContext = PathRenderContext(device: device, msaaSampleCount: 1) else { return nil } self.pathRenderContext = pathRenderContext } } final class RenderState: RenderToLayerState { let pipelineState: MTLRenderPipelineState required init?(device: MTLDevice) { guard let library = metalLibrary(device: device) else { return nil } guard let vertexFunction = library.makeFunction(name: "blitVertex"), let fragmentFunction = library.makeFunction(name: "blitFragment") else { return nil } let pipelineDescriptor = MTLRenderPipelineDescriptor() pipelineDescriptor.vertexFunction = vertexFunction pipelineDescriptor.fragmentFunction = fragmentFunction pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else { return nil } self.pipelineState = pipelineState } } init(animationContainer: LottieAnimationContainer) { self.animationContainer = animationContainer #if DEBUG && false let startTime = CFAbsoluteTimeGetCurrent() let buffer = WriteBuffer() for i in 0 ..< animationContainer.animation.frameCount { animationContainer.update(i) serializeNode(buffer: buffer, node: animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0))) } buffer.trim() let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime) let zippedData = TGGZipData(buffer.data, 1.0) print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB") #endif super.init() self.isOpaque = false } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func fillPath(frameState: PathFrameState, path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) { let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform) path.enumerateItems { pathItem in switch pathItem.pointee.type { case .moveTo: let point = pathItem.pointee.points.0 fillState.begin(point: SIMD2(Float(point.x), Float(point.y))) case .lineTo: let point = pathItem.pointee.points.0 fillState.addLine(to: SIMD2(Float(point.x), Float(point.y))) case .curveTo: let cp1 = pathItem.pointee.points.0 let cp2 = pathItem.pointee.points.1 let point = pathItem.pointee.points.2 fillState.addCurve( to: SIMD2(Float(point.x), Float(point.y)), cp1: SIMD2(Float(cp1.x), Float(cp1.y)), cp2: SIMD2(Float(cp2.x), Float(cp2.y)) ) case .close: fillState.close() @unknown default: break } } fillState.close() frameState.add(fill: fillState) } private func strokePath(frameState: PathFrameState, path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) { let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform) path.enumerateItems { pathItem in switch pathItem.pointee.type { case .moveTo: let point = pathItem.pointee.points.0 strokeState.begin(point: SIMD2(Float(point.x), Float(point.y))) case .lineTo: let point = pathItem.pointee.points.0 strokeState.addLine(to: SIMD2(Float(point.x), Float(point.y))) case .curveTo: let cp1 = pathItem.pointee.points.0 let cp2 = pathItem.pointee.points.1 let point = pathItem.pointee.points.2 strokeState.addCurve( to: SIMD2(Float(point.x), Float(point.y)), cp1: SIMD2(Float(cp1.x), Float(cp1.y)), cp2: SIMD2(Float(cp2.x), Float(cp2.y)) ) case .close: strokeState.close() @unknown default: break } } strokeState.complete() frameState.add(stroke: strokeState) } func update(context: MetalEngineSubjectContext) { if self.bounds.isEmpty { return } let size = CGSize(width: 800.0, height: 800.0) let msaaSampleCount = 1 let renderSpec = RenderLayerSpec(size: RenderSize(width: Int(size.width), height: Int(size.height))) guard let animationContainer = self.animationContainer else { return } animationContainer.update(self.frameIndex) func defaultTransformForSize(_ size: CGSize) -> CATransform3D { var transform = CATransform3DIdentity transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0) transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0) transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0) transform = CATransform3DScale(transform, 1.0, -1.0, 1.0) return transform } let canvasSize = size var transform = defaultTransformForSize(canvasSize) concat(CATransform3DMakeScale(canvasSize.width / animationContainer.animation.size.width, canvasSize.height / animationContainer.animation.size.height, 1.0)) var transformStack: [CATransform3D] = [] func saveState() { transformStack.append(transform) } func restoreState() { transform = transformStack.removeLast() } func concat(_ other: CATransform3D) { transform = CATransform3DConcat(other, transform) } func renderNodeContent(frameState: PathFrameState, item: LottieRenderContent, alpha: Double) { if let fill = item.fill { if let solidShading = fill.shading as? LottieRenderContentSolidShading { self.fillPath( frameState: frameState, path: item.path, shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)), rule: fill.fillRule, transform: transform ) } else if let gradientShading = fill.shading as? LottieRenderContentGradientShading { let gradientType: PathShading.Gradient.GradientType switch gradientShading.gradientType { case .linear: gradientType = .linear case .radial: gradientType = .radial @unknown default: gradientType = .linear } var colorStops: [PathShading.Gradient.ColorStop] = [] for colorStop in gradientShading.colorStops { colorStops.append(PathShading.Gradient.ColorStop( color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha), location: Float(colorStop.location) )) } let gradientShading = PathShading.Gradient( gradientType: gradientType, colorStops: colorStops, start: SIMD2(Float(gradientShading.start.x), Float(gradientShading.start.y)), end: SIMD2(Float(gradientShading.end.x), Float(gradientShading.end.y)) ) self.fillPath( frameState: frameState, path: item.path, shading: .gradient(gradientShading), rule: fill.fillRule, transform: transform ) } } else if let stroke = item.stroke { if let solidShading = stroke.shading as? LottieRenderContentSolidShading { let color = solidShading.color strokePath( frameState: frameState, path: item.path, width: stroke.lineWidth, join: stroke.lineJoin, cap: stroke.lineCap, miterLimit: stroke.miterLimit, color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha), transform: transform ) } } } func renderNode(frameState: PathFrameState, node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) { let normalizedOpacity = node.opacity let layerAlpha = normalizedOpacity * parentAlpha if node.isHidden || normalizedOpacity == 0.0 { return } saveState() var needsTempContext = false if node.mask != nil { needsTempContext = true } else { needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds } var maskSurface: PathFrameState.MaskSurface? if needsTempContext { if node.mask != nil || node.masksToBounds { var maskMode: PathFrameState.MaskSurface.Mode = .regular frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height)) saveState() transform = defaultTransformForSize(node.globalRect.size) concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0)) concat(node.globalTransform) if node.masksToBounds { let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform) fillState.begin(point: SIMD2(Float(node.bounds.minX), Float(node.bounds.minY))) fillState.addLine(to: SIMD2(Float(node.bounds.minX), Float(node.bounds.maxY))) fillState.addLine(to: SIMD2(Float(node.bounds.maxX), Float(node.bounds.maxY))) fillState.addLine(to: SIMD2(Float(node.bounds.maxX), Float(node.bounds.minY))) fillState.close() frameState.add(fill: fillState) } if let maskNode = node.mask { if maskNode.isInvertedMatte { maskMode = .inverse } renderNode(frameState: frameState, node: maskNode, globalSize: globalSize, parentAlpha: 1.0) } restoreState() maskSurface = frameState.popOffscreenMask(mode: maskMode) } frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height)) saveState() transform = defaultTransformForSize(node.globalRect.size) concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0)) concat(node.globalTransform) } else { concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0)) concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0)) concat(node.transform) } var renderAlpha: CGFloat = 1.0 if needsTempContext { renderAlpha = 1.0 } else { renderAlpha = layerAlpha } if let renderContent = node.renderContent { renderNodeContent(frameState: frameState, item: renderContent, alpha: renderAlpha) } for subnode in node.subnodes { renderNode(frameState: frameState, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha) } if needsTempContext { restoreState() concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0)) concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0)) concat(node.transform) concat(CATransform3DInvert(node.globalTransform)) frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface) } restoreState() } self.currentBuffer.reset() self.currentBezierIndicesBuffer.reset() let frameState = PathFrameState(width: Int(size.width), height: Int(size.height), msaaSampleCount: 1, buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer) let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) renderNode(frameState: frameState, node: node, globalSize: canvasSize, parentAlpha: 1.0) final class ComputeOutput { let pathRenderContext: PathRenderContext let renderBufferHeap: MTLHeap let multisampleTexture: MTLTexture let takenMultisampleTextures: [MTLTexture] init(pathRenderContext: PathRenderContext, renderBufferHeap: MTLHeap, multisampleTexture: MTLTexture, takenMultisampleTextures: [MTLTexture]) { self.pathRenderContext = pathRenderContext self.renderBufferHeap = renderBufferHeap self.multisampleTexture = multisampleTexture self.takenMultisampleTextures = takenMultisampleTextures } } var customCompletion: (() -> Void)? let computeOutput = context.compute(state: PrepareState.self, commands: { commandBuffer, state -> ComputeOutput? in let renderBufferHeap: MTLHeap if let current = self.renderBufferHeap { renderBufferHeap = current } else { let heapDescriptor = MTLHeapDescriptor() heapDescriptor.size = 32 * 1024 * 1024 heapDescriptor.storageMode = .shared heapDescriptor.cpuCacheMode = .writeCombined if #available(iOS 13.0, *) { heapDescriptor.hazardTrackingMode = .tracked } guard let value = MetalEngine.shared.device.makeHeap(descriptor: heapDescriptor) else { print() return nil } self.renderBufferHeap = value renderBufferHeap = value } guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return nil } frameState.prepare(heap: renderBufferHeap) frameState.encodeCompute(context: state.pathRenderContext, computeEncoder: computeEncoder) computeEncoder.endEncoding() let multisampleTexture: MTLTexture if !self.multisampleTextureQueue.isEmpty { multisampleTexture = self.multisampleTextureQueue.removeFirst() } else { multisampleTexture = generateTexture(device: MetalEngine.shared.device, sideSize: Int(size.width), msaaSampleCount: 1) } let tempTexture: MTLTexture if !self.multisampleTextureQueue.isEmpty { tempTexture = self.multisampleTextureQueue.removeFirst() } else { tempTexture = generateTexture(device: MetalEngine.shared.device, sideSize: Int(size.width), msaaSampleCount: 1) } let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = multisampleTexture if msaaSampleCount == 1 { renderPassDescriptor.colorAttachments[0].storeAction = .store } else { //renderPassDescriptor.colorAttachments[0].resolveTexture = self.currentDrawable?.texture renderPassDescriptor.colorAttachments[0].storeAction = .multisampleResolve preconditionFailure() } renderPassDescriptor.colorAttachments[0].loadAction = .clear renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) renderPassDescriptor.colorAttachments[1].texture = tempTexture renderPassDescriptor.colorAttachments[1].loadAction = .clear renderPassDescriptor.colorAttachments[1].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) renderPassDescriptor.colorAttachments[1].storeAction = .dontCare if msaaSampleCount == 4 { renderPassDescriptor.setSamplePositions([ MTLSamplePosition(x: 0.25, y: 0.25), MTLSamplePosition(x: 0.75, y: 0.25), MTLSamplePosition(x: 0.75, y: 0.75), MTLSamplePosition(x: 0.25, y: 0.75) ]) } var offscreenHeapMemorySize = frameState.calculateOffscreenHeapMemorySize(device: MetalEngine.shared.device) offscreenHeapMemorySize = max(offscreenHeapMemorySize, 1 * 1024 * 1024) let offscreenHeap: MTLHeap if let current = self.offscreenHeap, current.size >= offscreenHeapMemorySize * 3 { offscreenHeap = current } else { print("Creating offscreen heap \(offscreenHeapMemorySize * 3 / (1024 * 1024)) MB (3 * \(offscreenHeapMemorySize / (1024 * 1024)) MB)") let heapDescriptor = MTLHeapDescriptor() heapDescriptor.size = offscreenHeapMemorySize * 3 heapDescriptor.storageMode = .private heapDescriptor.cpuCacheMode = .defaultCache if #available(iOS 13.0, *) { heapDescriptor.hazardTrackingMode = .tracked } offscreenHeap = MetalEngine.shared.device.makeHeap(descriptor: heapDescriptor)! self.offscreenHeap = offscreenHeap } frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: canvasSize) guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { self.multisampleTextureQueue.append(multisampleTexture) self.multisampleTextureQueue.append(tempTexture) return nil } frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: canvasSize) renderEncoder.endEncoding() return ComputeOutput( pathRenderContext: state.pathRenderContext, renderBufferHeap: renderBufferHeap, multisampleTexture: multisampleTexture, takenMultisampleTextures: [multisampleTexture, tempTexture] ) }) context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, inputs: computeOutput, commands: { [weak self] encoder, placement, computeOutput in guard let computeOutput else { return } let effectiveRect = placement.effectiveRect var rect = SIMD4(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height)) encoder.setVertexBytes(&rect, length: 4 * 4, index: 0) encoder.setFragmentTexture(computeOutput.multisampleTexture, index: 0) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) let takenMultisampleTextures = computeOutput.takenMultisampleTextures customCompletion = { guard let self else { return } for texture in takenMultisampleTextures { self.multisampleTextureQueue.append(texture) } } }) context.addCustomCompletion({ customCompletion?() }) } } public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode { private final class LoadFrameTask { var isCancelled: Bool = false } private let hierarchyTrackingLayer: HierarchyTrackingLayer public var automaticallyLoadFirstFrame: Bool = false public var automaticallyLoadLastFrame: Bool = false public var playToCompletionOnStop: Bool = false private var layoutSize: CGSize? private var lottieInstance: LottieAnimationContainer? private var renderLayer: LottieContentLayer? private var displayLinkSubscription: SharedDisplayLinkDriver.Link? private var didStart: Bool = false public var started: () -> Void = {} public var completed: (Bool) -> Void = { _ in } private var didComplete: Bool = false public var frameUpdated: (Int, Int) -> Void = { _, _ in } public var currentFrameIndex: Int { get { return self.frameIndex } set(value) { } } public var currentFrameCount: Int { get { if let lottieInstance = self.lottieInstance { return Int(lottieInstance.animation.frameCount) } else { return 0 } } set(value) { } } public var currentFrameImage: UIImage? { return nil } public private(set) var isPlaying: Bool = false public var stopAtNearestLoop: Bool = false private let statusPromise = Promise() public var status: Signal { return self.statusPromise.get() } public var autoplay: Bool = true public var visibility: Bool = false { didSet { self.updatePlayback() } } public var overrideVisibility: Bool = false public var isPlayingChanged: (Bool) -> Void = { _ in } private var sourceDisposable: Disposable? private var playbackSize: CGSize? private var frameIndex: Int = 0 private var playbackMode: AnimatedStickerPlaybackMode = .loop override public init() { self.hierarchyTrackingLayer = HierarchyTrackingLayer() super.init() self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in guard let self else { return } self.updatePlayback() } self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in guard let self else { return } self.updatePlayback() } } deinit { self.sourceDisposable?.dispose() } public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) { } public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode) { self.didStart = false self.didComplete = false self.sourceDisposable?.dispose() self.playbackSize = CGSize(width: CGFloat(width), height: CGFloat(height)) self.playbackMode = playbackMode self.sourceDisposable = (source.directDataPath(attemptSynchronously: false) |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] path in guard let self, let path = path else { return } if source.isVideo { } else { guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return } let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data guard let lottieAnimation = LottieAnimation(data: decompressedData) else { print("Could not load sticker data") return } let lottieInstance = LottieAnimationContainer(animation: lottieAnimation) self.setupPlayback(lottieInstance: lottieInstance) } }).strict() } private func updatePlayback() { let isPlaying = self.visibility && self.lottieInstance != nil if self.isPlaying != isPlaying { self.isPlaying = isPlaying self.isPlayingChanged(self.isPlaying) } if isPlaying, let lottieInstance = self.lottieInstance { if self.displayLinkSubscription == nil { let fps: Int if lottieInstance.animation.framesPerSecond == 30 { fps = 30 } else { fps = 60 } self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add(framesPerSecond: .fps(fps), { [weak self] deltaTime in guard let self, let lottieInstance = self.lottieInstance, let renderLayer = self.renderLayer else { return } if renderLayer.frameIndex == lottieInstance.animation.frameCount - 1 { switch self.playbackMode { case .loop: self.completed(false) case let .count(count): if count <= 1 { if !self.didComplete { self.didComplete = true self.completed(true) } return } else { self.playbackMode = .count(count - 1) self.completed(false) } case .once: if !self.didComplete { self.didComplete = true self.completed(true) } return case .still: break } } self.frameIndex = (self.frameIndex + 1) % lottieInstance.animation.frameCount renderLayer.frameIndex = self.frameIndex renderLayer.setNeedsUpdate() }) self.renderLayer?.setNeedsUpdate() } } else { self.displayLinkSubscription = nil } } private func advanceFrameIfPossible() { /*var frameCount: Int? if let lottieInstance = self.lottieInstance { frameCount = Int(lottieInstance.frameCount) } else if let videoSource = self.videoSource { frameCount = Int(videoSource.frameCount) } guard let frameCount = frameCount else { return } if self.frameIndex == frameCount - 1 { switch self.playbackMode { case .loop: self.completed(false) case let .count(count): if count <= 1 { if !self.didComplete { self.didComplete = true self.completed(true) } return } else { self.playbackMode = .count(count - 1) self.completed(false) } case .once: if !self.didComplete { self.didComplete = true self.completed(true) } return case .still: break } } let nextFrameIndex = (self.frameIndex + 1) % frameCount self.frameIndex = nextFrameIndex self.updateFrameImageIfNeeded() self.updateLoadFrameTasks()*/ } private func setupPlayback(lottieInstance: LottieAnimationContainer) { self.lottieInstance = lottieInstance let renderLayer = LottieContentLayer(animationContainer: lottieInstance) self.renderLayer = renderLayer if let layoutSize = self.layoutSize { renderLayer.frame = CGRect(origin: CGPoint(), size: layoutSize) } self.layer.addSublayer(renderLayer) self.updatePlayback() } public func reset() { } public func playOnce() { } public func playLoop() { } public func play(firstFrame: Bool, fromIndex: Int?) { if let fromIndex = fromIndex { self.frameIndex = fromIndex } } public func pause() { } public func stop() { } public func seekTo(_ position: AnimatedStickerPlaybackPosition) { } public func playIfNeeded() -> Bool { return false } public func updateLayout(size: CGSize) { self.layoutSize = size if let renderLayer = self.renderLayer { renderLayer.frame = CGRect(origin: CGPoint(), size: size) } } public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) { } }