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:
co-authored by
Claude Opus 5
parent
261cdee2f2
commit
b1973c6890
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user