mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Streaming sticker caching
This commit is contained in:
parent
1ccb78dc77
commit
6f731d3b63
@ -5,8 +5,10 @@ import Display
|
|||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import RLottieBinding
|
import RLottieBinding
|
||||||
import GZip
|
import GZip
|
||||||
|
import YuvConversion
|
||||||
|
|
||||||
private let sharedQueue = Queue()
|
private let sharedQueue = Queue()
|
||||||
|
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
|
||||||
|
|
||||||
private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
||||||
private var value: Bool = false
|
private var value: Bool = false
|
||||||
@ -46,7 +48,7 @@ private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
|||||||
|
|
||||||
public enum AnimatedStickerMode {
|
public enum AnimatedStickerMode {
|
||||||
case cached
|
case cached
|
||||||
case direct
|
case direct(cachePathPrefix: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AnimatedStickerPlaybackPosition {
|
public enum AnimatedStickerPlaybackPosition {
|
||||||
@ -267,11 +269,321 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func wrappedWrite(_ fd: Int32, _ data: UnsafeRawPointer, _ count: Int) -> Int {
|
||||||
|
return write(fd, data, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func wrappedRead(_ fd: Int32, _ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
|
||||||
|
return read(fd, data, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: separate ManagedFile into its own module
|
||||||
|
private final class ManagedFileImpl {
|
||||||
|
enum Mode {
|
||||||
|
case read
|
||||||
|
case readwrite
|
||||||
|
case append
|
||||||
|
}
|
||||||
|
|
||||||
|
private let queue: Queue?
|
||||||
|
private let fd: Int32
|
||||||
|
private let mode: Mode
|
||||||
|
|
||||||
|
init?(queue: Queue?, path: String, mode: Mode) {
|
||||||
|
if let queue = queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
self.queue = queue
|
||||||
|
self.mode = mode
|
||||||
|
let fileMode: Int32
|
||||||
|
let accessMode: UInt16
|
||||||
|
switch mode {
|
||||||
|
case .read:
|
||||||
|
fileMode = O_RDONLY
|
||||||
|
accessMode = S_IRUSR
|
||||||
|
case .readwrite:
|
||||||
|
fileMode = O_RDWR | O_CREAT
|
||||||
|
accessMode = S_IRUSR | S_IWUSR
|
||||||
|
case .append:
|
||||||
|
fileMode = O_WRONLY | O_CREAT | O_APPEND
|
||||||
|
accessMode = S_IRUSR | S_IWUSR
|
||||||
|
}
|
||||||
|
let fd = open(path, fileMode, accessMode)
|
||||||
|
if fd >= 0 {
|
||||||
|
self.fd = fd
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
close(self.fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func write(_ data: UnsafeRawPointer, count: Int) -> Int {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
return wrappedWrite(self.fd, data, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func read(_ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
return wrappedRead(self.fd, data, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func readData(count: Int) -> Data {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
var result = Data(count: count)
|
||||||
|
result.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<Int8>) -> Void in
|
||||||
|
let readCount = self.read(bytes, count)
|
||||||
|
assert(readCount == count)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public func seek(position: Int64) {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
lseek(self.fd, position, SEEK_SET)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func truncate(count: Int64) {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
ftruncate(self.fd, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getSize() -> Int? {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
var value = stat()
|
||||||
|
if fstat(self.fd, &value) == 0 {
|
||||||
|
return Int(value.st_size)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sync() {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
fsync(self.fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func compressFrame(width: Int, height: Int, rgbData: Data) -> Data? {
|
||||||
|
let bytesPerRow = rgbData.count / height
|
||||||
|
|
||||||
|
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
|
||||||
|
assert(yuvaPixelsPerAlphaRow % 2 == 0)
|
||||||
|
|
||||||
|
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
|
||||||
|
var yuvaFrameData = malloc(yuvaLength)!
|
||||||
|
defer {
|
||||||
|
free(yuvaFrameData)
|
||||||
|
}
|
||||||
|
memset(yuvaFrameData, 0, yuvaLength)
|
||||||
|
|
||||||
|
var compressedFrameData = Data(count: yuvaLength)
|
||||||
|
let compressedFrameDataLength = compressedFrameData.count
|
||||||
|
|
||||||
|
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
|
||||||
|
defer {
|
||||||
|
free(scratchData)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rgbData = rgbData
|
||||||
|
rgbData.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) -> Void in
|
||||||
|
if let baseAddress = buffer.baseAddress {
|
||||||
|
encodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), baseAddress.assumingMemoryBound(to: UInt8.self), Int32(width), Int32(height), Int32(bytesPerRow))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maybeResultSize: Int?
|
||||||
|
|
||||||
|
compressedFrameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||||
|
let length = compression_encode_buffer(bytes, compressedFrameDataLength, yuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
|
||||||
|
maybeResultSize = length
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let resultSize = maybeResultSize else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
compressedFrameData.count = resultSize
|
||||||
|
return compressedFrameData
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class AnimatedStickerDirectFrameSourceCache {
|
||||||
|
private let queue: Queue
|
||||||
|
private let storeQueue: Queue
|
||||||
|
private let file: ManagedFileImpl
|
||||||
|
private let frameCount: Int
|
||||||
|
private let width: Int
|
||||||
|
private let height: Int
|
||||||
|
|
||||||
|
private var isStoringFrames = Set<Int>()
|
||||||
|
|
||||||
|
private var scratchBuffer: Data
|
||||||
|
private var decodeBuffer: Data
|
||||||
|
|
||||||
|
init?(queue: Queue, pathPrefix: String, width: Int, height: Int, frameCount: Int) {
|
||||||
|
self.queue = queue
|
||||||
|
self.storeQueue = sharedStoreQueue
|
||||||
|
|
||||||
|
self.frameCount = frameCount
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
|
||||||
|
let path = "\(pathPrefix)_\(width):\(height).stickerframecache"
|
||||||
|
var file = ManagedFileImpl(queue: queue, path: path, mode: .readwrite)
|
||||||
|
if let file = file {
|
||||||
|
self.file = file
|
||||||
|
} else {
|
||||||
|
let _ = try? FileManager.default.removeItem(atPath: path)
|
||||||
|
file = ManagedFileImpl(queue: queue, path: path, mode: .readwrite)
|
||||||
|
if let file = file {
|
||||||
|
self.file = file
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
|
||||||
|
|
||||||
|
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
|
||||||
|
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
|
||||||
|
self.decodeBuffer = Data(count: yuvaLength)
|
||||||
|
|
||||||
|
self.initializeFrameTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initializeFrameTable() {
|
||||||
|
if let size = self.file.getSize(), size >= self.frameCount * 4 * 2 {
|
||||||
|
} else {
|
||||||
|
self.file.truncate(count: 0)
|
||||||
|
for _ in 0 ..< self.frameCount {
|
||||||
|
var zero: Int32 = 0
|
||||||
|
let _ = self.file.write(&zero, count: 4)
|
||||||
|
let _ = self.file.write(&zero, count: 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func readFrameRange(index: Int) -> Range<Int>? {
|
||||||
|
if index < 0 || index >= self.frameCount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.file.seek(position: Int64(index * 4 * 2))
|
||||||
|
var offset: Int32 = 0
|
||||||
|
var length: Int32 = 0
|
||||||
|
if self.file.read(&offset, 4) != 4 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if self.file.read(&length, 4) != 4 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if length == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if length < 0 || offset < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (Int(offset) ..< Int(offset + length))
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
|
||||||
|
if index < 0 || index >= self.frameCount {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.isStoringFrames.contains(index) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isStoringFrames.insert(index)
|
||||||
|
|
||||||
|
let width = self.width
|
||||||
|
let height = self.height
|
||||||
|
|
||||||
|
let queue = self.queue
|
||||||
|
self.storeQueue.async { [weak self] in
|
||||||
|
let compressedData = compressFrame(width: width, height: height, rgbData: rgbData)
|
||||||
|
|
||||||
|
queue.async {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let currentSize = strongSelf.file.getSize() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let compressedData = compressedData else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.file.seek(position: Int64(index * 4 * 2))
|
||||||
|
var offset = Int32(currentSize)
|
||||||
|
var length = Int32(compressedData.count)
|
||||||
|
let _ = strongSelf.file.write(&offset, count: 4)
|
||||||
|
let _ = strongSelf.file.write(&length, count: 4)
|
||||||
|
strongSelf.file.seek(position: Int64(currentSize))
|
||||||
|
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
|
||||||
|
if let baseAddress = buffer.baseAddress {
|
||||||
|
let _ = strongSelf.file.write(baseAddress, count: Int(length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUncompressedYuvFrame(index: Int) -> Data? {
|
||||||
|
if index < 0 || index >= self.frameCount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let range = self.readFrameRange(index: index) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.file.seek(position: Int64(range.lowerBound))
|
||||||
|
let length = range.upperBound - range.lowerBound
|
||||||
|
let compressedData = self.file.readData(count: length)
|
||||||
|
if compressedData.count != length {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameData: Data?
|
||||||
|
|
||||||
|
let decodeBufferLength = self.decodeBuffer.count
|
||||||
|
|
||||||
|
compressedData.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
|
||||||
|
self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||||
|
self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||||
|
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||||
|
|
||||||
|
frameData = Data(bytes: decodeBytes, count: resultLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frameData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||||
private let queue: Queue
|
private let queue: Queue
|
||||||
private let data: Data
|
private let data: Data
|
||||||
private let width: Int
|
private let width: Int
|
||||||
private let height: Int
|
private let height: Int
|
||||||
|
private let cache: AnimatedStickerDirectFrameSourceCache?
|
||||||
private let bytesPerRow: Int
|
private let bytesPerRow: Int
|
||||||
let frameCount: Int
|
let frameCount: Int
|
||||||
let frameRate: Int
|
let frameRate: Int
|
||||||
@ -282,7 +594,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
|||||||
return self.currentFrame % self.frameCount
|
return self.currentFrame % self.frameCount
|
||||||
}
|
}
|
||||||
|
|
||||||
init?(queue: Queue, data: Data, width: Int, height: Int) {
|
init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?) {
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.data = data
|
self.data = data
|
||||||
self.width = width
|
self.width = width
|
||||||
@ -294,8 +606,13 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
self.animation = animation
|
self.animation = animation
|
||||||
self.frameCount = Int(animation.frameCount)
|
let frameCount = Int(animation.frameCount)
|
||||||
|
self.frameCount = frameCount
|
||||||
self.frameRate = Int(animation.frameRate)
|
self.frameRate = Int(animation.frameRate)
|
||||||
|
|
||||||
|
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
|
||||||
|
AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -306,12 +623,19 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
|||||||
let frameIndex = self.currentFrame % self.frameCount
|
let frameIndex = self.currentFrame % self.frameCount
|
||||||
self.currentFrame += 1
|
self.currentFrame += 1
|
||||||
if draw {
|
if draw {
|
||||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
if let cache = self.cache, let yuvData = cache.readUncompressedYuvFrame(index: frameIndex) {
|
||||||
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: 0, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||||
memset(bytes, 0, self.bytesPerRow * self.height)
|
} else {
|
||||||
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow))
|
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||||
|
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||||
|
memset(bytes, 0, self.bytesPerRow * self.height)
|
||||||
|
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow))
|
||||||
|
}
|
||||||
|
if let cache = self.cache {
|
||||||
|
cache.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
|
||||||
|
}
|
||||||
|
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||||
}
|
}
|
||||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -409,7 +733,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
|
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
|
||||||
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
|
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
|
||||||
|
|
||||||
private var directData: (Data, String, Int, Int)?
|
private var directData: (Data, String, Int, Int, String?)?
|
||||||
private var cachedData: (Data, Bool)?
|
private var cachedData: (Data, Bool)?
|
||||||
|
|
||||||
private var renderer: (AnimationRenderer & ASDisplayNode)?
|
private var renderer: (AnimationRenderer & ASDisplayNode)?
|
||||||
@ -479,13 +803,13 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
self.playbackMode = playbackMode
|
self.playbackMode = playbackMode
|
||||||
switch mode {
|
switch mode {
|
||||||
case .direct:
|
case let .direct(cachePathPrefix):
|
||||||
let f: (String) -> Void = { [weak self] path in
|
let f: (String) -> Void = { [weak self] path in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||||
strongSelf.directData = (directData, path, width, height)
|
strongSelf.directData = (directData, path, width, height, cachePathPrefix)
|
||||||
}
|
}
|
||||||
if case let .still(position) = playbackMode {
|
if case let .still(position) = playbackMode {
|
||||||
strongSelf.seekTo(position)
|
strongSelf.seekTo(position)
|
||||||
@ -568,7 +892,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
if maybeFrameSource == nil {
|
if maybeFrameSource == nil {
|
||||||
let notifyUpdated: (() -> Void)? = nil
|
let notifyUpdated: (() -> Void)? = nil
|
||||||
if let directData = directData {
|
if let directData = directData {
|
||||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||||
if #available(iOS 9.0, *) {
|
if #available(iOS 9.0, *) {
|
||||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
||||||
@ -640,7 +964,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||||
let notifyUpdated: (() -> Void)? = nil
|
let notifyUpdated: (() -> Void)? = nil
|
||||||
if let directData = directData {
|
if let directData = directData {
|
||||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||||
if #available(iOS 9.0, *) {
|
if #available(iOS 9.0, *) {
|
||||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
||||||
@ -730,7 +1054,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||||
if let directData = directData {
|
if let directData = directData {
|
||||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||||
if case .end = position {
|
if case .end = position {
|
||||||
maybeFrameSource?.skipToEnd()
|
maybeFrameSource?.skipToEnd()
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,13 @@ import YuvConversion
|
|||||||
final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||||
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, completion: @escaping () -> Void) {
|
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, completion: @escaping () -> Void) {
|
||||||
queue.async { [weak self] in
|
queue.async { [weak self] in
|
||||||
let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15)
|
switch type {
|
||||||
assert(bytesPerRow == calculatedBytesPerRow)
|
case .argb:
|
||||||
|
let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15)
|
||||||
|
assert(bytesPerRow == calculatedBytesPerRow)
|
||||||
|
case .yuva:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
let image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, bytesPerRow in
|
let image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, bytesPerRow in
|
||||||
switch type {
|
switch type {
|
||||||
|
@ -68,7 +68,7 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
animationName = "ChatListEmpty"
|
animationName = "ChatListEmpty"
|
||||||
}
|
}
|
||||||
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
|
||||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct)
|
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
||||||
self.animationNode.visibility = true
|
self.animationNode.visibility = true
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode {
|
|||||||
animationName = "ChatListNewFolder"
|
animationName = "ChatListNewFolder"
|
||||||
}
|
}
|
||||||
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
|
||||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
|
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
strongSelf.animationNode.visibility = true
|
strongSelf.animationNode.visibility = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView {
|
|||||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource)
|
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource)
|
||||||
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct)
|
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||||
|
|
||||||
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
||||||
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
||||||
|
@ -700,14 +700,14 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
|
|||||||
case .emailConfirmation:
|
case .emailConfirmation:
|
||||||
if let path = getAppBundle().path(forResource: "TwoFactorSetupMail", ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: "TwoFactorSetupMail", ofType: "tgs") {
|
||||||
let animatedStickerNode = AnimatedStickerNode()
|
let animatedStickerNode = AnimatedStickerNode()
|
||||||
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct)
|
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
animatedStickerNode.visibility = true
|
animatedStickerNode.visibility = true
|
||||||
self.animatedStickerNode = animatedStickerNode
|
self.animatedStickerNode = animatedStickerNode
|
||||||
}
|
}
|
||||||
case .passwordHint:
|
case .passwordHint:
|
||||||
if let path = getAppBundle().path(forResource: "TwoFactorSetupHint", ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: "TwoFactorSetupHint", ofType: "tgs") {
|
||||||
let animatedStickerNode = AnimatedStickerNode()
|
let animatedStickerNode = AnimatedStickerNode()
|
||||||
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct)
|
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
animatedStickerNode.visibility = true
|
animatedStickerNode.visibility = true
|
||||||
self.animatedStickerNode = animatedStickerNode
|
self.animatedStickerNode = animatedStickerNode
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode {
|
|||||||
buttonText = self.presentationData.strings.TwoFactorSetup_Intro_Action
|
buttonText = self.presentationData.strings.TwoFactorSetup_Intro_Action
|
||||||
|
|
||||||
if let path = getAppBundle().path(forResource: "TwoFactorSetupIntro", ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: "TwoFactorSetupIntro", ofType: "tgs") {
|
||||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct)
|
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
||||||
self.animationNode.visibility = true
|
self.animationNode.visibility = true
|
||||||
}
|
}
|
||||||
@ -122,7 +122,7 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode {
|
|||||||
buttonText = self.presentationData.strings.TwoFactorSetup_Done_Action
|
buttonText = self.presentationData.strings.TwoFactorSetup_Done_Action
|
||||||
|
|
||||||
if let path = getAppBundle().path(forResource: "TwoFactorSetupDone", ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: "TwoFactorSetupDone", ofType: "tgs") {
|
||||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, mode: .direct)
|
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, mode: .direct(cachePathPrefix: nil))
|
||||||
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
||||||
self.animationNode.visibility = true
|
self.animationNode.visibility = true
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ class PeersNearbyHeaderItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
self.animationNode = AnimatedStickerNode()
|
self.animationNode = AnimatedStickerNode()
|
||||||
if let path = getAppBundle().path(forResource: "Compass", ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: "Compass", ofType: "tgs") {
|
||||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
|
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
self.animationNode.visibility = true
|
self.animationNode.visibility = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +239,11 @@ public final class MediaBox {
|
|||||||
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)"
|
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func shortLivedResourceCachePathPrefix(_ id: MediaResourceId) -> String {
|
||||||
|
let cacheString = "short-cache"
|
||||||
|
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id))"
|
||||||
|
}
|
||||||
|
|
||||||
public func storeResourceData(_ id: MediaResourceId, data: Data, synchronous: Bool = false) {
|
public func storeResourceData(_ id: MediaResourceId, data: Data, synchronous: Bool = false) {
|
||||||
let begin = {
|
let begin = {
|
||||||
let paths = self.storePathsForId(id)
|
let paths = self.storePathsForId(id)
|
||||||
|
@ -55,7 +55,7 @@ final class StatsEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
|
|||||||
|
|
||||||
self.animationNode = AnimatedStickerNode()
|
self.animationNode = AnimatedStickerNode()
|
||||||
if let path = getAppBundle().path(forResource: "Charts", ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: "Charts", ofType: "tgs") {
|
||||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct)
|
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||||
self.animationNode.visibility = true
|
self.animationNode.visibility = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
|||||||
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))
|
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))
|
||||||
|
|
||||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct)
|
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||||
self.animationNode?.visibility = true
|
self.animationNode?.visibility = true
|
||||||
self.animationNode?.addSubnode(self.textNode)
|
self.animationNode?.addSubnode(self.textNode)
|
||||||
} else {
|
} else {
|
||||||
|
@ -377,12 +377,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
if let file = file {
|
if let file = file {
|
||||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||||
let mode: AnimatedStickerMode
|
|
||||||
if file.resource is LocalFileReferenceMediaResource {
|
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||||
mode = .direct
|
let mode: AnimatedStickerMode = .direct(cachePathPrefix: pathPrefix)
|
||||||
} else {
|
|
||||||
mode = .cached
|
|
||||||
}
|
|
||||||
animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
|
animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,7 +327,7 @@ public final class NotificationViewControllerImpl {
|
|||||||
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
|
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0))
|
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0))
|
||||||
strongSelf.imageNode.setSignal(chatMessageAnimatedSticker(postbox: accountAndImage.0.postbox, file: fileReference.media, small: false, size: fittedDimensions))
|
strongSelf.imageNode.setSignal(chatMessageAnimatedSticker(postbox: accountAndImage.0.postbox, file: fileReference.media, small: false, size: fittedDimensions))
|
||||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: accountAndImage.0, resource: fileReference.media.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct)
|
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: accountAndImage.0, resource: fileReference.media.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||||
animatedStickerNode.visibility = true
|
animatedStickerNode.visibility = true
|
||||||
|
|
||||||
accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true))
|
accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true))
|
||||||
|
@ -89,12 +89,12 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||||||
break
|
break
|
||||||
case .chatListPress:
|
case .chatListPress:
|
||||||
if let path = getAppBundle().path(forResource: "ChatListFoldersTooltip", ofType: "json") {
|
if let path = getAppBundle().path(forResource: "ChatListFoldersTooltip", ofType: "json") {
|
||||||
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct)
|
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
self.animatedStickerNode.automaticallyLoadFirstFrame = true
|
self.animatedStickerNode.automaticallyLoadFirstFrame = true
|
||||||
}
|
}
|
||||||
case .info:
|
case .info:
|
||||||
if let path = getAppBundle().path(forResource: "anim_infotip", ofType: "json") {
|
if let path = getAppBundle().path(forResource: "anim_infotip", ofType: "json") {
|
||||||
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct)
|
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
self.animatedStickerNode.automaticallyLoadFirstFrame = true
|
self.animatedStickerNode.automaticallyLoadFirstFrame = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||||||
self.animationNode = nil
|
self.animationNode = nil
|
||||||
self.animatedStickerNode = AnimatedStickerNode()
|
self.animatedStickerNode = AnimatedStickerNode()
|
||||||
self.animatedStickerNode?.visibility = true
|
self.animatedStickerNode?.visibility = true
|
||||||
self.animatedStickerNode?.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 100, height: 100, playbackMode: .once, mode: .direct)
|
self.animatedStickerNode?.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 100, height: 100, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
|
|
||||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||||
@ -349,7 +349,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||||||
case let .result(_, items, _):
|
case let .result(_, items, _):
|
||||||
let item = items[Int(value)]
|
let item = items[Int(value)]
|
||||||
if let item = item as? StickerPackItem {
|
if let item = item as? StickerPackItem {
|
||||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: 120, height: 120, playbackMode: .once, mode: .direct)
|
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: 120, height: 120, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
Loading…
x
Reference in New Issue
Block a user