Decode video preview on background queue

commit_hash:a30188831672e51e39dcde7729bf6a8e451e9008
This commit is contained in:
denlvovich
2026-04-06 23:33:22 +03:00
parent 4beae5f05d
commit f1304bf75b
7 changed files with 130 additions and 32 deletions
+1
View File
@@ -18738,6 +18738,7 @@
"client/ios/DivKit/Form/DivSubmitter.swift":"divkit/public/client/ios/DivKit/Form/DivSubmitter.swift",
"client/ios/DivKit/Form/SubmitRequest.swift":"divkit/public/client/ios/DivKit/Form/SubmitRequest.swift",
"client/ios/DivKit/IdToPath.swift":"divkit/public/client/ios/DivKit/IdToPath.swift",
"client/ios/DivKit/Images/AsyncDataImageHolder.swift":"divkit/public/client/ios/DivKit/Images/AsyncDataImageHolder.swift",
"client/ios/DivKit/Images/CachedRemoteImageHolder.swift":"divkit/public/client/ios/DivKit/Images/CachedRemoteImageHolder.swift",
"client/ios/DivKit/Images/DivImageHolderFactory.swift":"divkit/public/client/ios/DivKit/Images/DivImageHolderFactory.swift",
"client/ios/DivKit/LayoutProvider/DivLayoutProviderHandler.swift":"divkit/public/client/ios/DivKit/LayoutProvider/DivLayoutProviderHandler.swift",
@@ -38,7 +38,13 @@ extension DivVideo: DivBlockModeling {
let elapsedTime: Binding<Int>? = elapsedTimeVariable.flatMap {
context.makeBinding(variableName: $0, defaultValue: 0)
}
let preview: Image? = resolvePreview(resolver).flatMap(_makeImage(base64:))
let preview: ImageHolder = context
.imageHolderFactory.make(
nil,
resolvePreview(resolver)
.map { .imageData(ImageData(base64: $0)) }
)
let videoData = VideoData(videos: videoSources?
.compactMap { $0.makeVideo(resolver: resolver) } ?? []
)
@@ -124,18 +130,3 @@ extension DivVideoScale {
}
}
}
private func _makeImage(base64: String) -> Image? {
decode(base64: base64).flatMap(Image.init(data:))
}
fileprivate func decode(base64: String) -> Data? {
if let data = Data(base64Encoded: base64) {
return data
}
if let url = URL(string: base64),
let dataHoldingURL = try? Data(contentsOf: url) {
return dataHoldingURL
}
return nil
}
@@ -0,0 +1,85 @@
import Foundation
import VGSL
@preconcurrency @MainActor
final class AsyncDataImageHolder: ImageHolder {
private(set) weak var image: Image?
let placeholder: ImagePlaceholder?
private let imageData: ImageData
private let imageProcessingQueue: OperationQueueType
private let imageDecoder: (@Sendable (_ data: Data) -> Image?)?
nonisolated var debugDescription: String {
onMainThreadSync {
let imagePart = if let image {
String(format: "%.0fx%.0f", image.size.width, image.size.height)
} else {
"nil"
}
return "AsyncDataImageHolder(image=\(imagePart), placeholder=\(dbgStr(placeholder?.debugDescription)), customDecoder=\(imageDecoder != nil), queue=\(imageProcessingQueue), imageData=\(String(describing: imageData)))"
}
}
init(
data: ImageData,
imageProcessingQueue: OperationQueueType,
imageDecoder: (@Sendable (_ data: Data) -> Image?)? = nil,
placeholder: ImagePlaceholder? = nil
) {
self.imageData = data
self.imageProcessingQueue = imageProcessingQueue
self.imageDecoder = imageDecoder
self.placeholder = placeholder
}
func requestImageWithCompletion(
_ completion: @escaping @MainActor (Image?) -> Void
) -> Cancellable? {
imageData.makeImage(
queue: imageProcessingQueue,
decoder: imageDecoder
) { [weak self] image in
guard let self else { return }
completion(image)
self.image = image
}
return nil
}
func reused(
with placeholder: ImagePlaceholder?,
remoteImageURL url: URL?
) -> (any ImageHolder)? {
(self.placeholder === placeholder && url == nil) ? self : nil
}
func equals(_ other: any ImageHolder) -> Bool {
guard let other = other as? Self else {
return false
}
return self.imageData == other.imageData && self.imageProcessingQueue === other
.imageProcessingQueue
}
}
extension ImagePlaceholder {
public func toAsyncDataImageHolder(
imageProcessingQueue: OperationQueueType,
decoder: (@Sendable (Data) -> Image?)? = nil
) -> ImageHolder {
switch self {
case .image, .color, .view:
toImageHolder()
case let .imageData(imageData):
AsyncDataImageHolder(
data: imageData,
imageProcessingQueue: imageProcessingQueue,
imageDecoder: decoder
)
}
}
}
@@ -80,7 +80,9 @@ final class DefaultImageHolderFactory: DivImageHolderFactory {
func make(_ url: URL?, _ placeholder: ImagePlaceholder?) -> ImageHolder {
guard let url else {
return placeholder?.toImageHolder() ?? NilImageHolder()
return placeholder?.toAsyncDataImageHolder(
imageProcessingQueue: imageProcessingQueue
) ?? NilImageHolder()
}
return RemoteImageHolder(
url: url,
@@ -29,17 +29,25 @@ public final class SVGImageHolderFactory: DivImageHolderFactory {
}
public func make(_ url: URL?, _ placeholder: ImagePlaceholder?) -> ImageHolder {
guard let url else {
return placeholder?.toImageHolder() ?? NilImageHolder()
let decoder: (
@Sendable (_ data: Data) -> Image?
)? = { [weak self] in
self?.svgDecoder.decode(data: $0)
}
return if let url {
RemoteImageHolder(
url: url,
placeholder: placeholder,
requester: requester,
imageProcessingQueue: imageProcessingQueue,
imageDecoder: decoder
)
} else {
placeholder?.toAsyncDataImageHolder(
imageProcessingQueue: imageProcessingQueue,
decoder: decoder
) ?? NilImageHolder()
}
return RemoteImageHolder(
url: url,
placeholder: placeholder,
requester: requester,
imageProcessingQueue: imageProcessingQueue,
imageDecoder: { [weak self] in
self?.svgDecoder.decode(data: $0)
}
)
}
}
@@ -157,8 +157,14 @@ private final class VideoBlockView: BlockView, VisibleBoundsTrackingContainer {
player?.set(isMuted: model.playbackConfig.isMuted)
}
if let previewImage = model.preview {
preview.value.image = previewImage
if let preview = model.preview {
if !compare(preview, oldValue.preview) {
preview.requestImageWithCompletion { [weak self] image in
self?.updatePreview(image)
}
}
} else {
self.preview.value.image = nil
}
if let elapsedTime = model.elapsedTime?.value,
@@ -168,6 +174,11 @@ private final class VideoBlockView: BlockView, VisibleBoundsTrackingContainer {
}
}
private func updatePreview(_ image: Image?) {
preview.value.image = image
preview.value.frame = adjustPreviewFrame()
}
private func adjustPreviewFrame() -> CGRect {
guard let videoView,
let videoRatio = videoView.videoRatio else {
@@ -5,7 +5,7 @@ public struct VideoBlockViewModel: Equatable {
public let playbackConfig: PlaybackConfig
public var elapsedTime: Binding<Int>?
public var duration: Binding<Int>?
public let preview: Image?
public let preview: ImageHolder?
public let resumeActions: [UserInterfaceAction]
public let pauseActions: [UserInterfaceAction]
public let bufferingActions: [UserInterfaceAction]
@@ -18,7 +18,7 @@ public struct VideoBlockViewModel: Equatable {
public init(
videoData: VideoData,
playbackConfig: PlaybackConfig,
preview: Image? = nil,
preview: ImageHolder? = nil,
elapsedTime: Binding<Int>? = nil,
duration: Binding<Int>? = nil,
resumeActions: [UserInterfaceAction] = [],