feat: 视频上传
This commit is contained in:
parent
95f4d7c52e
commit
9e9463571b
@ -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)
|
||||
|
||||
/// 获取文件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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user