import Foundation import Observation import FlixyPlayback /// Was der Nutzer über die Wiedergabe eingestellt hat. /// /// Getrennt nach WLAN und Mobilfunk, weil sich die Anforderungen dort /// grundlegend unterscheiden: im Heimnetz ist die Originaldatei das Beste, /// unterwegs zählt das Datenvolumen. @MainActor @Observable final class PlaybackSettings { /// Die Stufen, die zur Auswahl stehen. /// /// Bewusst wenige und in Schritten, die man spürt. Eine freie Eingabe in /// Kilobit hilft niemandem bei der Frage „reicht das für heute Abend". enum Quality: Int, CaseIterable, Identifiable, Sendable { case original = 0 case high = 12_000 case medium = 8_000 case low = 4_000 case minimal = 2_000 var id: Int { rawValue } /// `nil` heißt: keine Grenze, also die Originaldatei. var bitrateLimit: Int? { self == .original ? nil : rawValue } } private enum Key { static let wifiQuality = "flixy.playback.quality.wifi" static let cellularQuality = "flixy.playback.quality.cellular" static let autoSkipIntro = "flixy.playback.autoSkipIntro" static let autoSkipCredits = "flixy.playback.autoSkipCredits" static let autoPlayNext = "flixy.playback.autoPlayNext" static let preferredAudioLanguage = "flixy.playback.audioLanguage" static let preferredSubtitleLanguage = "flixy.playback.subtitleLanguage" } private let defaults: UserDefaults var wifiQuality: Quality { didSet { defaults.set(wifiQuality.rawValue, forKey: Key.wifiQuality) } } var cellularQuality: Quality { didSet { defaults.set(cellularQuality.rawValue, forKey: Key.cellularQuality) } } var autoSkipIntro: Bool { didSet { defaults.set(autoSkipIntro, forKey: Key.autoSkipIntro) } } var autoSkipCredits: Bool { didSet { defaults.set(autoSkipCredits, forKey: Key.autoSkipCredits) } } var autoPlayNext: Bool { didSet { defaults.set(autoPlayNext, forKey: Key.autoPlayNext) } } /// ISO-Sprachcode, etwa `deu`. Leer heißt: nehmen, was der Server vorgibt. var preferredAudioLanguage: String { didSet { defaults.set(preferredAudioLanguage, forKey: Key.preferredAudioLanguage) } } var preferredSubtitleLanguage: String { didSet { defaults.set(preferredSubtitleLanguage, forKey: Key.preferredSubtitleLanguage) } } init(defaults: UserDefaults = .standard) { self.defaults = defaults // Voreinstellung: im WLAN das Original, unterwegs 4 Mbit. Das ist die // Aufteilung, die im Alltag am seltensten stört — zuhause die beste // Qualität, unterwegs ein Wert, der auch bei mäßigem Empfang trägt. self.wifiQuality = Quality(rawValue: defaults.object(forKey: Key.wifiQuality) as? Int ?? 0) ?? .original self.cellularQuality = Quality(rawValue: defaults.object(forKey: Key.cellularQuality) as? Int ?? 4_000) ?? .low self.autoSkipIntro = defaults.object(forKey: Key.autoSkipIntro) as? Bool ?? false self.autoSkipCredits = defaults.object(forKey: Key.autoSkipCredits) as? Bool ?? false self.autoPlayNext = defaults.object(forKey: Key.autoPlayNext) as? Bool ?? true self.preferredAudioLanguage = defaults.string(forKey: Key.preferredAudioLanguage) ?? "" self.preferredSubtitleLanguage = defaults.string(forKey: Key.preferredSubtitleLanguage) ?? "" } /// Die Richtlinie für die aktuelle Lage. /// /// Das ist die Stelle, an der Phase 2 greift: `allowsTranscoding` steht /// jetzt auf `true`. Am Decider selbst musste dafür nichts geändert werden. func policy(for conditions: NetworkConditions) -> PlaybackPolicy { let quality = conditions.isExpensive || conditions.isConstrained ? cellularQuality : wifiQuality return PlaybackPolicy( allowsTranscoding: true, allowsFallbackEngine: true, maximumBitrate: quality.bitrateLimit ) } }