import AVKit import SwiftUI import FlixyPlayback import MediaKit /// Hängt Apples Player in den SwiftUI-Baum. /// /// **Wem was gehört.** Der `AVPlayerViewController` gehört dem Coordinator, /// nicht der SwiftUI-Struktur — die wird bei jedem Neuzeichnen neu gebaut — /// und nicht dem `PlayerModel`, das frei von UIKit bleiben soll. Das Modell /// gibt nur seinen `AVPlayer` heraus, und das ist AVFoundation. @MainActor struct TVPlayerHost: UIViewControllerRepresentable { let model: PlayerModel /// Eine Momentaufnahme dessen, was den Controller betrifft. /// /// **Als Wert, nicht als Verweis.** SwiftUI ruft `updateUIViewController` /// nur, wenn sich am Repräsentanten etwas geändert hat. Stünde hier allein /// das Modell, änderte sich nie etwas: es ist eine Klasse, und ihre /// Kennung bleibt über jeden Spurwechsel hinweg dieselbe. Der Wert wird im /// Rumpf der umgebenden Ansicht gebildet — dort und nur dort entsteht die /// Beobachtungsbeziehung zu `@Observable`. let snapshot: TVPlayerSnapshot /// Der Untertitelstil — er steht im selben Menü wie die Spurwahl. let appearance: SubtitleAppearance let onDismiss: () -> Void func makeUIViewController(context: Context) -> AVPlayerViewController { context.coordinator.controller } func updateUIViewController(_ controller: AVPlayerViewController, context: Context) { context.coordinator.apply(snapshot, model: model) } func makeCoordinator() -> TVPlayerCoordinator { TVPlayerCoordinator(appearance: appearance, onDismiss: onDismiss) } } /// Was der Controller von der Wiedergabe wissen muss — und mehr nicht. struct TVPlayerSnapshot: Equatable { /// Wechselt bei jedem Neuaufbau. Daran hängt der Austausch des Players. let playerIdentity: ObjectIdentifier? let audio: [TrackMenu.Entry] let subtitles: [TrackMenu.Entry] /// Läuft gerade ein Neuaufbau? Dann ist das Bild schwarz und das Menü /// gesperrt. let isRebuilding: Bool let title: String let subtitle: String? /// Der Zeitbereich einer Marke — für „Intro überspringen". let markerLabel: String? /// Der eingestellte Untertitelstil. /// /// **Gehört in die Momentaufnahme, obwohl der Player ihn nicht braucht.** /// Er steht im selben Menü wie die Spurwahl, und ohne ihn hier merkte /// SwiftUI nicht, dass sich etwas geändert hat — die Haken blieben beim /// alten Wert stehen, während die Untertitel längst anders aussähen. let subtitleStyle: SubtitleStyle /// `@MainActor`, weil das Modell es ist: gelesen wird hier zwölfmal aus /// ihm, und zwar im Rumpf einer View — also ohnehin auf dem Hauptakteur. @MainActor init(_ model: PlayerModel, appearance: SubtitleAppearance, offLabel: String) { subtitleStyle = appearance.style playerIdentity = model.systemPlayer.map(ObjectIdentifier.init) let direkt: Bool = if case .directPlay = model.decision?.method { true } else { false } audio = TrackMenu.audio( from: model.audioStreams, selected: model.selectedAudioStream, isDirectPlay: direkt, playerTrackCount: model.audioTracks.count ) subtitles = TrackMenu.subtitles( from: model.subtitleStreams, selected: model.selectedSubtitleStream, isDirectPlay: direkt, playerTrackLanguages: model.subtitleTracks.map(\.languageCode), offLabel: offLabel ) isRebuilding = model.isChangingQuality title = model.title subtitle = model.subtitle markerLabel = model.activeMarker.map { String(localized: $0.isIntro ? "player.skipIntro" : "player.skipCredits") } } }