wake-ios/wake/View/Components/Upload/ImageUploadService.swift
2025-09-01 19:42:32 +08:00

461 lines
19 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import UIKit
///
public class ImageUploadService {
// MARK: - Shared Instance
public static let shared = ImageUploadService()
// MARK: - Properties
private let uploader: ImageUploaderGetID
// MARK: - Initialization
public init(uploader: ImageUploaderGetID = ImageUploaderGetID()) {
self.uploader = uploader
}
// MARK: - Public Methods
///
/// - Parameters:
/// - image:
/// - progressHandler: (0.0 1.0)
/// - completion:
public func uploadImage(
_ image: UIImage,
progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void,
completion: @escaping (Result<ImageUploaderGetID.UploadResult, Error>) -> Void
) {
uploader.uploadImage(
image,
progress: { progress in
let progressInfo = ImageUploadService.UploadProgress(
current: Int(progress * 100),
total: 100,
progress: progress,
isOriginal: true
)
DispatchQueue.main.async {
progressHandler(progressInfo)
}
},
completion: { result in
DispatchQueue.main.async {
completion(result)
}
}
)
}
///
/// - Parameters:
/// - image:
/// - compressionQuality: (0.0 1.0)
/// - progressHandler: (0.0 1.0)
/// - completion:
public func uploadCompressedImage(
_ image: UIImage,
compressionQuality: CGFloat = 0.5,
progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void,
completion: @escaping (Result<ImageUploaderGetID.UploadResult, Error>) -> Void
) {
guard let compressedImage = image.jpegData(compressionQuality: compressionQuality).flatMap(UIImage.init(data:)) else {
completion(.failure(NSError(domain: "com.wake.upload", code: -1, userInfo: [NSLocalizedDescriptionKey: "图片压缩失败"])))
return
}
uploadImage(
compressedImage,
progress: progressHandler,
completion: completion
)
}
///
/// - Parameters:
/// - image:
/// - compressionQuality: (0.0 1.0)
/// - progressHandler:
/// - completion:
public func uploadOriginalAndCompressedImage(
_ image: UIImage,
compressionQuality: CGFloat = 0.5,
progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void,
completion: @escaping (Result<ImageUploadService.UploadResults, Error>) -> Void
) {
//
uploadImage(image, progress: { progress in
let originalProgress = ImageUploadService.UploadProgress(
current: Int(progress.progress * 100),
total: 200, // 200100 + 100
progress: progress.progress * 0.5, // 50%
isOriginal: true
)
progressHandler(originalProgress)
}) { [weak self] originalResult in
guard let self = self else { return }
switch originalResult {
case .success(let originalUploadResult):
//
self.uploadCompressedImage(
image,
compressionQuality: compressionQuality,
progress: { progress in
let compressedProgress = ImageUploadService.UploadProgress(
current: 100 + Int(progress.progress * 100), // 100
total: 200, // 200100 + 100
progress: 0.5 + (progress.progress * 0.5), // 50%
isOriginal: false
)
progressHandler(compressedProgress)
},
completion: { compressedResult in
switch compressedResult {
case .success(let compressedUploadResult):
let results = ImageUploadService.UploadResults(
original: originalUploadResult,
compressed: compressedUploadResult
)
completion(.success(results))
case .failure(let error):
completion(.failure(error))
}
}
)
case .failure(let error):
completion(.failure(error))
}
}
}
// MARK: - Unified Media Upload
///
/// - Parameters:
/// - media:
/// - progress: (0.0 1.0)
/// - completion:
public func uploadMedia(
_ media: MediaType,
progress: @escaping (UploadProgress) -> Void,
completion: @escaping (Result<MediaUploadResult, Error>) -> Void
) {
switch media {
case .image(let image):
print("🖼️ 开始处理图片上传")
uploadImage(
image,
progress: { progressInfo in
print("📊 图片上传进度: \(progressInfo.current)%")
progress(progressInfo)
},
completion: { result in
switch result {
case .success(let uploadResult):
print("✅ 图片上传完成, fileId: \(uploadResult.fileId)")
completion(.success(.file(uploadResult)))
case .failure(let error):
print("❌ 图片上传失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
)
case .video(let videoURL, _):
print("🎥 开始处理视频上传: \(videoURL.lastPathComponent)")
uploader.uploadVideo(
videoURL,
progress: { uploadProgress in
print("📊 视频上传进度: \(Int(uploadProgress * 100))%")
let progressInfo = UploadProgress(
current: Int(uploadProgress * 100),
total: 100,
progress: uploadProgress,
isOriginal: true
)
progress(progressInfo)
},
completion: { result in
switch result {
case .success(let videoResult):
print("✅ 视频文件上传完成, fileId: \(videoResult.fileId)")
print("🖼️ 开始提取视频缩略图...")
MediaUtils.extractFirstFrame(from: videoURL) { thumbnailResult in
switch thumbnailResult {
case .success(let thumbnailImage):
print("🖼️ 视频缩略图提取成功")
if let compressedThumbnail = thumbnailImage.resized(to: CGSize(width: 800, height: 800)) {
print("🖼️ 开始上传视频缩略图...")
self.uploader.uploadImage(
compressedThumbnail,
progress: { uploadProgress in
// 10%
let progressInfo = UploadProgress(
current: 90 + Int(uploadProgress * 10),
total: 100,
progress: 0.9 + (uploadProgress * 0.1),
isOriginal: false
)
progress(progressInfo)
},
completion: { thumbnailResult in
switch thumbnailResult {
case .success(let thumbnailUploadResult):
print("✅ 视频缩略图上传完成, fileId: \(thumbnailUploadResult.fileId)")
// preview_file_id ID
let finalVideoResult = ImageUploaderGetID.UploadResult(
fileUrl: videoResult.fileUrl,
fileName: videoResult.fileName,
fileSize: videoResult.fileSize,
fileId: videoResult.fileId,
previewFileId: thumbnailUploadResult.fileId // 使IDpreview_file_id
)
completion(.success(.video(video: finalVideoResult, thumbnail: thumbnailUploadResult)))
case .failure(let error):
print("❌ 视频缩略图上传失败: \(error.localizedDescription)")
// 使
completion(.success(.video(video: videoResult, thumbnail: nil)))
}
}
)
} else {
//
completion(.success(.video(video: videoResult, thumbnail: nil)))
}
case .failure(let error):
print("❌ 视频缩略图提取失败: \(error.localizedDescription)")
//
completion(.success(.video(video: videoResult, thumbnail: nil)))
}
}
case .failure(let error):
print("❌ 视频文件上传失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
)
}
}
///
private func uploadVideoWithThumbnail(
videoURL: URL,
existingThumbnail: UIImage?,
compressionQuality: CGFloat,
progress progressHandler: @escaping (UploadProgress) -> Void,
completion: @escaping (Result<MediaUploadResult, Error>) -> Void
) {
// 1.
func processThumbnail(_ thumbnail: UIImage) {
// 2.
guard let compressedThumbnail = thumbnail.jpegData(compressionQuality: compressionQuality).flatMap(UIImage.init(data:)) else {
let error = NSError(domain: "com.wake.upload", code: -1, userInfo: [NSLocalizedDescriptionKey: "缩略图压缩失败"])
completion(.failure(error))
return
}
// 3.
let videoProgress = Progress(totalUnitCount: 100)
let thumbnailProgress = Progress(totalUnitCount: 100)
//
let totalProgress = Progress(totalUnitCount: 200) // 100 + 100
totalProgress.addChild(videoProgress, withPendingUnitCount: 100)
totalProgress.addChild(thumbnailProgress, withPendingUnitCount: 100)
//
self.uploader.uploadVideo(
videoURL,
progress: { progress in
videoProgress.completedUnitCount = Int64(progress * 100)
let currentProgress = Double(totalProgress.completedUnitCount) / 200.0
progressHandler(UploadProgress(
current: Int(progress * 100),
total: 100,
progress: currentProgress,
isOriginal: true
))
},
completion: { videoResult in
switch videoResult {
case .success(let videoUploadResult):
// 4.
self.uploadCompressedImage(
compressedThumbnail,
compressionQuality: 1.0, //
progress: { progress in
thumbnailProgress.completedUnitCount = Int64(progress.progress * 100)
let currentProgress = Double(totalProgress.completedUnitCount) / 200.0
progressHandler(UploadProgress(
current: 100 + Int(progress.progress * 100),
total: 200,
progress: currentProgress,
isOriginal: false
))
},
completion: { thumbnailResult in
switch thumbnailResult {
case .success(let thumbnailUploadResult):
let result = MediaUploadResult.video(
video: videoUploadResult,
thumbnail: thumbnailUploadResult
)
completion(.success(result))
case .failure(let error):
completion(.failure(error))
}
}
)
case .failure(let error):
completion(.failure(error))
}
}
)
}
// 使
if let thumbnail = existingThumbnail {
processThumbnail(thumbnail)
} else {
//
MediaUtils.extractFirstFrame(from: videoURL) { result in
switch result {
case .success(let thumbnail):
processThumbnail(thumbnail)
case .failure(let error):
completion(.failure(error))
}
}
}
}
// MARK: - Supporting Types
///
public enum MediaType: Equatable {
case image(UIImage)
case video(URL, UIImage?)
// id
var id: String {
switch self {
case .image(let uiImage):
return "image_\(uiImage.hashValue)"
case .video(let url, _):
return "video_\(url.absoluteString.hashValue)"
}
}
}
///
public enum MediaUploadResult {
case file(ImageUploaderGetID.UploadResult)
case video(video: ImageUploaderGetID.UploadResult, thumbnail: ImageUploaderGetID.UploadResult?)
/// IDID
public var fileId: String {
switch self {
case .file(let result):
return result.fileId
case .video(let videoResult, _):
return videoResult.fileId
}
}
/// IDID
public var previewFileId: String? {
switch self {
case .file:
return nil
case .video(_, let thumbnailResult):
return thumbnailResult?.fileId
}
}
/// URLURL
public var fileUrl: String {
switch self {
case .file(let result):
return result.fileUrl
case .video(let videoResult, _):
return videoResult.fileUrl
}
}
/// URL
public var thumbnailUrl: String? {
switch self {
case .file:
return nil
case .video(_, let thumbnailResult):
return thumbnailResult?.fileUrl
}
}
}
///
public struct UploadProgress {
public let current: Int
public let total: Int
public let progress: Double
public let isOriginal: Bool
public init(current: Int, total: Int, progress: Double, isOriginal: Bool) {
self.current = current
self.total = total
self.progress = progress
self.isOriginal = isOriginal
}
}
///
public struct UploadResults {
public let original: ImageUploaderGetID.UploadResult
public let compressed: ImageUploaderGetID.UploadResult
public init(original: ImageUploaderGetID.UploadResult,
compressed: ImageUploaderGetID.UploadResult) {
self.original = original
self.compressed = compressed
}
}
}
// MARK: - UIImage Extension
private extension UIImage {
func resized(to size: CGSize) -> UIImage? {
let widthRatio = size.width / self.size.width
let heightRatio = size.height / self.size.height
let ratio = min(widthRatio, heightRatio)
let newSize = CGSize(
width: self.size.width * ratio,
height: self.size.height * ratio
)
let renderer = UIGraphicsImageRenderer(size: newSize)
return renderer.image { _ in
self.draw(in: CGRect(origin: .zero, size: newSize))
}
}
}