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>
133 lines
5.5 KiB
Swift
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
|
|
}
|
|
}
|