Reimplement video stickers

This commit is contained in:
Ilya Laktyushin 2022-01-27 13:06:44 +03:00
parent 97f224f0a8
commit bbc082e991
60 changed files with 1611 additions and 1462 deletions

View File

@ -31,6 +31,8 @@ swift_library(
"//submodules/GZip:GZip",
"//submodules/rlottie:RLottieBinding",
"//submodules/MediaResources:MediaResources",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/ManagedFile:ManagedFile",
],
visibility = [
"//visibility:public",

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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
// }
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/Display:Display",
"//submodules/FastBlur:FastBlur",
"//submodules/MozjpegBinding:MozjpegBinding",
"//submodules/ManagedFile:ManagedFile",
],
visibility = [
"//visibility:public",

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -22,7 +22,6 @@ swift_library(
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/SoftwareVideo:SoftwareVideo",
],
visibility = [
"//visibility:public",

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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))

View File

@ -76,6 +76,7 @@ swift_library(
deps = [
":LottieMeshBinding",
"//submodules/Postbox:Postbox",
"//submodules/ManagedFile:ManagedFile",
],
visibility = [
"//visibility:public",

View File

@ -1,5 +1,6 @@
import Foundation
import Postbox
import ManagedFile
private let emptyMemory = malloc(1)!

View File

@ -3,6 +3,7 @@ import Metal
import MetalKit
import LottieMeshBinding
import Postbox
import ManagedFile
enum TriangleFill {
struct Color {

View 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",
],
)

View File

@ -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)
}

View File

@ -18,6 +18,7 @@ swift_library(
"//submodules/TelegramAudio:TelegramAudio",
"//submodules/FFMpegBinding:FFMpegBinding",
"//submodules/RingBuffer:RingBuffer",
"//submodules/YuvConversion:YuvConversion",
],
visibility = [
"//visibility:public",

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -15,6 +15,7 @@ swift_library(
"//submodules/sqlcipher:sqlcipher",
"//submodules/MurMurHash32:MurMurHash32",
"//submodules/StringTransliteration:StringTransliteration",
"//submodules/ManagedFile:ManagedFile",
],
visibility = [
"//visibility:public",

View File

@ -1,5 +1,6 @@
import Foundation
import SwiftSignalKit
import ManagedFile
private final class ResourceStatusContext {
var status: MediaResourceStatus?

View File

@ -1,8 +1,7 @@
import Foundation
import SwiftSignalKit
import Crc32
import ManagedFile
private final class MediaBoxFileMap {
fileprivate(set) var sum: Int32

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -31,7 +31,6 @@ swift_library(
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/UndoUI:UndoUI",
"//submodules/ContextUI:ContextUI",
"//submodules/SoftwareVideo:SoftwareVideo",
],
visibility = [
"//visibility:public",

View File

@ -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()

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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",

View File

@ -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)
}

View File

@ -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
}

View File

@ -19,6 +19,7 @@ swift_library(
"//submodules/CryptoUtils:CryptoUtils",
"//submodules/NetworkLogging:NetworkLogging",
"//submodules/Reachability:Reachability",
"//submodules/ManagedFile:ManagedFile",
],
visibility = [
"//visibility:public",

View File

@ -4,6 +4,7 @@ import TelegramApi
import SwiftSignalKit
import MtProtoKit
import CryptoUtils
import ManagedFile
private typealias SignalKitTimer = SwiftSignalKit.Timer

View File

@ -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 {

View File

@ -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)

View File

@ -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
}

View File

@ -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,

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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))

View File

@ -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()
}

View File

@ -23,6 +23,7 @@ import ChatImportUI
import ZipArchive
import ActivityIndicator
import DebugSettingsUI
import ManagedFile
private let inForeground = ValuePromise<Bool>(false, ignoreRepeated: true)

View File

@ -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 {

View File

@ -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

View File

@ -26,7 +26,6 @@ swift_library(
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
"//submodules/AvatarNode:AvatarNode",
"//submodules/AccountContext:AccountContext",
"//submodules/SoftwareVideo:SoftwareVideo",
],
visibility = [
"//visibility:public",

View File

@ -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()

View File

@ -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);

View File

@ -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;
}
}