demo app + SDK updates:\n- Add direct ANI transfer (prepare -> transfer)\n- Add firmware update flow (fetch latest w/ x-api-key, OTA transfer)\n- Extract x-api-key to NetworkConstants\n- Rename example app to demo; move folder and entry to DemoApp\n- Switch from SPM package dependency to local framework target\n- Enable automatic Info.plist generation for framework target\n- Add placeholder test to satisfy SwiftPM test target\n- Regenerate XcodeGen project (demo.xcodeproj)
This commit is contained in:
84
demo/Sources/AniConverter.swift
Normal file
84
demo/Sources/AniConverter.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
|
||||
/// ANI 转换接口错误。
|
||||
struct AniConverterError: LocalizedError {
|
||||
let message: String
|
||||
var errorDescription: String? { message }
|
||||
}
|
||||
|
||||
/// 将源视频转换为设备可用的 ANI 文件。
|
||||
///
|
||||
/// 接口定义详见 duooomi-ble-sdk/CLAUDE.md 的"外部依赖 > ANI 转换 API"。
|
||||
/// 除 `videoUrl` 外,`width / height / fps` 全部固定写死,不得暴露为可调参数。
|
||||
enum AniConverter {
|
||||
private static let endpoint = URL(string: "https://api.mixvideo.bowong.cc/api/auth/loomart/file/convert-to-ani")!
|
||||
|
||||
/// 专用 session:延长超时,避免 ANI 转换耗时长被空闲连接关闭(-1005)。
|
||||
private static let session: URLSession = {
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = 120
|
||||
config.timeoutIntervalForResource = 180
|
||||
config.waitsForConnectivity = true
|
||||
return URLSession(configuration: config)
|
||||
}()
|
||||
|
||||
/// 调用 ANI 转换接口,返回 ani 文件的 CDN URL。
|
||||
/// - Parameter videoUrl: 源视频的公网 URL。
|
||||
/// - Returns: 转换后 ani 文件的 CDN URL。
|
||||
/// - Throws: 网络异常、服务返回失败或响应解析失败等错误。
|
||||
/// - Note: 遇到 -1005(连接丢失)时自动重试一次。
|
||||
static func convert(videoUrl: String) async throws -> String {
|
||||
do {
|
||||
return try await performConvert(videoUrl: videoUrl)
|
||||
} catch let error as NSError where error.code == NSURLErrorNetworkConnectionLost {
|
||||
// -1005 重试一次
|
||||
return try await performConvert(videoUrl: videoUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/// 实际执行转换请求。
|
||||
/// - Parameter videoUrl: 源视频 URL。
|
||||
/// - Returns: ani 文件 URL。
|
||||
/// - Throws: 各类网络或业务错误。
|
||||
private static func performConvert(videoUrl: String) async throws -> String {
|
||||
var request = URLRequest(url: endpoint)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
||||
request.setValue(NetworkConfig.apiKey, forHTTPHeaderField: "x-api-key")
|
||||
request.timeoutInterval = 120
|
||||
|
||||
let body: [String: Any] = [
|
||||
"videoUrl": videoUrl,
|
||||
"width": 360,
|
||||
"height": 360,
|
||||
"fps": "24",
|
||||
]
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
|
||||
throw AniConverterError(message: "HTTP \(http.statusCode)")
|
||||
}
|
||||
|
||||
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw AniConverterError(message: "Invalid JSON response")
|
||||
}
|
||||
|
||||
guard (json["success"] as? Bool) == true,
|
||||
let outer = json["data"] as? [String: Any] else {
|
||||
throw AniConverterError(message: "ANI API reported failure")
|
||||
}
|
||||
|
||||
if (outer["status"] as? Bool) != true {
|
||||
let msg = (outer["msg"] as? String) ?? "ANI conversion failed"
|
||||
throw AniConverterError(message: msg)
|
||||
}
|
||||
|
||||
guard let aniUrl = outer["data"] as? String, !aniUrl.isEmpty else {
|
||||
throw AniConverterError(message: "Missing ani url in response")
|
||||
}
|
||||
|
||||
return aniUrl
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user