Files
Flaro/Packages/FlixyKit/Sources/FlixyPlayback/Decision/DowngradeRule.swift
T
ScarriffleandClaude Opus 5 e324a474fd Watchdog heisst Watchdog, Engine heisst Engine
Eingedeutschte Fachbegriffe in den Kommentaren — „Wachhund" für
Watchdog, „Motor" für Engine. Das liest sich albern und ist unklarer als
das Original; niemand sagt Wachhund, wenn er einen Watchdog meint.

Acht Stellen berichtigt. Gewöhnliche Sprache bleibt deutsch, der Stil
der Kommentare ändert sich nicht.

Dazu die Plex-Leiter messbar gemacht: `--leiter` baut dieselbe Anfrage
zweimal, mit und ohne `autoAdjustQuality=1`, und zählt die Stufen. Die
Zeile steht seit dem Umbau im Code und war nie geprüft — genau die Art
ungeprüfter Annahme über Serververhalten, die bei Jellyfin vier Runden
gekostet hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:47:31 +02:00

133 lines
5.5 KiB
Swift

import Foundation
/// Wann der **teure** Weg gegangen werden darf.
///
/// Der billige Hebel ist `preferredPeakBitRate`: er wirkt ohne Neuaufbau.
/// Wenn er nichts ausrichtet — weil die Playlist nur eine Stufe hat oder weil
/// schon die unterste läuft —, bleibt nur der Neuaufbau mit kleinerer Bitrate:
/// Transcoder anhalten, Engine abreißen, neu aushandeln, zurückspringen. Das
/// sind Sekunden mit schwarzem Bild.
///
/// **Ein Notnagel, keine Regelschleife.** Zu oft ausgelöst erzeugt er genau
/// das Ruckeln, das er beheben soll. Deshalb steht hier eine Liste von
/// Bedingungen, die **alle** gelten müssen — und nicht eine Schwelle.
///
/// Eigener Typ ohne Uhr und ohne Netz, damit sich jede einzelne Bedingung
/// einzeln hinschreiben und prüfen lässt. Eine Regel, die nur im laufenden
/// Film zu beobachten ist, ist keine Regel.
public enum DowngradeRule {
/// Wie lange nach dem Start nichts ausgelöst wird.
///
/// Das anfängliche Füllen des Puffers sieht aus wie eine Notlage und ist
/// keine.
public static let settleIn: Duration = .seconds(10)
/// Wie lange nach einem Sprung nichts ausgelöst wird.
///
/// Ein Sprung leert den Puffer **immer**. Ihn als Beleg für eine schlechte
/// Leitung zu nehmen hieße, sich für das eigene Tun zu bestrafen.
public static let afterSeek: Duration = .seconds(10)
/// Sperrfrist zwischen zwei Absenkungen.
public static let cooldown: Duration = .seconds(180)
/// Das Fenster, in dem Rückler gezählt werden.
public static let window: Duration = .seconds(90)
/// Ab wie vielen Rücklern im Fenster es genügt.
public static let stallsNeeded = 3
/// Ab welcher Länge ein **einzelner** Rückler genügt.
///
/// Fünfzehn Sekunden Stillstand sind keine Schwankung mehr. Ihn
/// mitzuzählen, bis drei zusammenkommen, hieße viereinhalb Minuten
/// zusehen.
public static let longStall: Duration = .seconds(15)
/// Was zum Zeitpunkt der Frage gilt.
public struct Facts: Sendable, Equatable {
/// Ob überhaupt eingegriffen werden darf. Eine ausdrücklich gewählte
/// Stufe ist eine Ansage und wird nicht überstimmt.
public let isAutomatic: Bool
/// Ob die Playlist mehrere Stufen führt.
public let hasLadder: Bool
/// Ob schon die unterste davon läuft.
public let isAtLowestRung: Bool
public let condition: StreamCondition
public let stallsInWindow: Int
public let longestStallInWindow: Duration
/// Wie lange schon gespielt wird.
public let playedFor: Duration
/// Wie lange der letzte Sprung her ist. `nil` heißt: keiner gewesen.
public let sinceLastSeek: Duration?
/// Wie lange die letzte Absenkung her ist. `nil` heißt: keine gewesen.
public let sinceLastDowngrade: Duration?
/// Wie viele es in dieser Wiedergabe schon gab.
public let used: Int
/// Wie viele es höchstens geben darf.
public let budget: Int
public init(
isAutomatic: Bool,
hasLadder: Bool,
isAtLowestRung: Bool,
condition: StreamCondition,
stallsInWindow: Int,
longestStallInWindow: Duration,
playedFor: Duration,
sinceLastSeek: Duration? = nil,
sinceLastDowngrade: Duration? = nil,
used: Int = 0,
budget: Int = 2
) {
self.isAutomatic = isAutomatic
self.hasLadder = hasLadder
self.isAtLowestRung = isAtLowestRung
self.condition = condition
self.stallsInWindow = stallsInWindow
self.longestStallInWindow = longestStallInWindow
self.playedFor = playedFor
self.sinceLastSeek = sinceLastSeek
self.sinceLastDowngrade = sinceLastDowngrade
self.used = used
self.budget = budget
}
}
/// Warum **nicht** abgesenkt wird.
///
/// Als Aufzählung statt als `Bool`, weil genau das im Protokoll stehen
/// soll: „es hakt, aber wir tun nichts" ist ohne Grund von einem Fehler
/// nicht zu unterscheiden.
public enum Verdict: Sendable, Equatable {
case downgrade
/// Eine gewählte Stufe wird nicht überstimmt.
case notAutomatic
/// Der billige Hebel ist noch nicht ausgereizt.
case cheaperPathRemains
/// Es hakt nicht genug.
case notBadEnough
/// Zu früh nach Start oder Sprung.
case tooEarly
/// Sperrfrist läuft noch.
case cooling
/// Verbraucht. Ab hier gehört es dem Nutzer gesagt.
case budgetSpent
}
public static func verdict(_ facts: Facts) -> Verdict {
guard facts.isAutomatic else { return .notAutomatic }
// Solange es noch eine Sprosse tiefer gibt, ist der billige Weg der
// richtige — er kostet kein schwarzes Bild.
if facts.hasLadder, !facts.isAtLowestRung { return .cheaperPathRemains }
let schlimmGenug = facts.stallsInWindow >= stallsNeeded
|| facts.longestStallInWindow >= longStall
guard schlimmGenug, facts.condition != .healthy else { return .notBadEnough }
guard facts.playedFor >= settleIn else { return .tooEarly }
if let seit = facts.sinceLastSeek, seit < afterSeek { return .tooEarly }
if let seit = facts.sinceLastDowngrade, seit < cooldown { return .cooling }
guard facts.used < facts.budget else { return .budgetSpent }
return .downgrade
}
}