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
|
// 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 struct UploadProgress {
|
||||||
public let current: Int
|
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: - 私有方法
|
// MARK: - 私有方法
|
||||||
|
|
||||||
/// 获取上传URL
|
/// 获取上传URL
|
||||||
@ -248,6 +320,84 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
task.resume()
|
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) {
|
private func confirmUpload(fileId: String, fileName: String, fileSize: Int, completion: @escaping (Result<UploadResult, Error>) -> Void) {
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
|
|||||||
@ -130,19 +130,12 @@ struct ExampleView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 上传单个媒体文件
|
// 上传单个媒体文件
|
||||||
private func uploadMedia(_ media: MediaType, id: String) {
|
private func uploadMedia(_ media: ImageUploadService.MediaType, id: String) {
|
||||||
guard case .image(let image) = media else {
|
print("🔄 开始处理媒体: \(id)")
|
||||||
print("❌ 暂不支持上传视频文件")
|
|
||||||
uploadStatus[id] = .failed(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "暂不支持上传视频文件"]))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("🔄 开始压缩并上传媒体: \(id)")
|
|
||||||
uploadStatus[id] = .uploading(progress: 0)
|
uploadStatus[id] = .uploading(progress: 0)
|
||||||
|
|
||||||
uploader.uploadCompressedImage(
|
uploader.uploadMedia(
|
||||||
image,
|
media,
|
||||||
compressionQuality: 0.7, // 压缩质量为0.7,可根据需要调整
|
|
||||||
progress: { progress in
|
progress: { progress in
|
||||||
print("📊 上传进度 (\(id)): \(progress.current)%")
|
print("📊 上传进度 (\(id)): \(progress.current)%")
|
||||||
uploadStatus[id] = .uploading(progress: progress.progress)
|
uploadStatus[id] = .uploading(progress: progress.progress)
|
||||||
@ -166,7 +159,15 @@ struct ExampleView: View {
|
|||||||
for (index, media) in selectedMedia.enumerated() {
|
for (index, media) in selectedMedia.enumerated() {
|
||||||
let id = "\(index)"
|
let id = "\(index)"
|
||||||
if case .pending = uploadStatus[id] ?? .pending {
|
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