This commit is contained in:
isaac 2026-04-26 02:44:12 +04:00
parent 2ac8a490b0
commit e9d958809a
2 changed files with 9 additions and 243 deletions

View file

@ -2664,6 +2664,13 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
}
}
private var nextAnimationId: Int = 0
private func takeNextAnimationId() -> Int {
let value = self.nextAnimationId
self.nextAnimationId += 1
return value
}
private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, animateFullTransition: Bool, customAnimationTransition: ControlledTransition?, synchronous: Bool, synchronousLoads: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set<Int>, scrollToItem originalScrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, forceInvertOffsetDirection: Bool = false, completion: () -> Void) {
var scrollToItem: ListViewScrollToItem?
var isExperimentalSnapToScrollToItem = false
@ -3319,7 +3326,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
animation.completion = { [weak self] _ in
self?.updateItemNodesVisibilities(onlyPositive: false)
}
self.layer.add(animation, forKey: "animation-\(ObjectIdentifier(animation))")
self.layer.add(animation, forKey: "animation-\(self.takeNextAnimationId())")
if !completeOffset.isZero {
for itemNode in self.itemNodes {
itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -completeOffset), animationCurve: animationCurve, duration: animationDuration)
@ -3747,7 +3754,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
headerNode.removeFromSupernode()
}
}
self.layer.add(animation, forKey: "animation-\(ObjectIdentifier(animation))")
self.layer.add(animation, forKey: "animation-\(self.takeNextAnimationId()))")
}
for itemNode in self.itemNodes {

View file

@ -1,246 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import SwiftSignalKit
import CoreMedia
import UniversalMediaPlayer
import AVFoundation
public let softwareVideoApplyQueue = Queue()
public let softwareVideoWorkers = ThreadPool(threadCount: 3, threadPriority: 0.2)
private var nextWorker = 0
public final class SoftwareVideoLayerFrameManager {
private let fetchDisposable: Disposable
private var dataDisposable = MetaDisposable()
private let source = Atomic<SoftwareVideoSource?>(value: nil)
private let hintVP9: Bool
private var baseTimestamp: Double?
private var frames: [MediaTrackFrame] = []
private var minPts: CMTime?
private var maxPts: CMTime?
private let account: Account
private let resource: MediaResource
private let secondaryResource: MediaResource?
private let queue: ThreadPoolQueue
private let layerHolder: SampleBufferLayer?
private weak var layer: AVSampleBufferDisplayLayer?
private var rotationAngle: CGFloat = 0.0
private var aspect: CGFloat = 1.0
private var layerRotationAngleAndAspect: (CGFloat, CGFloat)?
private var didStart = false
public var started: () -> Void = { }
public init(account: Account, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, layerHolder: SampleBufferLayer?, layer: AVSampleBufferDisplayLayer? = nil, hintVP9: Bool = false) {
var resource = fileReference.media.resource
var secondaryResource: MediaResource?
for attribute in fileReference.media.attributes {
if case .Video = attribute {
if let thumbnail = fileReference.media.videoThumbnails.first {
resource = thumbnail.resource
secondaryResource = fileReference.media.resource
}
}
}
nextWorker += 1
self.account = account
self.resource = resource
self.hintVP9 = hintVP9
self.secondaryResource = secondaryResource
self.queue = ThreadPoolQueue(threadPool: softwareVideoWorkers)
self.layerHolder = layerHolder
self.layer = layer ?? layerHolder?.layer
self.layer?.videoGravity = .resizeAspectFill
self.layer?.masksToBounds = true
self.fetchDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: fileReference.resourceReference(resource)).start()
}
deinit {
self.fetchDisposable.dispose()
self.dataDisposable.dispose()
}
public func start() {
func stringForResource(_ resource: MediaResource?) -> String {
guard let resource = resource else {
return "<none>"
}
if let resource = resource as? WebFileReferenceMediaResource {
return resource.url
} else {
return resource.id.stringRepresentation
}
}
Logger.shared.log("SoftwareVideo", "load video from \(stringForResource(self.resource)) or \(stringForResource(self.secondaryResource))")
let secondarySignal: Signal<(String, MediaResource)?, NoError>
if let secondaryResource = self.secondaryResource {
secondarySignal = self.account.postbox.mediaBox.resourceData(secondaryResource, option: .complete(waitUntilFetchStatus: false))
|> map { data -> (String, MediaResource)? in
if data.complete {
return (data.path, secondaryResource)
} else {
return nil
}
}
} else {
secondarySignal = .single(nil)
}
let firstResource = self.resource
let firstReady: Signal<(String, MediaResource), NoError> = combineLatest(
self.account.postbox.mediaBox.resourceData(self.resource, option: .complete(waitUntilFetchStatus: false)),
secondarySignal
)
|> mapToSignal { first, second -> Signal<(String, MediaResource), NoError> in
if first.complete {
return .single((first.path, firstResource))
} else if let second = second {
return .single(second)
} else {
return .complete()
}
}
|> take(1)
self.dataDisposable.set((firstReady
|> 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))")
let _ = strongSelf.source.swap(SoftwareVideoSource(path: path, hintVP9: strongSelf.hintVP9, unpremultiplyAlpha: true))
}
}))
}
public func tick(timestamp: Double) {
softwareVideoApplyQueue.async {
if self.baseTimestamp == nil && !self.frames.isEmpty {
self.baseTimestamp = timestamp
}
if let baseTimestamp = self.baseTimestamp {
var index = 0
var latestFrameIndex: Int?
while index < self.frames.count {
if baseTimestamp + self.frames[index].position.seconds + self.frames[index].duration.seconds <= timestamp {
latestFrameIndex = index
//print("latestFrameIndex = \(index)")
}
index += 1
}
if let latestFrameIndex = latestFrameIndex {
let frame = self.frames[latestFrameIndex]
for i in (0 ... latestFrameIndex).reversed() {
self.frames.remove(at: i)
}
if self.layer?.status == .failed {
self.layer?.flush()
}
/*if self.layerRotationAngleAndAspect?.0 != self.rotationAngle || self.layerRotationAngleAndAspect?.1 != self.aspect {
self.layerRotationAngleAndAspect = (self.rotationAngle, self.aspect)
var transform = CGAffineTransform(rotationAngle: CGFloat(self.rotationAngle))
if !self.rotationAngle.isZero {
transform = transform.scaledBy(x: CGFloat(self.aspect), y: CGFloat(1.0 / self.aspect))
}
self.layerHolder.layer.setAffineTransform(transform)
}*/
self.layer?.enqueue(frame.sampleBuffer)
if !self.didStart {
self.didStart = true
Queue.mainQueue().async {
self.started()
}
}
}
}
self.poll()
}
}
private var polling = false
private func poll() {
if self.frames.count < 2 && !self.polling {
self.polling = true
let minPts = self.minPts
let maxPts = self.maxPts
self.queue.addTask(ThreadPoolTask { [weak self] state in
if state.cancelled.with({ $0 }) {
return
}
if let strongSelf = self {
var frameAndLoop: (MediaTrackFrame?, CGFloat, CGFloat, Bool)?
var hadLoop = false
for _ in 0 ..< 1 {
frameAndLoop = (strongSelf.source.with { $0 })?.readFrame(maxPts: maxPts)
if let frameAndLoop = frameAndLoop {
if frameAndLoop.0 != nil || minPts != nil {
break
} else {
if frameAndLoop.3 {
hadLoop = true
}
//print("skip nil frame loop: \(frameAndLoop.3)")
}
} else {
break
}
}
if let loop = frameAndLoop?.3, loop {
hadLoop = true
}
softwareVideoApplyQueue.async {
if let strongSelf = self {
strongSelf.polling = false
if let (_, rotationAngle, aspect, _) = frameAndLoop {
strongSelf.rotationAngle = rotationAngle
strongSelf.aspect = aspect
}
if let frame = frameAndLoop?.0 {
if strongSelf.minPts == nil || CMTimeCompare(strongSelf.minPts!, frame.position) < 0 {
var position = CMTimeAdd(frame.position, frame.duration)
for _ in 0 ..< 1 {
position = CMTimeAdd(position, frame.duration)
}
strongSelf.minPts = position
}
strongSelf.frames.append(frame)
strongSelf.frames.sort(by: { lhs, rhs in
if CMTimeCompare(lhs.position, rhs.position) < 0 {
return true
} else {
return false
}
})
//print("add frame at \(CMTimeGetSeconds(frame.position))")
//let positions = strongSelf.frames.map { CMTimeGetSeconds($0.position) }
//print("frames: \(positions)")
} else {
//print("not adding frames")
}
if hadLoop {
strongSelf.maxPts = strongSelf.minPts
strongSelf.minPts = nil
//print("loop at \(strongSelf.minPts)")
}
strongSelf.poll()
}
}
}
})
}
}
}