405 lines
17 KiB
Swift
405 lines
17 KiB
Swift
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, // 总进度为200(原图100 + 压缩图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, // 总进度为200(原图100 + 压缩图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: { _ in },
|
||
completion: { thumbnailResult in
|
||
switch thumbnailResult {
|
||
case .success(let thumbnailUploadResult):
|
||
print("✅ 视频缩略图上传完成, fileId: \(thumbnailUploadResult.fileId)")
|
||
let result = MediaUploadResult.video(
|
||
video: videoResult,
|
||
thumbnail: thumbnailUploadResult
|
||
)
|
||
completion(.success(result))
|
||
|
||
case .failure(let error):
|
||
print("❌ 视频缩略图上传失败: \(error.localizedDescription)")
|
||
completion(.failure(error))
|
||
}
|
||
}
|
||
)
|
||
} else {
|
||
let error = NSError(domain: "ImageUploadService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to compress thumbnail"])
|
||
print("❌ 视频缩略图压缩失败")
|
||
completion(.failure(error))
|
||
}
|
||
|
||
case .failure(let error):
|
||
print("❌ 视频缩略图提取失败: \(error.localizedDescription)")
|
||
completion(.failure(error))
|
||
}
|
||
}
|
||
|
||
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 {
|
||
case image(UIImage)
|
||
case video(URL, UIImage?)
|
||
}
|
||
|
||
/// 媒体上传结果
|
||
public enum MediaUploadResult {
|
||
case file(ImageUploaderGetID.UploadResult)
|
||
case video(video: ImageUploaderGetID.UploadResult, thumbnail: ImageUploaderGetID.UploadResult)
|
||
|
||
/// 获取文件ID(对于视频,返回视频文件的ID)
|
||
public var fileId: String {
|
||
switch self {
|
||
case .file(let result):
|
||
return result.fileId
|
||
case .video(let videoResult, _):
|
||
return videoResult.fileId
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 上传进度信息
|
||
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))
|
||
}
|
||
}
|
||
}
|