From f42944bb5818be5b9c59d4137d44fa92fb491065 Mon Sep 17 00:00:00 2001 From: km2023 Date: Mon, 20 Apr 2026 15:40:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E6=A0=A1=E9=AA=8C=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20JPEG=E3=80=81PNG=E3=80=81GIF=E3=80=81MP4=E3=80=81WebM?= =?UTF-8?q?=E3=80=81WebP=20=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 ++- Sources/DuooomiBleSDK/Core/BleTypes.swift | 3 + Sources/DuooomiBleSDK/DuooomiBleSDK.swift | 70 +++++++++++++++++++++-- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a80347c..904e4d0 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ sdk.deleteFile(key: "file-key") { result in /* ... */ } ### 文件传输 ```swift -// 一步完成:自动 ANI 转换 → prepare → transfer +// 一步完成:格式校验 → ANI 转换 → prepare → transfer sdk.transferMedia(fileUrl: "https://example.com/video.mp4") { result in switch result { case .success: print("Transfer complete") @@ -120,6 +120,13 @@ sdk.transferMedia(fileUrl: "https://example.com/video.mp4") { result in // 进度通过 delegate didUpdateProgress 回调 ``` +**支持的媒体格式:** JPEG、PNG、GIF、MP4、WebM、WebP + +格式校验规则: +- 优先从 URL path 提取扩展名(自动忽略查询参数) +- 无扩展名时通过 HEAD 请求检查 `Content-Type` +- 不支持的格式返回 `DuooomiBleError.unsupportedFormat` + ### 固件升级 ```swift diff --git a/Sources/DuooomiBleSDK/Core/BleTypes.swift b/Sources/DuooomiBleSDK/Core/BleTypes.swift index 2497e2a..f469141 100644 --- a/Sources/DuooomiBleSDK/Core/BleTypes.swift +++ b/Sources/DuooomiBleSDK/Core/BleTypes.swift @@ -35,6 +35,7 @@ public enum DuooomiBleError: LocalizedError { case transferFailed(String) case invalidFrame case permissionDenied + case unsupportedFormat(String) public var errorDescription: String? { switch self { @@ -64,6 +65,8 @@ public enum DuooomiBleError: LocalizedError { return "Invalid protocol frame" case .permissionDenied: return "Bluetooth permission denied" + case .unsupportedFormat(let ext): + return "Unsupported file format: \(ext). Supported: jpeg, png, gif, mp4, webm, webp" } } } diff --git a/Sources/DuooomiBleSDK/DuooomiBleSDK.swift b/Sources/DuooomiBleSDK/DuooomiBleSDK.swift index 69a32af..89ff42d 100644 --- a/Sources/DuooomiBleSDK/DuooomiBleSDK.swift +++ b/Sources/DuooomiBleSDK/DuooomiBleSDK.swift @@ -419,6 +419,57 @@ public final class DuooomiBleSDK: NSObject { // MARK: - High-Level APIs + private static let supportedMediaExtensions: Set = [ + "jpeg", "jpg", "png", "gif", "mp4", "webm", "webp" + ] + + private static let supportedMimeTypes: Set = [ + "image/jpeg", "image/png", "image/gif", "image/webp", + "video/mp4", "video/webm" + ] + + private func extractExtension(from urlString: String) -> String? { + guard let comps = URLComponents(string: urlString) else { return nil } + let path = comps.path + let ext = (path.lowercased() as NSString).pathExtension + guard !ext.isEmpty else { return nil } + return ext + } + + private func validateMediaFormat(url: String, completion: @escaping (Result) -> Void) { + if let ext = extractExtension(from: url) { + if DuooomiBleSDK.supportedMediaExtensions.contains(ext) { + completion(.success(())) + } else { + completion(.failure(DuooomiBleError.unsupportedFormat(ext))) + } + return + } + + guard let requestUrl = URL(string: url) else { + completion(.failure(DuooomiBleError.transferFailed("Invalid URL: \(url)"))) + return + } + var request = URLRequest(url: requestUrl) + request.httpMethod = "HEAD" + URLSession.shared.dataTask(with: request) { _, response, error in + if let error = error { + completion(.failure(DuooomiBleError.transferFailed("Format check failed: \(error.localizedDescription)"))) + return + } + guard let mime = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String else { + completion(.failure(DuooomiBleError.unsupportedFormat("unknown"))) + return + } + let baseMime = mime.components(separatedBy: ";").first?.trimmingCharacters(in: .whitespaces).lowercased() ?? "" + if DuooomiBleSDK.supportedMimeTypes.contains(baseMime) { + completion(.success(())) + } else { + completion(.failure(DuooomiBleError.unsupportedFormat(baseMime))) + } + }.resume() + } + /// 传输媒体文件到设备(一步完成) public func transferMedia(fileUrl: String, completion: @escaping (Result) -> Void) { guard connectedDevice != nil else { @@ -437,14 +488,21 @@ public final class DuooomiBleSDK: NSObject { BleLog.i("Direct ANI transfer: \(url)", "Transfer") transferMediaStep2(aniUrl: url, originalUrl: url, completion: completion) } else { - BleLog.i("Converting to ANI: \(url)", "Transfer") - aniConverter.convert(fileUrl: url) { [weak self] result in + validateMediaFormat(url: url) { [weak self] result in switch result { case .failure(let error): - completion(.failure(DuooomiBleError.transferFailed("ANI conversion failed: \(error.localizedDescription)"))) - case .success(let aniUrl): - BleLog.i("ANI ready: \(aniUrl)", "Transfer") - self?.transferMediaStep2(aniUrl: aniUrl, originalUrl: url, completion: completion) + completion(.failure(error)) + case .success: + BleLog.i("Converting to ANI: \(url)", "Transfer") + self?.aniConverter.convert(fileUrl: url) { [weak self] result in + switch result { + case .failure(let error): + completion(.failure(DuooomiBleError.transferFailed("ANI conversion failed: \(error.localizedDescription)"))) + case .success(let aniUrl): + BleLog.i("ANI ready: \(aniUrl)", "Transfer") + self?.transferMediaStep2(aniUrl: aniUrl, originalUrl: url, completion: completion) + } + } } } }