import Foundation /// HLS 变体解析器 public struct HLSVariantResolver { public struct Variant { public let bandwidth: Int public let resolution: String public let path: String public let fullURL: URL public var width: Int? { let parts = resolution.components(separatedBy: "x") return parts.count == 2 ? Int(parts[0]) : nil } public var height: Int? { let parts = resolution.components(separatedBy: "x") return parts.count == 2 ? Int(parts[1]) : nil } } /// 解析 master playlist,返回最佳变体 URL /// - Parameters: /// - url: master playlist URL /// - maxWidth: 最大宽度限制(默认 1280) /// - Returns: 最佳变体 URL,如果不是 master playlist 则返回 nil public static func resolve(url: URL, maxWidth: Int = 1280) -> URL? { guard url.absoluteString.contains(".m3u8"), let data = try? Data(contentsOf: url), let content = String(data: data, encoding: .utf8), content.contains("EXT-X-STREAM-INF") else { return nil } let variants = parseVariants(from: content, baseURL: url) guard !variants.isEmpty else { return nil } // 选择最佳变体(带宽最高且宽度 <= maxWidth) let sorted = variants.sorted { $0.bandwidth > $1.bandwidth } let chosen = sorted.first { variant in guard let w = variant.width else { return true } return w <= maxWidth } ?? sorted[0] return chosen.fullURL } private static func parseVariants(from content: String, baseURL: URL) -> [Variant] { var variants: [Variant] = [] let lines = content.components(separatedBy: "\n") var i = 0 while i < lines.count - 1 { let line = lines[i] if line.hasPrefix("#EXT-X-STREAM-INF:") { var bandwidth = 0 var resolution = "" let attrs = line.replacingOccurrences(of: "#EXT-X-STREAM-INF:", with: "") for attr in attrs.components(separatedBy: ",") { let trimmed = attr.trimmingCharacters(in: .whitespaces) if trimmed.hasPrefix("BANDWIDTH=") { bandwidth = Int(trimmed.replacingOccurrences(of: "BANDWIDTH=", with: "")) ?? 0 } else if trimmed.hasPrefix("RESOLUTION=") { resolution = trimmed.replacingOccurrences(of: "RESOLUTION=", with: "") } } let nextLine = lines[i + 1].trimmingCharacters(in: .whitespacesAndNewlines) if !nextLine.isEmpty && !nextLine.hasPrefix("#") { let variantURL = baseURL.deletingLastPathComponent().appendingPathComponent(nextLine) variants.append(Variant( bandwidth: bandwidth, resolution: resolution, path: nextLine, fullURL: variantURL )) } } i += 1 } return variants } }