import Foundation /// Eine HLS-Abspielliste — gelesen und umgeschrieben. /// /// **Wozu.** Damit ein Vorratsspeicher zwischen Player und Server passen /// kann, muss er zweierlei können: wissen, welche Schnipsel es gibt und in /// welcher Reihenfolge, und dem Player eine Liste geben, die auf ihn selbst /// zeigt statt auf den Server. Beides ist reine Textarbeit an einer Datei mit /// einem sehr einfachen Aufbau. /// /// **Warum als eigener Typ ohne Netz.** Weil hier ein Fehler besonders teuer /// wäre: eine falsch umgeschriebene Liste ergibt entweder ein schwarzes Bild /// oder — schlimmer — eine, die stellenweise am Speicher vorbeigeht und nur /// manchmal hakt. So etwas ist im laufenden Film nicht zu finden. Mit einer /// Zeichenkette hinein und einer heraus schon. /// /// Gelesen wird nur, was gebraucht wird: die Zeilen, die auf Dateien zeigen. /// Alles andere — Verschlüsselung, Zeitmarken, Untertitelspuren — bleibt /// unangetastet stehen. Eine Abspielliste, die wir nicht verstehen, soll /// hindurchgehen, nicht verstümmelt werden. public struct MediaPlaylist: Sendable, Equatable { /// Die Zeilen, die auf eine Datei zeigen — in der Reihenfolge der Liste. public let segments: [String] /// Ob es eine Liste von Stufen ist statt einer von Schnipseln. /// /// Die beiden sehen sich ähnlich und meinen Verschiedenes: eine /// Master-Liste zählt Qualitätsstufen auf, eine Medienliste die Schnipsel /// einer Stufe. Wer das verwechselt, lädt Abspiellisten vor statt Bild. public let isMaster: Bool private let lines: [String] public init(_ text: String) { let zeilen = text.components(separatedBy: .newlines) self.lines = zeilen var gefunden: [String] = [] var master = false var nächsteIstStufe = false for zeile in zeilen { let getrimmt = zeile.trimmingCharacters(in: .whitespaces) if getrimmt.hasPrefix("#EXT-X-STREAM-INF") { master = true nächsteIstStufe = true continue } guard !getrimmt.isEmpty else { continue } // Kommentare und Anweisungen beginnen mit `#`. Alles andere ist // eine Adresse. guard !getrimmt.hasPrefix("#") else { continue } if nächsteIstStufe { nächsteIstStufe = false // Bei einer Master-Liste zeigen die Zeilen auf weitere // Listen, nicht auf Bild. Sie zählen trotzdem als Adresse. } gefunden.append(getrimmt) } self.segments = gefunden self.isMaster = master } /// Schreibt jede Adresse um, ohne sonst etwas anzufassen. /// /// Die Reihenfolge bleibt, die Anweisungen bleiben, die Leerzeilen /// bleiben. Nur wo eine Adresse stand, steht danach eine andere. public func rewritingReferences(_ transform: (String) -> String) -> String { var ergebnis: [String] = [] ergebnis.reserveCapacity(lines.count) for zeile in lines { let getrimmt = zeile.trimmingCharacters(in: .whitespaces) if getrimmt.isEmpty { ergebnis.append(zeile) } else if getrimmt.hasPrefix("#EXT-X-MAP:") { // **Die eine Anweisung, in der eine Adresse steckt.** // // Sie zeigt auf das Kopfstück einer fragmentierten MP4 — ohne // das ist kein einziger Schnipsel zu dekodieren. Bliebe sie // stehen, zeigte sie relativ auf den lokalen Server, und dort // gibt es sie nicht: schwarzes Bild, ohne dass an den // Schnipseln irgendetwas falsch wäre. ergebnis.append(Self.rewritingMapURI(zeile, transform)) } else if getrimmt.hasPrefix("#") { ergebnis.append(zeile) } else { ergebnis.append(transform(getrimmt)) } } return ergebnis.joined(separator: "\n") } /// Tauscht die Adresse in `#EXT-X-MAP:URI="…"` aus, sonst nichts. private static func rewritingMapURI( _ zeile: String, _ transform: (String) -> String ) -> String { let marke = "URI=\"" guard let start = zeile.range(of: marke), let ende = zeile[start.upperBound...].firstIndex(of: "\"") else { return zeile } let ende2 = ende.. 0 else { return nil } return .seconds(sekunden) } return nil } }