- StreamRecorderKit: 跨平台库 - StreamRecorderEngine: 协议+类型 - HLSVariantResolver: HLS master 解析 - FFmpegRecorder: macOS (Process+ffmpeg) - AVFoundationRecorder: iOS (AVAssetReader+Writer) - RecorderFactory: 平台工厂 - StreamRecorder: CLI 入口 - 15秒录制测试通过
87 lines
3.2 KiB
Swift
87 lines
3.2 KiB
Swift
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
|
||
}
|
||
}
|