mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 11:23:48 +00:00
Rewrite gif rendering
This commit is contained in:
parent
91074ca83a
commit
54ee2c17aa
@ -429,10 +429,6 @@ public final class FFMpegFileReader {
|
||||
if let stream = self.stream, Int(packet.streamIndex) == stream.info.index {
|
||||
let packetPts = packet.pts
|
||||
|
||||
/*if let focusedPart = self.focusedPart, packetPts >= focusedPart.endPts.value {
|
||||
self.hasReadToEnd = true
|
||||
}*/
|
||||
|
||||
let pts = CMTimeMake(value: packetPts, timescale: stream.info.timeScale)
|
||||
let dts = CMTimeMake(value: packet.dts, timescale: stream.info.timeScale)
|
||||
|
||||
@ -442,7 +438,7 @@ public final class FFMpegFileReader {
|
||||
if frameDuration != 0 {
|
||||
duration = CMTimeMake(value: frameDuration * stream.info.timeBase, timescale: stream.info.timeScale)
|
||||
} else {
|
||||
duration = stream.info.fps
|
||||
duration = CMTimeConvertScale(CMTimeMakeWithSeconds(1.0 / stream.info.fps.seconds, preferredTimescale: stream.info.timeScale), timescale: stream.info.timeScale, method: .quickTime)
|
||||
}
|
||||
|
||||
let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration)
|
||||
|
22
submodules/TelegramUI/Components/BatchVideoRendering/BUILD
Normal file
22
submodules/TelegramUI/Components/BatchVideoRendering/BUILD
Normal file
@ -0,0 +1,22 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "BatchVideoRendering",
|
||||
module_name = "BatchVideoRendering",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/Display",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramCore",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,341 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import UniversalMediaPlayer
|
||||
import AccountContext
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import CoreMedia
|
||||
|
||||
public protocol BatchVideoRenderingContextTarget: AnyObject {
|
||||
var batchVideoRenderingTargetState: BatchVideoRenderingContext.TargetState? { get set }
|
||||
|
||||
func setSampleBuffer(sampleBuffer: CMSampleBuffer)
|
||||
}
|
||||
|
||||
public final class BatchVideoRenderingContext {
|
||||
public typealias Target = BatchVideoRenderingContextTarget
|
||||
|
||||
public final class TargetHandle {
|
||||
private weak var context: BatchVideoRenderingContext?
|
||||
private let id: Int
|
||||
|
||||
init(context: BatchVideoRenderingContext, id: Int) {
|
||||
self.context = context
|
||||
self.id = id
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.context?.targetRemoved(id: self.id)
|
||||
}
|
||||
}
|
||||
|
||||
public final class TargetState {
|
||||
var currentFrameExpirationTimestamp: Double?
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReadingContext {
|
||||
let dataPath: String
|
||||
|
||||
var isFailed: Bool = false
|
||||
var reader: FFMpegFileReader?
|
||||
|
||||
init(dataPath: String) {
|
||||
self.dataPath = dataPath
|
||||
}
|
||||
|
||||
func advance() -> CMSampleBuffer? {
|
||||
outer: while true {
|
||||
if self.isFailed {
|
||||
break outer
|
||||
}
|
||||
if self.reader == nil {
|
||||
let reader = FFMpegFileReader(
|
||||
source: .file(self.dataPath),
|
||||
useHardwareAcceleration: false,
|
||||
selectedStream: .mediaType(.video),
|
||||
seek: nil,
|
||||
maxReadablePts: nil
|
||||
)
|
||||
if reader == nil {
|
||||
self.isFailed = true
|
||||
break outer
|
||||
}
|
||||
self.reader = reader
|
||||
}
|
||||
|
||||
guard let reader = self.reader else {
|
||||
break outer
|
||||
}
|
||||
|
||||
switch reader.readFrame() {
|
||||
case let .frame(frame):
|
||||
return createSampleBuffer(fromSampleBuffer: frame.sampleBuffer, withTimeOffset: .zero, duration: nil, displayImmediately: true)
|
||||
case .error:
|
||||
self.isFailed = true
|
||||
break outer
|
||||
case .endOfStream:
|
||||
self.reader = nil
|
||||
case .waitingForMoreData:
|
||||
self.isFailed = true
|
||||
break outer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class TargetContext {
|
||||
weak var target: Target?
|
||||
let file: TelegramMediaFile
|
||||
let userLocation: MediaResourceUserLocation
|
||||
|
||||
var readingContext: QueueLocalObject<ReadingContext>?
|
||||
var fetchDisposable: Disposable?
|
||||
var dataDisposable: Disposable?
|
||||
var dataPath: String?
|
||||
|
||||
init(
|
||||
target: Target,
|
||||
file: TelegramMediaFile,
|
||||
userLocation: MediaResourceUserLocation
|
||||
) {
|
||||
self.target = target
|
||||
self.file = file
|
||||
self.userLocation = userLocation
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchDisposable?.dispose()
|
||||
self.dataDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private static let sharedQueue = Queue(name: "BatchVideoRenderingContext", qos: .default)
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
private var targetContexts: [Int: TargetContext] = [:]
|
||||
private var nextId: Int = 0
|
||||
|
||||
private var isRendering: Bool = false
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
|
||||
public init(context: AccountContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
public func add(target: Target, file: TelegramMediaFile, userLocation: MediaResourceUserLocation) -> TargetHandle {
|
||||
let id = self.nextId
|
||||
self.nextId += 1
|
||||
|
||||
self.targetContexts[id] = TargetContext(
|
||||
target: target,
|
||||
file: file,
|
||||
userLocation: userLocation
|
||||
)
|
||||
self.update()
|
||||
|
||||
return TargetHandle(context: self, id: id)
|
||||
}
|
||||
|
||||
private func targetRemoved(id: Int) {
|
||||
if self.targetContexts.removeValue(forKey: id) != nil {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private func update() {
|
||||
var removeIds: [Int] = []
|
||||
for (id, targetContext) in self.targetContexts {
|
||||
if targetContext.target != nil {
|
||||
if targetContext.fetchDisposable == nil {
|
||||
//TODO:release pass resource reference
|
||||
targetContext.fetchDisposable = fetchedMediaResource(
|
||||
mediaBox: self.context.account.postbox.mediaBox,
|
||||
userLocation: targetContext.userLocation,
|
||||
userContentType: .sticker,
|
||||
reference: .media(media: .standalone(media: targetContext.file), resource: targetContext.file.resource)
|
||||
).startStrict()
|
||||
}
|
||||
if targetContext.dataDisposable == nil {
|
||||
targetContext.dataDisposable = (self.context.account.postbox.mediaBox.resourceData(targetContext.file.resource)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self, weak targetContext] data in
|
||||
guard let self, let targetContext else {
|
||||
return
|
||||
}
|
||||
if data.complete && targetContext.dataPath == nil {
|
||||
targetContext.dataPath = data.path
|
||||
self.update()
|
||||
}
|
||||
})
|
||||
}
|
||||
if targetContext.readingContext == nil, let dataPath = targetContext.dataPath {
|
||||
targetContext.readingContext = QueueLocalObject(queue: BatchVideoRenderingContext.sharedQueue, generate: {
|
||||
return ReadingContext(dataPath: dataPath)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
removeIds.append(id)
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.targetContexts.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
if !self.targetContexts.isEmpty {
|
||||
if self.displayLink == nil {
|
||||
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateRendering()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.displayLink = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRendering() {
|
||||
if self.isRendering {
|
||||
return
|
||||
}
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
var removeIds: [Int] = []
|
||||
var renderIds: [Int] = []
|
||||
for (id, targetContext) in self.targetContexts {
|
||||
guard let target = targetContext.target else {
|
||||
removeIds.append(id)
|
||||
continue
|
||||
}
|
||||
let targetState: TargetState
|
||||
if let current = target.batchVideoRenderingTargetState {
|
||||
targetState = current
|
||||
} else {
|
||||
targetState = TargetState()
|
||||
target.batchVideoRenderingTargetState = targetState
|
||||
}
|
||||
|
||||
if let currentFrameExpirationTimestamp = targetState.currentFrameExpirationTimestamp {
|
||||
if timestamp >= currentFrameExpirationTimestamp {
|
||||
renderIds.append(id)
|
||||
}
|
||||
} else {
|
||||
renderIds.append(id)
|
||||
}
|
||||
}
|
||||
|
||||
for id in removeIds {
|
||||
self.targetContexts.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
if !renderIds.isEmpty {
|
||||
self.isRendering = true
|
||||
|
||||
var readingContexts: [Int: QueueLocalObject<ReadingContext>] = [:]
|
||||
for id in renderIds {
|
||||
guard let targetContext = self.targetContexts[id] else {
|
||||
continue
|
||||
}
|
||||
if let readingContext = targetContext.readingContext {
|
||||
readingContexts[id] = readingContext
|
||||
}
|
||||
}
|
||||
BatchVideoRenderingContext.sharedQueue.async { [weak self] in
|
||||
var sampleBuffers: [Int: CMSampleBuffer?] = [:]
|
||||
for (id, readingContext) in readingContexts {
|
||||
guard let readingContext = readingContext.unsafeGet() else {
|
||||
sampleBuffers[id] = nil
|
||||
continue
|
||||
}
|
||||
if let sampleBuffer = readingContext.advance() {
|
||||
sampleBuffers[id] = sampleBuffer
|
||||
} else {
|
||||
sampleBuffers[id] = nil
|
||||
}
|
||||
}
|
||||
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isRendering = false
|
||||
|
||||
for (id, sampleBuffer) in sampleBuffers {
|
||||
guard let targetContext = self.targetContexts[id], let target = targetContext.target, let targetState = target.batchVideoRenderingTargetState else {
|
||||
return
|
||||
}
|
||||
if let sampleBuffer {
|
||||
target.setSampleBuffer(sampleBuffer: sampleBuffer)
|
||||
if let targetState = target.batchVideoRenderingTargetState {
|
||||
targetState.currentFrameExpirationTimestamp = CACurrentMediaTime() + CMSampleBufferGetDuration(sampleBuffer).seconds
|
||||
}
|
||||
} else {
|
||||
targetState.currentFrameExpirationTimestamp = CACurrentMediaTime() + 1.0 / 30.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.targetContexts.isEmpty {
|
||||
if self.displayLink == nil {
|
||||
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateRendering()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.displayLink = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createSampleBuffer(fromSampleBuffer sampleBuffer: CMSampleBuffer, withTimeOffset timeOffset: CMTime, duration: CMTime?, displayImmediately: Bool) -> CMSampleBuffer? {
|
||||
var itemCount: CMItemCount = 0
|
||||
var status = CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: 0, arrayToFill: nil, entriesNeededOut: &itemCount)
|
||||
if status != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(duration: CMTimeMake(value: 0, timescale: 0), presentationTimeStamp: CMTimeMake(value: 0, timescale: 0), decodeTimeStamp: CMTimeMake(value: 0, timescale: 0)), count: itemCount)
|
||||
status = CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: itemCount, arrayToFill: &timingInfo, entriesNeededOut: &itemCount)
|
||||
if status != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let dur = duration {
|
||||
for i in 0 ..< itemCount {
|
||||
timingInfo[i].decodeTimeStamp = CMTimeAdd(timingInfo[i].decodeTimeStamp, timeOffset)
|
||||
timingInfo[i].presentationTimeStamp = CMTimeAdd(timingInfo[i].presentationTimeStamp, timeOffset)
|
||||
timingInfo[i].duration = dur
|
||||
}
|
||||
} else {
|
||||
for i in 0 ..< itemCount {
|
||||
timingInfo[i].decodeTimeStamp = CMTimeAdd(timingInfo[i].decodeTimeStamp, timeOffset)
|
||||
timingInfo[i].presentationTimeStamp = CMTimeAdd(timingInfo[i].presentationTimeStamp, timeOffset)
|
||||
}
|
||||
}
|
||||
|
||||
var sampleBufferOffset: CMSampleBuffer?
|
||||
CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault, sampleBuffer: sampleBuffer, sampleTimingEntryCount: itemCount, sampleTimingArray: &timingInfo, sampleBufferOut: &sampleBufferOffset)
|
||||
guard let sampleBufferOffset else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if displayImmediately {
|
||||
let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBufferOffset, createIfNecessary: true)! as NSArray
|
||||
let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary
|
||||
dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber
|
||||
}
|
||||
|
||||
return sampleBufferOffset
|
||||
}
|
@ -50,6 +50,7 @@ swift_library(
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/TelegramCore/FlatBuffers",
|
||||
"//submodules/TelegramCore/FlatSerialization",
|
||||
"//submodules/TelegramUI/Components/BatchVideoRendering",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -19,18 +19,20 @@ import SoftwareVideo
|
||||
import AVFoundation
|
||||
import PhotoResources
|
||||
import ShimmerEffect
|
||||
import BatchVideoRendering
|
||||
|
||||
private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
||||
private class GifVideoLayer: AVSampleBufferDisplayLayer, BatchVideoRenderingContext.Target {
|
||||
private let context: AccountContext
|
||||
private let batchVideoContext: BatchVideoRenderingContext
|
||||
private let userLocation: MediaResourceUserLocation
|
||||
private let file: TelegramMediaFile?
|
||||
|
||||
private var frameManager: SoftwareVideoLayerFrameManager?
|
||||
private var batchVideoTargetHandle: BatchVideoRenderingContext.TargetHandle?
|
||||
var batchVideoRenderingTargetState: BatchVideoRenderingContext.TargetState?
|
||||
|
||||
private var thumbnailDisposable: Disposable?
|
||||
|
||||
private var playbackTimestamp: Double = 0.0
|
||||
private var playbackTimer: SwiftSignalKit.Timer?
|
||||
private var isReadyToRender: Bool = false
|
||||
|
||||
var started: (() -> Void)?
|
||||
|
||||
@ -39,28 +41,13 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
||||
if self.shouldBeAnimating == oldValue {
|
||||
return
|
||||
}
|
||||
|
||||
if self.shouldBeAnimating {
|
||||
self.playbackTimer?.invalidate()
|
||||
let startTimestamp = self.playbackTimestamp + CFAbsoluteTimeGetCurrent()
|
||||
self.playbackTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let timestamp = CFAbsoluteTimeGetCurrent() - startTimestamp
|
||||
strongSelf.frameManager?.tick(timestamp: timestamp)
|
||||
strongSelf.playbackTimestamp = timestamp
|
||||
}, queue: .mainQueue())
|
||||
self.playbackTimer?.start()
|
||||
} else {
|
||||
self.playbackTimer?.invalidate()
|
||||
self.playbackTimer = nil
|
||||
}
|
||||
self.updateShouldBeRendering()
|
||||
}
|
||||
}
|
||||
|
||||
init(context: AccountContext, userLocation: MediaResourceUserLocation, file: TelegramMediaFile?, synchronousLoad: Bool) {
|
||||
init(context: AccountContext, batchVideoContext: BatchVideoRenderingContext, userLocation: MediaResourceUserLocation, file: TelegramMediaFile?, synchronousLoad: Bool) {
|
||||
self.context = context
|
||||
self.batchVideoContext = batchVideoContext
|
||||
self.userLocation = userLocation
|
||||
self.file = file
|
||||
|
||||
@ -102,6 +89,7 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
||||
}
|
||||
|
||||
self.context = layer.context
|
||||
self.batchVideoContext = layer.batchVideoContext
|
||||
self.userLocation = layer.userLocation
|
||||
self.file = layer.file
|
||||
|
||||
@ -117,18 +105,28 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
||||
}
|
||||
|
||||
private func setupVideo() {
|
||||
guard let file = self.file else {
|
||||
return
|
||||
}
|
||||
let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: self.userLocation, userContentType: .other, fileReference: .savedGif(media: file), layerHolder: nil, layer: self)
|
||||
self.frameManager = frameManager
|
||||
frameManager.started = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
self.isReadyToRender = true
|
||||
self.updateShouldBeRendering()
|
||||
}
|
||||
|
||||
private func updateShouldBeRendering() {
|
||||
let shouldBeRendering = self.shouldBeAnimating && self.isReadyToRender
|
||||
|
||||
if shouldBeRendering, let file = self.file {
|
||||
if self.batchVideoTargetHandle == nil {
|
||||
self.batchVideoTargetHandle = self.batchVideoContext.add(target: self, file: file, userLocation: self.userLocation)
|
||||
}
|
||||
let _ = strongSelf
|
||||
} else {
|
||||
self.batchVideoTargetHandle = nil
|
||||
}
|
||||
}
|
||||
|
||||
func setSampleBuffer(sampleBuffer: CMSampleBuffer) {
|
||||
if #available(iOS 17.0, *) {
|
||||
self.sampleBufferRenderer.enqueue(sampleBuffer)
|
||||
} else {
|
||||
self.enqueue(sampleBuffer)
|
||||
}
|
||||
frameManager.start()
|
||||
}
|
||||
}
|
||||
|
||||
@ -382,6 +380,7 @@ public final class GifPagerContentComponent: Component {
|
||||
init(
|
||||
item: Item?,
|
||||
context: AccountContext,
|
||||
batchVideoContext: BatchVideoRenderingContext,
|
||||
groupId: String,
|
||||
attemptSynchronousLoad: Bool,
|
||||
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
|
||||
@ -389,7 +388,7 @@ public final class GifPagerContentComponent: Component {
|
||||
self.item = item
|
||||
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
|
||||
|
||||
super.init(context: context, userLocation: .other, file: item?.file.media, synchronousLoad: attemptSynchronousLoad)
|
||||
super.init(context: context, batchVideoContext: batchVideoContext, userLocation: .other, file: item?.file.media, synchronousLoad: attemptSynchronousLoad)
|
||||
|
||||
if item == nil {
|
||||
self.updateDisplayPlaceholder(displayPlaceholder: true, duration: 0.0)
|
||||
@ -594,6 +593,7 @@ public final class GifPagerContentComponent: Component {
|
||||
private var pagerEnvironment: PagerComponentChildEnvironment?
|
||||
private var theme: PresentationTheme?
|
||||
private var itemLayout: ItemLayout?
|
||||
private var batchVideoContext: BatchVideoRenderingContext?
|
||||
|
||||
private var currentLoadMoreToken: String?
|
||||
|
||||
@ -833,6 +833,14 @@ public final class GifPagerContentComponent: Component {
|
||||
searchInset += itemLayout.searchHeight
|
||||
}
|
||||
|
||||
let batchVideoContext: BatchVideoRenderingContext
|
||||
if let current = self.batchVideoContext {
|
||||
batchVideoContext = current
|
||||
} else {
|
||||
batchVideoContext = BatchVideoRenderingContext(context: component.context)
|
||||
self.batchVideoContext = batchVideoContext
|
||||
}
|
||||
|
||||
if let itemRange = itemLayout.visibleItems(for: self.scrollView.bounds) {
|
||||
for index in itemRange.lowerBound ..< itemRange.upperBound {
|
||||
var item: Item?
|
||||
@ -869,6 +877,7 @@ public final class GifPagerContentComponent: Component {
|
||||
itemLayer = ItemLayer(
|
||||
item: item,
|
||||
context: component.context,
|
||||
batchVideoContext: batchVideoContext,
|
||||
groupId: "savedGif",
|
||||
attemptSynchronousLoad: attemptSynchronousLoads,
|
||||
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
|
||||
|
Loading…
x
Reference in New Issue
Block a user