Drei Gruende, und der erste erklaert das ganze Aergernis. AVPlayer besteht nach einem Sprung darauf, erst seinen eigenen Puffer zu fuellen. Der stand auf sechzig Sekunden — richtig, solange er selbst vorladen sollte. Mit einem Speicher davor ist es das Gegenteil: springt man mitten in den Vorrat, liegt dahinter vielleicht noch eine halbe Minute, zu wenig fuer seine sechzig, und er wartet auf den Server, obwohl alles da ist. Der Abstand liegt jetzt bei uns; seiner darf klein sein. Der Vorrat holte ausserdem weiter Schnipsel hinter der Abspielstelle, wenn der Player vorausgesprungen war — und der eine, auf den gerade gewartet wurde, kam als letzter. Er macht jetzt dort weiter, wo gesprungen wurde. Und dieselbe Liste noch einmal zu holen setzte den Vorrat zurueck. Der Player holt sie staendig: nach einem Sprung, beim Stufenwechsel, von sich aus. Zurueckgesetzt wird nur noch bei einem echten Wechsel der Quelle. Dazu ein latenter Fehler, der beim Umpacken ein Schwarzbild gegeben haette: in EXT-X-MAP steckt die Adresse des Kopfstuecks einer fragmentierten MP4, ohne das kein einziger Schnipsel zu dekodieren ist. Sie blieb stehen und zeigte damit relativ auf den lokalen Server. Der alte Test hielt genau dieses Verhalten fest — er hat mitgeaendert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
5.6 KiB
Swift
137 lines
5.6 KiB
Swift
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..<zeile.index(after: ende)
|
|
|
|
let adresse = String(zeile[start.upperBound..<ende2.lowerBound])
|
|
return zeile.replacingCharacters(
|
|
in: start.upperBound..<ende2.lowerBound,
|
|
with: transform(adresse)
|
|
)
|
|
}
|
|
|
|
/// Ob die Liste vollständig ist — dann kommen keine Schnipsel mehr dazu.
|
|
///
|
|
/// Bei einem laufenden Sender fehlt diese Marke, und die Liste muss
|
|
/// wiederholt geholt werden. Bei einem Film steht sie am Ende, und dann
|
|
/// weiss der Vorratsspeicher, dass er die Liste kennt.
|
|
public var isComplete: Bool {
|
|
lines.contains { $0.trimmingCharacters(in: .whitespaces) == "#EXT-X-ENDLIST" }
|
|
}
|
|
|
|
/// Wie lang die Schnipsel dauern — für die Frage, wie viele einem
|
|
/// Vorsprung von N Sekunden entsprechen.
|
|
public var targetDuration: Duration? {
|
|
for zeile in lines {
|
|
let getrimmt = zeile.trimmingCharacters(in: .whitespaces)
|
|
guard getrimmt.hasPrefix("#EXT-X-TARGETDURATION:") else { continue }
|
|
let wert = getrimmt.dropFirst("#EXT-X-TARGETDURATION:".count)
|
|
guard let sekunden = Double(wert.trimmingCharacters(in: .whitespaces)),
|
|
sekunden > 0
|
|
else { return nil }
|
|
return .seconds(sekunden)
|
|
}
|
|
return nil
|
|
}
|
|
}
|