feat: 视频上传

This commit is contained in:
jinyaqiu 2025-08-21 08:44:20 +08:00
parent 95f4d7c52e
commit 9e9463571b
3 changed files with 318 additions and 12 deletions

View File

@ -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<MediaUploadResult, Error>) -> 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<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)
/// IDID
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

View File

@ -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<UploadResult, Error>) -> 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<UploadResult, Error>) -> Void) {
<<<<<<< HEAD

View File

@ -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)
}
}
}