import Foundation /// Wann **hinauf** gegangen wird — und wann der teure Weg dafür erlaubt ist. /// /// **Die Falle, um die es hier geht.** Damit ein Film zügig anläuft, wird mit /// einer niedrigen Stufe begonnen. Der Server baut seine Leiter aber aus /// genau dieser Bestellung: wer mit drei Megabit anfängt, bekommt eine /// Leiter, deren oberste Sprosse drei Megabit ist. Das billige Hinaufschalten /// über `preferredPeakBitRate` bewegt sich **innerhalb** dieser Leiter — es /// käme also nie darüber hinaus, und aus dem schnellen Start würde ein Film, /// der den ganzen Abend in 720p bleibt. /// /// Höher geht es nur mit einem Neuaufbau: neu aushandeln, mit einer grösseren /// Grenze. Der kostet Sekunden mit schwarzem Bild — deshalb dieselbe Bauform /// wie `DowngradeRule`: eine Liste von Bedingungen, die **alle** gelten /// müssen, kein Schwellwert. /// /// **Und deutlich zurückhaltender als das Absenken.** Ein zu spätes Absenken /// bedeutet Ruckeln; ein zu spätes Hinaufschalten bedeutet ein etwas /// weicheres Bild. Das erste ist ein Fehler, das zweite eine Geschmacksfrage. public enum UpgradeRule { /// Wie lange es ruhig laufen muss, bevor überhaupt gefragt wird. /// /// Lang genug, dass es kein anfängliches Aufatmen ist: der Puffer füllt /// sich nach dem Start immer, und das sieht zwanzig Sekunden lang nach /// einer hervorragenden Leitung aus. public static let settleIn: Duration = .seconds(45) /// Sperrfrist zwischen zwei Neuaufbauten nach oben. public static let cooldown: Duration = .seconds(150) /// Wie lange nach einem Rückler gar nicht hinaufgegangen wird. /// /// Wer gerade gestockt hat, hat keine Reserve — und ein Neuaufbau nach /// oben wäre dann die zweite Unterbrechung binnen einer Minute. public static let afterStall: Duration = .seconds(90) /// Wie viel Vorrat dasein muss. /// /// Nicht „es läuft gerade", sondern „es liegt etwas auf der hohen Kante". /// Nur das trägt einen Neuaufbau. public static let bufferNeeded: Duration = .seconds(20) /// Was zum Zeitpunkt der Frage gilt. public struct Facts: Sendable, Equatable { /// Eine ausdrücklich gewählte Stufe ist eine Ansage und wird nicht /// überstimmt — auch nicht nach oben. public let isAutomatic: Bool public let condition: StreamCondition /// Ob die Leiter noch eine Sprosse nach oben hat. Solange ja, ist der /// **billige** Weg der richtige. public let hasHigherRung: Bool /// Ob die eingestellte Obergrenze überhaupt mehr zuliesse. public let ceilingAllowsMore: Bool /// Wie viel Vorrat vor der Abspielstelle liegt. public let bufferedAhead: Duration? /// Wie lange schon gespielt wird. public let playedFor: Duration /// Wie lange der letzte Rückler her ist. `nil` heisst: keiner gewesen. public let sinceLastStall: Duration? /// Wie lange der letzte Sprung her ist. public let sinceLastSeek: Duration? /// Wie lange der letzte Neuaufbau nach oben her ist. public let sinceLastUpgrade: 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, condition: StreamCondition, hasHigherRung: Bool, ceilingAllowsMore: Bool, bufferedAhead: Duration?, playedFor: Duration, sinceLastStall: Duration? = nil, sinceLastSeek: Duration? = nil, sinceLastUpgrade: Duration? = nil, used: Int = 0, budget: Int = 2 ) { self.isAutomatic = isAutomatic self.condition = condition self.hasHigherRung = hasHigherRung self.ceilingAllowsMore = ceilingAllowsMore self.bufferedAhead = bufferedAhead self.playedFor = playedFor self.sinceLastStall = sinceLastStall self.sinceLastSeek = sinceLastSeek self.sinceLastUpgrade = sinceLastUpgrade self.used = used self.budget = budget } } /// Warum **nicht** hinaufgegangen wird. /// /// Als Aufzählung statt als `Bool`, aus demselben Grund wie beim /// Absenken: „es läuft gut und es passiert nichts" ist ohne Begründung /// von einem Fehler nicht zu unterscheiden. public enum Verdict: Sendable, Equatable { case rebuildHigher /// Eine gewählte Stufe wird nicht überstimmt. case notAutomatic /// Die Leiter hat noch eine Sprosse — der billige Weg genügt. case cheaperPathRemains /// Die Einstellung lässt nicht mehr zu. Kein Fehler, eine Ansage. case atCeiling /// Es läuft nicht ruhig genug. case notCalmEnough /// Zu früh nach Start, Sprung oder Rückler. case tooEarly /// Sperrfrist läuft noch. case cooling /// Verbraucht. case budgetSpent } public static func verdict(_ facts: Facts) -> Verdict { guard facts.isAutomatic else { return .notAutomatic } guard facts.ceilingAllowsMore else { return .atCeiling } // **Erst die Leiter ausreizen.** Solange es eine Sprosse höher gibt, // kostet das Hinaufgehen kein schwarzes Bild — und wäre ein // Neuaufbau dafür Verschwendung. if facts.hasHigherRung { return .cheaperPathRemains } guard facts.condition == .healthy else { return .notCalmEnough } guard let vorrat = facts.bufferedAhead, vorrat >= bufferNeeded else { return .notCalmEnough } guard facts.playedFor >= settleIn else { return .tooEarly } if let seit = facts.sinceLastSeek, seit < DowngradeRule.afterSeek { return .tooEarly } if let seit = facts.sinceLastStall, seit < afterStall { return .tooEarly } if let seit = facts.sinceLastUpgrade, seit < cooldown { return .cooling } guard facts.used < facts.budget else { return .budgetSpent } return .rebuildHigher } }