import AVKit import SwiftUI import FlixyPlayback #if canImport(UIKit) import UIKit #else import AppKit #endif #if canImport(FlixyVLC) import FlixyVLC #endif /// Die Ansichtsklasse der jeweiligen Plattform. /// /// UIKit und AppKit heißen verschieden und meinen dasselbe. Ein Name dafür /// spart in dieser Datei ein Dutzend `#if`. #if canImport(UIKit) typealias PlatformView = UIView #else typealias PlatformView = NSView #endif /// 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 { #if canImport(FlixyVLC) if let vlcEngine = engine as? VLCPlayerEngine { SurfaceContainer(view: vlcEngine.videoView) } else if engine is AVPlayerEngine { SurfaceContainer(view: surface) } else { Color.black } #else if engine is AVPlayerEngine { SurfaceContainer(view: surface) } else { Color.black } #endif } } /// 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. #if canImport(UIKit) final class PlayerLayerView: UIView { override static var layerClass: AnyClass { AVPlayerLayer.self } var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } } #else /// Auf dem Mac gibt es kein `layerClass`: eine `NSView` bekommt ihre Ebene /// zugewiesen, statt ihre Art vorzugeben. final class PlayerLayerView: NSView { let playerLayer = AVPlayerLayer() override init(frame: NSRect) { super.init(frame: frame) wantsLayer = true layer = playerLayer // Ohne das bleibt die Ebene beim Vergrößern des Fensters stehen, wo // sie war, und das Bild sitzt in der Ecke. layer?.needsDisplayOnBoundsChange = true autoresizingMask = [.width, .height] } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("nicht aus einem Storyboard") } } #endif /// 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. #if canImport(UIKit) private struct SurfaceContainer: UIViewRepresentable { let view: PlatformView func makeUIView(context: Context) -> PlatformView { let host = UIView() host.backgroundColor = .black adopt(view, into: host) return host } func updateUIView(_ host: PlatformView, context: Context) { guard view.superview !== host else { return } adopt(view, into: host) } } #else private struct SurfaceContainer: NSViewRepresentable { let view: PlatformView func makeNSView(context: Context) -> PlatformView { let host = NSView() host.wantsLayer = true host.layer?.backgroundColor = NSColor.black.cgColor adopt(view, into: host) return host } func updateNSView(_ host: PlatformView, context: Context) { guard view.superview !== host else { return } adopt(view, into: host) } } #endif /// Hängt die Fläche in ihre Hülle. /// /// Sie wird **nicht** unmittelbar an SwiftUI zurückgegeben, sondern in eine /// eigene Hülle gehängt. Der Unterschied zählt genau in dem Moment, in dem es /// Kachel und Vollbild kurz gleichzeitig gibt: eine Ansicht hat nur einen /// Vorgesetzten, und wer sie zuletzt bekommt, gewinnt. Gab SwiftUI sie /// unmittelbar heraus, nahm das Abräumen der einen der anderen das Bild weg. private func adopt(_ view: PlatformView, into host: PlatformView) { #if canImport(UIKit) view.translatesAutoresizingMaskIntoConstraints = true view.frame = host.bounds view.autoresizingMask = [.flexibleWidth, .flexibleHeight] #else view.translatesAutoresizingMaskIntoConstraints = true view.frame = host.bounds view.autoresizingMask = [.width, .height] #endif host.addSubview(view) }