From bbc082e99149cca15c37b7ce43bf839126af419b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 27 Jan 2022 13:06:44 +0300 Subject: [PATCH] Reimplement video stickers --- submodules/AnimatedStickerNode/BUILD | 2 + .../Sources/AnimatedStickerFrameSource.swift | 514 +++++++++++++ .../Sources/AnimatedStickerNode.swift | 691 +----------------- .../Sources/FitzModifier.swift | 77 -- .../Sources/FrameCompression.swift | 80 ++ .../Sources/SoftwareAnimationRenderer.swift | 2 +- .../Sources/VideoStickerFrameSource.swift | 380 ++++++++++ submodules/DirectMediaImageCache/BUILD | 1 + .../Sources/DirectMediaImageCache.swift | 1 + .../Sources/StickerPackPreviewGridItem.swift | 48 +- .../Sources/StickerPreviewPeekContent.swift | 29 +- submodules/ItemListStickerPackItem/BUILD | 1 - .../Sources/ItemListStickerPackItem.swift | 54 +- .../Sources/LegacyPaintStickerView.swift | 2 +- .../Sources/LegacyPaintStickersContext.swift | 4 +- submodules/LottieMeshSwift/BUILD | 1 + .../LottieMeshSwift/Sources/Buffer.swift | 1 + .../Sources/MeshAnimation.swift | 1 + submodules/ManagedFile/BUILD | 18 + .../Sources/ManagedFile.swift | 22 +- submodules/MediaPlayer/BUILD | 1 + .../FFMpegMediaVideoFrameDecoder.swift | 123 ++-- .../Sources/SoftwareVideoSource.swift | 8 + .../CachedResourceRepresentations.swift | 32 + submodules/Postbox/BUILD | 1 + submodules/Postbox/Sources/MediaBox.swift | 1 + submodules/Postbox/Sources/MediaBoxFile.swift | 3 +- .../SoftwareVideoLayerFrameManager.swift | 12 +- .../Sources/VideoStickerNode.swift | 126 ---- submodules/StickerPackPreviewUI/BUILD | 1 - .../StickerPackPreviewController.swift | 4 +- .../Sources/StickerPackPreviewGridItem.swift | 33 +- .../Sources/StickerPreviewPeekContent.swift | 23 +- .../Sources/StickerResources.swift | 4 +- submodules/TelegramAnimatedStickerNode/BUILD | 3 + .../Sources/AnimatedStickerUtils.swift | 180 ++++- .../Sources/TelegramAnimatedStickerNode.swift | 7 +- submodules/TelegramCore/BUILD | 1 + .../Sources/Network/MultipartUpload.swift | 1 + .../SyncCore/SyncCore_TelegramMediaFile.swift | 3 - .../TelegramCore/Sources/Utils/Log.swift | 1 + .../Sources/MessageContentKind.swift | 12 +- submodules/TelegramUI/BUILD | 1 + .../ChatMediaInputStickerGridItem.swift | 40 +- .../ChatMediaInputStickerPackItem.swift | 99 +-- .../ChatMessageAnimatedStickerItemNode.swift | 54 +- .../ChatMessageInteractiveMediaNode.swift | 2 +- .../Sources/FetchCachedRepresentations.swift | 32 +- ...ListContextResultsChatInputPanelItem.swift | 44 +- .../Sources/HorizontalStickerGridItem.swift | 12 +- .../Sources/MediaInputPaneTrendingItem.swift | 12 +- .../Sources/NotificationContentContext.swift | 10 +- .../TelegramUI/Sources/PrefetchManager.swift | 2 +- .../Sources/ShareExtensionContext.swift | 1 + .../StickerPaneSearchStickerItem.swift | 6 +- .../StickerPaneTrendingListGridItem.swift | 61 +- submodules/UndoUI/BUILD | 1 - .../Sources/UndoOverlayControllerNode.swift | 71 +- .../PublicHeaders/YuvConversion/YUV.h | 3 + submodules/YuvConversion/Sources/YUV.m | 113 +++ 60 files changed, 1611 insertions(+), 1462 deletions(-) create mode 100644 submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift delete mode 100644 submodules/AnimatedStickerNode/Sources/FitzModifier.swift create mode 100644 submodules/AnimatedStickerNode/Sources/FrameCompression.swift create mode 100644 submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift create mode 100644 submodules/ManagedFile/BUILD rename submodules/{Postbox => ManagedFile}/Sources/ManagedFile.swift (88%) delete mode 100644 submodules/SoftwareVideo/Sources/VideoStickerNode.swift diff --git a/submodules/AnimatedStickerNode/BUILD b/submodules/AnimatedStickerNode/BUILD index 6727d8fb62..64dd9c69ab 100644 --- a/submodules/AnimatedStickerNode/BUILD +++ b/submodules/AnimatedStickerNode/BUILD @@ -31,6 +31,8 @@ swift_library( "//submodules/GZip:GZip", "//submodules/rlottie:RLottieBinding", "//submodules/MediaResources:MediaResources", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/ManagedFile:ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift new file mode 100644 index 0000000000..2f44fa3cdf --- /dev/null +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift @@ -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) + 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() + + 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 + } +} diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 680ed20964..2fa2186a8f 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -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) - 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() - - 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 @@ -833,7 +166,7 @@ public final class AnimatedStickerNode: ASDisplayNode { private let timer = Atomic(value: nil) private let frameSource = Atomic?>(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() } diff --git a/submodules/AnimatedStickerNode/Sources/FitzModifier.swift b/submodules/AnimatedStickerNode/Sources/FitzModifier.swift deleted file mode 100644 index 40dd1a0141..0000000000 --- a/submodules/AnimatedStickerNode/Sources/FitzModifier.swift +++ /dev/null @@ -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 -// } -} diff --git a/submodules/AnimatedStickerNode/Sources/FrameCompression.swift b/submodules/AnimatedStickerNode/Sources/FrameCompression.swift new file mode 100644 index 0000000000..caca96f12d --- /dev/null +++ b/submodules/AnimatedStickerNode/Sources/FrameCompression.swift @@ -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 +} diff --git a/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift b/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift index eddb2d65f0..1c343b3773 100644 --- a/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift +++ b/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift @@ -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 diff --git a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift new file mode 100644 index 0000000000..ec870feac1 --- /dev/null +++ b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift @@ -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) + 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() + + 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 + } +} diff --git a/submodules/DirectMediaImageCache/BUILD b/submodules/DirectMediaImageCache/BUILD index 96b64ee510..fea404afaa 100644 --- a/submodules/DirectMediaImageCache/BUILD +++ b/submodules/DirectMediaImageCache/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/Display:Display", "//submodules/FastBlur:FastBlur", "//submodules/MozjpegBinding:MozjpegBinding", + "//submodules/ManagedFile:ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift index 0aa30ab5ac..adf84cb470 100644 --- a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift +++ b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift @@ -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) diff --git a/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift index 7182a53a63..5409cda938 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift @@ -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 diff --git a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift index fc67597ed2..b23b544ce3 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift @@ -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) } diff --git a/submodules/ItemListStickerPackItem/BUILD b/submodules/ItemListStickerPackItem/BUILD index 5ed2ed50b4..5752bd50bc 100644 --- a/submodules/ItemListStickerPackItem/BUILD +++ b/submodules/ItemListStickerPackItem/BUILD @@ -22,7 +22,6 @@ swift_library( "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/ShimmerEffect:ShimmerEffect", - "//submodules/SoftwareVideo:SoftwareVideo", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index b39946925d..88c82d36c3 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -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) } } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift index b38cef6f8f..893c35cd00 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift @@ -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) diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 2c6cfdf256..3be96d471b 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -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)) diff --git a/submodules/LottieMeshSwift/BUILD b/submodules/LottieMeshSwift/BUILD index 0253490349..ef088b0ef7 100644 --- a/submodules/LottieMeshSwift/BUILD +++ b/submodules/LottieMeshSwift/BUILD @@ -76,6 +76,7 @@ swift_library( deps = [ ":LottieMeshBinding", "//submodules/Postbox:Postbox", + "//submodules/ManagedFile:ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/LottieMeshSwift/Sources/Buffer.swift b/submodules/LottieMeshSwift/Sources/Buffer.swift index a620fbab3d..cff168947f 100644 --- a/submodules/LottieMeshSwift/Sources/Buffer.swift +++ b/submodules/LottieMeshSwift/Sources/Buffer.swift @@ -1,5 +1,6 @@ import Foundation import Postbox +import ManagedFile private let emptyMemory = malloc(1)! diff --git a/submodules/LottieMeshSwift/Sources/MeshAnimation.swift b/submodules/LottieMeshSwift/Sources/MeshAnimation.swift index b145a96a6d..d1236bdf23 100644 --- a/submodules/LottieMeshSwift/Sources/MeshAnimation.swift +++ b/submodules/LottieMeshSwift/Sources/MeshAnimation.swift @@ -3,6 +3,7 @@ import Metal import MetalKit import LottieMeshBinding import Postbox +import ManagedFile enum TriangleFill { struct Color { diff --git a/submodules/ManagedFile/BUILD b/submodules/ManagedFile/BUILD new file mode 100644 index 0000000000..ec4ed331cb --- /dev/null +++ b/submodules/ManagedFile/BUILD @@ -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", + ], +) diff --git a/submodules/Postbox/Sources/ManagedFile.swift b/submodules/ManagedFile/Sources/ManagedFile.swift similarity index 88% rename from submodules/Postbox/Sources/ManagedFile.swift rename to submodules/ManagedFile/Sources/ManagedFile.swift index e2a7336074..4db88c8929 100644 --- a/submodules/Postbox/Sources/ManagedFile.swift +++ b/submodules/ManagedFile/Sources/ManagedFile.swift @@ -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) } diff --git a/submodules/MediaPlayer/BUILD b/submodules/MediaPlayer/BUILD index a13978b14e..6ec063af61 100644 --- a/submodules/MediaPlayer/BUILD +++ b/submodules/MediaPlayer/BUILD @@ -18,6 +18,7 @@ swift_library( "//submodules/TelegramAudio:TelegramAudio", "//submodules/FFMpegBinding:FFMpegBinding", "//submodules/RingBuffer:RingBuffer", + "//submodules/YuvConversion:YuvConversion", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index dd0b30ed8b..2dd62a4297 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -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, Int)? + private var uvPlane: (UnsafeMutablePointer, 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 - if let (existingDstPlane, existingDstPlaneSize) = self.dstPlane, existingDstPlaneSize == dstPlaneSize { - dstPlane = existingDstPlane + let uvPlane: UnsafeMutablePointer + 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) + } } } diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 8c66f0c2ca..5653fa8028 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -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) diff --git a/submodules/MediaResources/Sources/CachedResourceRepresentations.swift b/submodules/MediaResources/Sources/CachedResourceRepresentations.swift index d087dc0340..2a5099e420 100644 --- a/submodules/MediaResources/Sources/CachedResourceRepresentations.swift +++ b/submodules/MediaResources/Sources/CachedResourceRepresentations.swift @@ -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 diff --git a/submodules/Postbox/BUILD b/submodules/Postbox/BUILD index 7c870ad8ae..5e93f5a24c 100644 --- a/submodules/Postbox/BUILD +++ b/submodules/Postbox/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/sqlcipher:sqlcipher", "//submodules/MurMurHash32:MurMurHash32", "//submodules/StringTransliteration:StringTransliteration", + "//submodules/ManagedFile:ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 9d98b5b0f3..3887b62ed7 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -1,5 +1,6 @@ import Foundation import SwiftSignalKit +import ManagedFile private final class ResourceStatusContext { var status: MediaResourceStatus? diff --git a/submodules/Postbox/Sources/MediaBoxFile.swift b/submodules/Postbox/Sources/MediaBoxFile.swift index 197613c33d..66259bdc81 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -1,8 +1,7 @@ import Foundation import SwiftSignalKit - import Crc32 - +import ManagedFile private final class MediaBoxFileMap { fileprivate(set) var sum: Int32 diff --git a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift index 70d00e8aba..e085159f60 100644 --- a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift @@ -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 { diff --git a/submodules/SoftwareVideo/Sources/VideoStickerNode.swift b/submodules/SoftwareVideo/Sources/VideoStickerNode.swift deleted file mode 100644 index 85c89d557b..0000000000 --- a/submodules/SoftwareVideo/Sources/VideoStickerNode.swift +++ /dev/null @@ -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) - } -} diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index be58773907..548a88134f 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -31,7 +31,6 @@ swift_library( "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/UndoUI:UndoUI", "//submodules/ContextUI:ContextUI", - "//submodules/SoftwareVideo:SoftwareVideo", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index 7a4b65ad2f..7529e37e44 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -291,8 +291,8 @@ public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCol let signal = Signal { 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() diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index a613cd7879..80eafddf00 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -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) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 83e1821310..d0df2d5dc2 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -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) } diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index ea6cf010ad..3c3f88d1d6 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -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 { - 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 { + 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 diff --git a/submodules/TelegramAnimatedStickerNode/BUILD b/submodules/TelegramAnimatedStickerNode/BUILD index 8515400051..8383f9921b 100644 --- a/submodules/TelegramAnimatedStickerNode/BUILD +++ b/submodules/TelegramAnimatedStickerNode/BUILD @@ -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", diff --git a/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift b/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift index 557946930a..1f45bc7c4f 100644 --- a/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift +++ b/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift @@ -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 { 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 { +public func cacheAnimatedStickerFrames(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal { return Signal({ subscriber in let cancelled = Atomic(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 { + return Signal { subscriber in + let cancelled = Atomic(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) +} diff --git a/submodules/TelegramAnimatedStickerNode/Sources/TelegramAnimatedStickerNode.swift b/submodules/TelegramAnimatedStickerNode/Sources/TelegramAnimatedStickerNode.swift index 4337e7f729..ca20f39c04 100644 --- a/submodules/TelegramAnimatedStickerNode/Sources/TelegramAnimatedStickerNode.swift +++ b/submodules/TelegramAnimatedStickerNode/Sources/TelegramAnimatedStickerNode.swift @@ -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 } diff --git a/submodules/TelegramCore/BUILD b/submodules/TelegramCore/BUILD index 28436f87e6..05e4423fc7 100644 --- a/submodules/TelegramCore/BUILD +++ b/submodules/TelegramCore/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/CryptoUtils:CryptoUtils", "//submodules/NetworkLogging:NetworkLogging", "//submodules/Reachability:Reachability", + "//submodules/ManagedFile:ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift index d5181b4a90..0d969eb729 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift @@ -4,6 +4,7 @@ import TelegramApi import SwiftSignalKit import MtProtoKit import CryptoUtils +import ManagedFile private typealias SignalKitTimer = SwiftSignalKit.Timer diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 788cf5095d..bc99b909ee 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/Utils/Log.swift b/submodules/TelegramCore/Sources/Utils/Log.swift index c12f70b4f0..65f44bb7ea 100644 --- a/submodules/TelegramCore/Sources/Utils/Log.swift +++ b/submodules/TelegramCore/Sources/Utils/Log.swift @@ -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) diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index a85a01fd1d..9ad5773ec9 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -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 } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 66ea767ea2..b18b86672a 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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, diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index 45e2644df8..069bed8229 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -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) } } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift index 974b729a9e..2e18981162 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 20c1ce5d47..b7533b2157 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -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) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 08514b4d60..23b9f3e003 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift index 66f142a002..e7aa5763fd 100644 --- a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift +++ b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift @@ -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 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 in @@ -710,15 +718,11 @@ private func fetchAnimatedStickerFirstFrameRepresentation(account: Account, reso private func fetchAnimatedStickerRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedAnimatedStickerRepresentation) -> Signal { 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 { + 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 { return Signal({ subscriber in diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index 1248d10f8d..6d75431d50 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -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) } } diff --git a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift index 2777027bdf..2d7d12b128 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/MediaInputPaneTrendingItem.swift b/submodules/TelegramUI/Sources/MediaInputPaneTrendingItem.swift index 6f43518638..a4ce743b45 100644 --- a/submodules/TelegramUI/Sources/MediaInputPaneTrendingItem.swift +++ b/submodules/TelegramUI/Sources/MediaInputPaneTrendingItem.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 7d6151ecf1..cd7d5d198f 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -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)) diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index 5bcf4f1c62..3ec707682b 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -245,7 +245,7 @@ private final class PrefetchManagerInnerImpl { |> mapToSignal { sticker -> Signal 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 in return .complete() } diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index 649ca55f98..f6622e108e 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -23,6 +23,7 @@ import ChatImportUI import ZipArchive import ActivityIndicator import DebugSettingsUI +import ManagedFile private let inForeground = ValuePromise(false, ignoreRepeated: true) diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift b/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift index c10c0851c1..d6982e88d4 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift +++ b/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift b/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift index 5e145972df..cd62df8d14 100644 --- a/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift +++ b/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift @@ -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 diff --git a/submodules/UndoUI/BUILD b/submodules/UndoUI/BUILD index 44c97c3572..3887213ddc 100644 --- a/submodules/UndoUI/BUILD +++ b/submodules/UndoUI/BUILD @@ -26,7 +26,6 @@ swift_library( "//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode", "//submodules/AvatarNode:AvatarNode", "//submodules/AccountContext:AccountContext", - "//submodules/SoftwareVideo:SoftwareVideo", ], visibility = [ "//visibility:public", diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 95f9a9f082..55f8a42358 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -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() diff --git a/submodules/YuvConversion/PublicHeaders/YuvConversion/YUV.h b/submodules/YuvConversion/PublicHeaders/YuvConversion/YUV.h index ab00691a46..18b1f3bfff 100644 --- a/submodules/YuvConversion/PublicHeaders/YuvConversion/YUV.h +++ b/submodules/YuvConversion/PublicHeaders/YuvConversion/YUV.h @@ -1,4 +1,7 @@ #import 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); diff --git a/submodules/YuvConversion/Sources/YUV.m b/submodules/YuvConversion/Sources/YUV.m index d5c2f0ad16..7a51d1b892 100644 --- a/submodules/YuvConversion/Sources/YUV.m +++ b/submodules/YuvConversion/Sources/YUV.m @@ -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; + } +} +