Der Decider brauchte keine Änderung — nur eine andere Richtlinie. Genau dafür war er gebaut. Transcoding - TranscodeRequest baut die Parameter für /video/:/transcode/universal. directStream=1 lässt Plex nur umpacken, wenn der Bildcodec passt, statt pauschal neu zu kodieren. - Vor dem Start wird der Server gefragt (decision); sagt er nein, steht der Grund im Klartext da statt eines schwarzen Bildes. - Beim Verlassen wird der Transcoder angehalten. Ohne dieses Signal rechnet der Server minutenlang für niemanden weiter. - Die adaptive Bitrate kommt mit HLS: AVPlayer wechselt sie selbst. Qualitätseinstellungen - Getrennt für WLAN und Mobilfunk, fünf spürbare Stufen. Voreinstellung: zuhause Original, unterwegs 4 Mbit. - NetworkConditions erkennt Mobilfunk über isExpensive — das deckt auch den Hotspot ab, der technisch WLAN ist und trotzdem Volumen kostet. Spuren und Marken - Ton- und Untertitelwahl im Player, bevorzugte Sprache in den Einstellungen, nachsichtiger Sprachvergleich (de/deu/ger/de-DE). - Intro- und Abspann-Marken mit Überspringen-Knopf, optional automatisch. Bereits übersprungene Marken werden gemerkt, sonst käme man nach einem Rücksprung nie ins Intro zurück. - Nächste Folge inkl. Staffelwechsel; Specials in Staffel 0 werden nicht vorgezogen. Fortschritt - Alle zehn Sekunden an den Server, plus Schlussmeldung beim Verlassen. Erst die nimmt den Titel aus „wird gerade abgespielt" heraus. - markWatched/markUnwatched/rate/refreshLibrary stehen bereit. 86 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
3.9 KiB
Swift
83 lines
3.9 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 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
|
|
)
|
|
}
|
|
}
|