Files
Flaro/Flixy/Session/PlaybackSettings.swift
T
ScarriffleandClaude Opus 5 6b927c21b0 VLC ist weg — es gibt nur noch einen Spieler
Du hattest recht mit der Erwartung, nur nicht mit dem Stand: entfernt war er
nicht. Was wir gemacht haben, war, ihm Arbeit wegzunehmen — DTS, TrueHD und
Vorbis gehen seit Kurzem über das Umpacken beim Server. Übrig blieb er an
einer Stelle, `PlaybackDecider.fallback`.

Jetzt ist er ganz weg, samt dem 920-MB-Binärpaket.

**Was an seine Stelle tritt.** Die Entscheidung sagt nicht mehr, *wer* spielt,
sondern *was ankommt*: direkt, umgepackt oder umgerechnet. `PlaybackEngineKind`
fällt damit weg, `directPlay` trägt keine Engine mehr, und `PlaybackPolicy`
kennt kein `allowsFallbackEngine`.

**Das Auffangnetz bleibt, nur anders geknüpft.** Scheiterte das Umpacken am
Player, sprang bisher VLC ein — der Server entscheidet erst beim Ausliefern,
ob er die Ströme wirklich kopieren kann, und manche Antworten nimmt AVPlayer
nicht an. Statt der zweiten Engine rechnet der Server jetzt neu
(`fallBackToTranscode`). Teurer für ihn, aber es kommt etwas an, statt einer
schwarzen Wand mit einer Fehlernummer.

**Offline wäre das ein Rückschritt gewesen** — dort gibt es keinen Server, der
einspringt. Deshalb ging der vorige Commit voraus: was heruntergeladen wird,
kommt schon in einer Hülle, die der Systemplayer öffnet.

Was dadurch überall gilt, auch für Dateien, die bisher bei VLC landeten:
Bild-in-Bild, AirPlay, Bedienung im Sperrbildschirm — und dasselbe Verhalten
auf dem Apple TV, wo es VLC ohnehin nie gab.

Die Attrappe heißt ihren Schalter jetzt `--dts` statt `--vlc`: der Fall ist
geblieben, sein Ziel nicht.

iOS und tvOS bauen, 494 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:04:23 +02:00

145 lines
7.1 KiB
Swift

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 locksLandscape = "flixy.playback.locksLandscape"
static let rewindSeconds = "flixy.playback.rewindSeconds"
static let forwardSeconds = "flixy.playback.forwardSeconds"
static let defaultRate = "flixy.playback.defaultRate"
static let remembersQuality = "flixy.playback.remembersQuality"
static let rememberedQuality = "flixy.playback.rememberedQuality"
static let showsBitrate = "flixy.playback.showsBitrate"
static let lastPlaybackStruggled = "flixy.playback.struggled"
}
/// Die Sprünge, die es zur Auswahl gibt.
///
/// Nicht frei einstellbar: ein Schieberegler für Sekunden lädt dazu ein,
/// 23 einzustellen, und dafür gibt es kein Symbol. Diese fünf haben eins.
static let skipChoices = [5, 10, 15, 30, 45]
/// Die Geschwindigkeiten, mit denen eine Wiedergabe beginnen kann.
static let rateChoices: [Double] = [0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
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.
/// Ob der Player beim Öffnen ins Querformat dreht und dort bleibt.
///
/// Für die allermeisten Filme richtig — und für den, der im Bett auf der
/// Seite liegt, das Gegenteil davon. Deshalb abschaltbar.
var locksLandscape: Bool { didSet { defaults.set(locksLandscape, forKey: Key.locksLandscape) } }
/// Wie weit der Rücksprung geht, in Sekunden.
var rewindSeconds: Int { didSet { defaults.set(rewindSeconds, forKey: Key.rewindSeconds) } }
/// Wie weit der Vorsprung geht. Getrennt vom Rücksprung, weil die beiden
/// verschiedene Zwecke haben: zurück, weil man etwas nicht verstanden hat;
/// vor, weil man etwas überspringen will.
var forwardSeconds: Int { didSet { defaults.set(forwardSeconds, forKey: Key.forwardSeconds) } }
/// Mit welcher Geschwindigkeit eine Wiedergabe beginnt.
var defaultRate: Double { didSet { defaults.set(defaultRate, forKey: Key.defaultRate) } }
/// Ob die im Player gewählte Stufe für den nächsten Titel gilt.
var remembersQuality: Bool { didSet { defaults.set(remembersQuality, forKey: Key.remembersQuality) } }
/// Ob im Qualitätsmenü steht, was gerade tatsächlich ankommt.
var showsBitrate: Bool { didSet { defaults.set(showsBitrate, forKey: Key.showsBitrate) } }
/// Die zuletzt gewählte Stufe, als Zahl.
///
/// Hier und nicht im Player: der lebt nur so lange wie ein Film. Was über
/// mehrere Filme hinweg gelten soll, gehört in die Einstellungen.
var rememberedQuality: Int? {
get {
guard remembersQuality else { return nil }
let value = defaults.integer(forKey: Key.rememberedQuality)
return value == 0 ? nil : value
}
set { defaults.set(newValue ?? 0, forKey: Key.rememberedQuality) }
}
/// Ob die letzte Wiedergabe von der Leitung nicht getragen wurde.
///
/// **Ein Beleg, keine Vermutung.** Der Hinweis bei „ohne Grenze" soll nur
/// erscheinen, wenn es tatsächlich gehakt hat — eine Warnung, die immer
/// dasteht, liest nach dem zweiten Mal niemand mehr.
///
/// Gesetzt vom Player, wenn im gleitenden Fenster ein Viertel der Zeit
/// fürs Warten draufging oder ein einzelner Rückler über fünfzehn
/// Sekunden dauerte.
var lastPlaybackStruggled: Bool {
get { defaults.bool(forKey: Key.lastPlaybackStruggled) }
set { defaults.set(newValue, forKey: Key.lastPlaybackStruggled) }
}
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.locksLandscape = defaults.object(forKey: Key.locksLandscape) as? Bool ?? true
self.rewindSeconds = defaults.object(forKey: Key.rewindSeconds) as? Int ?? 10
self.forwardSeconds = defaults.object(forKey: Key.forwardSeconds) as? Int ?? 10
self.defaultRate = defaults.object(forKey: Key.defaultRate) as? Double ?? 1
self.remembersQuality = defaults.object(forKey: Key.remembersQuality) as? Bool ?? false
self.showsBitrate = defaults.object(forKey: Key.showsBitrate) as? Bool ?? true
}
/// 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,
maximumBitrate: quality.bitrateLimit
)
}
}