Bild-in-Bild gab es nur beim Weggehen — jetzt auch auf Zuruf

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-03 07:54:46 +02:00
co-authored by Claude Opus 5
parent 261cdee2f2
commit b1973c6890
3 changed files with 75 additions and 0 deletions
+18
View File
@@ -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": { "player.play": {
"extractionState": "stale", "extractionState": "stale",
"localizations": { "localizations": {
+41
View File
@@ -201,6 +201,18 @@ final class PlayerModel {
/// `@Observable` einen `lazy`-Delegaten nicht überlebt. /// `@Observable` einen `lazy`-Delegaten nicht überlebt.
@ObservationIgnored private var pictureInPicture: AVPictureInPictureController? @ObservationIgnored private var pictureInPicture: AVPictureInPictureController?
@ObservationIgnored private var pictureInPictureDelegate: PictureInPictureBridge? @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. /// Was beim Beenden von Bild-in-Bild wieder ins Vollbild führen soll.
@ObservationIgnored private var restoreFromPictureInPicture: (() -> Void)? @ObservationIgnored private var restoreFromPictureInPicture: (() -> Void)?
@@ -1578,8 +1590,11 @@ final class PlayerModel {
streamServer = nil streamServer = nil
streamCache = nil streamCache = nil
prefetchStatus = nil prefetchStatus = nil
pictureInPictureObservation = nil
pictureInPicture = nil pictureInPicture = nil
pictureInPictureDelegate = nil pictureInPictureDelegate = nil
canUsePictureInPicture = false
isPictureInPictureActive = false
surface.playerLayer.player = nil surface.playerLayer.player = nil
isPlaying = false isPlaying = false
isBuffering = false isBuffering = false
@@ -1766,6 +1781,32 @@ final class PlayerModel {
controller?.delegate = bridge controller?.delegate = bridge
pictureInPictureDelegate = bridge pictureInPictureDelegate = bridge
pictureInPicture = controller 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
} }
} }
+16
View File
@@ -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 { if model.engine?.supportsAirPlay == true {
AirPlayButton(tint: accent) AirPlayButton(tint: accent)
.frame(width: 28, height: 28) .frame(width: 28, height: 28)