From b1973c68903625de1af3ece2bf5f59a8d0f79be2 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 3 Oct 2026 07:54:46 +0200 Subject: [PATCH] =?UTF-8?q?Bild-in-Bild=20gab=20es=20nur=20beim=20Weggehen?= =?UTF-8?q?=20=E2=80=94=20jetzt=20auch=20auf=20Zuruf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Du hast keinen Knopf gefunden, weil es keinen gab: `AVPictureInPictureController` stand da, aber nur mit `canStartPictureInPictureAutomaticallyFromInline`. Das greift, wenn man die App verlässt — und auf dem Mac gar nicht. Der Knopf erscheint nur, wenn es wirklich ginge. Das ist nicht immer so: `attachSurface` steigt für alles ausser `AVPlayerEngine` sofort aus, und `AVPictureInPictureController` braucht eine `AVPlayerLayer`. Läuft ein Titel über VLC, gibt es kein Bild-in-Bild — nur Ton im Hintergrund. Genau das war deine Beobachtung. **Beobachtet, nicht abgefragt.** `isPictureInPicturePossible` steht erst fest, wenn der Player ein Bild hat; wer beim Zeichnen fragt, bekommt `false` und zeichnet den Knopf nie wieder. Also meldet es sich über KVO — vom Input-Thread, deshalb der Sprung auf den Hauptakteur, wie bei den übrigen Beobachtungen. Co-Authored-By: Claude Opus 5 --- Flixy/Resources/Localizable.xcstrings | 18 ++++++++++++ Flixy/Views/Player/PlayerModel.swift | 41 +++++++++++++++++++++++++++ Flixy/Views/Player/PlayerView.swift | 16 +++++++++++ 3 files changed, 75 insertions(+) diff --git a/Flixy/Resources/Localizable.xcstrings b/Flixy/Resources/Localizable.xcstrings index c4fd04e..9c4d2ee 100644 --- a/Flixy/Resources/Localizable.xcstrings +++ b/Flixy/Resources/Localizable.xcstrings @@ -3968,6 +3968,24 @@ } } }, + "player.pictureInPicture": { + "comment": "Knopf im Player: das Bild in ein kleines Fenster legen", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bild im Bild" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Picture in Picture" + } + } + } + }, "player.play": { "extractionState": "stale", "localizations": { diff --git a/Flixy/Views/Player/PlayerModel.swift b/Flixy/Views/Player/PlayerModel.swift index 83649c1..a052b63 100644 --- a/Flixy/Views/Player/PlayerModel.swift +++ b/Flixy/Views/Player/PlayerModel.swift @@ -201,6 +201,18 @@ final class PlayerModel { /// `@Observable` einen `lazy`-Delegaten nicht überlebt. @ObservationIgnored private var pictureInPicture: AVPictureInPictureController? @ObservationIgnored private var pictureInPictureDelegate: PictureInPictureBridge? + @ObservationIgnored private var pictureInPictureObservation: NSKeyValueObservation? + + /// Ob Bild-in-Bild **jetzt** ginge. + /// + /// **Beobachtet, nicht abgefragt.** `isPictureInPicturePossible` steht erst + /// fest, wenn der Player ein Bild hat — wer beim Zeichnen fragt, bekommt + /// `false` und zeichnet den Knopf nie wieder. Also meldet es sich selbst. + /// + /// Bleibt bei VLC dauerhaft falsch: `AVPictureInPictureController` braucht + /// eine `AVPlayerLayer`, und VLC zeichnet in eine eigene Ansicht. + private(set) var canUsePictureInPicture = false + private(set) var isPictureInPictureActive = false /// Was beim Beenden von Bild-in-Bild wieder ins Vollbild führen soll. @ObservationIgnored private var restoreFromPictureInPicture: (() -> Void)? @@ -1578,8 +1590,11 @@ final class PlayerModel { streamServer = nil streamCache = nil prefetchStatus = nil + pictureInPictureObservation = nil pictureInPicture = nil pictureInPictureDelegate = nil + canUsePictureInPicture = false + isPictureInPictureActive = false surface.playerLayer.player = nil isPlaying = false isBuffering = false @@ -1766,6 +1781,32 @@ final class PlayerModel { controller?.delegate = bridge pictureInPictureDelegate = bridge pictureInPicture = controller + // KVO meldet auf einem beliebigen Thread — deshalb der Sprung, + // wie bei den Beobachtungen in `AVPlayerEngine`. + pictureInPictureObservation = controller?.observe( + \.isPictureInPicturePossible, + options: [.initial, .new] + ) { [weak self] controller, _ in + let moeglich = controller.isPictureInPicturePossible + Task { @MainActor [weak self] in self?.canUsePictureInPicture = moeglich } + } + } + } + + /// Schaltet Bild-in-Bild ein oder wieder aus. + /// + /// **Von Hand, nicht nur beim Verlassen der App.** Automatisch wechselt der + /// Player schon, wenn man zum Homebildschirm geht — aber nur dann, und auf + /// dem Mac gar nicht. Wer das Bild klein neben etwas anderes legen will, + /// ohne die App zu verlassen, hatte dafür keinen Weg. + func togglePictureInPicture() { + guard let pictureInPicture else { return } + if pictureInPicture.isPictureInPictureActive { + pictureInPicture.stopPictureInPicture() + isPictureInPictureActive = false + } else { + pictureInPicture.startPictureInPicture() + isPictureInPictureActive = true } } diff --git a/Flixy/Views/Player/PlayerView.swift b/Flixy/Views/Player/PlayerView.swift index 16545ba..55ca4f3 100644 --- a/Flixy/Views/Player/PlayerView.swift +++ b/Flixy/Views/Player/PlayerView.swift @@ -516,6 +516,22 @@ struct PlayerView: View { ) } + // **Bild-in-Bild von Hand.** Automatisch geht es bisher nur beim + // Verlassen der App — und auf dem Mac gar nicht. Der Knopf + // erscheint nur, wenn es wirklich ginge: bei VLC gibt es keine + // `AVPlayerLayer`, und ohne die kann das System kein kleines Fenster + // aufziehen. + if model.canUsePictureInPicture { + Button { + model.togglePictureInPicture() + } label: { + Image(systemName: model.isPictureInPictureActive ? "pip.exit" : "pip.enter") + .font(.title3) + .padding(FlixySpacing.snug) + } + .accessibilityLabel(Text("player.pictureInPicture")) + } + if model.engine?.supportsAirPlay == true { AirPlayButton(tint: accent) .frame(width: 28, height: 28)