Abspielen am Fernseher — mit Apples Player und unseren Spuren
Schritt sieben. Wiedergabe, Spurwahl, Titelzeile, „Intro überspringen". **Die schwierige Stelle war die Spurwahl.** Unsere Ton- und Untertitelwahl geht als `audioStreamIndex` an Jellyfin und verlangt bei einem Transcode einen **neuen Stream**. Apples eingebauter Spurwähler zeigt dagegen die Auswahlgruppen des **laufenden** Streams — beim Transcode genau eine. Er ist die richtige Oberfläche für die falsche Liste. Unsere Menüs sitzen deshalb über `transportBarCustomMenuItems` daneben; `customInfoViewControllers` wäre falsch, denn die Tonspur ist ein Bedienelement der Wiedergabe und kein Begleitmaterial. **Der Neuaufbau wird angesagt, nicht erklärt.** Jeder Eintrag, der Sekunden mit schwarzem Bild kostet, trägt das als Untertitel — vorher, nicht hinterher. Wer drückt, sieht den Haken sofort wandern, weil das Menü sich schliesst und darin kein Platz für einen Kreisel ist. Das schwarze Bild deckt eine Überlagerung in `contentOverlayView` zu, die hinter den Bedienelementen liegt und keinen Fokus nimmt — `customOverlayViewController` täte beides nicht und machte die Fernbedienung währenddessen tot. Und solange umgebaut wird, sind alle Einträge gesperrt: sonst räumt `restart()` eine Engine ab, die eine andere gerade baut. **Die Regel dahinter ist jetzt prüfbar.** „Wann kostet ein Spurwechsel einen Neuaufbau" lag bisher mitten im Vollzug von `selectAudioStream` und liess sich nur an einem echten Server mit mehreren Tonspuren auslösen. Als `TrackMenu` steht sie neben den übrigen Entscheidungsregeln, mit sieben Tests — darunter der Fall, auf den der Kommentar dort seit jeher hinweist: stimmt die Spurzahl nicht überein, ist die Zuordnung über die Reihenfolge falsch, und es muss neu aufgebaut werden. **Zwei stille Fehler nebenbei.** Die Tonsitzung stand hinter `#if os(iOS)` und wäre auf tvOS nie gesetzt worden — verfügbar ist sie dort seit 9.0, man hätte es nur daran gemerkt, dass sich die Wiedergabe gegen anderen Ton nicht durchsetzt. Und `PlayerModel.downloads` ist jetzt optional statt eines leeren Koordinators: ein leerer bräuchte einen `ModelContainer`, und der ist das Einzige, wofür diese App SwiftData benutzt. **Belegt statt vermutet.** Im Simulator lief die Wiedergabe gegen die Attrappe, und die Titelzeile zeigte „Unbekannt". Statt an AVKit zu drehen, hat ein Protokolleintrag die Frage beantwortet: `externalMetadata` wird gesetzt, der Wert stimmt, und „Unbekannt" ist wörtlich das, was die Attrappe für unbekannte Kennungen liefert. Geblieben ist davon nur die Erkenntnis, dass die Beschriftung auf das Item warten muss — beim Austausch des Players gibt es noch keines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
87f23afe90
commit
12bfc9e530
@@ -1975,7 +1975,6 @@
|
||||
}
|
||||
},
|
||||
"home.empty.title": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -3841,6 +3840,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"player.rebuilding": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Stream wird neu aufgebaut …"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Rebuilding stream …"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"player.rebuilds": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Stream wird neu aufgebaut"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Stream will be rebuilt"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"player.relayWarning": {
|
||||
"comment": "Warnung im Player, wenn die Verbindung über die Plex-Vermittlung läuft",
|
||||
"extractionState": "stale",
|
||||
|
||||
@@ -4,7 +4,9 @@ import MediaKit
|
||||
import Observation
|
||||
import FlixyDownloads
|
||||
import FlixyPlayback
|
||||
#if canImport(FlixyVLC)
|
||||
// Siehe `PlaybackSettings`: `!os(tvOS)` muss dazu, sonst bricht der
|
||||
// Abhängigkeits-Sucher an VLCKit ab, das für dieses Ziel gar nicht gebaut wird.
|
||||
#if canImport(FlixyVLC) && !os(tvOS)
|
||||
import FlixyVLC
|
||||
#endif
|
||||
import PlexKit
|
||||
@@ -154,6 +156,17 @@ final class PlayerModel {
|
||||
/// bei jedem Wechsel eine neue `AVPlayerLayer`, und das Bild setzte neu an.
|
||||
let surface = PlayerLayerView()
|
||||
|
||||
/// Der Systemplayer der laufenden Engine — oder nichts, solange keine läuft.
|
||||
///
|
||||
/// **AVFoundation, nicht UIKit.** Das Modell weiss weiterhin nicht, wer
|
||||
/// das Bild zeichnet: auf dem Telefon hängt die gehaltene `AVPlayerLayer`
|
||||
/// daran, auf dem Fernseher ein `AVPlayerViewController`. Beide holen sich
|
||||
/// hier dasselbe ab.
|
||||
///
|
||||
/// `nil` bei VLC — kein Sonderfall, sondern dieselbe Auskunft, die
|
||||
/// `VideoSurface` heute schon auswertet.
|
||||
var systemPlayer: AVPlayer? { (engine as? AVPlayerEngine)?.player }
|
||||
|
||||
/// Wie das Bild in die Fläche gelegt wird.
|
||||
///
|
||||
/// **Im Modell und nicht in der Ansicht**, aus demselben Grund wie die
|
||||
@@ -201,7 +214,14 @@ final class PlayerModel {
|
||||
/// gibt, steht hier kein Plex-Server mehr, sondern wer auch immer
|
||||
/// Live-TV kann.
|
||||
private let liveTV: (any LiveTVBackend)?
|
||||
private let downloads: DownloadCoordinator
|
||||
/// Der Bestand auf der Platte — auf dem Fernseher gibt es keinen.
|
||||
///
|
||||
/// **Optional und nicht ein leerer Koordinator.** Ein leerer bräuchte
|
||||
/// einen `ModelContainer`, und der ist das Einzige, wofür diese App
|
||||
/// SwiftData überhaupt benutzt; auf tvOS bliebe damit beides weg. Ein
|
||||
/// Protokoll mit Null-Objekt wäre die dritte Möglichkeit und die
|
||||
/// schlechteste: ein Typ mehr, um drei `if let` zu vermeiden.
|
||||
private let downloads: DownloadCoordinator?
|
||||
private let diagnostics: PlaybackDiagnostics
|
||||
private let policy: PlaybackPolicy
|
||||
private let settings: PlaybackSettings
|
||||
@@ -454,7 +474,7 @@ final class PlayerModel {
|
||||
request: PlaybackRequest,
|
||||
backend: (any MediaBackend)?,
|
||||
liveTV: (any LiveTVBackend)?,
|
||||
downloads: DownloadCoordinator,
|
||||
downloads: DownloadCoordinator? = nil,
|
||||
diagnostics: PlaybackDiagnostics,
|
||||
policy: PlaybackPolicy,
|
||||
settings: PlaybackSettings,
|
||||
@@ -491,7 +511,7 @@ final class PlayerModel {
|
||||
// Die lokale Datei hat immer Vorrang. Sie ist schneller, kostet kein
|
||||
// Datenvolumen und funktioniert auch dann, wenn der Server gerade
|
||||
// nicht erreichbar ist — der eigentliche Sinn des Herunterladens.
|
||||
if let localURL = downloads.localURL(forRatingKey: request.ratingKey) {
|
||||
if let downloads, let localURL = downloads.localURL(forRatingKey: request.ratingKey) {
|
||||
await startLocal(url: localURL)
|
||||
return
|
||||
}
|
||||
@@ -713,7 +733,7 @@ final class PlayerModel {
|
||||
}
|
||||
|
||||
private func startLocal(url: URL) async {
|
||||
let offline = downloads.offlineMedia(forRatingKey: request.ratingKey)
|
||||
let offline = downloads?.offlineMedia(forRatingKey: request.ratingKey) ?? nil
|
||||
|
||||
let decision = PlaybackDecider().decide(
|
||||
// Früher standen hier **erfundene** Plex-Modelle: eine Media mit
|
||||
@@ -1746,7 +1766,7 @@ final class PlayerModel {
|
||||
}
|
||||
|
||||
private func makeEngine(for decision: PlaybackDecision) -> any VideoPlayerEngine {
|
||||
#if canImport(FlixyVLC)
|
||||
#if canImport(FlixyVLC) && !os(tvOS)
|
||||
switch decision.engine {
|
||||
case .vlc: VLCPlayerEngine()
|
||||
// Auch der Transcode-Weg endet beim Systemplayer: er liefert HLS, und
|
||||
@@ -1842,9 +1862,11 @@ final class PlayerModel {
|
||||
// Grundlage fürs automatische Löschen nach dem Ansehen —
|
||||
// und der Auslöser fürs Nachladen: der Vorrat ist jetzt
|
||||
// um eine Folge kleiner.
|
||||
downloads.markWatched(ratingKey: request.ratingKey)
|
||||
Task { [backend, downloads] in
|
||||
await DownloadRefiller(backend: backend, coordinator: downloads).refill()
|
||||
if let downloads {
|
||||
downloads.markWatched(ratingKey: request.ratingKey)
|
||||
Task { [backend, downloads] in
|
||||
await DownloadRefiller(backend: backend, coordinator: downloads).refill()
|
||||
}
|
||||
}
|
||||
case .tracksResolved(let audio, let subtitle):
|
||||
audioTracks = audio
|
||||
|
||||
@@ -6,7 +6,9 @@ import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
#if canImport(FlixyVLC)
|
||||
// Siehe `PlaybackSettings`: `!os(tvOS)` muss dazu, sonst bricht der
|
||||
// Abhängigkeits-Sucher an VLCKit ab, das für dieses Ziel gar nicht gebaut wird.
|
||||
#if canImport(FlixyVLC) && !os(tvOS)
|
||||
import FlixyVLC
|
||||
#endif
|
||||
|
||||
@@ -59,7 +61,7 @@ struct VideoSurface: View {
|
||||
var bottomInset: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
#if canImport(FlixyVLC)
|
||||
#if canImport(FlixyVLC) && !os(tvOS)
|
||||
if let vlcEngine = engine as? VLCPlayerEngine {
|
||||
SurfaceContainer(view: vlcEngine.videoView, isActive: isActive, bottomInset: bottomInset)
|
||||
} else if engine is AVPlayerEngine {
|
||||
|
||||
Reference in New Issue
Block a user