mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Reimplement video stickers
This commit is contained in:
parent
97f224f0a8
commit
bbc082e991
@ -31,6 +31,8 @@ swift_library(
|
||||
"//submodules/GZip:GZip",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/MediaResources:MediaResources",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -0,0 +1,514 @@
|
||||
import Foundation
|
||||
import Compression
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import MediaResources
|
||||
import RLottieBinding
|
||||
import GZip
|
||||
import ManagedFile
|
||||
|
||||
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
|
||||
|
||||
public extension EmojiFitzModifier {
|
||||
var lottieFitzModifier: LottieFitzModifier {
|
||||
switch self {
|
||||
case .type12:
|
||||
return .type12
|
||||
case .type3:
|
||||
return .type3
|
||||
case .type4:
|
||||
return .type4
|
||||
case .type5:
|
||||
return .type5
|
||||
case .type6:
|
||||
return .type6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AnimatedStickerFrameSource: AnyObject {
|
||||
var frameRate: Int { get }
|
||||
var frameCount: Int { get }
|
||||
var frameIndex: Int { get }
|
||||
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame?
|
||||
func skipToEnd()
|
||||
func skipToFrameIndex(_ index: Int)
|
||||
}
|
||||
|
||||
final class AnimatedStickerFrameSourceWrapper {
|
||||
let value: AnimatedStickerFrameSource
|
||||
|
||||
init(_ value: AnimatedStickerFrameSource) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private var data: Data
|
||||
private var dataComplete: Bool
|
||||
private let notifyUpdated: () -> Void
|
||||
|
||||
private var scratchBuffer: Data
|
||||
let width: Int
|
||||
let bytesPerRow: Int
|
||||
let height: Int
|
||||
public let frameRate: Int
|
||||
public let frameCount: Int
|
||||
public var frameIndex: Int
|
||||
private let initialOffset: Int
|
||||
private var offset: Int
|
||||
var decodeBuffer: Data
|
||||
var frameBuffer: Data
|
||||
|
||||
public init?(queue: Queue, data: Data, complete: Bool, notifyUpdated: @escaping () -> Void) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.dataComplete = complete
|
||||
self.notifyUpdated = notifyUpdated
|
||||
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
|
||||
|
||||
var offset = 0
|
||||
var width = 0
|
||||
var height = 0
|
||||
var bytesPerRow = 0
|
||||
var frameRate = 0
|
||||
var frameCount = 0
|
||||
|
||||
if !self.data.withUnsafeBytes({ buffer -> Bool in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return false
|
||||
}
|
||||
var frameRateValue: Int32 = 0
|
||||
var frameCountValue: Int32 = 0
|
||||
var widthValue: Int32 = 0
|
||||
var heightValue: Int32 = 0
|
||||
var bytesPerRowValue: Int32 = 0
|
||||
memcpy(&frameRateValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&frameCountValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&widthValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&heightValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&bytesPerRowValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
frameRate = Int(frameRateValue)
|
||||
frameCount = Int(frameCountValue)
|
||||
width = Int(widthValue)
|
||||
height = Int(heightValue)
|
||||
bytesPerRow = Int(bytesPerRowValue)
|
||||
|
||||
return true
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.bytesPerRow = bytesPerRow
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.frameRate = frameRate
|
||||
self.frameCount = frameCount
|
||||
|
||||
self.frameIndex = 0
|
||||
self.initialOffset = offset
|
||||
self.offset = offset
|
||||
|
||||
self.decodeBuffer = Data(count: self.bytesPerRow * height)
|
||||
self.frameBuffer = Data(count: self.bytesPerRow * height)
|
||||
let frameBufferLength = self.frameBuffer.count
|
||||
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
memset(bytes, 0, frameBufferLength)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
var frameData: Data?
|
||||
var isLastFrame = false
|
||||
|
||||
let dataLength = self.data.count
|
||||
let decodeBufferLength = self.decodeBuffer.count
|
||||
let frameBufferLength = self.frameBuffer.count
|
||||
|
||||
let frameIndex = self.frameIndex
|
||||
|
||||
self.data.withUnsafeBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.offset + 4 > dataLength {
|
||||
if self.dataComplete {
|
||||
self.frameIndex = 0
|
||||
self.offset = self.initialOffset
|
||||
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
memset(bytes, 0, frameBufferLength)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var frameLength: Int32 = 0
|
||||
memcpy(&frameLength, bytes.advanced(by: self.offset), 4)
|
||||
|
||||
if self.offset + 4 + Int(frameLength) > dataLength {
|
||||
return
|
||||
}
|
||||
|
||||
self.offset += 4
|
||||
|
||||
if draw {
|
||||
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
|
||||
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
|
||||
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.frameBuffer.withUnsafeMutableBytes { frameBuffer -> Void in
|
||||
guard let frameBytes = frameBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self)
|
||||
var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self)
|
||||
for _ in 0 ..< decodeBufferLength / 8 {
|
||||
lhs.pointee = lhs.pointee ^ rhs.pointee
|
||||
lhs = lhs.advanced(by: 1)
|
||||
rhs = rhs.advanced(by: 1)
|
||||
}
|
||||
var lhsRest = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
var rhsRest = UnsafeMutableRawPointer(decodeBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
for _ in (decodeBufferLength / 8) * 8 ..< decodeBufferLength {
|
||||
lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee
|
||||
lhsRest = lhsRest.advanced(by: 1)
|
||||
rhsRest = rhsRest.advanced(by: 1)
|
||||
}
|
||||
|
||||
frameData = Data(bytes: frameBytes, count: decodeBufferLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.frameIndex += 1
|
||||
self.offset += Int(frameLength)
|
||||
if self.offset == dataLength && self.dataComplete {
|
||||
isLastFrame = true
|
||||
self.frameIndex = 0
|
||||
self.offset = self.initialOffset
|
||||
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
memset(bytes, 0, frameBufferLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let frameData = frameData, draw {
|
||||
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame, totalFrames: self.frameCount)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateData(data: Data, complete: Bool) {
|
||||
self.data = data
|
||||
self.dataComplete = complete
|
||||
}
|
||||
|
||||
public func skipToEnd() {
|
||||
}
|
||||
|
||||
public func skipToFrameIndex(_ index: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class AnimatedStickerDirectFrameSourceCache {
|
||||
private enum FrameRangeResult {
|
||||
case range(Range<Int>)
|
||||
case notFound
|
||||
case corruptedFile
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let storeQueue: Queue
|
||||
private let file: ManagedFile
|
||||
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, fitzModifier: EmojiFitzModifier?) {
|
||||
self.queue = queue
|
||||
self.storeQueue = sharedStoreQueue
|
||||
|
||||
self.frameCount = frameCount
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
let suffix : String
|
||||
if let fitzModifier = fitzModifier {
|
||||
suffix = "_fitz\(fitzModifier.rawValue)"
|
||||
} else {
|
||||
suffix = ""
|
||||
}
|
||||
let path = "\(pathPrefix)_\(width):\(height)\(suffix).stickerframecache"
|
||||
var file = ManagedFile(queue: queue, path: path, mode: .readwrite)
|
||||
if let file = file {
|
||||
self.file = file
|
||||
} else {
|
||||
let _ = try? FileManager.default.removeItem(atPath: path)
|
||||
file = ManagedFile(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) -> FrameRangeResult {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return .notFound
|
||||
}
|
||||
|
||||
self.file.seek(position: Int64(index * 4 * 2))
|
||||
var offset: Int32 = 0
|
||||
var length: Int32 = 0
|
||||
if self.file.read(&offset, 4) != 4 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if self.file.read(&length, 4) != 4 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if length == 0 {
|
||||
return .notFound
|
||||
}
|
||||
if length < 0 || offset < 0 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if Int64(offset) + Int64(length) > 100 * 1024 * 1024 {
|
||||
return .corruptedFile
|
||||
}
|
||||
|
||||
return .range(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 readUncompressedYuvaFrame(index: Int) -> Data? {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return nil
|
||||
}
|
||||
let rangeResult = self.readFrameRange(index: index)
|
||||
|
||||
switch rangeResult {
|
||||
case let .range(range):
|
||||
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 { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
|
||||
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
|
||||
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
frameData = Data(bytes: decodeBytes, count: resultLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frameData
|
||||
case .notFound:
|
||||
return nil
|
||||
case .corruptedFile:
|
||||
self.file.truncate(count: 0)
|
||||
self.initializeFrameTable()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let data: Data
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
private let cache: AnimatedStickerDirectFrameSourceCache?
|
||||
private let bytesPerRow: Int
|
||||
let frameCount: Int
|
||||
let frameRate: Int
|
||||
fileprivate var currentFrame: Int
|
||||
private let animation: LottieInstance
|
||||
|
||||
var frameIndex: Int {
|
||||
return self.currentFrame % self.frameCount
|
||||
}
|
||||
|
||||
init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?, fitzModifier: EmojiFitzModifier?) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width))
|
||||
self.currentFrame = 0
|
||||
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
|
||||
|
||||
guard let animation = LottieInstance(data: decompressedData, fitzModifier: fitzModifier?.lottieFitzModifier ?? .none, cacheKey: "") else {
|
||||
return nil
|
||||
}
|
||||
self.animation = animation
|
||||
let frameCount = Int(animation.frameCount)
|
||||
self.frameCount = frameCount
|
||||
self.frameRate = Int(animation.frameRate)
|
||||
|
||||
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
|
||||
AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount, fitzModifier: fitzModifier)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
let frameIndex = self.currentFrame % self.frameCount
|
||||
self.currentFrame += 1
|
||||
if draw {
|
||||
if let cache = self.cache, let yuvData = cache.readUncompressedYuvaFrame(index: frameIndex) {
|
||||
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
|
||||
} else {
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
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, totalFrames: self.frameCount)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func skipToEnd() {
|
||||
self.currentFrame = self.frameCount - 1
|
||||
}
|
||||
|
||||
func skipToFrameIndex(_ index: Int) {
|
||||
self.currentFrame = index
|
||||
}
|
||||
}
|
@ -3,30 +3,10 @@ import SwiftSignalKit
|
||||
import Compression
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import RLottieBinding
|
||||
import GZip
|
||||
import YuvConversion
|
||||
import MediaResources
|
||||
|
||||
public extension EmojiFitzModifier {
|
||||
var lottieFitzModifier: LottieFitzModifier {
|
||||
switch self {
|
||||
case .type12:
|
||||
return .type12
|
||||
case .type3:
|
||||
return .type3
|
||||
case .type4:
|
||||
return .type4
|
||||
case .type5:
|
||||
return .type5
|
||||
case .type6:
|
||||
return .type6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let sharedQueue = Queue()
|
||||
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
|
||||
|
||||
private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
||||
private var value: Bool = false
|
||||
@ -106,654 +86,6 @@ public final class AnimatedStickerFrame {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AnimatedStickerFrameSource: AnyObject {
|
||||
var frameRate: Int { get }
|
||||
var frameCount: Int { get }
|
||||
var frameIndex: Int { get }
|
||||
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame?
|
||||
func skipToEnd()
|
||||
func skipToFrameIndex(_ index: Int)
|
||||
}
|
||||
|
||||
private final class AnimatedStickerFrameSourceWrapper {
|
||||
let value: AnimatedStickerFrameSource
|
||||
|
||||
init(_ value: AnimatedStickerFrameSource) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private var data: Data
|
||||
private var dataComplete: Bool
|
||||
private let notifyUpdated: () -> Void
|
||||
|
||||
private var scratchBuffer: Data
|
||||
let width: Int
|
||||
let bytesPerRow: Int
|
||||
let height: Int
|
||||
public let frameRate: Int
|
||||
public let frameCount: Int
|
||||
public var frameIndex: Int
|
||||
private let initialOffset: Int
|
||||
private var offset: Int
|
||||
var decodeBuffer: Data
|
||||
var frameBuffer: Data
|
||||
|
||||
public init?(queue: Queue, data: Data, complete: Bool, notifyUpdated: @escaping () -> Void) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.dataComplete = complete
|
||||
self.notifyUpdated = notifyUpdated
|
||||
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
|
||||
|
||||
var offset = 0
|
||||
var width = 0
|
||||
var height = 0
|
||||
var bytesPerRow = 0
|
||||
var frameRate = 0
|
||||
var frameCount = 0
|
||||
|
||||
if !self.data.withUnsafeBytes({ buffer -> Bool in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return false
|
||||
}
|
||||
var frameRateValue: Int32 = 0
|
||||
var frameCountValue: Int32 = 0
|
||||
var widthValue: Int32 = 0
|
||||
var heightValue: Int32 = 0
|
||||
var bytesPerRowValue: Int32 = 0
|
||||
memcpy(&frameRateValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&frameCountValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&widthValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&heightValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
memcpy(&bytesPerRowValue, bytes.advanced(by: offset), 4)
|
||||
offset += 4
|
||||
frameRate = Int(frameRateValue)
|
||||
frameCount = Int(frameCountValue)
|
||||
width = Int(widthValue)
|
||||
height = Int(heightValue)
|
||||
bytesPerRow = Int(bytesPerRowValue)
|
||||
|
||||
return true
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.bytesPerRow = bytesPerRow
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.frameRate = frameRate
|
||||
self.frameCount = frameCount
|
||||
|
||||
self.frameIndex = 0
|
||||
self.initialOffset = offset
|
||||
self.offset = offset
|
||||
|
||||
self.decodeBuffer = Data(count: self.bytesPerRow * height)
|
||||
self.frameBuffer = Data(count: self.bytesPerRow * height)
|
||||
let frameBufferLength = self.frameBuffer.count
|
||||
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
memset(bytes, 0, frameBufferLength)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
var frameData: Data?
|
||||
var isLastFrame = false
|
||||
|
||||
let dataLength = self.data.count
|
||||
let decodeBufferLength = self.decodeBuffer.count
|
||||
let frameBufferLength = self.frameBuffer.count
|
||||
|
||||
let frameIndex = self.frameIndex
|
||||
|
||||
self.data.withUnsafeBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.offset + 4 > dataLength {
|
||||
if self.dataComplete {
|
||||
self.frameIndex = 0
|
||||
self.offset = self.initialOffset
|
||||
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
memset(bytes, 0, frameBufferLength)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var frameLength: Int32 = 0
|
||||
memcpy(&frameLength, bytes.advanced(by: self.offset), 4)
|
||||
|
||||
if self.offset + 4 + Int(frameLength) > dataLength {
|
||||
return
|
||||
}
|
||||
|
||||
self.offset += 4
|
||||
|
||||
if draw {
|
||||
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
|
||||
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
|
||||
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.frameBuffer.withUnsafeMutableBytes { frameBuffer -> Void in
|
||||
guard let frameBytes = frameBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self)
|
||||
var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self)
|
||||
for _ in 0 ..< decodeBufferLength / 8 {
|
||||
lhs.pointee = lhs.pointee ^ rhs.pointee
|
||||
lhs = lhs.advanced(by: 1)
|
||||
rhs = rhs.advanced(by: 1)
|
||||
}
|
||||
var lhsRest = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
var rhsRest = UnsafeMutableRawPointer(decodeBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
for _ in (decodeBufferLength / 8) * 8 ..< decodeBufferLength {
|
||||
lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee
|
||||
lhsRest = lhsRest.advanced(by: 1)
|
||||
rhsRest = rhsRest.advanced(by: 1)
|
||||
}
|
||||
|
||||
frameData = Data(bytes: frameBytes, count: decodeBufferLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.frameIndex += 1
|
||||
self.offset += Int(frameLength)
|
||||
if self.offset == dataLength && self.dataComplete {
|
||||
isLastFrame = true
|
||||
self.frameIndex = 0
|
||||
self.offset = self.initialOffset
|
||||
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
memset(bytes, 0, frameBufferLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let frameData = frameData, draw {
|
||||
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame, totalFrames: self.frameCount)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateData(data: Data, complete: Bool) {
|
||||
self.data = data
|
||||
self.dataComplete = complete
|
||||
}
|
||||
|
||||
public func skipToEnd() {
|
||||
}
|
||||
|
||||
public func skipToFrameIndex(_ index: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
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 { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
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
|
||||
let 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 { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
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 enum FrameRangeResult {
|
||||
case range(Range<Int>)
|
||||
case notFound
|
||||
case corruptedFile
|
||||
}
|
||||
|
||||
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, fitzModifier: EmojiFitzModifier?) {
|
||||
self.queue = queue
|
||||
self.storeQueue = sharedStoreQueue
|
||||
|
||||
self.frameCount = frameCount
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
let suffix : String
|
||||
if let fitzModifier = fitzModifier {
|
||||
suffix = "_fitz\(fitzModifier.rawValue)"
|
||||
} else {
|
||||
suffix = ""
|
||||
}
|
||||
let path = "\(pathPrefix)_\(width):\(height)\(suffix).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) -> FrameRangeResult {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return .notFound
|
||||
}
|
||||
|
||||
self.file.seek(position: Int64(index * 4 * 2))
|
||||
var offset: Int32 = 0
|
||||
var length: Int32 = 0
|
||||
if self.file.read(&offset, 4) != 4 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if self.file.read(&length, 4) != 4 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if length == 0 {
|
||||
return .notFound
|
||||
}
|
||||
if length < 0 || offset < 0 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if Int64(offset) + Int64(length) > 100 * 1024 * 1024 {
|
||||
return .corruptedFile
|
||||
}
|
||||
|
||||
return .range(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
|
||||
}
|
||||
let rangeResult = self.readFrameRange(index: index)
|
||||
|
||||
switch rangeResult {
|
||||
case let .range(range):
|
||||
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 { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
|
||||
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
|
||||
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
frameData = Data(bytes: decodeBytes, count: resultLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frameData
|
||||
case .notFound:
|
||||
return nil
|
||||
case .corruptedFile:
|
||||
self.file.truncate(count: 0)
|
||||
self.initializeFrameTable()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let data: Data
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
private let cache: AnimatedStickerDirectFrameSourceCache?
|
||||
private let bytesPerRow: Int
|
||||
let frameCount: Int
|
||||
let frameRate: Int
|
||||
fileprivate var currentFrame: Int
|
||||
private let animation: LottieInstance
|
||||
|
||||
var frameIndex: Int {
|
||||
return self.currentFrame % self.frameCount
|
||||
}
|
||||
|
||||
init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?, fitzModifier: EmojiFitzModifier?) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width))
|
||||
self.currentFrame = 0
|
||||
let rawData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
|
||||
let decompressedData = transformedWithFitzModifier(data: rawData, fitzModifier: fitzModifier)
|
||||
|
||||
guard let animation = LottieInstance(data: decompressedData, fitzModifier: fitzModifier?.lottieFitzModifier ?? .none, cacheKey: "") else {
|
||||
return nil
|
||||
}
|
||||
self.animation = animation
|
||||
let frameCount = Int(animation.frameCount)
|
||||
self.frameCount = frameCount
|
||||
self.frameRate = Int(animation.frameRate)
|
||||
|
||||
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
|
||||
AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount, fitzModifier: fitzModifier)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
let frameIndex = self.currentFrame % self.frameCount
|
||||
self.currentFrame += 1
|
||||
if draw {
|
||||
if let cache = self.cache, let yuvData = cache.readUncompressedYuvFrame(index: frameIndex) {
|
||||
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
|
||||
} else {
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
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, totalFrames: self.frameCount)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func skipToEnd() {
|
||||
self.currentFrame = self.frameCount - 1
|
||||
}
|
||||
|
||||
func skipToFrameIndex(_ index: Int) {
|
||||
self.currentFrame = index
|
||||
}
|
||||
}
|
||||
|
||||
public final class AnimatedStickerFrameQueue {
|
||||
private let queue: Queue
|
||||
private let length: Int
|
||||
@ -807,6 +139,7 @@ public struct AnimatedStickerStatus: Equatable {
|
||||
|
||||
public protocol AnimatedStickerNodeSource {
|
||||
var fitzModifier: EmojiFitzModifier? { get }
|
||||
var isVideo: Bool { get }
|
||||
|
||||
func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError>
|
||||
func directDataPath() -> Signal<String, NoError>
|
||||
@ -833,7 +166,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
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?)?
|
||||
private var directData: (Data, String, Int, Int, String?, EmojiFitzModifier?, Bool)?
|
||||
private var cachedData: (Data, Bool, EmojiFitzModifier?)?
|
||||
|
||||
private var renderer: (AnimationRenderer & ASDisplayNode)?
|
||||
@ -937,7 +270,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||
strongSelf.directData = (directData, path, width, height, cachePathPrefix, source.fitzModifier)
|
||||
strongSelf.directData = (directData, path, width, height, cachePathPrefix, source.fitzModifier, source.isVideo)
|
||||
}
|
||||
if case let .still(position) = playbackMode {
|
||||
strongSelf.seekTo(position)
|
||||
@ -1047,7 +380,11 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
if maybeFrameSource == nil {
|
||||
let notifyUpdated: (() -> Void)? = nil
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, fitzModifier: directData.5)
|
||||
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, fitzModifier: directData.5)
|
||||
}
|
||||
} else if let (cachedData, cachedDataComplete, _) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
||||
@ -1147,7 +484,11 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
let notifyUpdated: (() -> Void)? = nil
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, fitzModifier: directData.5)
|
||||
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, fitzModifier: directData.5)
|
||||
}
|
||||
} else if let (cachedData, cachedDataComplete, _) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
||||
@ -1263,7 +604,11 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
if case .timestamp = position {
|
||||
} else {
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, fitzModifier: directData.5)
|
||||
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, fitzModifier: directData.5)
|
||||
}
|
||||
if case .end = position {
|
||||
maybeFrameSource?.skipToEnd()
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import MediaResources
|
||||
|
||||
let colorKeyRegex = try? NSRegularExpression(pattern: "\"k\":\\[[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\]")
|
||||
|
||||
public func transformedWithFitzModifier(data: Data, fitzModifier: EmojiFitzModifier?) -> Data {
|
||||
return data
|
||||
// if let fitzModifier = fitzModifier, var string = String(data: data, encoding: .utf8) {
|
||||
// let colors: [UIColor] = [0xf77e41, 0xffb139, 0xffd140, 0xffdf79].map { UIColor(rgb: $0) }
|
||||
// let replacementColors: [UIColor]
|
||||
// switch fitzModifier {
|
||||
// case .type12:
|
||||
// replacementColors = [0xcb7b55, 0xf6b689, 0xffcda7, 0xffdfc5].map { UIColor(rgb: $0) }
|
||||
// case .type3:
|
||||
// replacementColors = [0xa45a38, 0xdf986b, 0xedb183, 0xf4c3a0].map { UIColor(rgb: $0) }
|
||||
// case .type4:
|
||||
// replacementColors = [0x703a17, 0xab673d, 0xc37f4e, 0xd89667].map { UIColor(rgb: $0) }
|
||||
// case .type5:
|
||||
// replacementColors = [0x4a2409, 0x7d3e0e, 0x965529, 0xa96337].map { UIColor(rgb: $0) }
|
||||
// case .type6:
|
||||
// replacementColors = [0x200f0a, 0x412924, 0x593d37, 0x63453f].map { UIColor(rgb: $0) }
|
||||
// }
|
||||
//
|
||||
// func colorToString(_ color: UIColor) -> String {
|
||||
// var r: CGFloat = 0.0
|
||||
// var g: CGFloat = 0.0
|
||||
// var b: CGFloat = 0.0
|
||||
// if color.getRed(&r, green: &g, blue: &b, alpha: nil) {
|
||||
// return "\"k\":[\(r),\(g),\(b),1]"
|
||||
// }
|
||||
// return ""
|
||||
// }
|
||||
//
|
||||
// func match(_ a: Double, _ b: Double, eps: Double) -> Bool {
|
||||
// return abs(a - b) < eps
|
||||
// }
|
||||
//
|
||||
// var replacements: [(NSTextCheckingResult, String)] = []
|
||||
//
|
||||
// if let colorKeyRegex = colorKeyRegex {
|
||||
// let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string))
|
||||
// for result in results.reversed() {
|
||||
// if let range = Range(result.range, in: string) {
|
||||
// let substring = String(string[range])
|
||||
// let color = substring[substring.index(string.startIndex, offsetBy: "\"k\":[".count) ..< substring.index(before: substring.endIndex)]
|
||||
// let components = color.split(separator: ",")
|
||||
// if components.count == 4, let r = Double(components[0]), let g = Double(components[1]), let b = Double(components[2]), let a = Double(components[3]) {
|
||||
// if match(a, 1.0, eps: 0.01) {
|
||||
// for i in 0 ..< colors.count {
|
||||
// let color = colors[i]
|
||||
// var cr: CGFloat = 0.0
|
||||
// var cg: CGFloat = 0.0
|
||||
// var cb: CGFloat = 0.0
|
||||
// if color.getRed(&cr, green: &cg, blue: &cb, alpha: nil) {
|
||||
// if match(r, Double(cr), eps: 0.01) && match(g, Double(cg), eps: 0.01) && match(b, Double(cb), eps: 0.01) {
|
||||
// replacements.append((result, colorToString(replacementColors[i])))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for (result, text) in replacements {
|
||||
// if let range = Range(result.range, in: string) {
|
||||
// string = string.replacingCharacters(in: range, with: text)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return string.data(using: .utf8) ?? data
|
||||
// } else {
|
||||
// return data
|
||||
// }
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import Foundation
|
||||
import Compression
|
||||
import YuvConversion
|
||||
|
||||
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
|
||||
let 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 { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func compressFrame(width: Int, height: Int, yuvaData: Data) -> Data? {
|
||||
let yuvaLength = yuvaData.count
|
||||
|
||||
var compressedFrameData = Data(count: yuvaLength)
|
||||
let compressedFrameDataLength = compressedFrameData.count
|
||||
|
||||
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
|
||||
defer {
|
||||
free(scratchData)
|
||||
}
|
||||
|
||||
var maybeResultSize: Int?
|
||||
|
||||
yuvaData.withUnsafeBytes { yuvaBuffer -> Void in
|
||||
if let yuvaFrameData = yuvaBuffer.baseAddress {
|
||||
compressedFrameData.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
@ -15,7 +15,7 @@ final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||
queue.async { [weak self] in
|
||||
switch type {
|
||||
case .argb:
|
||||
let calculatedBytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width))
|
||||
let calculatedBytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width))
|
||||
assert(bytesPerRow == calculatedBytesPerRow)
|
||||
case .yuva:
|
||||
break
|
||||
|
@ -0,0 +1,380 @@
|
||||
import Foundation
|
||||
import Compression
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import UniversalMediaPlayer
|
||||
import CoreMedia
|
||||
import ManagedFile
|
||||
import Accelerate
|
||||
|
||||
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
|
||||
|
||||
private let maximumFrameCount = 30 * 10
|
||||
|
||||
private final class VideoStickerFrameSourceCache {
|
||||
private enum FrameRangeResult {
|
||||
case range(Range<Int>)
|
||||
case notFound
|
||||
case corruptedFile
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let storeQueue: Queue
|
||||
private let file: ManagedFile
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
|
||||
public var frameCount: Int32 = 0
|
||||
|
||||
private var isStoringFrames = Set<Int>()
|
||||
|
||||
private var scratchBuffer: Data
|
||||
private var decodeBuffer: Data
|
||||
|
||||
init?(queue: Queue, pathPrefix: String, width: Int, height: Int) {
|
||||
self.queue = queue
|
||||
self.storeQueue = sharedStoreQueue
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
let path = "\(pathPrefix)_\(width)x\(height).vstickerframecache"
|
||||
var file = ManagedFile(queue: queue, path: path, mode: .readwrite)
|
||||
if let file = file {
|
||||
self.file = file
|
||||
} else {
|
||||
let _ = try? FileManager.default.removeItem(atPath: path)
|
||||
file = ManagedFile(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 >= maximumFrameCount {
|
||||
let _ = self.readFrameRate()
|
||||
} else {
|
||||
self.file.truncate(count: 0)
|
||||
var zero: Int32 = 0
|
||||
let _ = self.file.write(&zero, count: 4)
|
||||
for _ in 0 ..< maximumFrameCount {
|
||||
let _ = self.file.write(&zero, count: 4)
|
||||
let _ = self.file.write(&zero, count: 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readFrameRate() -> Bool {
|
||||
guard self.frameCount == 0 else {
|
||||
return true
|
||||
}
|
||||
|
||||
self.file.seek(position: 0)
|
||||
|
||||
var frameCount: Int32 = 0
|
||||
if self.file.read(&frameCount, 4) != 4 {
|
||||
return false
|
||||
}
|
||||
if frameCount < 0 {
|
||||
return false
|
||||
}
|
||||
if frameCount == 0 {
|
||||
return false
|
||||
}
|
||||
self.frameCount = frameCount
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func readFrameRange(index: Int) -> FrameRangeResult {
|
||||
if index < 0 || index >= maximumFrameCount {
|
||||
return .notFound
|
||||
}
|
||||
|
||||
guard self.readFrameRate() else {
|
||||
return .notFound
|
||||
}
|
||||
|
||||
if index >= self.frameCount {
|
||||
return .notFound
|
||||
}
|
||||
|
||||
self.file.seek(position: Int64(4 + index * 4 * 2))
|
||||
var offset: Int32 = 0
|
||||
var length: Int32 = 0
|
||||
if self.file.read(&offset, 4) != 4 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if self.file.read(&length, 4) != 4 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if length == 0 {
|
||||
return .notFound
|
||||
}
|
||||
if length < 0 || offset < 0 {
|
||||
return .corruptedFile
|
||||
}
|
||||
if Int64(offset) + Int64(length) > 100 * 1024 * 1024 {
|
||||
return .corruptedFile
|
||||
}
|
||||
|
||||
return .range(Int(offset) ..< Int(offset + length))
|
||||
}
|
||||
|
||||
func storeFrameCount(_ count: Int) {
|
||||
self.file.seek(position: 0)
|
||||
var frameCount = Int32(count)
|
||||
let _ = self.file.write(&frameCount, count: 4)
|
||||
}
|
||||
|
||||
func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
|
||||
if index < 0 || index >= maximumFrameCount {
|
||||
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(4 + 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 storeUncompressedYuvaFrame(index: Int, yuvaData: Data) {
|
||||
if index < 0 || index >= maximumFrameCount {
|
||||
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, yuvaData: yuvaData)
|
||||
|
||||
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(4 + 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 readUncompressedYuvaFrame(index: Int) -> Data? {
|
||||
if index < 0 || index >= maximumFrameCount {
|
||||
return nil
|
||||
}
|
||||
let rangeResult = self.readFrameRange(index: index)
|
||||
|
||||
switch rangeResult {
|
||||
case let .range(range):
|
||||
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 { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
|
||||
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
|
||||
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
frameData = Data(bytes: decodeBytes, count: resultLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frameData
|
||||
case .notFound:
|
||||
return nil
|
||||
case .corruptedFile:
|
||||
self.file.truncate(count: 0)
|
||||
self.initializeFrameTable()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let path: String
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
private let cache: VideoStickerFrameSourceCache?
|
||||
private let bytesPerRow: Int
|
||||
var frameCount: Int
|
||||
let frameRate: Int
|
||||
fileprivate var currentFrame: Int
|
||||
|
||||
private let source: SoftwareVideoSource
|
||||
|
||||
var frameIndex: Int {
|
||||
return self.currentFrame % self.frameCount
|
||||
}
|
||||
|
||||
init?(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?) {
|
||||
self.queue = queue
|
||||
self.path = path
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(self.width))
|
||||
self.currentFrame = 0
|
||||
|
||||
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
|
||||
VideoStickerFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height)
|
||||
}
|
||||
|
||||
self.source = SoftwareVideoSource(path: path, hintVP9: true)
|
||||
self.frameRate = self.source.getFramerate()
|
||||
|
||||
self.frameCount = (self.cache?.frameCount).flatMap { Int($0) } ?? 0
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
let frameIndex: Int
|
||||
if self.frameCount > 0 {
|
||||
frameIndex = self.currentFrame % self.frameCount
|
||||
} else {
|
||||
frameIndex = self.currentFrame
|
||||
}
|
||||
|
||||
self.currentFrame += 1
|
||||
if draw {
|
||||
if let cache = self.cache, let yuvData = cache.readUncompressedYuvaFrame(index: frameIndex) {
|
||||
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
|
||||
} else {
|
||||
let frameAndLoop = self.source.readFrame(maxPts: nil)
|
||||
if frameAndLoop.0 == nil {
|
||||
if frameAndLoop.3 && self.frameCount == 0 {
|
||||
self.frameCount = frameIndex + 1
|
||||
self.cache?.storeFrameCount(self.frameCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let frame = frameAndLoop.0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
let imageBuffer = CMSampleBufferGetImageBuffer(frame.sampleBuffer)
|
||||
CVPixelBufferLockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer!)
|
||||
let width = CVPixelBufferGetWidth(imageBuffer!)
|
||||
let height = CVPixelBufferGetHeight(imageBuffer!)
|
||||
let srcData = CVPixelBufferGetBaseAddress(imageBuffer!)
|
||||
|
||||
var sourceBuffer = vImage_Buffer(data: srcData, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow)
|
||||
var destBuffer = vImage_Buffer(data: bytes, height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: self.bytesPerRow)
|
||||
|
||||
let _ = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, vImage_Flags(kvImageDoNotTile))
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
}
|
||||
|
||||
self.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, totalFrames: self.frameCount)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func skipToEnd() {
|
||||
self.currentFrame = self.frameCount - 1
|
||||
}
|
||||
|
||||
func skipToFrameIndex(_ index: Int) {
|
||||
self.currentFrame = index
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ swift_library(
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/FastBlur:FastBlur",
|
||||
"//submodules/MozjpegBinding:MozjpegBinding",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -8,6 +8,7 @@ import Display
|
||||
import FastBlur
|
||||
import MozjpegBinding
|
||||
import Accelerate
|
||||
import ManagedFile
|
||||
|
||||
private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
|
||||
let thumbnailContextSize = CGSize(width: 32.0, height: 32.0)
|
||||
|
@ -11,7 +11,6 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import TelegramPresentationData
|
||||
import ShimmerEffect
|
||||
import SoftwareVideo
|
||||
|
||||
final class StickerPackPreviewInteraction {
|
||||
var previewedItem: ImportStickerPack.Sticker?
|
||||
@ -61,7 +60,6 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
private var isVerified: Bool?
|
||||
private let imageNode: ASImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private var videoNode: VideoStickerNode?
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
@ -121,7 +119,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
self.imageNode.image = image
|
||||
dimensions = image.size
|
||||
}
|
||||
case .animation:
|
||||
case .animation, .video:
|
||||
self.imageNode.isHidden = true
|
||||
|
||||
if isVerified {
|
||||
@ -140,46 +138,17 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
|
||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
if let resource = stickerItem.resource {
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
var isVideo = false
|
||||
if case .video = stickerItem.content {
|
||||
isVideo = true
|
||||
}
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
}
|
||||
animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||
} else {
|
||||
let placeholderNode = ShimmerEffectNode()
|
||||
self.placeholderNode = placeholderNode
|
||||
|
||||
self.addSubnode(placeholderNode)
|
||||
if let (absoluteRect, containerSize) = self.absoluteLocation {
|
||||
placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize)
|
||||
}
|
||||
}
|
||||
case .video:
|
||||
self.imageNode.isHidden = true
|
||||
|
||||
if isVerified {
|
||||
let videoNode = VideoStickerNode()
|
||||
self.videoNode = videoNode
|
||||
|
||||
if let resource = stickerItem.resource as? TelegramMediaResource {
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||
|
||||
videoNode.update(account: account, fileReference: .standalone(media: dummyFile))
|
||||
}
|
||||
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.placeholderNode = nil
|
||||
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak placeholderNode] _ in
|
||||
placeholderNode?.removeFromSupernode()
|
||||
})
|
||||
self.insertSubnode(videoNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.addSubnode(videoNode)
|
||||
}
|
||||
|
||||
videoNode.update(isPlaying: self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true)
|
||||
} else {
|
||||
let placeholderNode = ShimmerEffectNode()
|
||||
self.placeholderNode = placeholderNode
|
||||
|
||||
self.addSubnode(placeholderNode)
|
||||
if let (absoluteRect, containerSize) = self.absoluteLocation {
|
||||
placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize)
|
||||
@ -210,11 +179,6 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
}
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
videoNode.updateLayout(size: imageSize)
|
||||
}
|
||||
|
||||
if let placeholderNode = self.placeholderNode, let theme = self.theme {
|
||||
placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.roundedRect(rect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: 11.0)], horizontal: true, size: imageSize)
|
||||
placeholderNode.frame = self.imageNode.frame
|
||||
|
@ -9,7 +9,6 @@ import StickerResources
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ContextUI
|
||||
import SoftwareVideo
|
||||
|
||||
public final class StickerPreviewPeekContent: PeekControllerContent {
|
||||
let account: Account
|
||||
@ -58,7 +57,6 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
||||
private var textNode: ASTextNode
|
||||
private var imageNode: ASImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private var videoNode: VideoStickerNode?
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
@ -72,25 +70,19 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
||||
switch item.content {
|
||||
case let .image(data):
|
||||
self.imageNode.image = UIImage(data: data)
|
||||
case .animation:
|
||||
case .animation, .video:
|
||||
let animationNode = AnimatedStickerNode()
|
||||
self.animationNode = animationNode
|
||||
let dimensions = PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))
|
||||
if let resource = item.resource {
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
var isVideo = false
|
||||
if case .video = item.content {
|
||||
isVideo = true
|
||||
}
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
}
|
||||
self.animationNode?.visibility = true
|
||||
case .video:
|
||||
let videoNode = VideoStickerNode()
|
||||
self.videoNode = videoNode
|
||||
|
||||
if let resource = item.resource as? TelegramMediaResource {
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||
|
||||
videoNode.update(account: account, fileReference: .standalone(media: dummyFile))
|
||||
}
|
||||
videoNode.update(isPlaying: true)
|
||||
}
|
||||
if case let .image(data) = item.content, let image = UIImage(data: data) {
|
||||
self.imageNode.image = image
|
||||
@ -101,9 +93,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
self.addSubnode(videoNode)
|
||||
} else if let animationNode = self.animationNode {
|
||||
if let animationNode = self.animationNode {
|
||||
self.addSubnode(animationNode)
|
||||
} else {
|
||||
self.addSubnode(self.imageNode)
|
||||
@ -122,10 +112,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
||||
|
||||
self.imageNode.frame = imageFrame
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.frame = imageFrame
|
||||
videoNode.updateLayout(size: imageFrame.size)
|
||||
} else if let animationNode = self.animationNode {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = imageFrame
|
||||
animationNode.updateLayout(size: imageFrame.size)
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ swift_library(
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -12,7 +12,6 @@ import StickerResources
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ShimmerEffect
|
||||
import SoftwareVideo
|
||||
|
||||
public struct ItemListStickerPackItemEditing: Equatable {
|
||||
public var editable: Bool
|
||||
@ -159,7 +158,6 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
fileprivate let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private var videoNode: VideoStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
private let unreadNode: ASImageNode
|
||||
private let titleNode: TextNode
|
||||
@ -197,7 +195,6 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
if wasVisible != isVisible {
|
||||
let visibility = isVisible && (self.layoutParams?.0.playAnimatedStickers ?? true)
|
||||
self.videoNode?.update(isPlaying: visibility)
|
||||
self.animationNode?.visibility = visibility
|
||||
}
|
||||
}
|
||||
@ -755,44 +752,21 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
||||
case let .still(representation):
|
||||
thumbnailDimensions = representation.dimensions
|
||||
case let .animated(resource, _, isVideo):
|
||||
if isVideo {
|
||||
let videoNode: VideoStickerNode
|
||||
if let current = strongSelf.videoNode {
|
||||
videoNode = current
|
||||
} else {
|
||||
videoNode = VideoStickerNode()
|
||||
strongSelf.videoNode = videoNode
|
||||
strongSelf.addSubnode(videoNode)
|
||||
|
||||
if let resource = resource as? TelegramMediaResource {
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||
videoNode.update(account: item.account, fileReference: .standalone(media: dummyFile))
|
||||
}
|
||||
}
|
||||
videoNode.updateLayout(size: imageFrame.size)
|
||||
videoNode.update(isPlaying: strongSelf.visibility != .none && item.playAnimatedStickers)
|
||||
videoNode.isHidden = !item.playAnimatedStickers
|
||||
strongSelf.imageNode.isHidden = item.playAnimatedStickers
|
||||
if let videoNode = strongSelf.videoNode {
|
||||
transition.updateFrame(node: videoNode, frame: imageFrame)
|
||||
}
|
||||
let animationNode: AnimatedStickerNode
|
||||
if let current = strongSelf.animationNode {
|
||||
animationNode = current
|
||||
} else {
|
||||
let animationNode: AnimatedStickerNode
|
||||
if let current = strongSelf.animationNode {
|
||||
animationNode = current
|
||||
} else {
|
||||
animationNode = AnimatedStickerNode()
|
||||
strongSelf.animationNode = animationNode
|
||||
strongSelf.addSubnode(animationNode)
|
||||
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: resource), width: 80, height: 80, mode: .cached)
|
||||
}
|
||||
animationNode.visibility = strongSelf.visibility != .none && item.playAnimatedStickers
|
||||
animationNode.isHidden = !item.playAnimatedStickers
|
||||
strongSelf.imageNode.isHidden = item.playAnimatedStickers
|
||||
if let animationNode = strongSelf.animationNode {
|
||||
transition.updateFrame(node: animationNode, frame: imageFrame)
|
||||
}
|
||||
animationNode = AnimatedStickerNode()
|
||||
strongSelf.animationNode = animationNode
|
||||
strongSelf.addSubnode(animationNode)
|
||||
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: resource, isVideo: isVideo), width: 80, height: 80, mode: .cached)
|
||||
}
|
||||
animationNode.visibility = strongSelf.visibility != .none && item.playAnimatedStickers
|
||||
animationNode.isHidden = !item.playAnimatedStickers
|
||||
strongSelf.imageNode.isHidden = item.playAnimatedStickers
|
||||
if let animationNode = strongSelf.animationNode {
|
||||
transition.updateFrame(node: animationNode, frame: imageFrame)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView {
|
||||
self.didSetUpAnimationNode = true
|
||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
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, isVideo: self.file.isVideoSticker)
|
||||
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)
|
||||
|
@ -96,8 +96,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity {
|
||||
self.file = file
|
||||
self.animated = file.isAnimatedSticker
|
||||
|
||||
if file.isAnimatedSticker {
|
||||
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource)
|
||||
if file.isAnimatedSticker || file.isVideoSticker {
|
||||
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker)
|
||||
if let source = self.source {
|
||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
|
||||
|
@ -76,6 +76,7 @@ swift_library(
|
||||
deps = [
|
||||
":LottieMeshBinding",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import ManagedFile
|
||||
|
||||
private let emptyMemory = malloc(1)!
|
||||
|
||||
|
@ -3,6 +3,7 @@ import Metal
|
||||
import MetalKit
|
||||
import LottieMeshBinding
|
||||
import Postbox
|
||||
import ManagedFile
|
||||
|
||||
enum TriangleFill {
|
||||
struct Color {
|
||||
|
18
submodules/ManagedFile/BUILD
Normal file
18
submodules/ManagedFile/BUILD
Normal file
@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ManagedFile",
|
||||
module_name = "ManagedFile",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -1,12 +1,6 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum ManagedFileMode {
|
||||
case read
|
||||
case readwrite
|
||||
case append
|
||||
}
|
||||
|
||||
private func wrappedWrite(_ fd: Int32, _ data: UnsafeRawPointer, _ count: Int) -> Int {
|
||||
return write(fd, data, count)
|
||||
}
|
||||
@ -16,11 +10,17 @@ private func wrappedRead(_ fd: Int32, _ data: UnsafeMutableRawPointer, _ count:
|
||||
}
|
||||
|
||||
public final class ManagedFile {
|
||||
public enum Mode {
|
||||
case read
|
||||
case readwrite
|
||||
case append
|
||||
}
|
||||
|
||||
private let queue: Queue?
|
||||
private let fd: Int32
|
||||
private let mode: ManagedFileMode
|
||||
private let mode: Mode
|
||||
|
||||
public init?(queue: Queue?, path: String, mode: ManagedFileMode) {
|
||||
public init?(queue: Queue?, path: String, mode: Mode) {
|
||||
if let queue = queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
@ -73,8 +73,10 @@ public final class ManagedFile {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
var result = Data(count: count)
|
||||
result.withUnsafeMutableBytes { rawBytes -> Void in
|
||||
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: Int8.self)
|
||||
result.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
let readCount = self.read(bytes, count)
|
||||
assert(readCount == count)
|
||||
}
|
@ -18,6 +18,7 @@ swift_library(
|
||||
"//submodules/TelegramAudio:TelegramAudio",
|
||||
"//submodules/FFMpegBinding:FFMpegBinding",
|
||||
"//submodules/RingBuffer:RingBuffer",
|
||||
"//submodules/YuvConversion:YuvConversion",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#else
|
||||
@ -7,6 +6,7 @@ import AppKit
|
||||
import CoreMedia
|
||||
import Accelerate
|
||||
import FFMpegBinding
|
||||
import YuvConversion
|
||||
|
||||
private let bufferCount = 32
|
||||
|
||||
@ -56,7 +56,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
||||
|
||||
private var delayedFrames: [MediaTrackFrame] = []
|
||||
|
||||
private var dstPlane: (UnsafeMutablePointer<UInt8>, Int)?
|
||||
private var uvPlane: (UnsafeMutablePointer<UInt8>, Int)?
|
||||
|
||||
public init(codecContext: FFMpegAVCodecContext) {
|
||||
self.codecContext = codecContext
|
||||
@ -64,7 +64,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let (dstPlane, _) = self.dstPlane {
|
||||
if let (dstPlane, _) = self.uvPlane {
|
||||
free(dstPlane)
|
||||
}
|
||||
}
|
||||
@ -251,7 +251,11 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
||||
case .YUV:
|
||||
pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
||||
case .YUVA:
|
||||
pixelFormat = kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar
|
||||
// if #available(iOS 13.0, *) {
|
||||
// pixelFormat = kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar
|
||||
// } else {
|
||||
pixelFormat = kCVPixelFormatType_32ARGB
|
||||
// }
|
||||
default:
|
||||
pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
||||
}
|
||||
@ -283,82 +287,87 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
||||
}
|
||||
|
||||
let srcPlaneSize = Int(frame.lineSize[1]) * Int(frame.height / 2)
|
||||
let dstPlaneSize = srcPlaneSize * 2
|
||||
let uvPlaneSize = srcPlaneSize * 2
|
||||
|
||||
let dstPlane: UnsafeMutablePointer<UInt8>
|
||||
if let (existingDstPlane, existingDstPlaneSize) = self.dstPlane, existingDstPlaneSize == dstPlaneSize {
|
||||
dstPlane = existingDstPlane
|
||||
let uvPlane: UnsafeMutablePointer<UInt8>
|
||||
if let (existingUvPlane, existingUvPlaneSize) = self.uvPlane, existingUvPlaneSize == uvPlaneSize {
|
||||
uvPlane = existingUvPlane
|
||||
} else {
|
||||
if let (existingDstPlane, _) = self.dstPlane {
|
||||
if let (existingDstPlane, _) = self.uvPlane {
|
||||
free(existingDstPlane)
|
||||
}
|
||||
dstPlane = malloc(dstPlaneSize)!.assumingMemoryBound(to: UInt8.self)
|
||||
self.dstPlane = (dstPlane, dstPlaneSize)
|
||||
uvPlane = malloc(uvPlaneSize)!.assumingMemoryBound(to: UInt8.self)
|
||||
self.uvPlane = (uvPlane, uvPlaneSize)
|
||||
}
|
||||
|
||||
fillDstPlane(dstPlane, frame.data[1]!, frame.data[2]!, srcPlaneSize)
|
||||
fillDstPlane(uvPlane, frame.data[1]!, frame.data[2]!, srcPlaneSize)
|
||||
|
||||
let status = CVPixelBufferLockBaseAddress(pixelBuffer, [])
|
||||
if status != kCVReturnSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
let bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
|
||||
let bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
|
||||
let bytesPerRowA = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)
|
||||
|
||||
var requiresAlphaMultiplication = false
|
||||
|
||||
var base: UnsafeMutableRawPointer
|
||||
if case .YUVA = frame.pixelFormat {
|
||||
requiresAlphaMultiplication = true
|
||||
if pixelFormat == kCVPixelFormatType_32ARGB {
|
||||
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
|
||||
decodeYUVAPlanesToRGBA(frame.data[0], uvPlane, frame.data[3], CVPixelBufferGetBaseAddress(pixelBuffer)?.assumingMemoryBound(to: UInt8.self), Int32(frame.width), Int32(frame.height), Int32(bytesPerRow))
|
||||
} else {
|
||||
let bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
|
||||
let bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
|
||||
let bytesPerRowA = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)
|
||||
|
||||
var requiresAlphaMultiplication = false
|
||||
|
||||
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2)!
|
||||
if bytesPerRowA == frame.lineSize[3] {
|
||||
memcpy(base, frame.data[3]!, bytesPerRowA * Int(frame.height))
|
||||
if pixelFormat == kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar {
|
||||
requiresAlphaMultiplication = true
|
||||
|
||||
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2)!
|
||||
if bytesPerRowA == frame.lineSize[3] {
|
||||
memcpy(base, frame.data[3]!, bytesPerRowA * Int(frame.height))
|
||||
} else {
|
||||
var dest = base
|
||||
var src = frame.data[3]!
|
||||
let lineSize = Int(frame.lineSize[3])
|
||||
for _ in 0 ..< Int(frame.height) {
|
||||
memcpy(dest, src, lineSize)
|
||||
dest = dest.advanced(by: bytesPerRowA)
|
||||
src = src.advanced(by: lineSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)!
|
||||
if bytesPerRowY == frame.lineSize[0] {
|
||||
memcpy(base, frame.data[0]!, bytesPerRowY * Int(frame.height))
|
||||
} else {
|
||||
var dest = base
|
||||
var src = frame.data[3]!
|
||||
let lineSize = Int(frame.lineSize[3])
|
||||
var src = frame.data[0]!
|
||||
let lineSize = Int(frame.lineSize[0])
|
||||
for _ in 0 ..< Int(frame.height) {
|
||||
memcpy(dest, src, lineSize)
|
||||
dest = dest.advanced(by: bytesPerRowA)
|
||||
dest = dest.advanced(by: bytesPerRowY)
|
||||
src = src.advanced(by: lineSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)!
|
||||
if bytesPerRowY == frame.lineSize[0] {
|
||||
memcpy(base, frame.data[0]!, bytesPerRowY * Int(frame.height))
|
||||
} else {
|
||||
var dest = base
|
||||
var src = frame.data[0]!
|
||||
let lineSize = Int(frame.lineSize[0])
|
||||
for _ in 0 ..< Int(frame.height) {
|
||||
memcpy(dest, src, lineSize)
|
||||
dest = dest.advanced(by: bytesPerRowY)
|
||||
src = src.advanced(by: lineSize)
|
||||
|
||||
if requiresAlphaMultiplication {
|
||||
var y = vImage_Buffer(data: CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)!, height: vImagePixelCount(frame.height), width: vImagePixelCount(bytesPerRowY), rowBytes: bytesPerRowY)
|
||||
var a = vImage_Buffer(data: CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2)!, height: vImagePixelCount(frame.height), width: vImagePixelCount(bytesPerRowY), rowBytes: bytesPerRowA)
|
||||
let _ = vImagePremultiplyData_Planar8(&y, &a, &y, vImage_Flags(kvImageDoNotTile))
|
||||
}
|
||||
}
|
||||
|
||||
if requiresAlphaMultiplication {
|
||||
var y = vImage_Buffer(data: CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)!, height: vImagePixelCount(frame.height), width: vImagePixelCount(bytesPerRowY), rowBytes: bytesPerRowY)
|
||||
var a = vImage_Buffer(data: CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2)!, height: vImagePixelCount(frame.height), width: vImagePixelCount(bytesPerRowY), rowBytes: bytesPerRowA)
|
||||
let _ = vImagePremultiplyData_Planar8(&y, &a, &y, vImage_Flags(kvImageDoNotTile))
|
||||
}
|
||||
|
||||
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)!
|
||||
if bytesPerRowUV == frame.lineSize[1] * 2 {
|
||||
memcpy(base, dstPlane, Int(frame.height / 2) * bytesPerRowUV)
|
||||
} else {
|
||||
var dest = base
|
||||
var src = dstPlane
|
||||
let lineSize = Int(frame.lineSize[1]) * 2
|
||||
for _ in 0 ..< Int(frame.height / 2) {
|
||||
memcpy(dest, src, lineSize)
|
||||
dest = dest.advanced(by: bytesPerRowUV)
|
||||
src = src.advanced(by: lineSize)
|
||||
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)!
|
||||
if bytesPerRowUV == frame.lineSize[1] * 2 {
|
||||
memcpy(base, uvPlane, Int(frame.height / 2) * bytesPerRowUV)
|
||||
} else {
|
||||
var dest = base
|
||||
var src = uvPlane
|
||||
let lineSize = Int(frame.lineSize[1]) * 2
|
||||
for _ in 0 ..< Int(frame.height / 2) {
|
||||
memcpy(dest, src, lineSize)
|
||||
dest = dest.advanced(by: bytesPerRowUV)
|
||||
src = src.advanced(by: lineSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,6 +199,14 @@ public final class SoftwareVideoSource {
|
||||
return (frames.first, endOfStream)
|
||||
}
|
||||
|
||||
public func getFramerate() -> Int {
|
||||
if let videoStream = self.videoStream {
|
||||
return Int(videoStream.fps.seconds)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
public func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) {
|
||||
guard let videoStream = self.videoStream, let avFormatContext = self.avFormatContext else {
|
||||
return (nil, 0.0, 1.0, false)
|
||||
|
@ -294,6 +294,38 @@ public final class CachedAnimatedStickerRepresentation: CachedMediaResourceRepre
|
||||
}
|
||||
}
|
||||
|
||||
public final class CachedVideoStickerRepresentation: CachedMediaResourceRepresentation {
|
||||
public let keepDuration: CachedMediaRepresentationKeepDuration = .shortLived
|
||||
|
||||
public let width: Int32
|
||||
public let height: Int32
|
||||
|
||||
public var uniqueId: String {
|
||||
let version: Int = 0
|
||||
return "video-sticker-\(self.width)x\(self.height)-v\(version)"
|
||||
|
||||
}
|
||||
|
||||
public init(width: Int32, height: Int32) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
|
||||
public func isEqual(to: CachedMediaResourceRepresentation) -> Bool {
|
||||
if let other = to as? CachedVideoStickerRepresentation {
|
||||
if other.width != self.width {
|
||||
return false
|
||||
}
|
||||
if other.height != self.height {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class CachedPreparedPatternWallpaperRepresentation: CachedMediaResourceRepresentation {
|
||||
public let keepDuration: CachedMediaRepresentationKeepDuration = .general
|
||||
|
||||
|
@ -15,6 +15,7 @@ swift_library(
|
||||
"//submodules/sqlcipher:sqlcipher",
|
||||
"//submodules/MurMurHash32:MurMurHash32",
|
||||
"//submodules/StringTransliteration:StringTransliteration",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import ManagedFile
|
||||
|
||||
private final class ResourceStatusContext {
|
||||
var status: MediaResourceStatus?
|
||||
|
@ -1,8 +1,7 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
|
||||
import Crc32
|
||||
|
||||
import ManagedFile
|
||||
|
||||
private final class MediaBoxFileMap {
|
||||
fileprivate(set) var sum: Int32
|
||||
|
@ -6,8 +6,8 @@ import SwiftSignalKit
|
||||
import CoreMedia
|
||||
import UniversalMediaPlayer
|
||||
|
||||
private let applyQueue = Queue()
|
||||
private let workers = ThreadPool(threadCount: 3, threadPriority: 0.2)
|
||||
public let softwareVideoApplyQueue = Queue()
|
||||
public let softwareVideoWorkers = ThreadPool(threadCount: 3, threadPriority: 0.2)
|
||||
private var nextWorker = 0
|
||||
|
||||
public final class SoftwareVideoLayerFrameManager {
|
||||
@ -52,7 +52,7 @@ public final class SoftwareVideoLayerFrameManager {
|
||||
self.resource = resource
|
||||
self.hintVP9 = hintVP9
|
||||
self.secondaryResource = secondaryResource
|
||||
self.queue = ThreadPoolQueue(threadPool: workers)
|
||||
self.queue = ThreadPoolQueue(threadPool: softwareVideoWorkers)
|
||||
self.layerHolder = layerHolder
|
||||
layerHolder.layer.videoGravity = .resizeAspectFill
|
||||
layerHolder.layer.masksToBounds = true
|
||||
@ -108,7 +108,7 @@ public final class SoftwareVideoLayerFrameManager {
|
||||
|> take(1)
|
||||
|
||||
self.dataDisposable.set((firstReady
|
||||
|> deliverOn(applyQueue)).start(next: { [weak self] path, resource in
|
||||
|> deliverOn(softwareVideoApplyQueue)).start(next: { [weak self] path, resource in
|
||||
if let strongSelf = self {
|
||||
let size = fileSize(path)
|
||||
Logger.shared.log("SoftwareVideo", "loaded video from \(stringForResource(resource)) (file size: \(String(describing: size))")
|
||||
@ -119,7 +119,7 @@ public final class SoftwareVideoLayerFrameManager {
|
||||
}
|
||||
|
||||
public func tick(timestamp: Double) {
|
||||
applyQueue.async {
|
||||
softwareVideoApplyQueue.async {
|
||||
if self.baseTimestamp == nil && !self.frames.isEmpty {
|
||||
self.baseTimestamp = timestamp
|
||||
}
|
||||
@ -199,7 +199,7 @@ public final class SoftwareVideoLayerFrameManager {
|
||||
hadLoop = true
|
||||
}
|
||||
|
||||
applyQueue.async {
|
||||
softwareVideoApplyQueue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.polling = false
|
||||
if let (_, rotationAngle, aspect, _) = frameAndLoop {
|
||||
|
@ -1,126 +0,0 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
private class VideoStickerNodeDisplayEvents: 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 class VideoStickerNode: ASDisplayNode {
|
||||
private let eventsNode: VideoStickerNodeDisplayEvents
|
||||
|
||||
private var layerHolder: SampleBufferLayer?
|
||||
private var manager: SoftwareVideoLayerFrameManager?
|
||||
|
||||
private var displayLink: ConstantDisplayLinkAnimator?
|
||||
private var displayLinkTimestamp: Double = 0.0
|
||||
|
||||
public var started: () -> Void = {}
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private var isDisplaying: Bool = false {
|
||||
didSet {
|
||||
self.updateIsPlaying()
|
||||
}
|
||||
}
|
||||
private var isPlaying: Bool = false
|
||||
|
||||
public override init() {
|
||||
self.eventsNode = VideoStickerNodeDisplayEvents()
|
||||
|
||||
super.init()
|
||||
|
||||
self.eventsNode.updated = { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isDisplaying = value
|
||||
}
|
||||
self.addSubnode(self.eventsNode)
|
||||
}
|
||||
|
||||
private func updateIsPlaying() {
|
||||
let isPlaying = self.isPlaying && self.isDisplaying
|
||||
if isPlaying {
|
||||
let displayLink: ConstantDisplayLinkAnimator
|
||||
if let current = self.displayLink {
|
||||
displayLink = current
|
||||
} else {
|
||||
displayLink = ConstantDisplayLinkAnimator { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.manager?.tick(timestamp: strongSelf.displayLinkTimestamp)
|
||||
strongSelf.displayLinkTimestamp += 1.0 / 30.0
|
||||
}
|
||||
displayLink.frameInterval = 2
|
||||
self.displayLink = displayLink
|
||||
}
|
||||
displayLink.isPaused = !isPlaying
|
||||
} else {
|
||||
self.displayLink?.isPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
public func update(isPlaying: Bool) {
|
||||
self.isPlaying = isPlaying
|
||||
self.updateIsPlaying()
|
||||
}
|
||||
|
||||
public func update(account: Account, fileReference: FileMediaReference) {
|
||||
let layerHolder = takeSampleBufferLayer()
|
||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
if let size = self.validLayout {
|
||||
layerHolder.layer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
self.layer.addSublayer(layerHolder.layer)
|
||||
self.layerHolder = layerHolder
|
||||
|
||||
let manager = SoftwareVideoLayerFrameManager(account: account, fileReference: fileReference, layerHolder: layerHolder, hintVP9: true)
|
||||
manager.started = self.started
|
||||
self.manager = manager
|
||||
manager.start()
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize) {
|
||||
self.validLayout = size
|
||||
|
||||
self.layerHolder?.layer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
@ -31,7 +31,6 @@ swift_library(
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -291,8 +291,8 @@ public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCol
|
||||
let signal = Signal<Bool, NoError> { subscriber in
|
||||
let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)).start()
|
||||
let dataDisposable: Disposable
|
||||
if info.flags.contains(.isAnimated) {
|
||||
dataDisposable = chatMessageAnimationData(mediaBox: account.postbox.mediaBox, resource: thumbnail.resource, width: 80, height: 80, synchronousLoad: false).start(next: { data in
|
||||
if info.flags.contains(.isAnimated) || info.flags.contains(.isVideo) {
|
||||
dataDisposable = chatMessageAnimationData(mediaBox: account.postbox.mediaBox, resource: thumbnail.resource, isVideo: info.flags.contains(.isVideo), width: 80, height: 80, synchronousLoad: false).start(next: { data in
|
||||
if data.complete {
|
||||
subscriber.putNext(true)
|
||||
subscriber.putCompletion()
|
||||
|
@ -11,7 +11,6 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import TelegramPresentationData
|
||||
import ShimmerEffect
|
||||
import SoftwareVideo
|
||||
|
||||
final class StickerPackPreviewInteraction {
|
||||
var previewedItem: StickerPreviewPeekItem?
|
||||
@ -61,7 +60,6 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
private var isEmpty: Bool?
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private var videoNode: VideoStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
@ -69,9 +67,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
override var isVisibleInGrid: Bool {
|
||||
didSet {
|
||||
let visibility = self.isVisibleInGrid && (self.interaction?.playAnimatedStickers ?? true)
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.update(isPlaying: visibility)
|
||||
} else if let animationNode = self.animationNode {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.visibility = visibility
|
||||
}
|
||||
}
|
||||
@ -146,22 +142,13 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
|
||||
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem || self.isEmpty != isEmpty {
|
||||
if let stickerItem = stickerItem {
|
||||
if stickerItem.file.isVideoSticker {
|
||||
if self.videoNode == nil {
|
||||
let videoNode = VideoStickerNode()
|
||||
self.videoNode = videoNode
|
||||
self.addSubnode(videoNode)
|
||||
videoNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
}
|
||||
self.videoNode?.update(account: account, fileReference: stickerPackFileReference(stickerItem.file))
|
||||
|
||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start())
|
||||
} else if stickerItem.file.isAnimatedSticker {
|
||||
if stickerItem.file.isAnimatedSticker || stickerItem.file.isVideoSticker {
|
||||
let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
|
||||
if stickerItem.file.isVideoSticker {
|
||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
|
||||
} else {
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
|
||||
}
|
||||
|
||||
if self.animationNode == nil {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
@ -173,7 +160,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
}
|
||||
}
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource, isVideo: stickerItem.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||
} else {
|
||||
@ -225,10 +212,6 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = imageFrame
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.frame = imageFrame
|
||||
videoNode.updateLayout(size: imageSize)
|
||||
}
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = imageFrame
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
|
@ -9,7 +9,6 @@ import StickerResources
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ContextUI
|
||||
import SoftwareVideo
|
||||
|
||||
public enum StickerPreviewPeekItem: Equatable {
|
||||
case pack(StickerPackItem)
|
||||
@ -72,7 +71,6 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
|
||||
private var textNode: ASTextNode
|
||||
public var imageNode: TransformImageNode
|
||||
public var animationNode: AnimatedStickerNode?
|
||||
public var videoNode: VideoStickerNode?
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
@ -88,22 +86,14 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
|
||||
break
|
||||
}
|
||||
|
||||
if item.file.isVideoSticker {
|
||||
let videoNode = VideoStickerNode()
|
||||
self.videoNode = videoNode
|
||||
|
||||
videoNode.update(account: self.account, fileReference: .standalone(media: item.file))
|
||||
videoNode.update(isPlaying: true)
|
||||
|
||||
videoNode.addSubnode(self.textNode)
|
||||
} else if item.file.isAnimatedSticker {
|
||||
if item.file.isAnimatedSticker || item.file.isVideoSticker {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
self.animationNode = animationNode
|
||||
|
||||
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))
|
||||
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource, isVideo: item.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
animationNode.visibility = true
|
||||
animationNode.addSubnode(self.textNode)
|
||||
} else {
|
||||
@ -117,9 +107,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
self.addSubnode(videoNode)
|
||||
} else if let animationNode = self.animationNode {
|
||||
if let animationNode = self.animationNode {
|
||||
self.addSubnode(animationNode)
|
||||
} else {
|
||||
self.addSubnode(self.imageNode)
|
||||
@ -137,10 +125,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
let imageFrame = CGRect(origin: CGPoint(x: 0.0, y: textSize.height + textSpacing), size: imageSize)
|
||||
self.imageNode.frame = imageFrame
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.frame = imageFrame
|
||||
videoNode.updateLayout(size: imageSize)
|
||||
} else if let animationNode = self.animationNode {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = imageFrame
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
}
|
||||
|
@ -225,8 +225,8 @@ private func chatMessageStickerPackThumbnailData(postbox: Postbox, resource: Med
|
||||
}
|
||||
}
|
||||
|
||||
public func chatMessageAnimationData(mediaBox: MediaBox, resource: MediaResource, fitzModifier: EmojiFitzModifier? = nil, width: Int, height: Int, synchronousLoad: Bool) -> Signal<MediaResourceData, NoError> {
|
||||
let representation = CachedAnimatedStickerRepresentation(width: Int32(width), height: Int32(height), fitzModifier: fitzModifier)
|
||||
public func chatMessageAnimationData(mediaBox: MediaBox, resource: MediaResource, fitzModifier: EmojiFitzModifier? = nil, isVideo: Bool = false, width: Int, height: Int, synchronousLoad: Bool) -> Signal<MediaResourceData, NoError> {
|
||||
let representation: CachedMediaResourceRepresentation = isVideo ? CachedVideoStickerRepresentation(width: Int32(width), height: Int32(height)) : CachedAnimatedStickerRepresentation(width: Int32(width), height: Int32(height), fitzModifier: fitzModifier)
|
||||
let maybeFetched = mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: false, fetch: false, attemptSynchronously: synchronousLoad)
|
||||
|
||||
return maybeFetched
|
||||
|
@ -20,6 +20,9 @@ swift_library(
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/YuvConversion:YuvConversion",
|
||||
"//submodules/GZip:GZip",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import CoreMedia
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
@ -12,6 +13,9 @@ import MobileCoreServices
|
||||
import MediaResources
|
||||
import YuvConversion
|
||||
import AnimatedStickerNode
|
||||
import ManagedFile
|
||||
import UniversalMediaPlayer
|
||||
import SoftwareVideo
|
||||
|
||||
public func fetchCompressedLottieFirstFrameAJpeg(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal<TempBoxFile, NoError> {
|
||||
return Signal({ subscriber in
|
||||
@ -26,7 +30,6 @@ public func fetchCompressedLottieFirstFrameAJpeg(data: Data, size: CGSize, fitzM
|
||||
|
||||
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024)
|
||||
if let decompressedData = decompressedData {
|
||||
let decompressedData = transformedWithFitzModifier(data: decompressedData, fitzModifier: fitzModifier)
|
||||
if let player = LottieInstance(data: decompressedData, fitzModifier: fitzModifier?.lottieFitzModifier ?? .none, cacheKey: cacheKey) {
|
||||
if cancelled.with({ $0 }) {
|
||||
return
|
||||
@ -109,7 +112,7 @@ private let threadPool: ThreadPool = {
|
||||
return ThreadPool(threadCount: 3, threadPriority: 0.5)
|
||||
}()
|
||||
|
||||
public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
public func cacheAnimatedStickerFrames(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
return Signal({ subscriber in
|
||||
let cancelled = Atomic<Bool>(value: false)
|
||||
|
||||
@ -125,7 +128,6 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
|
||||
|
||||
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024)
|
||||
if let decompressedData = decompressedData {
|
||||
let decompressedData = transformedWithFitzModifier(data: decompressedData, fitzModifier: fitzModifier)
|
||||
if let player = LottieInstance(data: decompressedData, fitzModifier: fitzModifier?.lottieFitzModifier ?? .none, cacheKey: cacheKey) {
|
||||
let endFrame = Int(player.frameCount)
|
||||
|
||||
@ -137,7 +139,6 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
|
||||
|
||||
var currentFrame: Int32 = 0
|
||||
|
||||
//let writeBuffer = WriteBuffer()
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "result.asticker")
|
||||
guard let file = ManagedFile(queue: nil, path: tempFile.path, mode: .readwrite) else {
|
||||
return
|
||||
@ -146,17 +147,7 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
|
||||
func writeData(_ data: UnsafeRawPointer, length: Int) {
|
||||
let _ = file.write(data, count: length)
|
||||
}
|
||||
|
||||
func commitData() {
|
||||
}
|
||||
|
||||
func completeWithCurrentResult() {
|
||||
subscriber.putNext(.tempFile(tempFile))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
var numberOfFramesCommitted = 0
|
||||
|
||||
|
||||
var fps: Int32 = player.frameRate
|
||||
var frameCount: Int32 = player.frameCount
|
||||
writeData(&fps, length: 4)
|
||||
@ -249,20 +240,9 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
|
||||
compressionTime += CACurrentMediaTime() - compressionStartTime
|
||||
|
||||
currentFrame += 1
|
||||
|
||||
numberOfFramesCommitted += 1
|
||||
|
||||
if numberOfFramesCommitted >= 5 {
|
||||
numberOfFramesCommitted = 0
|
||||
|
||||
commitData()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
commitData()
|
||||
|
||||
completeWithCurrentResult()
|
||||
|
||||
subscriber.putNext(.tempFile(tempFile))
|
||||
subscriber.putCompletion()
|
||||
/*print("animation render time \(CACurrentMediaTime() - startTime)")
|
||||
print("of which drawing time \(drawingTime)")
|
||||
@ -278,3 +258,147 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func cacheVideoStickerFrames(path: String, size: CGSize, cacheKey: String) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
return Signal { subscriber in
|
||||
let cancelled = Atomic<Bool>(value: false)
|
||||
|
||||
let source = SoftwareVideoSource(path: path, hintVP9: true)
|
||||
let queue = ThreadPoolQueue(threadPool: softwareVideoWorkers)
|
||||
|
||||
queue.addTask(ThreadPoolTask({ _ in
|
||||
if cancelled.with({ $0 }) {
|
||||
return
|
||||
}
|
||||
|
||||
if cancelled.with({ $0 }) {
|
||||
return
|
||||
}
|
||||
|
||||
let bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(size.width))
|
||||
|
||||
var currentFrame: Int32 = 0
|
||||
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "result.vsticker")
|
||||
guard let file = ManagedFile(queue: nil, path: tempFile.path, mode: .readwrite) else {
|
||||
return
|
||||
}
|
||||
|
||||
func writeData(_ data: UnsafeRawPointer, length: Int) {
|
||||
let _ = file.write(data, count: length)
|
||||
}
|
||||
|
||||
var fps: Int32 = Int32(source.getFramerate())
|
||||
var frameCount: Int32 = 0
|
||||
writeData(&fps, length: 4)
|
||||
writeData(&frameCount, length: 4)
|
||||
var widthValue: Int32 = Int32(size.width)
|
||||
var heightValue: Int32 = Int32(size.height)
|
||||
var bytesPerRowValue: Int32 = Int32(bytesPerRow)
|
||||
writeData(&widthValue, length: 4)
|
||||
writeData(&heightValue, length: 4)
|
||||
writeData(&bytesPerRowValue, length: 4)
|
||||
|
||||
let frameLength = bytesPerRow * Int(size.height)
|
||||
assert(frameLength % 16 == 0)
|
||||
|
||||
let currentFrameData = malloc(frameLength)!
|
||||
memset(currentFrameData, 0, frameLength)
|
||||
|
||||
let yuvaPixelsPerAlphaRow = (Int(size.width) + 1) & (~1)
|
||||
assert(yuvaPixelsPerAlphaRow % 2 == 0)
|
||||
|
||||
let yuvaLength = Int(size.width) * Int(size.height) * 2 + yuvaPixelsPerAlphaRow * Int(size.height) / 2
|
||||
var yuvaFrameData = malloc(yuvaLength)!
|
||||
memset(yuvaFrameData, 0, yuvaLength)
|
||||
|
||||
var previousYuvaFrameData = malloc(yuvaLength)!
|
||||
memset(previousYuvaFrameData, 0, yuvaLength)
|
||||
|
||||
defer {
|
||||
free(currentFrameData)
|
||||
free(previousYuvaFrameData)
|
||||
free(yuvaFrameData)
|
||||
}
|
||||
|
||||
var compressedFrameData = Data(count: frameLength)
|
||||
let compressedFrameDataLength = compressedFrameData.count
|
||||
|
||||
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
|
||||
defer {
|
||||
free(scratchData)
|
||||
}
|
||||
|
||||
var process = true
|
||||
|
||||
while process {
|
||||
let frameAndLoop = source.readFrame(maxPts: nil)
|
||||
if frameAndLoop.0 == nil {
|
||||
if frameAndLoop.3 {
|
||||
frameCount = currentFrame
|
||||
process = false
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
guard let frame = frameAndLoop.0 else {
|
||||
break
|
||||
}
|
||||
|
||||
let imageBuffer = CMSampleBufferGetImageBuffer(frame.sampleBuffer)
|
||||
CVPixelBufferLockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let originalBytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer!)
|
||||
let originalWidth = CVPixelBufferGetWidth(imageBuffer!)
|
||||
let originalHeight = CVPixelBufferGetHeight(imageBuffer!)
|
||||
if let srcBuffer = CVPixelBufferGetBaseAddress(imageBuffer!) {
|
||||
resizeAndEncodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), srcBuffer.assumingMemoryBound(to: UInt8.self), Int32(size.width), Int32(size.height), Int32(bytesPerRow), Int32(originalWidth), Int32(originalHeight), Int32(originalBytesPerRow))
|
||||
}
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
|
||||
var lhs = previousYuvaFrameData.assumingMemoryBound(to: UInt64.self)
|
||||
var rhs = yuvaFrameData.assumingMemoryBound(to: UInt64.self)
|
||||
for _ in 0 ..< yuvaLength / 8 {
|
||||
lhs.pointee = rhs.pointee ^ lhs.pointee
|
||||
lhs = lhs.advanced(by: 1)
|
||||
rhs = rhs.advanced(by: 1)
|
||||
}
|
||||
var lhsRest = previousYuvaFrameData.assumingMemoryBound(to: UInt8.self).advanced(by: (yuvaLength / 8) * 8)
|
||||
var rhsRest = yuvaFrameData.assumingMemoryBound(to: UInt8.self).advanced(by: (yuvaLength / 8) * 8)
|
||||
for _ in (yuvaLength / 8) * 8 ..< yuvaLength {
|
||||
lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee
|
||||
lhsRest = lhsRest.advanced(by: 1)
|
||||
rhsRest = rhsRest.advanced(by: 1)
|
||||
}
|
||||
|
||||
compressedFrameData.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
let length = compression_encode_buffer(bytes, compressedFrameDataLength, previousYuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
|
||||
var frameLengthValue: Int32 = Int32(length)
|
||||
writeData(&frameLengthValue, length: 4)
|
||||
writeData(bytes, length: length)
|
||||
}
|
||||
|
||||
let tmp = previousYuvaFrameData
|
||||
previousYuvaFrameData = yuvaFrameData
|
||||
yuvaFrameData = tmp
|
||||
|
||||
currentFrame += 1
|
||||
}
|
||||
|
||||
subscriber.putNext(.tempFile(tempFile))
|
||||
subscriber.putCompletion()
|
||||
/*print("animation render time \(CACurrentMediaTime() - startTime)")
|
||||
print("of which drawing time \(drawingTime)")
|
||||
print("of which appending time \(appendingTime)")
|
||||
print("of which delta time \(deltaTime)")
|
||||
|
||||
print("of which compression time \(compressionTime)")*/
|
||||
}))
|
||||
|
||||
return ActionDisposable {
|
||||
let _ = cancelled.swap(true)
|
||||
}
|
||||
} |> runOn(softwareVideoApplyQueue)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import AppBundle
|
||||
|
||||
public final class AnimatedStickerNodeLocalFileSource: AnimatedStickerNodeSource {
|
||||
public var fitzModifier: EmojiFitzModifier? = nil
|
||||
public let isVideo: Bool = false
|
||||
|
||||
public let name: String
|
||||
|
||||
@ -44,15 +45,17 @@ public final class AnimatedStickerResourceSource: AnimatedStickerNodeSource {
|
||||
public let account: Account
|
||||
public let resource: MediaResource
|
||||
public let fitzModifier: EmojiFitzModifier?
|
||||
public let isVideo: Bool
|
||||
|
||||
public init(account: Account, resource: MediaResource, fitzModifier: EmojiFitzModifier? = nil) {
|
||||
public init(account: Account, resource: MediaResource, fitzModifier: EmojiFitzModifier? = nil, isVideo: Bool = false) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.fitzModifier = fitzModifier
|
||||
self.isVideo = isVideo
|
||||
}
|
||||
|
||||
public func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError> {
|
||||
return chatMessageAnimationData(mediaBox: self.account.postbox.mediaBox, resource: self.resource, fitzModifier: self.fitzModifier, width: width, height: height, synchronousLoad: false)
|
||||
return chatMessageAnimationData(mediaBox: self.account.postbox.mediaBox, resource: self.resource, fitzModifier: self.fitzModifier, isVideo: self.isVideo, width: width, height: height, synchronousLoad: false)
|
||||
|> filter { data in
|
||||
return data.size != 0
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ swift_library(
|
||||
"//submodules/CryptoUtils:CryptoUtils",
|
||||
"//submodules/NetworkLogging:NetworkLogging",
|
||||
"//submodules/Reachability:Reachability",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -4,6 +4,7 @@ import TelegramApi
|
||||
import SwiftSignalKit
|
||||
import MtProtoKit
|
||||
import CryptoUtils
|
||||
import ManagedFile
|
||||
|
||||
private typealias SignalKitTimer = SwiftSignalKit.Timer
|
||||
|
||||
|
@ -479,9 +479,6 @@ public final class TelegramMediaFile: Media, Equatable, Codable {
|
||||
}
|
||||
|
||||
public var isVideoSticker: Bool {
|
||||
if self.mimeType == "video/webm" {
|
||||
return true
|
||||
}
|
||||
if self.mimeType == "video/webm" {
|
||||
var hasSticker = false
|
||||
for attribute in self.attributes {
|
||||
|
@ -3,6 +3,7 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
import NetworkLogging
|
||||
import ManagedFile
|
||||
|
||||
private let queue = DispatchQueue(label: "org.telegram.Telegram.trace", qos: .utility)
|
||||
|
||||
|
@ -263,14 +263,20 @@ public func trimToLineCount(_ text: String, lineCount: Int) -> String {
|
||||
if lineCount < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
let lines = text.split { $0.isNewline }
|
||||
|
||||
var result = ""
|
||||
for line in lines.prefix(lineCount) {
|
||||
|
||||
var i = 0
|
||||
text.enumerateLines { line, stop in
|
||||
if !result.isEmpty {
|
||||
result += "\n"
|
||||
}
|
||||
result += line
|
||||
i += 1
|
||||
if i == lineCount {
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -255,6 +255,7 @@ swift_library(
|
||||
"//submodules/Translate:Translate",
|
||||
"//submodules/TabBarUI:TabBarUI",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
@ -11,7 +11,6 @@ import AccountContext
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ShimmerEffect
|
||||
import SoftwareVideo
|
||||
|
||||
enum ChatMediaInputStickerGridSectionAccessory {
|
||||
case none
|
||||
@ -175,7 +174,6 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
|
||||
private var currentSize: CGSize?
|
||||
let imageNode: TransformImageNode
|
||||
private(set) var animationNode: AnimatedStickerNode?
|
||||
private(set) var videoNode: VideoStickerNode?
|
||||
private(set) var placeholderNode: StickerShimmerEffectNode?
|
||||
private var didSetUpAnimationNode = false
|
||||
private var item: ChatMediaInputStickerGridItem?
|
||||
@ -267,23 +265,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
if let dimensions = item.stickerItem.file.dimensions {
|
||||
if item.stickerItem.file.isVideoSticker {
|
||||
if self.videoNode == nil {
|
||||
let videoNode = VideoStickerNode()
|
||||
videoNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
|
||||
self.videoNode = videoNode
|
||||
videoNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.insertSubnode(videoNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.addSubnode(videoNode)
|
||||
}
|
||||
}
|
||||
self.imageNode.setSignal(chatMessageSticker(account: item.account, file: item.stickerItem.file, small: !item.large, synchronousLoad: synchronousLoads && isVisible))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(item.stickerItem.file), resource: item.stickerItem.file.resource).start())
|
||||
} else if item.stickerItem.file.isAnimatedSticker {
|
||||
if item.stickerItem.file.isAnimatedSticker || item.stickerItem.file.isVideoSticker {
|
||||
if self.animationNode == nil {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
|
||||
@ -299,7 +281,11 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
|
||||
}
|
||||
let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedSize = item.large ? CGSize(width: 384.0, height: 384.0) : CGSize(width: 160.0, height: 160.0)
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.account.postbox, file: item.stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(fittedSize)))
|
||||
if item.stickerItem.file.isVideoSticker {
|
||||
self.imageNode.setSignal(chatMessageSticker(account: item.account, file: item.stickerItem.file, small: !item.large, synchronousLoad: synchronousLoads && isVisible))
|
||||
} else {
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.account.postbox, file: item.stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(fittedSize)))
|
||||
}
|
||||
self.updateVisibility()
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(item.stickerItem.file), resource: item.stickerItem.file.resource).start())
|
||||
} else {
|
||||
@ -335,12 +321,6 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
|
||||
}
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
}
|
||||
if let videoNode = self.videoNode {
|
||||
if videoNode.supernode === self {
|
||||
videoNode.frame = imageFrame
|
||||
}
|
||||
videoNode.updateLayout(size: imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,17 +370,15 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
|
||||
if self.isPlaying != isPlaying {
|
||||
self.isPlaying = isPlaying
|
||||
self.animationNode?.visibility = isPlaying
|
||||
self.videoNode?.update(isPlaying: isPlaying)
|
||||
|
||||
if let item = self.item, isPlaying, !self.didSetUpAnimationNode {
|
||||
self.didSetUpAnimationNode = true
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.update(account: item.account, fileReference: stickerPackFileReference(item.stickerItem.file))
|
||||
} else if let animationNode = self.animationNode {
|
||||
if let animationNode = self.animationNode {
|
||||
let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fitSize = item.large ? CGSize(width: 384.0, height: 384.0) : CGSize(width: 160.0, height: 160.0)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(fitSize)
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: item.stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: item.stickerItem.file.resource, isVideo: item.stickerItem.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import ItemListStickerPackItem
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ShimmerEffect
|
||||
import SoftwareVideo
|
||||
|
||||
final class ChatMediaInputStickerPackItem: ListViewItem {
|
||||
let account: Account
|
||||
@ -84,7 +83,6 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
private let scalingNode: ASDisplayNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
private var videoNode: VideoStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
private let highlightNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
@ -109,9 +107,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
didSet {
|
||||
if self.visibilityStatus != oldValue {
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
let visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
self.videoNode?.update(isPlaying: visibility)
|
||||
self.animatedStickerNode?.visibility = visibility
|
||||
self.animatedStickerNode?.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -233,47 +229,23 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
|
||||
if isVideo {
|
||||
let videoNode: VideoStickerNode
|
||||
if let current = self.videoNode {
|
||||
videoNode = current
|
||||
} else {
|
||||
videoNode = VideoStickerNode()
|
||||
videoNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
self.videoNode = videoNode
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.scalingNode.insertSubnode(videoNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.scalingNode.addSubnode(videoNode)
|
||||
}
|
||||
|
||||
if let resource = resource as? TelegramMediaResource {
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||
videoNode.update(account: account, fileReference: .standalone(media: dummyFile))
|
||||
}
|
||||
}
|
||||
videoNode.update(isPlaying: self.visibilityStatus && loopAnimatedStickers)
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.scalingNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.scalingNode.addSubnode(animatedStickerNode)
|
||||
}
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 128, height: 128, mode: .cached)
|
||||
animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.scalingNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.scalingNode.addSubnode(animatedStickerNode)
|
||||
}
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: 128, height: 128, mode: .cached)
|
||||
}
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
}
|
||||
if let resourceReference = resourceReference {
|
||||
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference).start())
|
||||
@ -314,10 +286,6 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
animatedStickerNode.frame = self.imageNode.frame
|
||||
animatedStickerNode.updateLayout(size: self.imageNode.frame.size)
|
||||
}
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.frame = self.imageNode.frame
|
||||
videoNode.updateLayout(size: self.imageNode.frame.size)
|
||||
}
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize)
|
||||
placeholderNode.position = self.imageNode.position
|
||||
@ -374,7 +342,6 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
|
||||
var snapshotImageNode: TransformImageNode?
|
||||
var snapshotAnimationNode: AnimatedStickerNode?
|
||||
var snapshotVideoNode: VideoStickerNode?
|
||||
switch thumbnailItem {
|
||||
case let .still(representation):
|
||||
imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize)
|
||||
@ -387,27 +354,15 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
|
||||
snapshotImageNode = imageNode
|
||||
case let .animated(resource, _, isVideo):
|
||||
if isVideo {
|
||||
let videoNode = VideoStickerNode()
|
||||
if let resource = resource as? TelegramMediaResource {
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||
videoNode.update(account: account, fileReference: .standalone(media: dummyFile))
|
||||
}
|
||||
videoNode.update(isPlaying: self.visibilityStatus && loopAnimatedStickers)
|
||||
scalingNode.addSubnode(videoNode)
|
||||
|
||||
snapshotVideoNode = videoNode
|
||||
} else {
|
||||
let animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 128, height: 128, mode: .cached)
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
scalingNode.addSubnode(animatedStickerNode)
|
||||
|
||||
animatedStickerNode.cloneCurrentFrame(from: self.animatedStickerNode)
|
||||
animatedStickerNode.play(fromIndex: self.animatedStickerNode?.currentFrameIndex)
|
||||
|
||||
snapshotAnimationNode = animatedStickerNode
|
||||
}
|
||||
let animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: 128, height: 128, mode: .cached)
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
scalingNode.addSubnode(animatedStickerNode)
|
||||
|
||||
animatedStickerNode.cloneCurrentFrame(from: self.animatedStickerNode)
|
||||
animatedStickerNode.play(fromIndex: self.animatedStickerNode?.currentFrameIndex)
|
||||
|
||||
snapshotAnimationNode = animatedStickerNode
|
||||
}
|
||||
|
||||
containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize)
|
||||
@ -427,11 +382,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
animatedStickerNode.frame = imageFrame
|
||||
animatedStickerNode.updateLayout(size: imageFrame.size)
|
||||
}
|
||||
if let videoStickerNode = snapshotVideoNode {
|
||||
videoStickerNode.frame = imageFrame
|
||||
videoStickerNode.updateLayout(size: imageFrame.size)
|
||||
}
|
||||
|
||||
|
||||
let expanded = self.currentExpanded
|
||||
let scale = expanded ? 1.0 : boundingImageScale
|
||||
let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height)
|
||||
|
@ -26,7 +26,6 @@ import WallpaperBackgroundNode
|
||||
import LocalMediaResources
|
||||
import AppBundle
|
||||
import LottieMeshSwift
|
||||
import SoftwareVideo
|
||||
|
||||
private let nameFont = Font.medium(14.0)
|
||||
private let inlineBotPrefixFont = Font.regular(14.0)
|
||||
@ -55,19 +54,6 @@ extension SlotMachineAnimationNode: GenericAnimatedStickerNode {
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoStickerNode: GenericAnimatedStickerNode {
|
||||
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
|
||||
|
||||
}
|
||||
|
||||
var currentFrameIndex: Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func setFrameIndex(_ frameIndex: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageShareButton: HighlightableButtonNode {
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let iconNode: ASImageNode
|
||||
@ -451,25 +437,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
self.animationNode = animationNode
|
||||
}
|
||||
} else if let telegramFile = self.telegramFile, telegramFile.mimeType == "video/webm" {
|
||||
let videoNode = VideoStickerNode()
|
||||
videoNode.started = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.imageNode.alpha = 0.0
|
||||
if !strongSelf.enableSynchronousImageApply {
|
||||
let current = CACurrentMediaTime()
|
||||
if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 {
|
||||
if !strongSelf.placeholderNode.alpha.isZero {
|
||||
strongSelf.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
strongSelf.removePlaceholder(animated: true)
|
||||
}
|
||||
} else {
|
||||
strongSelf.removePlaceholder(animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.animationNode = videoNode
|
||||
} else {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
animationNode.started = { [weak self] in
|
||||
@ -587,21 +554,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
let isPlaying = self.visibilityStatus && !self.forceStopAnimations
|
||||
if let videoNode = self.animationNode as? VideoStickerNode {
|
||||
if self.isPlaying != isPlaying {
|
||||
self.isPlaying = isPlaying
|
||||
|
||||
if self.isPlaying && !self.didSetUpAnimationNode {
|
||||
self.didSetUpAnimationNode = true
|
||||
|
||||
if let file = self.telegramFile {
|
||||
videoNode.update(account: item.context.account, fileReference: .standalone(media: file))
|
||||
}
|
||||
}
|
||||
|
||||
videoNode.update(isPlaying: isPlaying)
|
||||
}
|
||||
} else if let animationNode = self.animationNode as? AnimatedStickerNode {
|
||||
if let animationNode = self.animationNode as? AnimatedStickerNode {
|
||||
if !isPlaying {
|
||||
for decorationNode in self.additionalAnimationNodes {
|
||||
if let transitionNode = item.controllerInteraction.getMessageTransitionNode() {
|
||||
@ -678,7 +631,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||
let mode: AnimatedStickerMode = .direct(cachePathPrefix: pathPrefix)
|
||||
self.animationSize = fittedSize
|
||||
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, isVideo: file.isVideoSticker), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1195,9 +1148,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode {
|
||||
strongSelf.animationNode?.frame = animationNodeFrame
|
||||
if let videoNode = strongSelf.animationNode as? VideoStickerNode {
|
||||
videoNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
|
||||
}
|
||||
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode {
|
||||
animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
|
||||
}
|
||||
|
@ -993,7 +993,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
strongSelf.animatedStickerNode = animatedStickerNode
|
||||
let dimensions = updatedAnimatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: updatedAnimatedStickerFile.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: updatedAnimatedStickerFile.resource, isVideo: updatedAnimatedStickerFile.isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
strongSelf.pinchContainerNode.contentNode.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode)
|
||||
animatedStickerNode.visibility = strongSelf.visibility
|
||||
}
|
||||
|
@ -111,6 +111,14 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR
|
||||
}
|
||||
return fetchAnimatedStickerRepresentation(account: account, resource: resource, resourceData: data, representation: representation)
|
||||
}
|
||||
} else if let representation = representation as? CachedVideoStickerRepresentation {
|
||||
return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false))
|
||||
|> mapToSignal { data -> Signal<CachedMediaResourceRepresentationResult, NoError> in
|
||||
if !data.complete {
|
||||
return .complete()
|
||||
}
|
||||
return fetchVideoStickerRepresentation(account: account, resource: resource, resourceData: data, representation: representation)
|
||||
}
|
||||
} else if let representation = representation as? CachedAnimatedStickerFirstFrameRepresentation {
|
||||
return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false))
|
||||
|> mapToSignal { data -> Signal<CachedMediaResourceRepresentationResult, NoError> in
|
||||
@ -710,15 +718,11 @@ private func fetchAnimatedStickerFirstFrameRepresentation(account: Account, reso
|
||||
private func fetchAnimatedStickerRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedAnimatedStickerRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
return Signal({ subscriber in
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) {
|
||||
if #available(iOS 9.0, *) {
|
||||
return experimentalConvertCompressedLottieToCombinedMp4(data: data, size: CGSize(width: CGFloat(representation.width), height: CGFloat(representation.height)), fitzModifier: representation.fitzModifier, cacheKey: "\(resource.id.stringRepresentation)-\(representation.uniqueId)").start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
return cacheAnimatedStickerFrames(data: data, size: CGSize(width: CGFloat(representation.width), height: CGFloat(representation.height)), fitzModifier: representation.fitzModifier, cacheKey: "\(resource.id.stringRepresentation)-\(representation.uniqueId)").start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
@ -726,6 +730,16 @@ private func fetchAnimatedStickerRepresentation(account: Account, resource: Medi
|
||||
|> runOn(Queue.concurrentDefaultQueue())
|
||||
}
|
||||
|
||||
private func fetchVideoStickerRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedVideoStickerRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
return Signal({ subscriber in
|
||||
return cacheVideoStickerFrames(path: resourceData.path, size: CGSize(width: CGFloat(representation.width), height: CGFloat(representation.height)), cacheKey: "\(resource.id.stringRepresentation)-\(representation.uniqueId)").start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
})
|
||||
|> runOn(Queue.concurrentDefaultQueue())
|
||||
}
|
||||
|
||||
private func fetchPreparedPatternWallpaperRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPreparedPatternWallpaperRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
return Signal({ subscriber in
|
||||
|
@ -88,7 +88,6 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
private let imageNodeBackground: ASDisplayNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private var videoStickerNode: VideoStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
|
||||
private var currentImageResource: TelegramMediaResource?
|
||||
@ -417,37 +416,28 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
animationNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let videoStickerNode = strongSelf.videoStickerNode {
|
||||
strongSelf.videoStickerNode = nil
|
||||
videoStickerNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let animatedStickerFile = animatedStickerFile {
|
||||
if animatedStickerFile.isVideoSticker {
|
||||
|
||||
let animationNode: AnimatedStickerNode
|
||||
if let currentAnimationNode = strongSelf.animationNode {
|
||||
animationNode = currentAnimationNode
|
||||
} else {
|
||||
let animationNode: AnimatedStickerNode
|
||||
if let currentAnimationNode = strongSelf.animationNode {
|
||||
animationNode = currentAnimationNode
|
||||
animationNode = AnimatedStickerNode()
|
||||
animationNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
animationNode.visibility = true
|
||||
if let placeholderNode = strongSelf.placeholderNode {
|
||||
strongSelf.insertSubnode(animationNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
animationNode = AnimatedStickerNode()
|
||||
animationNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
animationNode.visibility = true
|
||||
if let placeholderNode = strongSelf.placeholderNode {
|
||||
strongSelf.insertSubnode(animationNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
strongSelf.addSubnode(animationNode)
|
||||
}
|
||||
strongSelf.animationNode = animationNode
|
||||
strongSelf.addSubnode(animationNode)
|
||||
}
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.alpha = 0.0
|
||||
}
|
||||
let dimensions = animatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
strongSelf.fetchDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(animatedStickerFile), resource: animatedStickerFile.resource).start())
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: animatedStickerFile.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
strongSelf.animationNode = animationNode
|
||||
}
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.alpha = 0.0
|
||||
}
|
||||
let dimensions = animatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
strongSelf.fetchDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(animatedStickerFile), resource: animatedStickerFile.resource).start())
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: animatedStickerFile.resource, isVideo: animatedStickerFile.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,9 +132,7 @@ final class HorizontalStickerGridItemNode: GridItemNode {
|
||||
func setup(account: Account, item: HorizontalStickerGridItem) {
|
||||
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1.file.id != item.file.id {
|
||||
if let dimensions = item.file.dimensions {
|
||||
if item.file.isVideoSticker {
|
||||
|
||||
} else if item.file.isAnimatedSticker {
|
||||
if item.file.isAnimatedSticker || item.file.isVideoSticker {
|
||||
let animationNode: AnimatedStickerNode
|
||||
if let currentAnimationNode = self.animationNode {
|
||||
animationNode = currentAnimationNode
|
||||
@ -154,11 +152,15 @@ final class HorizontalStickerGridItemNode: GridItemNode {
|
||||
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: true, size: fittedDimensions, synchronousLoad: false))
|
||||
if item.file.isVideoSticker {
|
||||
self.imageNode.setSignal(chatMessageSticker(postbox: account.postbox, file: item.file, small: true, synchronousLoad: false))
|
||||
} else {
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: true, size: fittedDimensions, synchronousLoad: false))
|
||||
}
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.alpha = 0.0
|
||||
}
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource, isVideo: item.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start())
|
||||
} else {
|
||||
|
@ -115,9 +115,7 @@ final class TrendingTopItemNode: ASDisplayNode {
|
||||
self.file = item.file
|
||||
self.itemSize = itemSize
|
||||
|
||||
if item.file.isVideoSticker {
|
||||
|
||||
} else if item.file.isAnimatedSticker {
|
||||
if item.file.isAnimatedSticker || item.file.isVideoSticker {
|
||||
let animationNode: AnimatedStickerNode
|
||||
if let currentAnimationNode = self.animationNode {
|
||||
animationNode = currentAnimationNode
|
||||
@ -135,11 +133,15 @@ final class TrendingTopItemNode: ASDisplayNode {
|
||||
}
|
||||
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: false, size: fittedDimensions, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
|
||||
if item.file.isVideoSticker {
|
||||
self.imageNode.setSignal(chatMessageSticker(postbox: account.postbox, file: item.file, small: false, synchronousLoad: synchronousLoads))
|
||||
} else {
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: false, size: fittedDimensions, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
|
||||
}
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.alpha = 0.0
|
||||
}
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource, isVideo: item.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start())
|
||||
} else {
|
||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
|
||||
|
@ -287,7 +287,7 @@ public final class NotificationViewControllerImpl {
|
||||
return
|
||||
}
|
||||
if let fileReference = accountAndImage.1 {
|
||||
if file.isAnimatedSticker {
|
||||
if file.isAnimatedSticker || file.isVideoSticker {
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = strongSelf.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
@ -308,8 +308,12 @@ public final class NotificationViewControllerImpl {
|
||||
}
|
||||
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
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))
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: accountAndImage.0, resource: fileReference.media.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
if file.isVideoSticker {
|
||||
strongSelf.imageNode.setSignal(chatMessageSticker(postbox: accountAndImage.0.postbox, file: fileReference.media, small: false))
|
||||
} else {
|
||||
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, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
animatedStickerNode.visibility = true
|
||||
|
||||
accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true))
|
||||
|
@ -245,7 +245,7 @@ private final class PrefetchManagerInnerImpl {
|
||||
|> mapToSignal { sticker -> Signal<Void, NoError> in
|
||||
if let sticker = sticker {
|
||||
let _ = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: sticker)).start()
|
||||
return chatMessageAnimationData(mediaBox: account.postbox.mediaBox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false)
|
||||
return chatMessageAnimationData(mediaBox: account.postbox.mediaBox, resource: sticker.resource, fitzModifier: nil, isVideo: sticker.isVideoSticker, width: 384, height: 384, synchronousLoad: false)
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import ChatImportUI
|
||||
import ZipArchive
|
||||
import ActivityIndicator
|
||||
import DebugSettingsUI
|
||||
import ManagedFile
|
||||
|
||||
private let inForeground = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
|
||||
|
@ -156,9 +156,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
||||
self.textNode.attributedText = NSAttributedString(string: code ?? "", font: textFont, textColor: .black)
|
||||
|
||||
if let dimensions = stickerItem.file.dimensions {
|
||||
if stickerItem.file.isVideoSticker {
|
||||
|
||||
} else if stickerItem.file.isAnimatedSticker {
|
||||
if stickerItem.file.isAnimatedSticker || stickerItem.file.isVideoSticker {
|
||||
if self.animationNode == nil {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
|
||||
@ -167,7 +165,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
||||
}
|
||||
let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource, isVideo: stickerItem.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.animationNode?.visibility = self.isVisibleInGrid
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||
} else {
|
||||
|
@ -12,7 +12,6 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ShimmerEffect
|
||||
import MergeLists
|
||||
import SoftwareVideo
|
||||
|
||||
private let boundingSize = CGSize(width: 41.0, height: 41.0)
|
||||
private let boundingImageSize = CGSize(width: 28.0, height: 28.0)
|
||||
@ -155,7 +154,6 @@ private final class FeaturedPackItemNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
private var videoNode: VideoStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
private let unreadNode: ASImageNode
|
||||
|
||||
@ -259,11 +257,10 @@ private final class FeaturedPackItemNode: ListViewItemNode {
|
||||
if let thumbnail = info.thumbnail {
|
||||
if info.flags.contains(.isAnimated) || info.flags.contains(.isVideo) {
|
||||
thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions, info.flags.contains(.isVideo))
|
||||
resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)
|
||||
} else {
|
||||
thumbnailItem = .still(thumbnail)
|
||||
resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)
|
||||
}
|
||||
resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)
|
||||
} else if let item = item {
|
||||
if item.file.isAnimatedSticker || item.file.isVideoSticker {
|
||||
thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100), item.file.isVideoSticker)
|
||||
@ -293,47 +290,23 @@ private final class FeaturedPackItemNode: ListViewItemNode {
|
||||
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
|
||||
if isVideo {
|
||||
let videoNode: VideoStickerNode
|
||||
if let current = self.videoNode {
|
||||
videoNode = current
|
||||
} else {
|
||||
videoNode = VideoStickerNode()
|
||||
videoNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
self.videoNode = videoNode
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.containerNode.insertSubnode(videoNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.containerNode.addSubnode(videoNode)
|
||||
}
|
||||
|
||||
if let resource = resource as? TelegramMediaResource {
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||
videoNode.update(account: account, fileReference: .standalone(media: dummyFile))
|
||||
}
|
||||
}
|
||||
videoNode.update(isPlaying: self.visibilityStatus && loopAnimatedStickers)
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.containerNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.containerNode.addSubnode(animatedStickerNode)
|
||||
}
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 128, height: 128, mode: .cached)
|
||||
animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.containerNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.containerNode.addSubnode(animatedStickerNode)
|
||||
}
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: 128, height: 128, mode: .cached)
|
||||
}
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
}
|
||||
if let resourceReference = resourceReference {
|
||||
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference).start())
|
||||
@ -354,10 +327,6 @@ private final class FeaturedPackItemNode: ListViewItemNode {
|
||||
animatedStickerNode.frame = self.imageNode.frame
|
||||
animatedStickerNode.updateLayout(size: self.imageNode.frame.size)
|
||||
}
|
||||
if let videoStickerNode = self.videoNode {
|
||||
videoStickerNode.frame = self.imageNode.frame
|
||||
videoStickerNode.updateLayout(size: self.imageNode.frame.size)
|
||||
}
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize)
|
||||
placeholderNode.position = self.imageNode.position
|
||||
|
@ -26,7 +26,6 @@ swift_library(
|
||||
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
|
||||
"//submodules/AvatarNode:AvatarNode",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -17,7 +17,6 @@ import AnimationUI
|
||||
import StickerResources
|
||||
import AvatarNode
|
||||
import AccountContext
|
||||
import SoftwareVideo
|
||||
|
||||
final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
private let elevatedLayout: Bool
|
||||
@ -28,7 +27,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
private let iconCheckNode: RadialStatusNode?
|
||||
private let animationNode: AnimationNode?
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
private var videoNode: VideoStickerNode?
|
||||
private var slotMachineNode: SlotMachineAnimationNode?
|
||||
private var stillStickerNode: TransformImageNode?
|
||||
private var stickerImageSize: CGSize?
|
||||
@ -95,7 +93,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = nil
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
displayUndo = true
|
||||
self.originalRemainingSeconds = 5
|
||||
@ -116,7 +113,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
}
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
displayUndo = undo
|
||||
@ -127,7 +123,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_archiveswipe", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
displayUndo = undo
|
||||
@ -138,7 +134,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
displayUndo = undo
|
||||
@ -149,7 +145,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: isOn ? "anim_autoremove_on" : "anim_autoremove_off", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
if let title = title {
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
|
||||
}
|
||||
@ -162,7 +158,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -177,7 +172,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -192,7 +186,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
undoTextColor = UIColor(rgb: 0xff7b74)
|
||||
|
||||
@ -211,7 +204,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_linkcopied", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -226,7 +218,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_banned", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -244,7 +235,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = nil
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
displayUndo = false
|
||||
self.originalRemainingSeconds = 5
|
||||
@ -254,7 +245,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let formattedString = presentationData.strings.ChatList_AddedToFolderTooltip(chatTitle, folderTitle)
|
||||
|
||||
@ -272,8 +262,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let formattedString = presentationData.strings.ChatList_RemovedFromFolderTooltip(chatTitle, folderTitle)
|
||||
|
||||
let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString.string, font: Font.regular(14.0), textColor: .white))
|
||||
@ -290,8 +279,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_payment", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
|
||||
let formattedString = presentationData.strings.Checkout_SuccessfulTooltip(currencyValue, itemTitle)
|
||||
|
||||
let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString.string, font: Font.regular(14.0), textColor: .white))
|
||||
@ -308,7 +296,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: isHidden ? "anim_message_hidepin" : "anim_message_unpin", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -343,7 +330,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_swipereply", colors: [:], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
@ -435,19 +422,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
case .still:
|
||||
break
|
||||
case let .animated(resource, isVideo):
|
||||
if isVideo {
|
||||
let videoNode = VideoStickerNode()
|
||||
self.videoNode = videoNode
|
||||
|
||||
if let resource = resource._asResource() as? TelegramMediaResource {
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||
videoNode.update(account: context.account, fileReference: .standalone(media: dummyFile))
|
||||
}
|
||||
} else {
|
||||
let animatedStickerNode = AnimatedStickerNode()
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource._asResource()), width: 80, height: 80, mode: .cached)
|
||||
}
|
||||
let animatedStickerNode = AnimatedStickerNode()
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource._asResource(), isVideo: isVideo), width: 80, height: 80, mode: .cached)
|
||||
}
|
||||
}
|
||||
case let .dice(dice, context, text, action):
|
||||
@ -510,7 +487,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: cancelled ? "anim_proximity_cancelled" : "anim_proximity_set", colors: [:], scale: 0.45)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -529,7 +505,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = nil
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -547,7 +522,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: slowdown ? "anim_voicespeedstop" : "anim_voicespeed", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -563,7 +537,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: savedMessages ? "anim_savedmessages" : "anim_forward", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -579,7 +552,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_gigagroup", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -595,7 +567,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_linkrevoked", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -611,7 +582,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_vcrecord", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -627,7 +597,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_vcflag", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -643,7 +612,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_vcspeak", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -732,7 +700,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
case let .animated(resource):
|
||||
let animatedStickerNode = AnimatedStickerNode()
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource._asResource()), width: 80, height: 80, mode: .cached)
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource._asResource(), isVideo: file.isVideoSticker), width: 80, height: 80, mode: .cached)
|
||||
}
|
||||
}
|
||||
case let .copy(text):
|
||||
@ -741,7 +709,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_copy", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
@ -777,7 +744,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_inviterequest", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
self.videoNode = nil
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
@ -824,7 +790,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.animationNode.flatMap(self.panelWrapperNode.addSubnode)
|
||||
self.stillStickerNode.flatMap(self.panelWrapperNode.addSubnode)
|
||||
self.animatedStickerNode.flatMap(self.panelWrapperNode.addSubnode)
|
||||
self.videoNode.flatMap(self.panelWrapperNode.addSubnode)
|
||||
self.slotMachineNode.flatMap(self.panelWrapperNode.addSubnode)
|
||||
self.avatarNode.flatMap(self.panelWrapperNode.addSubnode)
|
||||
self.panelWrapperNode.addSubnode(self.titleNode)
|
||||
@ -854,9 +819,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.animatedStickerNode?.started = { [weak self] in
|
||||
self?.stillStickerNode?.isHidden = true
|
||||
}
|
||||
self.videoNode?.started = { [weak self] in
|
||||
self?.stillStickerNode?.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -1041,20 +1003,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
transition.updateFrame(node: stillStickerNode, frame: iconFrame)
|
||||
}
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.updateLayout(size: iconFrame.size)
|
||||
transition.updateFrame(node: videoNode, frame: iconFrame)
|
||||
} else if let animatedStickerNode = self.animatedStickerNode {
|
||||
if let animatedStickerNode = self.animatedStickerNode {
|
||||
animatedStickerNode.updateLayout(size: iconFrame.size)
|
||||
transition.updateFrame(node: animatedStickerNode, frame: iconFrame)
|
||||
} else if let slotMachineNode = self.slotMachineNode {
|
||||
transition.updateFrame(node: slotMachineNode, frame: iconFrame)
|
||||
}
|
||||
} else if let videoNode = self.videoNode {
|
||||
let iconSize = CGSize(width: 32.0, height: 32.0)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0)), size: iconSize)
|
||||
videoNode.updateLayout(size: iconFrame.size)
|
||||
transition.updateFrame(node: videoNode, frame: iconFrame)
|
||||
} else if let animatedStickerNode = self.animatedStickerNode {
|
||||
let iconSize = CGSize(width: 32.0, height: 32.0)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0)), size: iconSize)
|
||||
@ -1105,7 +1059,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
})
|
||||
}
|
||||
|
||||
self.videoNode?.update(isPlaying: true)
|
||||
self.animatedStickerNode?.visibility = true
|
||||
|
||||
self.checkTimer()
|
||||
|
@ -1,4 +1,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void encodeRGBAToYUVA(uint8_t *yuva, uint8_t const *argb, int width, int height, int bytesPerRow);
|
||||
void resizeAndEncodeRGBAToYUVA(uint8_t *yuva, uint8_t const *argb, int width, int height, int bytesPerRow, int originalWidth, int originalHeight, int originalBytesPerRow);
|
||||
|
||||
void decodeYUVAToRGBA(uint8_t const *yuva, uint8_t *argb, int width, int height, int bytesPerRow);
|
||||
void decodeYUVAPlanesToRGBA(uint8_t const *yPlane, uint8_t const *uvPlane, uint8_t const *alphaPlane, uint8_t *argb, int width, int height, int bytesPerRow);
|
||||
|
@ -51,6 +51,69 @@ void encodeRGBAToYUVA(uint8_t *yuva, uint8_t const *argb, int width, int height,
|
||||
return;
|
||||
}
|
||||
}
|
||||
void resizeAndEncodeRGBAToYUVA(uint8_t *yuva, uint8_t const *argb, int width, int height, int bytesPerRow, int originalWidth, int originalHeight, int originalBytesPerRow) {
|
||||
static vImage_ARGBToYpCbCr info;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 };
|
||||
vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &info, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0);
|
||||
});
|
||||
|
||||
vImage_Error error = kvImageNoError;
|
||||
|
||||
vImage_Buffer src;
|
||||
src.data = (void *)argb;
|
||||
src.width = originalWidth;
|
||||
src.height = originalHeight;
|
||||
src.rowBytes = originalBytesPerRow;
|
||||
|
||||
uint8_t *tmpData = malloc(bytesPerRow * height);
|
||||
|
||||
vImage_Buffer dst;
|
||||
dst.data = (void *)tmpData;
|
||||
dst.width = width;
|
||||
dst.height = height;
|
||||
dst.rowBytes = bytesPerRow;
|
||||
|
||||
error = vImageScale_ARGB8888(&src, &dst, NULL, kvImageDoNotTile);
|
||||
|
||||
uint8_t permuteMap[4] = {3, 2, 1, 0};
|
||||
error = vImagePermuteChannels_ARGB8888(&dst, &dst, permuteMap, kvImageDoNotTile);
|
||||
|
||||
error = vImageUnpremultiplyData_ARGB8888(&dst, &dst, kvImageDoNotTile);
|
||||
|
||||
uint8_t *alpha = yuva + width * height * 2;
|
||||
int i = 0;
|
||||
for (int y = 0; y < height; y += 1) {
|
||||
uint8_t const *argbRow = dst.data + y * bytesPerRow;
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
uint8_t a0 = (argbRow[x * 4 + 0] >> 4) << 4;
|
||||
uint8_t a1 = (argbRow[(x + 1) * 4 + 0] >> 4) << 4;
|
||||
alpha[i / 2] = (a0 & (0xf0U)) | ((a1 & (0xf0U)) >> 4);
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
|
||||
vImage_Buffer destYp;
|
||||
destYp.data = (void *)(yuva + 0);
|
||||
destYp.width = width;
|
||||
destYp.height = height;
|
||||
destYp.rowBytes = width;
|
||||
|
||||
vImage_Buffer destCbCr;
|
||||
destCbCr.data = (void *)(yuva + width * height * 1);
|
||||
destCbCr.width = width;
|
||||
destCbCr.height = height;
|
||||
destCbCr.rowBytes = width;
|
||||
|
||||
error = vImageConvert_ARGB8888To420Yp8_CbCr8(&dst, &destYp, &destCbCr, &info, NULL, kvImageDoNotTile);
|
||||
|
||||
free(tmpData);
|
||||
|
||||
if (error != kvImageNoError) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void decodeYUVAToRGBA(uint8_t const *yuva, uint8_t *argb, int width, int height, int bytesPerRow) {
|
||||
static vImage_YpCbCrToARGB info;
|
||||
@ -105,3 +168,53 @@ void decodeYUVAToRGBA(uint8_t const *yuva, uint8_t *argb, int width, int height,
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void decodeYUVAPlanesToRGBA(uint8_t const *yPlane, uint8_t const *uvPlane, uint8_t const *alphaPlane, uint8_t *argb, int width, int height, int bytesPerRow) {
|
||||
static vImage_YpCbCrToARGB info;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 };
|
||||
vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0);
|
||||
});
|
||||
|
||||
vImage_Error error = kvImageNoError;
|
||||
|
||||
vImage_Buffer srcYp;
|
||||
srcYp.data = (void *)(yPlane + 0);
|
||||
srcYp.width = width;
|
||||
srcYp.height = height;
|
||||
srcYp.rowBytes = width * 1;
|
||||
|
||||
vImage_Buffer srcCbCr;
|
||||
srcCbCr.data = (void *)(uvPlane);
|
||||
srcCbCr.width = width;
|
||||
srcCbCr.height = height;
|
||||
srcCbCr.rowBytes = width * 1;
|
||||
|
||||
vImage_Buffer dest;
|
||||
dest.data = (void *)argb;
|
||||
dest.width = width;
|
||||
dest.height = height;
|
||||
dest.rowBytes = bytesPerRow;
|
||||
|
||||
error = vImageConvert_420Yp8_CbCr8ToARGB8888(&srcYp, &srcCbCr, &dest, &info, NULL, 0xff, kvImageDoNotTile);
|
||||
|
||||
int i = 0;
|
||||
for (int y = 0; y < height; y += 1) {
|
||||
uint8_t *argbRow = argb + y * bytesPerRow;
|
||||
for (int x = 0; x < width; x += 1) {
|
||||
argbRow[x * 4] = alphaPlane[i];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
error = vImagePremultiplyData_ARGB8888(&dest, &dest, kvImageDoNotTile);
|
||||
|
||||
uint8_t permuteMap[4] = {3, 2, 1, 0};
|
||||
error = vImagePermuteChannels_ARGB8888(&dest, &dest, permuteMap, kvImageDoNotTile);
|
||||
|
||||
if (error != kvImageNoError) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user