Das Bild füllt den Schirm, und die Schrift bleibt darin
Vier Dinge am iPhone-Player, die zusammengehören, weil sie alle davon handeln, wo der Bildschirm aufhört. **Einpassen oder füllen.** `videoGravity` stand fest auf `.resizeAspect`, und zwar in einer Zeile, die nach jedem Neuaufbau erneut läuft — eine Wahl hätte dort also jeden Qualitätswechsel nicht überlebt. Jetzt steht sie im Modell, und die Zeile liest sie. Umgelegt wird mit zwei Fingern oder über einen Knopf; stufenlos wäre falsch, weil alles zwischen den zwei Zuständen ein Bild ist, das weder ganz zu sehen ist noch den Schirm füllt. Auf dem Mac erscheint der Knopf nicht: dort steht der Vollbildknopf darüber, und zwei Knöpfe, die beide „grösser" bedeuten, sind eine Zumutung. **Untertitel im Füllmodus.** Dort reicht das Bild über die untere Bildschirmkante hinaus, und genau dort zeichnet AVFoundation die Untertitel. Statt sie anzuschneiden, wird die Bildebene um den Sicherheitsrand eingerückt — rund zwanzig Punkt Bild gegen eine Zeile, die man lesen kann. Dafür bekommt die Bildfläche echtes Layout: eine `autoresizingMask` hält Ränder verhältnismässig, und ein fester Einzug von zwanzig Punkten wäre im Querformat plötzlich vierzig gewesen. **Die Bedienleiste kannte ihre Ränder nicht mehr.** Der Player bekommt `ignoresSafeArea()` von der Wurzel und sieht danach überall null — Schieberegler und Zeitangaben sassen im Home-Indikator, der Zurück-Pfeil hinter der Dynamic Island, wo ihn niemand drücken konnte. Gemessen wird jetzt eine Ebene weiter aussen, wo die Ränder noch gelten, und über die Umgebung nach innen gereicht. Dieselbe Bauart wie der Akzent. Die Zeile steht ganz am Ende der Kette: stünde sie vor dem Overlay, umschlösse das Overlay sie, und ausgerechnet der Player ginge leer aus. **Gedreht wird einmal, gesperrt wird nicht.** Die Maske blieb auf `.landscape` stehen — wer im Bett auf der Seite lag, konnte nicht mehr ins Hochformat drehen. Ein Film beginnt quer, weil er quer ist; danach gehört die Entscheidung dem Nutzer. Dazu ein Drehknopf für den Fall, dass die Ausrichtungssperre des Systems an ist, und ein Fehlerbehandler an `requestGeometryUpdate`: eine abgelehnte Drehung ist dann eine Einstellung und keine Panne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1ff65e823a
commit
4576eac67c
@@ -153,6 +153,34 @@ final class PlayerModel {
|
||||
/// zwischen Kachel und Vollbild, die Ansicht nicht. Läge sie dort, entstünde
|
||||
/// bei jedem Wechsel eine neue `AVPlayerLayer`, und das Bild setzte neu an.
|
||||
let surface = PlayerLayerView()
|
||||
|
||||
/// Wie das Bild in die Fläche gelegt wird.
|
||||
///
|
||||
/// **Im Modell und nicht in der Ansicht**, aus demselben Grund wie die
|
||||
/// Bildebene selbst: der Wechsel zwischen Kachel und Vollbild und jeder
|
||||
/// Neuaufbau der Engine gehen an der Ansicht vorbei. Läge die Wahl dort,
|
||||
/// spränge das Bild bei jedem Qualitätswechsel zurück auf „eingepasst".
|
||||
var videoFit: VideoFit = .fit {
|
||||
didSet { applyVideoGravity() }
|
||||
}
|
||||
|
||||
/// Wie viel unten ausgespart bleibt, damit Untertitel sichtbar sind.
|
||||
///
|
||||
/// Im Füllmodus reicht das Bild über die untere Bildschirmkante hinaus —
|
||||
/// und genau dort zeichnet AVFoundation die Untertitel. Statt sie
|
||||
/// anzuschneiden, wird die **Bildebene** eingerückt: das kostet rund
|
||||
/// zwanzig Punkt Bild und behält die Schrift. Im eingepassten Modus ist
|
||||
/// der Wert null, weil dort ohnehin schwarze Balken liegen.
|
||||
var subtitleBottomInset: CGFloat = 0
|
||||
|
||||
/// Wo das Bild aufhört.
|
||||
public enum VideoFit: Sendable {
|
||||
/// Ganz sichtbar, mit schwarzen Balken.
|
||||
case fit
|
||||
/// Bildfüllend, oben und unten beschnitten.
|
||||
case fill
|
||||
}
|
||||
|
||||
/// Bild-in-Bild hängt an genau dieser Ebene und wandert deshalb mit.
|
||||
///
|
||||
/// `@ObservationIgnored`, weil keine Ansicht darauf zeichnet — und weil
|
||||
@@ -1668,6 +1696,14 @@ final class PlayerModel {
|
||||
}
|
||||
|
||||
|
||||
/// Legt die gewählte Skalierung auf die Bildebene.
|
||||
///
|
||||
/// Eine eigene Funktion, weil es zwei Anlässe gibt: die Wahl des Nutzers
|
||||
/// und jeder Engine-Wechsel. Beide müssen dasselbe tun.
|
||||
private func applyVideoGravity() {
|
||||
surface.playerLayer.videoGravity = videoFit == .fill ? .resizeAspectFill : .resizeAspect
|
||||
}
|
||||
|
||||
/// Hängt die gehaltene Bildebene an den aktuellen Engine.
|
||||
///
|
||||
/// Nach **jedem** Engine-Wechsel nötig — also auch nach einem
|
||||
@@ -1677,7 +1713,11 @@ final class PlayerModel {
|
||||
guard let avEngine = engine as? AVPlayerEngine else { return }
|
||||
|
||||
surface.playerLayer.player = avEngine.player
|
||||
surface.playerLayer.videoGravity = .resizeAspect
|
||||
// **Den Zustand lesen, keine Konstante.** Hier stand fest
|
||||
// `.resizeAspect` — und weil diese Zeile nach jedem Neuaufbau erneut
|
||||
// läuft, fiel ein auf formatfüllend gestelltes Bild bei jedem
|
||||
// Qualitätswechsel klammheimlich zurück.
|
||||
applyVideoGravity()
|
||||
|
||||
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
|
||||
if pictureInPicture == nil {
|
||||
|
||||
Reference in New Issue
Block a user