mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
6.0 KiB
Swift
175 lines
6.0 KiB
Swift
3 years ago
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||
|
|
||
|
import Foundation
|
||
|
import AVFoundation
|
||
|
import SessionUtilitiesKit
|
||
|
|
||
|
public class ThumbnailService {
|
||
|
// MARK: - Singleton class
|
||
|
|
||
|
public static let shared: ThumbnailService = ThumbnailService()
|
||
|
|
||
|
public typealias SuccessBlock = (LoadedThumbnail) -> Void
|
||
|
public typealias FailureBlock = (Error) -> Void
|
||
|
|
||
|
private let serialQueue = DispatchQueue(label: "ThumbnailService")
|
||
|
|
||
|
// This property should only be accessed on the serialQueue.
|
||
|
//
|
||
|
// We want to process requests in _reverse_ order in which they
|
||
|
// arrive so that we prioritize the most recent view state.
|
||
|
private var requestStack = [Request]()
|
||
|
|
||
|
private func canThumbnailAttachment(attachment: Attachment) -> Bool {
|
||
|
return attachment.isImage || attachment.isAnimated || attachment.isVideo
|
||
|
}
|
||
|
|
||
|
public func ensureThumbnail(
|
||
|
for attachment: Attachment,
|
||
|
dimensions: UInt,
|
||
|
success: @escaping SuccessBlock,
|
||
|
failure: @escaping FailureBlock
|
||
|
) {
|
||
|
serialQueue.async {
|
||
|
self.requestStack.append(
|
||
|
Request(
|
||
|
attachment: attachment,
|
||
|
dimensions: dimensions,
|
||
|
success: success,
|
||
|
failure: failure
|
||
|
)
|
||
|
)
|
||
|
|
||
|
self.processNextRequestSync()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private func processNextRequestAsync() {
|
||
|
serialQueue.async {
|
||
|
self.processNextRequestSync()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// This should only be called on the serialQueue.
|
||
|
private func processNextRequestSync() {
|
||
|
guard let thumbnailRequest = requestStack.popLast() else { return }
|
||
|
|
||
|
do {
|
||
|
let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest)
|
||
|
DispatchQueue.global().async {
|
||
|
thumbnailRequest.success(loadedThumbnail)
|
||
|
}
|
||
|
}
|
||
|
catch {
|
||
|
DispatchQueue.global().async {
|
||
|
thumbnailRequest.failure(error)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// This should only be called on the serialQueue.
|
||
|
//
|
||
|
// It should be safe to assume that an attachment will never end up with two thumbnails of
|
||
|
// the same size since:
|
||
|
//
|
||
|
// * Thumbnails are only added by this method.
|
||
|
// * This method checks for an existing thumbnail using the same connection.
|
||
|
// * This method is performed on the serial queue.
|
||
|
private func process(thumbnailRequest: Request) throws -> LoadedThumbnail {
|
||
|
let attachment = thumbnailRequest.attachment
|
||
|
|
||
|
guard canThumbnailAttachment(attachment: attachment) else {
|
||
|
throw ThumbnailError.failure(description: "Cannot thumbnail attachment.")
|
||
|
}
|
||
|
|
||
|
let thumbnailPath = attachment.thumbnailPath(for: thumbnailRequest.dimensions)
|
||
|
|
||
|
if FileManager.default.fileExists(atPath: thumbnailPath) {
|
||
|
guard let image = UIImage(contentsOfFile: thumbnailPath) else {
|
||
|
throw ThumbnailError.failure(description: "Could not load thumbnail.")
|
||
|
}
|
||
|
return LoadedThumbnail(image: image, filePath: thumbnailPath)
|
||
|
}
|
||
|
|
||
|
let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent
|
||
|
|
||
|
guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else {
|
||
|
throw ThumbnailError.failure(description: "Could not create attachment's thumbnail directory.")
|
||
|
}
|
||
|
guard let originalFilePath = attachment.originalFilePath else {
|
||
|
throw ThumbnailError.failure(description: "Missing original file path.")
|
||
|
}
|
||
|
|
||
|
let maxDimension = CGFloat(thumbnailRequest.dimensions)
|
||
|
let thumbnailImage: UIImage
|
||
|
|
||
|
if attachment.isImage || attachment.isAnimated {
|
||
|
thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension)
|
||
|
}
|
||
|
else if attachment.isVideo {
|
||
|
thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension)
|
||
|
}
|
||
|
else {
|
||
|
throw ThumbnailError.assertionFailure(description: "Invalid attachment type.")
|
||
|
}
|
||
|
|
||
|
guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else {
|
||
|
throw ThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
|
||
|
}
|
||
|
|
||
|
do {
|
||
|
try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath, isDirectory: false), options: .atomic)
|
||
|
}
|
||
|
catch let error as NSError {
|
||
|
throw ThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
|
||
|
}
|
||
|
|
||
|
OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath)
|
||
|
|
||
|
return LoadedThumbnail(image: thumbnailImage, data: thumbnailData)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public extension ThumbnailService {
|
||
|
enum ThumbnailError: Error {
|
||
|
case failure(description: String)
|
||
|
case assertionFailure(description: String)
|
||
|
case externalError(description: String, underlyingError: Error)
|
||
|
}
|
||
|
|
||
|
struct LoadedThumbnail {
|
||
|
public typealias DataSourceBlock = () throws -> Data
|
||
|
|
||
|
public let image: UIImage
|
||
|
public let dataSourceBlock: DataSourceBlock
|
||
|
|
||
|
public init(image: UIImage, filePath: String) {
|
||
|
self.image = image
|
||
|
self.dataSourceBlock = {
|
||
|
return try Data(contentsOf: URL(fileURLWithPath: filePath))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public init(image: UIImage, data: Data) {
|
||
|
self.image = image
|
||
|
self.dataSourceBlock = {
|
||
|
return data
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public func data() throws -> Data {
|
||
|
return try dataSourceBlock()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private struct Request {
|
||
|
public typealias SuccessBlock = (LoadedThumbnail) -> Void
|
||
|
public typealias FailureBlock = (Error) -> Void
|
||
|
|
||
|
let attachment: Attachment
|
||
|
let dimensions: UInt
|
||
|
let success: SuccessBlock
|
||
|
let failure: FailureBlock
|
||
|
}
|
||
|
}
|