„Auto" gibt nicht mehr auf, es versucht es kleiner
Der Fehlstart im Zug hat den eigentlichen Mangel gezeigt: die ganze neue Regelung misst **ab dem ersten Bild**. Kommt nie eines, misst sie nichts, greift nichts — und „Auto" hat sich als das erwiesen, was es wörtlich heisst: „nimm, was in den Einstellungen steht". Steht dort „Original", bittet die App über eine Zugverbindung um eine 4K-Datei und wartet vierzig Sekunden auf etwas, das nie kommen kann. Statt aufzugeben geht der Watchdog jetzt eine Sprosse tiefer und baut neu auf — bei Filmen über `restart()`, bei Sendern über den Tuner-Abbau. Gezählt gegen dasselbe Budget wie die Sicherheitsleine, sonst schickt eine tote Leitung die App endlos im Kreis. Scheitert auch das, nennt die Meldung jetzt den Grund und den Weg statt nur „keine Daten mehr": wer ohne Grenze angefragt hat, soll erfahren, dass es daran liegt und wo es sich ändern lässt. Und „Strom" heisst Stream. Zweiunddreissig Stellen, dazu das Gedächtnis ergänzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4abce1c8b9
commit
efed8717a3
@@ -91,7 +91,7 @@ final class PlayerModel {
|
||||
/// Die eingestellte Stufe in zwei bis fünf Zeichen.
|
||||
///
|
||||
/// Steht auf dem Knopf im Player. „Automatisch" heißt dabei: was gerade
|
||||
/// tatsächlich ankommt — bei einem HLS-Strom entscheidet der Player selbst,
|
||||
/// tatsächlich ankommt — bei einem HLS-Stream entscheidet der Player selbst,
|
||||
/// und ihn danach zu fragen ist ehrlicher, als „Auto" hinzuschreiben und
|
||||
/// die Antwort schuldig zu bleiben.
|
||||
var qualityLabel: String {
|
||||
@@ -177,7 +177,7 @@ final class PlayerModel {
|
||||
/// Wacht darüber, dass aus „lädt" nicht „lädt für immer" wird.
|
||||
@ObservationIgnored private var bufferWatchdog: Task<Void, Never>?
|
||||
|
||||
/// Wie viel Vorrat bei einem Strom über das Netz angelegt wird.
|
||||
/// Wie viel Vorrat bei einem Stream über das Netz angelegt wird.
|
||||
///
|
||||
/// **Nur für Filme und Folgen, nicht für Sender und nicht für Dateien.**
|
||||
/// Ein laufender Sender soll nah am Jetzt bleiben — ein halbminütiger
|
||||
@@ -190,7 +190,7 @@ final class PlayerModel {
|
||||
/// gedrosselten Leitung ermittelt.
|
||||
private static let forwardBuffer: Duration = .seconds(25)
|
||||
|
||||
/// Wie oft der Zustand des Stroms geprüft wird.
|
||||
/// Wie oft der Zustand des Streams geprüft wird.
|
||||
///
|
||||
/// Einmal je Sekunde: fein genug, dass ein schrumpfender Puffer auffällt,
|
||||
/// bevor er leer ist, und grob genug, dass es nichts kostet.
|
||||
@@ -202,7 +202,7 @@ final class PlayerModel {
|
||||
/// kostet Schärfe, ein falscher nach oben einen Rückler.
|
||||
private static let stepUpCooldown: Duration = .seconds(60)
|
||||
|
||||
/// Der Zustand des Stroms, fortlaufend beurteilt.
|
||||
/// Der Zustand des Streams, fortlaufend beurteilt.
|
||||
@ObservationIgnored private var health = StreamHealth()
|
||||
@ObservationIgnored private var healthTask: Task<Void, Never>?
|
||||
/// Wann zuletzt eine Stufe höher gegangen wurde.
|
||||
@@ -357,7 +357,7 @@ final class PlayerModel {
|
||||
|
||||
// Die zuletzt gewählte Stufe, falls sie gemerkt werden soll. **Vor**
|
||||
// der Entscheidung, nicht danach: hinterher umzustellen hieße, den
|
||||
// Strom zweimal aufzubauen und den Anfang zweimal zu puffern.
|
||||
// Stream zweimal aufzubauen und den Anfang zweimal zu puffern.
|
||||
if let stored = settings.rememberedQuality,
|
||||
let restored = PlayerQuality.restored(from: stored, forSourceHeight: media.height) {
|
||||
quality = restored
|
||||
@@ -406,7 +406,7 @@ final class PlayerModel {
|
||||
await restart()
|
||||
}
|
||||
|
||||
/// Baut den Strom mit der **aktuell geltenden** Richtlinie neu auf.
|
||||
/// Baut den Stream mit der **aktuell geltenden** Richtlinie neu auf.
|
||||
///
|
||||
/// Geteilt zwischen der Wahl von Hand und der Sicherheitsleine — der
|
||||
/// Ablauf ist derselbe, nur der Anlass nicht. Was sich unterscheidet,
|
||||
@@ -549,7 +549,7 @@ final class PlayerModel {
|
||||
diagnostics.note("Engine gestartet")
|
||||
// Weder offline noch live gibt es einen Fortschritt zu melden.
|
||||
if !isOffline, !isLive { startReporting() }
|
||||
// Den Zustand des Stroms dagegen auch bei Live beobachten: dort
|
||||
// Den Zustand des Streams dagegen auch bei Live beobachten: dort
|
||||
// hakt es am ehesten, und der billige Hebel kostet nichts.
|
||||
startHealthWatch()
|
||||
} catch {
|
||||
@@ -617,7 +617,7 @@ final class PlayerModel {
|
||||
|
||||
// MARK: - Die Qualität selbst nachregeln
|
||||
|
||||
/// Beobachtet den Strom und passt die Obergrenze an.
|
||||
/// Beobachtet den Stream und passt die Obergrenze an.
|
||||
///
|
||||
/// Läuft nur bei „automatisch". Eine ausdrücklich gewählte Stufe ist eine
|
||||
/// Ansage und wird nicht überstimmt — `automatic` ist die Erlaubnis
|
||||
@@ -662,7 +662,7 @@ final class PlayerModel {
|
||||
|
||||
// MARK: - Die Sicherheitsleine
|
||||
|
||||
/// Prüft, ob der Strom neu aufgebaut werden muss — und tut es.
|
||||
/// Prüft, ob der Stream neu aufgebaut werden muss — und tut es.
|
||||
private func considerTheHardWay(
|
||||
_ condition: StreamCondition,
|
||||
statistics: PlaybackStatistics,
|
||||
@@ -699,7 +699,7 @@ final class PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Baut den Strom mit kleinerer Bitrate neu auf.
|
||||
/// Baut den Stream mit kleinerer Bitrate neu auf.
|
||||
private func downgradeTheHardWay(at now: Duration) async {
|
||||
guard let ziel = nextLowerRung() else {
|
||||
hasGivenUp = true
|
||||
@@ -719,6 +719,31 @@ final class PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein zweiter Versuch mit kleinerer Bitrate, bevor aufgegeben wird.
|
||||
///
|
||||
/// Gibt `true` zurück, wenn es einen gab. Zählt gegen dasselbe Budget wie
|
||||
/// die Sicherheitsleine — sonst könnte eine tote Leitung die App endlos
|
||||
/// im Kreis schicken.
|
||||
private func retrySmaller() async -> Bool {
|
||||
guard quality == .automatic,
|
||||
downgradesUsed < downgradeBudget,
|
||||
let ziel = nextLowerRung()
|
||||
else { return false }
|
||||
|
||||
automaticCeiling = ziel
|
||||
downgradesUsed += 1
|
||||
diagnostics.note("Nichts kam an — neuer Versuch mit höchstens \(ziel) kbit")
|
||||
|
||||
if isLive {
|
||||
await retuneLive(to: ziel)
|
||||
} else {
|
||||
await restart()
|
||||
}
|
||||
// Ist der Neuaufbau selbst gescheitert, steht die Meldung schon da.
|
||||
if case .failed = phase { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
/// Die nächste Sprosse unterhalb der aktuellen — aus **unserer** Leiter.
|
||||
///
|
||||
/// Nicht aus der Playlist: die hat in diesem Fall gerade keine, sonst
|
||||
@@ -914,11 +939,33 @@ final class PlayerModel {
|
||||
try? await Task.sleep(for: .seconds(40))
|
||||
guard !Task.isCancelled, let self, self.isBuffering else { return }
|
||||
|
||||
diagnostics.note("Seit 40 s keine Daten — abgebrochen")
|
||||
diagnostics.note("Seit 40 s keine Daten")
|
||||
// **Auch das ist ein Beleg**, und ein stärkerer als ein paar
|
||||
// Rückler: hier kam überhaupt nichts an. Ohne diese Zeile bliebe
|
||||
// der Hinweis in den Einstellungen aus, weil das Rücklerbuch erst
|
||||
// ab dem ersten Bild zählt — und das gab es nie.
|
||||
settings.lastPlaybackStruggled = true
|
||||
|
||||
// **Erst kleiner versuchen, dann aufgeben.**
|
||||
//
|
||||
// Hier lag der eigentliche Fehler: die ganze Regelung misst ab
|
||||
// dem ersten Bild. Kommt nie eines, misst sie nichts, greift
|
||||
// nichts, und „Auto" hat sich als das erwiesen, was es hiess —
|
||||
// „nimm, was in den Einstellungen steht". Steht dort „Original",
|
||||
// bittet die App über eine Zugverbindung um eine 4K-Datei und
|
||||
// wartet vierzig Sekunden auf etwas, das nie kommen kann.
|
||||
if await self.retrySmaller() { return }
|
||||
// Den Engine gleich stilllegen: er würde sonst weiter an einem
|
||||
// toten Stream ziehen, und bei Live-TV bliebe der Tuner belegt.
|
||||
silence()
|
||||
phase = .failed(message: String(localized: "player.stalled"))
|
||||
// Wer ohne Grenze angefragt hat, bekommt den Grund dazu. „Der
|
||||
// Stream ist abgerissen" ist wahr und führt nirgendwohin; die
|
||||
// Einstellung zu nennen ist der Unterschied zwischen einer
|
||||
// Meldung und einer Hilfe.
|
||||
let ohneGrenze = quality == .automatic && effectivePolicy.maximumBitrate == nil
|
||||
phase = .failed(message: String(
|
||||
localized: ohneGrenze ? "player.stalledUnlimited" : "player.stalled"
|
||||
))
|
||||
Task { await self.releaseOnServer() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user