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 } }