import AVKit import SwiftUI import FlixyPlayback import FlixyVLC /// Die Bildfläche des laufendie Engine. /// /// Die einzige Stelle in der App, an der der Unterschied zwischen den beiden /// Engines sichtbar wird — weil beide ihr Bild auf unterschiedliche Weise /// ausliefern: AVPlayer über eine `AVPlayerLayer`, VLC in eine gewöhnliche /// `UIView`. struct VideoSurface: View { let engine: (any VideoPlayerEngine)? /// Die Bildebene des Systemplayers, gehalten vom Modell. /// /// **Nicht** hier gebaut. Eine `AVPlayerLayer`, die in `makeUIView` /// entsteht, stirbt mit jedem Identitätswechsel der Ansicht — und genau /// das passiert beim Wechsel zwischen Kachel und Vollbild. Das Bild /// setzte dann jedes Mal neu an, sichtbar als Zucken und Schwarzbild. /// VLC macht es seit jeher richtig und reicht seine `videoView` durch; /// hier ist es jetzt derselbe Weg. let surface: PlayerLayerView var body: some View { switch engine { case is AVPlayerEngine: UIViewContainer(view: surface) case let vlcEngine as VLCPlayerEngine: UIViewContainer(view: vlcEngine.videoView) default: Color.black } } } /// Eine `UIView`, deren Ebene eine `AVPlayerLayer` ist. /// /// Der übliche Weg: eine Ebene von Hand als Unterebene einzuhängen bedeutet, /// ihre Größe bei jeder Drehung selbst nachzuführen. Als Hauptebene macht das /// UIKit. final class PlayerLayerView: UIView { override static var layerClass: AnyClass { AVPlayerLayer.self } var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } } /// Hängt eine bestehende `UIView` in den SwiftUI-Baum. /// /// Umhängen statt neu bauen: dieselbe `UIView` wandert zwischen Kachel und /// Vollbild hin und her, und die Wiedergabe merkt davon nichts. /// /// Sie wird dabei **nicht** unmittelbar zurückgegeben, sondern in eine eigene /// Hülle gehängt. Der Unterschied zählt genau in dem Moment, in dem es beide /// Ansichten kurz gleichzeitig gibt — beim Wechsel zwischen Kachel und /// Vollbild: eine `UIView` hat nur einen Vorgesetzten, und wer sie zuletzt /// bekommt, gewinnt. Gab SwiftUI sie unmittelbar heraus, nahm das Abräumen der /// einen Ansicht der anderen die Bildfläche weg, und übrig blieb eine schwarze /// Kachel — in einer fast schwarzen Oberfläche also scheinbar gar keine. /// /// Mit eigener Hülle gehört jeder Ansicht ihre eigene, und `updateUIView` holt /// die Fläche zurück, falls sie zwischenzeitlich woanders eingehängt wurde. private struct UIViewContainer: UIViewRepresentable { let view: UIView func makeUIView(context: Context) -> UIView { let host = UIView() host.backgroundColor = .black adopt(into: host) return host } func updateUIView(_ host: UIView, context: Context) { guard view.superview !== host else { return } adopt(into: host) } private func adopt(into host: UIView) { view.translatesAutoresizingMaskIntoConstraints = true view.frame = host.bounds view.autoresizingMask = [.flexibleWidth, .flexibleHeight] host.addSubview(view) } }