import Foundation struct AniConverterError: LocalizedError { let message: String var errorDescription: String? { message } } /// 将源视频/图片转换为设备可用的 ANI 文件。 final class AniConverter { private let config: DuooomiBleConfig private var endpoint: URL { URL(string: "\(config.apiHost)/api/auth/loomart/file/convert-to-ani")! } private let session: URLSession = { let cfg = URLSessionConfiguration.default cfg.timeoutIntervalForRequest = 120 cfg.timeoutIntervalForResource = 180 cfg.waitsForConnectivity = true return URLSession(configuration: cfg) }() init(config: DuooomiBleConfig) { self.config = config } /// 调用 ANI 转换接口,返回 ani 文件的 CDN URL。 func convert(fileUrl: String, completion: @escaping (Result) -> Void) { performConvert(fileUrl: fileUrl) { result in switch result { case .success: completion(result) case .failure(let error): // Retry once on connection lost if (error as NSError).code == NSURLErrorNetworkConnectionLost { self.performConvert(fileUrl: fileUrl, completion: completion) } else { completion(.failure(error)) } } } } private func performConvert(fileUrl: String, completion: @escaping (Result) -> Void) { var request = URLRequest(url: endpoint) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "content-type") request.setValue(config.apiKey, forHTTPHeaderField: "x-api-key") request.timeoutInterval = 120 let body: [String: Any] = [ "videoUrl": fileUrl, "width": config.aniWidth, "height": config.aniHeight, "fps": config.aniFps, ] do { request.httpBody = try JSONSerialization.data(withJSONObject: body) } catch { completion(.failure(error)) return } session.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(error)) return } if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) { completion(.failure(AniConverterError(message: "HTTP \(http.statusCode)"))) return } guard let data = data, let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else { completion(.failure(AniConverterError(message: "Invalid JSON response"))) return } guard (json["success"] as? Bool) == true, let outer = json["data"] as? [String: Any] else { completion(.failure(AniConverterError(message: "ANI API reported failure"))) return } if (outer["status"] as? Bool) != true { let msg = (outer["msg"] as? String) ?? "ANI conversion failed" completion(.failure(AniConverterError(message: msg))) return } guard let aniUrl = outer["data"] as? String, !aniUrl.isEmpty else { completion(.failure(AniConverterError(message: "Missing ani url in response"))) return } completion(.success(aniUrl)) }.resume() } }