Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. 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
|
|
}
|
|
}
|