import Foundation /// Buch über das Stocken, mit gleitendem Fenster. /// /// **Warum es das gibt.** Der Watchdog im Player kannte nur eine Frage: steht /// 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 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 /// als Zahlenreihe hinschreiben und das Urteil prüfen. Eine Regel, die man /// nur im laufenden Film beobachten kann, ist keine Regel, sondern ein /// Eindruck. /// /// Alle Zeitpunkte sind **seit Beginn dieser Wiedergabe** gemessen, nicht als /// Datum. Das macht die Sache gegen Zeitzonen, Sommerzeit und eine Systemuhr, /// die sich mitten im Film stellt, gleichgültig. public struct StallLedger: Sendable, Equatable { public struct Episode: Sendable, Equatable { /// Wann dieser Rückler vorbei war. public let endedAt: Duration public let length: Duration public init(endedAt: Duration, length: Duration) { self.endedAt = endedAt self.length = length } } /// Wie weit zurück gezählt wird. /// /// Drei Minuten: lang genug, dass ein Muster erkennbar wird, kurz genug, /// dass eine überstandene Durststrecke nicht noch eine Viertelstunde /// später nachwirkt. public let window: Duration private(set) var episodes: [Episode] = [] public init(window: Duration = .seconds(180)) { self.window = window } /// Trägt einen überstandenen Rückler ein. public mutating func record(_ episode: Episode) { episodes.append(episode) prune(at: episode.endedAt) } /// Wie viele Rückler im Fenster liegen. public func count(at now: Duration) -> Int { episodes.count { $0.endedAt >= now - window } } /// Welcher Anteil des Fensters mit Warten verbracht wurde — 0 bis 1. /// /// Der laufende Rückler zählt mit, sofern er übergeben wird: wer mitten /// im dritten Stocken steht, soll nicht erst nach dessen Ende erfahren, /// dass es zu viel war. public func share(at now: Duration, ongoing: Duration = .zero) -> Double { let horizon = now - window let waited = episodes .filter { $0.endedAt >= horizon } .reduce(Duration.zero) { $0 + $1.length } + ongoing // Am Anfang ist das Fenster noch nicht voll. Dann gegen die // tatsächlich vergangene Zeit rechnen, sonst sähe eine Wiedergabe, // die nach zwanzig Sekunden fünf Sekunden hakte, mit 3 % harmlos aus // statt mit 25 %. let elapsed = min(now, window) guard elapsed > .zero else { return 0 } return min(1, waited / elapsed) } /// Der längste einzelne Rückler im Fenster. /// /// Ein einzelner langer ist etwas anderes als viele kurze: der eine sagt /// „die Leitung war weg", die vielen sagen „sie trägt es dauerhaft nicht". /// Beide sollen auslösen dürfen, aber nicht über dieselbe Schwelle. public func longest(at now: Duration) -> Duration { episodes .filter { $0.endedAt >= now - window } .map(\.length) .max() ?? .zero } /// Wann der letzte Rückler zu Ende war. /// /// `nil`, wenn es im Fenster keinen gab — und das ist etwas anderes als /// „vor langer Zeit": wer noch nie gestockt hat, wartet auf keine Frist. /// /// Gebraucht fürs Hinaufschalten: wer gerade gestockt hat, hat keine /// Reserve, und ein Neuaufbau nach oben wäre die zweite Unterbrechung /// binnen einer Minute. public func lastStall(at now: Duration) -> Duration? { episodes .filter { $0.endedAt >= now - window } .map(\.endedAt) .max() } /// Wirft heraus, was aus dem Fenster gefallen ist. public mutating func prune(at now: Duration) { let horizon = now - window episodes.removeAll { $0.endedAt < horizon } } public mutating func removeAll() { episodes.removeAll() } } private extension Duration { /// Anteil zweier Dauern. `Duration` kann sich nicht selbst teilen. static func / (lhs: Duration, rhs: Duration) -> Double { let left = Double(lhs.components.seconds) + Double(lhs.components.attoseconds) / 1e18 let right = Double(rhs.components.seconds) + Double(rhs.components.attoseconds) / 1e18 guard right != 0 else { return 0 } return left / right } }