„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:
Scarriffle
2026-09-27 18:30:10 +02:00
co-authored by Claude Opus 5
parent 4abce1c8b9
commit efed8717a3
17 changed files with 98 additions and 34 deletions
@@ -6,7 +6,7 @@ import Foundation
/// es seit vierzig Sekunden am Stück? Bei jedem kurzen Erholen setzte er sich
/// zurück — zehn Rückler à dreißig Sekunden lösten nichts aus, obwohl die
/// halbe Stunde unbrauchbar war. Genau dieser Fall ist der, über den geklagt
/// wird: nicht der tote Strom, sondern der, der immer wieder hakt.
/// wird: nicht der tote Stream, sondern der, der immer wieder hakt.
///
/// **Warum als eigener Typ.** Keine Uhr, kein AVFoundation, kein Netz — die
/// Zeiten kommen von außen herein. Damit lässt sich eine Folge von Rücklern
@@ -1,6 +1,6 @@
import Foundation
/// Eine Probe vom laufenden Strom.
/// Eine Probe vom laufenden Stream.
///
/// Alle Zeiten **seit Beginn dieser Wiedergabe**, nicht als Datum — dasselbe
/// wie beim `StallLedger`, und aus demselben Grund: eine Systemuhr, die sich
@@ -31,7 +31,7 @@ public struct StreamSample: Sendable, Equatable {
}
}
/// Wie es dem Strom geht.
/// Wie es dem Stream geht.
public enum StreamCondition: Sendable, Equatable {
/// Trägt. Es darf wieder eine Stufe höher gehen.
case healthy
@@ -41,7 +41,7 @@ public enum StreamCondition: Sendable, Equatable {
case failing
}
/// Beurteilt den Strom aus einer Folge von Proben.
/// Beurteilt den Stream aus einer Folge von Proben.
///
/// **Warum als reiner Typ.** Kein AVFoundation, keine Uhr, kein Netz — Proben
/// hinein, Urteil heraus. Dieselbe Bauform wie `PlaybackDecider`, und aus
@@ -76,7 +76,7 @@ public final class AVPlayerEngine: VideoPlayerEngine {
///
/// Aus `playbackStalledNotification` und **nicht** aus dem
/// Zugriffsprotokoll: die Meldung kommt bei jeder Quelle, das Protokoll
/// nur bei Strömen über das Netz. Beides zu addieren zählte doppelt.
/// nur bei Streamsn über das Netz. Beides zu addieren zählte doppelt.
private var stallCount = 0
/// Aufsummierte Wartezeit. Siehe `noteStall(from:to:)`.
private var accumulatedStall: Duration = .zero
@@ -17,7 +17,7 @@ public struct PlaybackSource: Sendable, Equatable {
/// Wie viel Vorrat der Player anlegen soll.
///
/// `nil` überlässt es dem System. Das ist für eine Datei auf der Platte
/// richtig und für einen Sender richtig — bei einem Strom über eine
/// richtig und für einen Sender richtig — bei einem Stream über eine
/// wackelige Leitung ist es zu wenig: der Systemwert ist auf schnellen
/// Start ausgelegt, nicht darauf, einen Aussetzer zu überstehen.
///
@@ -369,7 +369,7 @@ func erklärungWarumKeineLeiter() {
/// **Die Playlist zu bekommen beweist aber noch nichts.** Sie ist Text und
/// kostet nichts; die Datenrate steckt in den Schnipseln. Entscheidend ist
/// deshalb allein, ob die Schnipsel bei kleinerer Zahl auch kleiner werden —
/// oder ob der Server stur denselben Strom ausliefert, weil die Sitzung schon
/// oder ob der Server stur denselben Stream ausliefert, weil die Sitzung schon
/// läuft. Deshalb wird hier gewogen, nicht gezählt.
func prüfeSelbstgebauteLeiter(master: URL, kopfzeilen: [String: String]) async {
print("")
@@ -401,7 +401,7 @@ func prüfeSelbstgebauteLeiter(master: URL, kopfzeilen: [String: String]) async
print("")
print(" Werden die Schnipsel kleiner, trägt die Idee. Bleiben sie gleich")
print(" gross, rechnet der Server trotz anderer Zahl denselben Strom —")
print(" gross, rechnet der Server trotz anderer Zahl denselben Stream —")
print(" dann bringt ein selbstgebautes Menü nichts.")
}
@@ -130,7 +130,7 @@ extension JellyfinBackend: LiveTVBackend {
}
// Kein Transcoder-Weg: dann unverändert. Manche Empfänger liefern
// bereits einen Strom, den das Gerät ohne Umrechnung versteht.
// bereits einen Stream, den das Gerät ohne Umrechnung versteht.
if let url = client.url(
path: "/Videos/\(channel.id)/stream",
query: [
@@ -164,7 +164,7 @@ extension JellyfinBackend {
URLQueryItem(name: "maxStreamingBitrate", value: String(bitsPerSecond)),
// **Kein** `startTimeTicks`. Es gäbe zwei Stellen, die
// über den Startpunkt entscheiden: der Server ließe den
// Strom dort beginnen, und der Player spränge anschließend
// Stream dort beginnen, und der Player spränge anschließend
// noch einmal um dieselbe Strecke vor — bei einundzwanzig
// Minuten Fortschritt landete man bei zweiundvierzig.
// Gesprungen wird an genau einer Stelle, und das ist der
@@ -302,7 +302,7 @@ public struct JellyfinBackend: MediaBackend {
/// Eine verkleinerte Fassung zum Herunterladen.
///
/// **Fortlaufendes MP4, nicht HLS.** Zum Abspielen liefert Jellyfin eine
/// Abspielliste mit hunderten Schnipseln; das ist für einen Strom richtig
/// Abspielliste mit hunderten Schnipseln; das ist für einen Stream richtig
/// und für eine Datei, die auf dem Gerät liegen soll, unbrauchbar. Der
/// Endpunkt `/Videos/{id}/stream.mp4` gibt dieselbe Umrechnung als **eine**
/// Datei heraus.
@@ -46,7 +46,7 @@ public struct TranscodeSettings: Sendable, Equatable {
/// Ob der Server eine **Leiter** liefern soll statt einer festen Stufe.
///
/// Dann enthält die Playlist mehrere Varianten, und der Player wechselt
/// zwischen ihnen, ohne dass der Strom neu aufgebaut werden muss. Die
/// zwischen ihnen, ohne dass der Stream neu aufgebaut werden muss. Die
/// angegebene Bitrate bleibt dabei die **oberste** Sprosse; was darunter
/// liegt, erfindet der Server.
///
@@ -1010,7 +1010,7 @@ extension PlexServer {
return nil
}
/// Baut eine Stromadresse und ruft sie einmal ab.
/// Baut eine Stream-Adresse und ruft sie einmal ab.
///
/// Getrennte Methode statt einer Hilfsfunktion in `tune`: eine solche
/// Funktion greift über jede Unterbrechung hinweg auf die veränderlichen
@@ -1076,7 +1076,7 @@ extension PlexServer {
}
}
/// Holt eine Stromadresse einmal selbst ab und sagt, was zurückkam.
/// Holt eine Stream-Adresse einmal selbst ab und sagt, was zurückkam.
///
/// **Ohne Plex-Kopfzeilen**, weil AVPlayer sie ebenfalls nicht schickt.
/// Mit Kopfzeilen zu prüfen hieße, etwas anderes zu prüfen als das, was
@@ -179,7 +179,7 @@ public struct PlexBackend: MediaBackend {
// Wiedergabe verhindert. Plex' eigene Clients schicken ihn; welche
// weitere Angabe sie dabei mitgeben, ist offen. Bis das gemessen ist,
// bleibt er draußen: eine Leiter, die es vielleicht gibt, ist keinen
// Strom wert, der sicher nicht läuft.
// Stream wert, der sicher nicht läuft.
//
// `TranscodeRequest.adaptive` bleibt samt Tests bestehen — das
// Gerüst steht, es fehlt die richtige Beschwörungsformel.
@@ -73,7 +73,7 @@ struct DowngradeRuleTests {
#expect(DowngradeRule.verdict(facts) == .downgrade)
}
@Test("Ein gesunder Strom löst nichts aus")
@Test("Ein gesunder Stream löst nichts aus")
func healthyNeverTriggers() {
#expect(DowngradeRule.verdict(notlage(condition: .healthy)) == .notBadEnough)
}
@@ -2,7 +2,7 @@ import Testing
import Foundation
@testable import FlixyPlayback
@Suite("Zustand des Stroms")
@Suite("Zustand des Streams")
struct StreamHealthTests {
/// Baut eine Folge von Proben im Sekundentakt.
private func lauf(
@@ -21,7 +21,7 @@ public final class VLCPlayerEngine: NSObject, VideoPlayerEngine {
private let player = VLCMediaPlayer()
/// Was VLC über den laufenden Strom weiß.
/// Was VLC über den laufenden Stream weiß.
///
/// Bisher meldete dieser Weg gar nichts: die Standardimplementierung gibt
/// `nil` zurück, und die Bitratenanzeige blieb bei VLC dauerhaft leer.
@@ -227,7 +227,7 @@ extension VLCPlayerEngine: VLCMediaPlayerDelegate {
/// Aufzählung führt nur `opening`, `playing`, `paused`, `stopped` und
/// `error`. Weil diese Methode bisher fehlte, ging der Zustand
/// `.buffering` auf dem VLC-Weg **nie** an — und damit löste der
/// Watchdog im Player dort nie aus. Ein hängender Strom lief stumm ins
/// Watchdog im Player dort nie aus. Ein hängender Stream lief stumm ins
/// Nichts, ohne Kreisel, ohne Meldung, ohne Abbruch.
///
/// Nur aus dem Spielen heraus gewertet. Die Dokumentation sagt, dass vor