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 /// Ob **diese** Ansicht die Bildfläche gerade haben soll. /// /// **Der Kern des Schwarzbilds.** Kachel und Vollbild bestehen beim /// Wechsel kurz gleichzeitig, und beide holen sich die Fläche in ihre /// Hülle. Eine Ansicht hat aber nur einen Vorgesetzten — wer zuletzt /// aktualisiert, gewinnt, und das war nicht verlässlich die richtige. /// Blieb die Fläche bei der Kachel, die gleich darauf verschwand, lief /// im Vollbild der Ton weiter und das Bild blieb schwarz. /// /// Die Hülle allein genügte dagegen nicht: sie verhindert, dass das /// Abräumen der einen der anderen etwas wegnimmt, entscheidet aber /// nicht, wer sie bekommen **soll**. Das steht hier. var isActive = true var body: some View { #if canImport(FlixyVLC) if let vlcEngine = engine as? VLCPlayerEngine { SurfaceContainer(view: vlcEngine.videoView, isActive: isActive) } else if engine is AVPlayerEngine { SurfaceContainer(view: surface, isActive: isActive) } else { Color.black } #else if engine is AVPlayerEngine { SurfaceContainer(view: surface, isActive: isActive) } 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 } /// Die Fläche, auf der wirklich Bild steht — ohne die schwarzen Ränder. /// /// Vom Player ausgerechnet, aus dem, was gerade ankommt. Vor dem ersten /// Bild ist sie leer; wer sie benutzt, muss das prüfen. var videoRect: CGRect { playerLayer.videoRect } } #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() /// Siehe die UIKit-Fassung oben. var videoRect: CGRect { playerLayer.videoRect } 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 let isActive: Bool func makeUIView(context: Context) -> PlatformView { let host = UIView() host.backgroundColor = .black if isActive { adopt(view, into: host) } return host } func updateUIView(_ host: PlatformView, context: Context) { // Wer nicht dran ist, greift auch nicht zu. Ohne diese Zeile holen // sich beide Ansichten dieselbe Fläche, und die Reihenfolge // entscheidet — mal richtig, mal schwarz. guard isActive, view.superview !== host else { return } adopt(view, into: host) } } #else private struct SurfaceContainer: NSViewRepresentable { let view: PlatformView let isActive: Bool func makeNSView(context: Context) -> PlatformView { let host = NSView() host.wantsLayer = true host.layer?.backgroundColor = NSColor.black.cgColor if isActive { adopt(view, into: host) } return host } func updateNSView(_ host: PlatformView, context: Context) { guard isActive, 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) }