Untertitel brachten den Vorratsspeicher auf 404

Untertitel stehen in der Abspielliste als eigene Spur unter EXT-X-MEDIA —
eine Anweisung mit einer Adresse darin. Umgeschrieben habe ich bisher nur
EXT-X-MAP, also blieb diese relativ stehen und zeigte danach auf den
lokalen Server, wo es sie nicht gibt. Zu sehen war der Fehler erst, als
jemand Untertitel einschaltete: HTTP 404, mitten im Film.

Jetzt sagt der Umschreiber, **was** fuer eine Adresse er gerade in der Hand
hat: eine Qualitaetsstufe, eine Nebenspur, ein Kopfstueck oder ein
Schnipsel. Das ist noetig, weil sie Verschiedenes wollen — hinter einer
Stufe liegt Bild und die soll vorgeladen werden, hinter einer Untertitel-
spur nicht. Sonst lueden wir Untertitel statt Bild: der Film bliebe stehen,
waehrend die Anzeige reichlich Vorrat meldet.

Dazu wandern alle Adressen im lokalen Server auf die **vollstaendige**
Form. Eine relative gilt je nach Liste gegen eine andere Grundadresse — die
Untertitelspur liegt woanders als das Bild —, und wer das erst beim Abholen
aufloest, loest es irgendwann gegen die falsche auf. Das waere der naechste
Fehler derselben Art gewesen.

