Phase 2: Transcoding, adaptive Bitrate, Spuren, Marker, Sync
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>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import SwiftUI
|
||||
import FlixyDesign
|
||||
import FlixyPlayback
|
||||
|
||||
/// Auswahl von Ton- und Untertitelspur.
|
||||
struct TrackPickerSheet: View {
|
||||
let model: PlayerModel
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.flixyAccent) private var accent
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
if !model.audioTracks.isEmpty {
|
||||
Section("player.audioTracks") {
|
||||
ForEach(model.audioTracks) { track in
|
||||
row(
|
||||
title: track.displayName,
|
||||
isSelected: model.selectedAudioTrack == track
|
||||
) {
|
||||
model.selectAudioTrack(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !model.subtitleTracks.isEmpty {
|
||||
Section("player.subtitleTracks") {
|
||||
// „Aus" gehört in dieselbe Liste wie die Spuren, nicht
|
||||
// als eigener Schalter daneben: es ist eine Auswahl
|
||||
// unter mehreren, keine getrennte Eigenschaft.
|
||||
row(
|
||||
title: String(localized: "player.subtitlesOff"),
|
||||
isSelected: model.selectedSubtitleTrack == nil
|
||||
) {
|
||||
model.selectSubtitleTrack(nil)
|
||||
}
|
||||
|
||||
ForEach(model.subtitleTracks) { track in
|
||||
row(
|
||||
title: track.displayName,
|
||||
isSelected: model.selectedSubtitleTrack == track
|
||||
) {
|
||||
model.selectSubtitleTrack(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.flixyCanvas)
|
||||
.navigationTitle(Text("player.tracks"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("action.close") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
}
|
||||
|
||||
private func row(title: String, isSelected: Bool, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.foregroundStyle(Color.flixyTextPrimary)
|
||||
Spacer()
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark").foregroundStyle(accent)
|
||||
}
|
||||
}
|
||||
.contentShape(.rect)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.listRowBackground(Color.flixySurface)
|
||||
.accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user