mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
786 lines
32 KiB
Swift
786 lines
32 KiB
Swift
import Foundation
|
|
import SwiftSignalKit
|
|
import Compression
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import YuvConversion
|
|
import MediaResources
|
|
import AnimationCompression
|
|
|
|
private let sharedQueue = Queue()
|
|
|
|
private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
|
private var value: Bool = false
|
|
var updated: ((Bool) -> Void)?
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
self.isLayerBacked = true
|
|
}
|
|
|
|
override func didEnterHierarchy() {
|
|
super.didEnterHierarchy()
|
|
|
|
if !self.value {
|
|
self.value = true
|
|
self.updated?(true)
|
|
}
|
|
}
|
|
|
|
override func didExitHierarchy() {
|
|
super.didExitHierarchy()
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.isInHierarchy {
|
|
if strongSelf.value {
|
|
strongSelf.value = false
|
|
strongSelf.updated?(false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum AnimatedStickerMode {
|
|
case cached
|
|
case direct(cachePathPrefix: String?)
|
|
}
|
|
|
|
public enum AnimatedStickerPlaybackPosition {
|
|
case start
|
|
case end
|
|
case timestamp(Double)
|
|
case frameIndex(Int)
|
|
}
|
|
|
|
public enum AnimatedStickerPlaybackMode {
|
|
case once
|
|
case count(Int)
|
|
case loop
|
|
case still(AnimatedStickerPlaybackPosition)
|
|
}
|
|
|
|
public final class AnimatedStickerFrame {
|
|
public let data: Data
|
|
public let type: AnimationRendererFrameType
|
|
public let width: Int
|
|
public let height: Int
|
|
public let bytesPerRow: Int
|
|
let index: Int
|
|
let isLastFrame: Bool
|
|
let totalFrames: Int
|
|
let multiplyAlpha: Bool
|
|
|
|
init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool, totalFrames: Int, multiplyAlpha: Bool = false) {
|
|
self.data = data
|
|
self.type = type
|
|
self.width = width
|
|
self.height = height
|
|
self.bytesPerRow = bytesPerRow
|
|
self.index = index
|
|
self.isLastFrame = isLastFrame
|
|
self.totalFrames = totalFrames
|
|
self.multiplyAlpha = multiplyAlpha
|
|
}
|
|
}
|
|
|
|
public final class AnimatedStickerFrameQueue {
|
|
private let queue: Queue
|
|
private let length: Int
|
|
private let source: AnimatedStickerFrameSource
|
|
private var frames: [AnimatedStickerFrame] = []
|
|
|
|
public init(queue: Queue, length: Int, source: AnimatedStickerFrameSource) {
|
|
self.queue = queue
|
|
self.length = length
|
|
self.source = source
|
|
}
|
|
|
|
deinit {
|
|
assert(self.queue.isCurrent())
|
|
}
|
|
|
|
public func take(draw: Bool) -> AnimatedStickerFrame? {
|
|
if self.frames.isEmpty {
|
|
if let frame = self.source.takeFrame(draw: draw) {
|
|
self.frames.append(frame)
|
|
}
|
|
}
|
|
if !self.frames.isEmpty {
|
|
let frame = self.frames.removeFirst()
|
|
return frame
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public func generateFramesIfNeeded() {
|
|
if self.frames.isEmpty {
|
|
if let frame = self.source.takeFrame(draw: true) {
|
|
self.frames.append(frame)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct AnimatedStickerStatus: Equatable {
|
|
public let playing: Bool
|
|
public let duration: Double
|
|
public let timestamp: Double
|
|
|
|
public init(playing: Bool, duration: Double, timestamp: Double) {
|
|
self.playing = playing
|
|
self.duration = duration
|
|
self.timestamp = timestamp
|
|
}
|
|
}
|
|
|
|
public protocol AnimatedStickerNodeSource {
|
|
var fitzModifier: EmojiFitzModifier? { get }
|
|
var isVideo: Bool { get }
|
|
|
|
func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError>
|
|
func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError>
|
|
}
|
|
|
|
public protocol AnimatedStickerNode: ASDisplayNode {
|
|
var automaticallyLoadFirstFrame: Bool { get set }
|
|
var automaticallyLoadLastFrame: Bool { get set }
|
|
var playToCompletionOnStop: Bool { get set }
|
|
var started: () -> Void { get set }
|
|
|
|
var completed: (Bool) -> Void { get set }
|
|
var frameUpdated: (Int, Int) -> Void { get set }
|
|
var currentFrameIndex: Int { get }
|
|
var currentFrameCount: Int { get }
|
|
var isPlaying: Bool { get }
|
|
var stopAtNearestLoop: Bool { get set }
|
|
|
|
var status: Signal<AnimatedStickerStatus, NoError> { get }
|
|
|
|
var autoplay: Bool { get set }
|
|
|
|
var visibility: Bool { get set }
|
|
|
|
var isPlayingChanged: (Bool) -> Void { get }
|
|
|
|
func cloneCurrentFrame(from otherNode: AnimatedStickerNode?)
|
|
func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode)
|
|
func reset()
|
|
func playOnce()
|
|
func play(firstFrame: Bool, fromIndex: Int?)
|
|
func pause()
|
|
func stop()
|
|
func seekTo(_ position: AnimatedStickerPlaybackPosition)
|
|
func playIfNeeded() -> Bool
|
|
func updateLayout(size: CGSize)
|
|
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool)
|
|
}
|
|
|
|
public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedStickerNode {
|
|
private let queue: Queue
|
|
private let disposable = MetaDisposable()
|
|
private let fetchDisposable = MetaDisposable()
|
|
private let eventsNode: AnimatedStickerNodeDisplayEvents
|
|
|
|
public var automaticallyLoadFirstFrame: Bool = false
|
|
public var automaticallyLoadLastFrame: Bool = false
|
|
public var playToCompletionOnStop: Bool = false
|
|
|
|
public var started: () -> Void = {}
|
|
private var reportedStarted = false
|
|
|
|
public var completed: (Bool) -> Void = { _ in }
|
|
public var frameUpdated: (Int, Int) -> Void = { _, _ in }
|
|
public private(set) var currentFrameIndex: Int = 0
|
|
public private(set) var currentFrameCount: Int = 0
|
|
private var playFromIndex: Int?
|
|
|
|
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
|
|
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
|
|
|
|
private var directData: (Data, String, Int, Int, String?, EmojiFitzModifier?, Bool)?
|
|
private var cachedData: (Data, Bool, EmojiFitzModifier?)?
|
|
|
|
private let useMetalCache: Bool
|
|
private var renderer: AnimationRendererPool.Holder?
|
|
|
|
public var isPlaying: Bool = false
|
|
private var currentLoopCount: Int = 0
|
|
private var canDisplayFirstFrame: Bool = false
|
|
private var playbackMode: AnimatedStickerPlaybackMode = .loop
|
|
|
|
public var stopAtNearestLoop: Bool = false
|
|
|
|
private let playbackStatus = Promise<AnimatedStickerStatus>()
|
|
public var status: Signal<AnimatedStickerStatus, NoError> {
|
|
return self.playbackStatus.get()
|
|
}
|
|
|
|
public var autoplay = false
|
|
|
|
public var visibility = false {
|
|
didSet {
|
|
if self.visibility != oldValue {
|
|
self.updateIsPlaying()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isDisplaying = false {
|
|
didSet {
|
|
if self.isDisplaying != oldValue {
|
|
self.updateIsPlaying()
|
|
}
|
|
}
|
|
}
|
|
|
|
public var isPlayingChanged: (Bool) -> Void = { _ in }
|
|
|
|
private var overlayColor: (UIColor?, Bool)? = nil
|
|
private var size: CGSize?
|
|
|
|
public init(useMetalCache: Bool = false) {
|
|
self.queue = sharedQueue
|
|
self.eventsNode = AnimatedStickerNodeDisplayEvents()
|
|
|
|
self.useMetalCache = useMetalCache
|
|
|
|
super.init()
|
|
|
|
self.eventsNode.updated = { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.isDisplaying = value
|
|
}
|
|
self.addSubnode(self.eventsNode)
|
|
}
|
|
|
|
deinit {
|
|
self.disposable.dispose()
|
|
self.fetchDisposable.dispose()
|
|
self.timer.swap(nil)?.invalidate()
|
|
}
|
|
|
|
private static let hardwareRendererPool = AnimationRendererPool(generate: {
|
|
if #available(iOS 10.0, *) {
|
|
return CompressedAnimationRenderer()
|
|
} else {
|
|
return SoftwareAnimationRenderer()
|
|
}
|
|
})
|
|
|
|
private static let softwareRendererPool = AnimationRendererPool(generate: {
|
|
return SoftwareAnimationRenderer()
|
|
})
|
|
|
|
private weak var nodeToCopyFrameFrom: DefaultAnimatedStickerNodeImpl?
|
|
override public func didLoad() {
|
|
super.didLoad()
|
|
|
|
if #available(iOS 10.0, *), (self.useMetalCache/* || "".isEmpty*/) {
|
|
self.renderer = DefaultAnimatedStickerNodeImpl.hardwareRendererPool.take()
|
|
} else {
|
|
self.renderer = DefaultAnimatedStickerNodeImpl.softwareRendererPool.take()
|
|
if let contents = self.nodeToCopyFrameFrom?.renderer?.renderer.contents {
|
|
self.renderer?.renderer.contents = contents
|
|
}
|
|
}
|
|
|
|
self.renderer?.renderer.frame = CGRect(origin: CGPoint(), size: self.size ?? self.bounds.size)
|
|
if let (overlayColor, replace) = self.overlayColor {
|
|
self.renderer?.renderer.setOverlayColor(overlayColor, replace: replace, animated: false)
|
|
}
|
|
self.nodeToCopyFrameFrom = nil
|
|
self.addSubnode(self.renderer!.renderer)
|
|
}
|
|
|
|
public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) {
|
|
guard let otherNode = otherNode as? DefaultAnimatedStickerNodeImpl else {
|
|
self.nodeToCopyFrameFrom = nil
|
|
return
|
|
}
|
|
|
|
if let renderer = self.renderer?.renderer as? SoftwareAnimationRenderer, let otherRenderer = otherNode.renderer?.renderer as? SoftwareAnimationRenderer {
|
|
if let contents = otherRenderer.contents {
|
|
renderer.contents = contents
|
|
}
|
|
} else {
|
|
self.nodeToCopyFrameFrom = otherNode
|
|
}
|
|
}
|
|
|
|
public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) {
|
|
if width < 2 || height < 2 {
|
|
return
|
|
}
|
|
self.playbackMode = playbackMode
|
|
switch mode {
|
|
case let .direct(cachePathPrefix):
|
|
let f: (String) -> Void = { [weak self] path in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
|
strongSelf.directData = (directData, path, width, height, cachePathPrefix, source.fitzModifier, source.isVideo)
|
|
}
|
|
if case let .still(position) = strongSelf.playbackMode {
|
|
strongSelf.seekTo(position)
|
|
} else if strongSelf.isPlaying || strongSelf.autoplay {
|
|
if strongSelf.autoplay {
|
|
strongSelf.isSetUpForPlayback = false
|
|
strongSelf.isPlaying = true
|
|
}
|
|
let fromIndex = strongSelf.playFromIndex
|
|
strongSelf.playFromIndex = nil
|
|
strongSelf.play(fromIndex: fromIndex)
|
|
} else if strongSelf.canDisplayFirstFrame {
|
|
strongSelf.play(firstFrame: true)
|
|
}
|
|
}
|
|
self.disposable.set((source.directDataPath(attemptSynchronously: false)
|
|
|> filter { $0 != nil }
|
|
|> deliverOnMainQueue).start(next: { path in
|
|
f(path!)
|
|
}))
|
|
case .cached:
|
|
self.disposable.set((source.cachedDataPath(width: width, height: height)
|
|
|> deliverOnMainQueue).start(next: { [weak self] path, complete in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
|
if let (_, currentComplete, _) = strongSelf.cachedData {
|
|
if !currentComplete {
|
|
strongSelf.cachedData = (data, complete, source.fitzModifier)
|
|
strongSelf.frameSource.with { frameSource in
|
|
frameSource?.with { frameSource in
|
|
if let frameSource = frameSource.value as? AnimatedStickerCachedFrameSource {
|
|
frameSource.updateData(data: data, complete: complete)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
strongSelf.cachedData = (data, complete, source.fitzModifier)
|
|
if strongSelf.isPlaying {
|
|
strongSelf.play()
|
|
} else if strongSelf.canDisplayFirstFrame {
|
|
strongSelf.play(firstFrame: true)
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
public func reset() {
|
|
self.disposable.set(nil)
|
|
self.fetchDisposable.set(nil)
|
|
}
|
|
|
|
private func updateIsPlaying() {
|
|
if !self.autoplay {
|
|
let isPlaying = self.visibility && self.isDisplaying
|
|
if self.isPlaying != isPlaying {
|
|
self.isPlaying = isPlaying
|
|
if isPlaying {
|
|
self.play()
|
|
} else{
|
|
self.pause()
|
|
}
|
|
|
|
self.isPlayingChanged(isPlaying)
|
|
}
|
|
}
|
|
if self.automaticallyLoadLastFrame {
|
|
if self.isDisplaying {
|
|
self.seekTo(.end)
|
|
}
|
|
} else {
|
|
let canDisplayFirstFrame = self.automaticallyLoadFirstFrame && self.isDisplaying
|
|
if self.canDisplayFirstFrame != canDisplayFirstFrame {
|
|
self.canDisplayFirstFrame = canDisplayFirstFrame
|
|
if canDisplayFirstFrame {
|
|
self.play(firstFrame: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isSetUpForPlayback = false
|
|
|
|
public func playOnce() {
|
|
self.playbackMode = .once
|
|
self.play()
|
|
}
|
|
|
|
public func play(firstFrame: Bool = false, fromIndex: Int? = nil) {
|
|
if !firstFrame {
|
|
switch self.playbackMode {
|
|
case .once:
|
|
self.isPlaying = true
|
|
case .count:
|
|
self.currentLoopCount = 0
|
|
self.isPlaying = true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
if self.isSetUpForPlayback {
|
|
let directData = self.directData
|
|
let cachedData = self.cachedData
|
|
let queue = self.queue
|
|
let timerHolder = self.timer
|
|
let frameSourceHolder = self.frameSource
|
|
let useMetalCache = self.useMetalCache
|
|
self.queue.async { [weak self] in
|
|
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }.value
|
|
if maybeFrameSource == nil {
|
|
let notifyUpdated: (() -> Void)? = nil
|
|
if let directData = directData {
|
|
if directData.6 {
|
|
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
|
} else {
|
|
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
|
|
}
|
|
} else if let (cachedData, cachedDataComplete, _) = cachedData {
|
|
if #available(iOS 9.0, *) {
|
|
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
|
notifyUpdated?()
|
|
})
|
|
}
|
|
}
|
|
let _ = frameSourceHolder.swap(maybeFrameSource.flatMap { maybeFrameSource in
|
|
return QueueLocalObject(queue: queue, generate: {
|
|
return AnimatedStickerFrameSourceWrapper(maybeFrameSource)
|
|
})
|
|
})
|
|
}
|
|
guard let frameSource = maybeFrameSource else {
|
|
return
|
|
}
|
|
if let fromIndex = fromIndex {
|
|
frameSource.skipToFrameIndex(fromIndex)
|
|
}
|
|
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
|
|
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
|
|
})
|
|
timerHolder.swap(nil)?.invalidate()
|
|
|
|
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
|
|
let frameRate = frameSource.frameRate
|
|
|
|
let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
|
|
let frame = frameQueue.syncWith { frameQueue in
|
|
return frameQueue.take(draw: true)
|
|
}
|
|
if let frame = frame {
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.reportedStarted {
|
|
strongSelf.reportedStarted = true
|
|
strongSelf.started()
|
|
}
|
|
})
|
|
|
|
strongSelf.frameUpdated(frame.index, frame.totalFrames)
|
|
strongSelf.currentFrameIndex = frame.index
|
|
strongSelf.currentFrameCount = frame.totalFrames
|
|
|
|
if frame.isLastFrame {
|
|
var stopped = false
|
|
var stopNow = false
|
|
if case .still = strongSelf.playbackMode {
|
|
stopNow = true
|
|
} else if case .once = strongSelf.playbackMode {
|
|
stopNow = true
|
|
} else if case let .count(count) = strongSelf.playbackMode {
|
|
strongSelf.currentLoopCount += 1
|
|
if count <= strongSelf.currentLoopCount {
|
|
stopNow = true
|
|
}
|
|
} else if strongSelf.stopAtNearestLoop {
|
|
stopNow = true
|
|
}
|
|
if stopNow {
|
|
strongSelf.stop()
|
|
strongSelf.isPlaying = false
|
|
stopped = true
|
|
}
|
|
|
|
strongSelf.completed(stopped)
|
|
}
|
|
|
|
let timestamp: Double = frameRate > 0 ? Double(frame.index) / Double(frameRate) : 0
|
|
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: strongSelf.isPlaying, duration: duration, timestamp: timestamp)))
|
|
}
|
|
}
|
|
frameQueue.with { frameQueue in
|
|
frameQueue.generateFramesIfNeeded()
|
|
}
|
|
}, queue: queue)
|
|
let _ = timerHolder.swap(timer)
|
|
timer.start()
|
|
}
|
|
} else {
|
|
self.isSetUpForPlayback = true
|
|
let directData = self.directData
|
|
let cachedData = self.cachedData
|
|
if directData == nil && cachedData == nil {
|
|
self.playFromIndex = fromIndex
|
|
}
|
|
let queue = self.queue
|
|
let timerHolder = self.timer
|
|
let frameSourceHolder = self.frameSource
|
|
let useMetalCache = self.useMetalCache
|
|
self.queue.async { [weak self] in
|
|
var maybeFrameSource: AnimatedStickerFrameSource?
|
|
let notifyUpdated: (() -> Void)? = nil
|
|
if let directData = directData {
|
|
if directData.6 {
|
|
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
|
} else {
|
|
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
|
|
}
|
|
} else if let (cachedData, cachedDataComplete, _) = cachedData {
|
|
if #available(iOS 9.0, *) {
|
|
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
|
notifyUpdated?()
|
|
})
|
|
}
|
|
}
|
|
let _ = frameSourceHolder.swap(maybeFrameSource.flatMap { maybeFrameSource in
|
|
return QueueLocalObject(queue: queue, generate: {
|
|
return AnimatedStickerFrameSourceWrapper(maybeFrameSource)
|
|
})
|
|
})
|
|
guard let frameSource = maybeFrameSource else {
|
|
return
|
|
}
|
|
if let fromIndex = fromIndex {
|
|
frameSource.skipToFrameIndex(fromIndex)
|
|
}
|
|
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
|
|
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
|
|
})
|
|
timerHolder.swap(nil)?.invalidate()
|
|
|
|
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
|
|
let frameRate = frameSource.frameRate
|
|
|
|
let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
|
|
let frame = frameQueue.syncWith { frameQueue in
|
|
return frameQueue.take(draw: true)
|
|
}
|
|
if let frame = frame {
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.reportedStarted {
|
|
strongSelf.reportedStarted = true
|
|
strongSelf.started()
|
|
}
|
|
})
|
|
|
|
strongSelf.frameUpdated(frame.index, frame.totalFrames)
|
|
strongSelf.currentFrameIndex = frame.index
|
|
strongSelf.currentFrameCount = frame.totalFrames;
|
|
|
|
if frame.isLastFrame {
|
|
var stopped = false
|
|
var stopNow = false
|
|
if case .still = strongSelf.playbackMode {
|
|
stopNow = true
|
|
} else if case .once = strongSelf.playbackMode {
|
|
stopNow = true
|
|
} else if case let .count(count) = strongSelf.playbackMode {
|
|
strongSelf.currentLoopCount += 1
|
|
if count <= strongSelf.currentLoopCount {
|
|
stopNow = true
|
|
}
|
|
} else if strongSelf.stopAtNearestLoop {
|
|
stopNow = true
|
|
}
|
|
if stopNow {
|
|
strongSelf.stop()
|
|
strongSelf.isPlaying = false
|
|
stopped = true
|
|
}
|
|
|
|
strongSelf.completed(stopped)
|
|
}
|
|
|
|
let timestamp: Double = frameRate > 0 ? Double(frame.index) / Double(frameRate) : 0
|
|
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: strongSelf.isPlaying, duration: duration, timestamp: timestamp)))
|
|
}
|
|
}
|
|
frameQueue.with { frameQueue in
|
|
frameQueue.generateFramesIfNeeded()
|
|
}
|
|
}, queue: queue)
|
|
let _ = timerHolder.swap(timer)
|
|
timer.start()
|
|
}
|
|
}
|
|
}
|
|
|
|
public func pause() {
|
|
self.timer.swap(nil)?.invalidate()
|
|
}
|
|
|
|
public func stop() {
|
|
self.isSetUpForPlayback = false
|
|
self.reportedStarted = false
|
|
self.timer.swap(nil)?.invalidate()
|
|
if self.playToCompletionOnStop {
|
|
self.seekTo(.start)
|
|
}
|
|
}
|
|
|
|
public func seekTo(_ position: AnimatedStickerPlaybackPosition) {
|
|
self.isPlaying = false
|
|
|
|
let directData = self.directData
|
|
let cachedData = self.cachedData
|
|
let queue = self.queue
|
|
let frameSourceHolder = self.frameSource
|
|
let timerHolder = self.timer
|
|
let useMetalCache = self.useMetalCache
|
|
self.queue.async { [weak self] in
|
|
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }.value
|
|
if case .timestamp = position {
|
|
} else {
|
|
if let directData = directData {
|
|
if directData.6 {
|
|
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
|
} else {
|
|
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
|
|
}
|
|
if case .end = position {
|
|
maybeFrameSource?.skipToEnd()
|
|
}
|
|
} else if let (cachedData, cachedDataComplete, _) = cachedData {
|
|
if #available(iOS 9.0, *) {
|
|
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {})
|
|
}
|
|
}
|
|
}
|
|
|
|
guard let frameSource = maybeFrameSource else {
|
|
return
|
|
}
|
|
if frameSource.frameCount == 0 {
|
|
return
|
|
}
|
|
|
|
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
|
|
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
|
|
})
|
|
timerHolder.swap(nil)?.invalidate()
|
|
|
|
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
|
|
|
|
var maybeFrame: AnimatedStickerFrame??
|
|
if case let .timestamp(timestamp) = position {
|
|
var stickerTimestamp = timestamp
|
|
while stickerTimestamp > duration {
|
|
stickerTimestamp -= duration
|
|
}
|
|
let targetFrame = Int(stickerTimestamp / duration * Double(frameSource.frameCount))
|
|
if targetFrame == frameSource.frameIndex {
|
|
return
|
|
}
|
|
|
|
var delta = targetFrame - frameSource.frameIndex
|
|
if delta < 0 {
|
|
delta = frameSource.frameCount + delta
|
|
}
|
|
for i in 0 ..< delta {
|
|
maybeFrame = frameQueue.syncWith { frameQueue in
|
|
return frameQueue.take(draw: i == delta - 1)
|
|
}
|
|
}
|
|
} else if case let .frameIndex(frameIndex) = position {
|
|
let targetFrame = frameIndex
|
|
if targetFrame == frameSource.frameIndex {
|
|
return
|
|
}
|
|
|
|
var delta = targetFrame - frameSource.frameIndex
|
|
if delta < 0 {
|
|
delta = frameSource.frameCount + delta
|
|
}
|
|
for i in 0 ..< delta {
|
|
maybeFrame = frameQueue.syncWith { frameQueue in
|
|
return frameQueue.take(draw: i == delta - 1)
|
|
}
|
|
}
|
|
} else {
|
|
maybeFrame = frameQueue.syncWith { frameQueue in
|
|
return frameQueue.take(draw: true)
|
|
}
|
|
}
|
|
if let maybeFrame = maybeFrame, let frame = maybeFrame {
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.reportedStarted {
|
|
strongSelf.reportedStarted = true
|
|
strongSelf.started()
|
|
}
|
|
})
|
|
|
|
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: false, duration: duration, timestamp: 0.0)))
|
|
}
|
|
}
|
|
frameQueue.with { frameQueue in
|
|
frameQueue.generateFramesIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
|
|
public func playIfNeeded() -> Bool {
|
|
if !self.isPlaying {
|
|
self.isPlaying = true
|
|
self.play()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
public func updateLayout(size: CGSize) {
|
|
self.size = size
|
|
self.renderer?.renderer.frame = CGRect(origin: CGPoint(), size: size)
|
|
}
|
|
|
|
public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
|
|
self.overlayColor = (color, replace)
|
|
self.renderer?.renderer.setOverlayColor(color, replace: replace, animated: animated)
|
|
}
|
|
}
|