Drei Tests halten die Unterscheidung fest, darunter die Liste mit
Untertitelspur, die den 404 ausgeloest hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-28 21:36:35 +02:00
co-authored by Claude Opus 5
parent 88ba1a540c
commit cff0c06745
4 changed files with 180 additions and 83 deletions
@@ -77,49 +77,55 @@ public actor StreamCache {
/// Nimmt die Abspielliste entgegen und beginnt vorauszulaufen.
///
/// Gibt die umgeschriebene Liste zurück — die, die der Player bekommt.
///
/// **Zwei Ebenen, und beide müssen durch uns.** Bei „Auto" bestellen wir
/// eine Liste von **Stufen**; die Schnipsel stehen eine Ebene tiefer. Wer
/// hier aussteigt, reicht relative Adressen durch, die danach auf den
/// lokalen Server zeigen — und dort gibt es sie nicht. Genau daran ist
/// der erste Anlauf gescheitert: es wurde nichts vorgeholt, und beinahe
/// wäre auch nichts mehr gelaufen.
///
/// Also wird auch die Stufenliste umgeschrieben. Holt der Player dann
/// eine Stufe, kommt er wieder bei uns an, und **dort** beginnt das
/// Vorauslaufen — bei der Stufe, für die er sich entschieden hat.
/// **Alles wird auf eine vollständige Adresse umgeschrieben.** Relative
/// Angaben gelten je nach Liste gegen eine andere Grundadresse — die
/// Untertitelspur liegt woanders als das Bild. Wer das erst beim Abholen
/// auflöst, löst es irgendwann gegen die falsche auf. Hier steht die
/// Grundadresse noch fest.
public func start(upstream url: URL, headers: [String: String] = [:]) async throws -> String {
self.headers = headers
let (liste, _) = try await load(url)
guard !liste.isMaster else {
return liste.rewritingReferences { eintrag in
let absolut = URL(string: eintrag, relativeTo: url)?.absoluteString ?? eintrag
return LocalStreamServer.localPlaylistPath(for: absolut)
}
}
return adopt(liste, from: url)
let liste = try await load(url)
return rewrite(liste, base: url, adopting: !liste.isMaster)
}
/// Eine Stufe, die der Player angefordert hat.
/// Eine Liste, die der Player eine Ebene tiefer angefordert hat.
///
/// Ab hier stehen die Schnipsel fest, und der Vorrat beginnt zu laufen.
public func playlist(at address: String) async -> String? {
guard let url = URL(string: address), let (liste, _) = try? await load(url) else { return nil }
guard !liste.isMaster else { return liste.rewritingReferences { $0 } }
return adopt(liste, from: url)
/// `prefetch` sagt, ob dahinter Bild liegt. Bei einer Nebenspur — also
/// Untertiteln — steht es auf `false`: sonst lüde der Vorrat Untertitel
/// statt Bild, und der Film bliebe stehen, während die Kachel meldet, es
/// sei reichlich geladen.
public func playlist(at address: String, prefetch: Bool) async -> String? {
guard let url = URL(string: address), let liste = try? await load(url) else { return nil }
return rewrite(liste, base: url, adopting: prefetch && !liste.isMaster)
}
private func rewrite(_ liste: MediaPlaylist, base: URL, adopting: Bool) -> String {
if adopting { adopt(liste, base: base) }
return liste.rewritingReferences { adresse, art in
let absolut = URL(string: adresse, relativeTo: base)?.absoluteString ?? adresse
switch art {
case .level:
return LocalStreamServer.localPlaylistPath(for: absolut, prefetch: true)
case .rendition:
return LocalStreamServer.localPlaylistPath(for: absolut, prefetch: false)
case .map, .segment:
return LocalStreamServer.localPath(for: absolut)
}
}
}
/// Übernimmt eine Liste von Schnipseln als die, der wir vorauslaufen.
private func adopt(_ liste: MediaPlaylist, from url: URL) -> String {
private func adopt(_ liste: MediaPlaylist, base: URL) {
// **Nur bei einem echten Wechsel von vorn.** Der Player holt dieselbe
// Liste immer wieder — nach einem Sprung, beim Stufenwechsel, von
// sich aus. Jedes Mal zurückzusetzen hiesse, den Vorrat jedes Mal zu
// vergessen und von Schnipsel null neu zu zählen.
let neueQuelle = upstream != url
upstream = url
segments = liste.segments
let neueQuelle = upstream != base
upstream = base
segments = liste.segments.map {
URL(string: $0, relativeTo: base)?.absoluteString ?? $0
}
if let dauer = liste.targetDuration { targetDuration = dauer }
if neueQuelle {
@@ -127,29 +133,26 @@ public actor StreamCache {
fetched = 0
}
beginPrefetching()
return liste.rewritingReferences { LocalStreamServer.localPath(for: $0) }
}
private func load(_ url: URL) async throws -> (MediaPlaylist, Data) {
private func load(_ url: URL) async throws -> MediaPlaylist {
var request = URLRequest(url: url)
for (name, wert) in headers { request.setValue(wert, forHTTPHeaderField: name) }
let (daten, _) = try await session.data(for: request)
return (MediaPlaylist(String(decoding: daten, as: UTF8.self)), daten)
return MediaPlaylist(String(decoding: daten, as: UTF8.self))
}
// MARK: - Ausliefern
/// Die Daten zu einem Pfad — aus dem Vorrat, sonst frisch geholt.
public func data(for path: String) async -> Data? {
if let index = segments.firstIndex(of: path) {
// Woran der Player gerade ist. Daran hängt, wie weit wir
// vorauslaufen — und was wir wegwerfen dürfen.
/// Die Daten zu einer Adresse — aus dem Vorrat, sonst frisch geholt.
public func data(for address: String) async -> Data? {
if let index = segments.firstIndex(of: address) {
lastRequested = max(lastRequested, index)
trimBehind()
}
if let ort = stored[path], let daten = try? Data(contentsOf: ort) { return daten }
return await fetch(path)
if let ort = stored[address], let daten = try? Data(contentsOf: ort) { return daten }
return await fetch(address)
}
// MARK: - Vorauslaufen
@@ -201,14 +204,13 @@ public actor StreamCache {
}
@discardableResult
private func fetch(_ path: String) async -> Data? {
guard let upstream, let ziel = URL(string: path, relativeTo: upstream)?.absoluteURL
else { return nil }
private func fetch(_ address: String) async -> Data? {
guard let ziel = URL(string: address) else { return nil }
do {
let (daten, antwort) = try await session.data(from: ziel)
guard (antwort as? HTTPURLResponse)?.statusCode ?? 200 < 400 else { return nil }
store(daten, at: path)
store(daten, at: address)
return daten
} catch {
return nil