diff --git a/wake/View/Components/Upload/ImageUploadService.swift b/wake/View/Components/Upload/ImageUploadService.swift index 085919a..0cc1dc6 100644 --- a/wake/View/Components/Upload/ImageUploadService.swift +++ b/wake/View/Components/Upload/ImageUploadService.swift @@ -135,8 +135,163 @@ public class ImageUploadService { } } + // MARK: - Unified Media Upload + + /// 上传媒体文件(图片或视频) + /// - Parameters: + /// - media: 媒体类型,可以是图片或视频 + /// - compressionQuality: 缩略图/图片压缩质量 (0.0 到 1.0) + /// - progressHandler: 上传进度回调 (0.0 到 1.0) + /// - completion: 完成回调,返回上传结果或错误 + public func uploadMedia( + _ media: MediaType, + compressionQuality: CGFloat = 0.7, + progress progressHandler: @escaping (UploadProgress) -> Void, + completion: @escaping (Result) -> Void + ) { + switch media { + case .image(let image): + // 处理图片上传 + uploadCompressedImage( + image, + compressionQuality: compressionQuality, + progress: progressHandler, + completion: { result in + let mediaResult = result.map { MediaUploadResult.file($0) } + completion(mediaResult) + } + ) + + case .video(let videoURL, let thumbnail): + // 处理视频上传 + uploadVideoWithThumbnail( + videoURL: videoURL, + existingThumbnail: thumbnail, + compressionQuality: compressionQuality, + progress: progressHandler, + completion: completion + ) + } + } + + /// 上传视频及其缩略图 + private func uploadVideoWithThumbnail( + videoURL: URL, + existingThumbnail: UIImage?, + compressionQuality: CGFloat, + progress progressHandler: @escaping (UploadProgress) -> Void, + completion: @escaping (Result) -> 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 diff --git a/wake/View/Components/Upload/ImageUploaderGetID.swift b/wake/View/Components/Upload/ImageUploaderGetID.swift index 11a4266..b975846 100644 --- a/wake/View/Components/Upload/ImageUploaderGetID.swift +++ b/wake/View/Components/Upload/ImageUploaderGetID.swift @@ -179,6 +179,78 @@ public class ImageUploaderGetID: ObservableObject { } } + // MARK: - Video Upload + + /// 上传视频文件到服务器 + /// - Parameters: + /// - videoURL: 要上传的视频文件URL + /// - progress: 上传进度回调 (0.0 到 1.0) + /// - completion: 完成回调 + public func uploadVideo( + _ videoURL: URL, + progress: @escaping (Double) -> Void, + completion: @escaping (Result) -> Void + ) { + print("🔄 开始准备上传视频...") + + // 1. 读取视频文件数据 + do { + let videoData = try Data(contentsOf: videoURL) + let fileExtension = videoURL.pathExtension.lowercased() + let mimeType: String + + // 根据文件扩展名设置MIME类型 + switch fileExtension { + case "mp4": + mimeType = "video/mp4" + case "mov": + mimeType = "video/quicktime" + case "m4v": + mimeType = "video/x-m4v" + case "avi": + mimeType = "video/x-msvideo" + default: + mimeType = "video/mp4" // 默认使用mp4 + } + + // 2. 获取上传URL + getUploadURL(for: videoData, mimeType: mimeType, originalFilename: videoURL.lastPathComponent) { [weak self] result in + switch result { + case .success((let fileId, let uploadURL)): + print("📤 获取到视频上传URL,开始上传文件...") + + // 3. 上传文件 + _ = self?.uploadFile( + fileData: videoData, + to: uploadURL, + mimeType: mimeType, + onProgress: progress, + completion: { result in + switch result { + case .success: + // 4. 确认上传完成 + self?.confirmUpload( + fileId: fileId, + fileName: videoURL.lastPathComponent, + fileSize: videoData.count, + completion: completion + ) + case .failure(let error): + completion(.failure(error)) + } + } + ) + + case .failure(let error): + completion(.failure(error)) + } + } + } catch { + print("❌ 读取视频文件失败: \(error.localizedDescription)") + completion(.failure(error)) + } + } + // MARK: - 私有方法 /// 获取上传URL @@ -248,6 +320,84 @@ public class ImageUploaderGetID: ObservableObject { task.resume() } + /// 获取上传URL + /// - Parameters: + /// - fileData: 要上传的文件数据 + /// - mimeType: 文件MIME类型 + /// - originalFilename: 原始文件名 + /// - completion: 完成回调 + private func getUploadURL( + for fileData: Data, + mimeType: String, + originalFilename: String? = nil, + completion: @escaping (Result<(fileId: String, uploadURL: URL), Error>) -> Void + ) { + let fileName = originalFilename ?? "file_\(UUID().uuidString)" + let parameters: [String: Any] = [ + "filename": fileName, + "content_type": mimeType, + "file_size": fileData.count + ] + + let urlString = "\(apiConfig.baseURL)/file/generate-upload-url" + guard let url = URL(string: urlString) else { + completion(.failure(UploadError.invalidURL)) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = apiConfig.authHeaders + + do { + request.httpBody = try JSONSerialization.data(withJSONObject: parameters) + print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(fileData.count) / 1024.0) KB") + } catch { + print("❌ 序列化请求参数失败: \(error.localizedDescription)") + completion(.failure(error)) + return + } + + let task = session.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(UploadError.uploadFailed(error))) + return + } + + guard let httpResponse = response as? HTTPURLResponse else { + completion(.failure(UploadError.invalidResponse)) + return + } + + guard let data = data else { + completion(.failure(UploadError.invalidResponse)) + return + } + + // 打印调试信息 + if let responseString = String(data: data, encoding: .utf8) { + print("📥 上传URL响应: \(responseString)") + } + + do { + let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] + guard let code = json?["code"] as? Int, code == 0, + let dataDict = json?["data"] as? [String: Any], + let fileId = dataDict["file_id"] as? String, + let uploadURLString = dataDict["upload_url"] as? String, + let uploadURL = URL(string: uploadURLString) else { + throw UploadError.invalidResponse + } + + completion(.success((fileId: fileId, uploadURL: uploadURL))) + } catch { + completion(.failure(UploadError.invalidResponse)) + } + } + + task.resume() + } + /// 确认上传 private func confirmUpload(fileId: String, fileName: String, fileSize: Int, completion: @escaping (Result) -> Void) { <<<<<<< HEAD diff --git a/wake/View/Examples/MediaUpload.swift b/wake/View/Examples/MediaUpload.swift index 768f4f9..4690c40 100644 --- a/wake/View/Examples/MediaUpload.swift +++ b/wake/View/Examples/MediaUpload.swift @@ -130,19 +130,12 @@ struct ExampleView: View { } // 上传单个媒体文件 - private func uploadMedia(_ media: MediaType, id: String) { - guard case .image(let image) = media else { - print("❌ 暂不支持上传视频文件") - uploadStatus[id] = .failed(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "暂不支持上传视频文件"])) - return - } - - print("🔄 开始压缩并上传媒体: \(id)") + private func uploadMedia(_ media: ImageUploadService.MediaType, id: String) { + print("🔄 开始处理媒体: \(id)") uploadStatus[id] = .uploading(progress: 0) - uploader.uploadCompressedImage( - image, - compressionQuality: 0.7, // 压缩质量为0.7,可根据需要调整 + uploader.uploadMedia( + media, progress: { progress in print("📊 上传进度 (\(id)): \(progress.current)%") uploadStatus[id] = .uploading(progress: progress.progress) @@ -166,7 +159,15 @@ struct ExampleView: View { for (index, media) in selectedMedia.enumerated() { let id = "\(index)" if case .pending = uploadStatus[id] ?? .pending { - uploadMedia(media, id: id) + // Convert MediaType to ImageUploadService.MediaType + let uploadMediaType: ImageUploadService.MediaType + switch media { + case .image(let image): + uploadMediaType = .image(image) + case .video(let url, let thumbnail): + uploadMediaType = .video(url, thumbnail) + } + uploadMedia(uploadMediaType, id: id) } } }