Bei VLC sass das Bild nie richtig — und der Knopf wirkte nie

Zwei Fehler, die dieselbe Ursache haben: alles, was über VLC läuft, ging
an der Bildebene vorbei, an der die App gedreht hat.

**Das Bild sass nicht mittig.** Nicht ein paar Punkte daneben — gemessen
eine Anzeigefläche von 1200×480 Punkt in einer Zeichenfläche von 711×400,
unten rechts angeschlagen, im falschen Seitenverhältnis. Der Grund steht
im Vertrag von `VLCDrawable`: VLCKit fragt die Grösse **einmal** beim
Anlegen ab und überlässt seine Anzeigefläche danach der
Autoresizing-Maske. Eine Maske hält aber den Unterschied fest, nicht das
Verhältnis — was beim Anlegen zu gross war, bleibt für immer um denselben
Betrag zu gross. Und angelegt wird sie mitten in der Drehung ins
Querformat, während SwiftUI sie einblendet: also regelmässig falsch.

Die Zeichenfläche ist jetzt eine eigene Klasse, die ihrer Anzeigefläche
bei jedem Layout wieder die eigene Grösse gibt. Danach gemessen: 695,7 ×
400 Punkt, Rand links 130,0, rechts 130,3 — mittig.

**Der Knopf für einpassen/füllen tat nichts.** Er schrieb `videoGravity`
auf eine `AVPlayerLayer`, die bei VLC gar nicht im Baum hängt. Die
Einstellung geht deshalb jetzt durch die Engine: `setVideoFit` am
Protokoll, beim Systemplayer eine Voreinstellung, die nichts tut — dort
macht es die Bildebene —, bei VLC `videoFitMode`. `smaller` heisst bei
libvlc „fit inside", `larger` „fit outside".

Gemessen, im Querformat auf einem Pro Max, über VLC:

    eingepasst   695,7 × 400 pt   Rand l/r 130,0/130,3   o/u 20,0/20,0
    füllend      935,3 × 440 pt   Rand l/r  10,3/ 10,3   o/u  0,0/ 0,0

**Zurückgenommen:** im Kommentar an `videoFit` stand, ein `didSet` auf
einer `@Observable`-Klasse werde nicht gemeldet. Das stimmt nicht —
`HomeLayoutSettings` lebt davon. Ein Kommentar, der einen falschen
Mechanismus behauptet, schickt den nächsten in die falsche Richtung;
jetzt steht der belegte Grund dort.

Dazu, damit dieser Weg überhaupt prüfbar ist: die Attrappe meldet mit
`--vlc` DTS-Ton. AVFoundation kann das nicht, also fällt die App auf
VLCKit zurück. Die Beispieldatei bleibt dieselbe — geprüft wird der Weg,
nicht der Ton. Ohne diesen Schalter ist der VLC-Zweig im Simulator gar
nicht zu erreichen, und genau deshalb lagen beide Fehler so lange da.

Ebenfalls dabei: `EdgeInsets.symmetrical` mit fünf Tests und das Polster
im Player, das damit nicht mehr verschiebt statt einrückt.

iOS und tvOS bauen, 465 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-30 21:05:57 +02:00
co-authored by Claude Opus 5
parent 51abcabb27
commit b0db4f8f5f
8 changed files with 229 additions and 15 deletions
+33 -12
View File
@@ -173,9 +173,20 @@ final class PlayerModel {
/// 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() }
}
///
/// **Warum der Knopf lange nichts tat, und warum der Grund nicht der
/// `didSet` war, der hier stand.** Ein `didSet` auf einer verfolgten
/// Eigenschaft einer `@Observable`-Klasse wird durchaus gemeldet —
/// `HomeLayoutSettings` lebt davon. Der Knopf schrieb `videoGravity` auf
/// `surface.playerLayer`, und diese Ebene hängt bei einer Datei, die über
/// VLC läuft, überhaupt nicht im Baum: `VideoSurface` nimmt dort
/// `vlcEngine.videoView`, und `attachSurface` steigt für alles ausser
/// AVPlayer gleich wieder aus. Geschrieben wurde also auf eine Ebene, die
/// niemand sieht.
///
/// Deshalb geht die Einstellung jetzt durch die Engine — siehe
/// `applyVideoFit()`.
var videoFit: VideoFit = .fit
/// Wie viel unten ausgespart bleibt, damit Untertitel sichtbar sind.
///
@@ -187,12 +198,11 @@ final class PlayerModel {
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
}
///
/// Liegt bei `FlixyPlayback`, weil beide Engines es brauchen. Der Name
/// hier bleibt, damit die Ansicht weiter `PlayerModel.VideoFit` schreiben
/// kann.
typealias VideoFit = FlixyPlayback.VideoFit
/// Bild-in-Bild hängt an genau dieser Ebene und wandert deshalb mit.
///
@@ -768,6 +778,10 @@ final class PlayerModel {
let engine = makeEngine(for: decision)
self.engine = engine
attachSurface(to: engine)
// **Auch für VLC**, und deshalb hier und nicht in `attachSurface`:
// das steigt für alles ausser AVPlayer sofort aus, weil es die
// Bildebene anhängt. Die Skalierung geht aber beide an.
engine.setVideoFit(videoFit)
observe(engine)
// **Vor dem Öffnen.** Ein frisch angelegter Gegenstand ist
// ungestaltet, und der erste Untertitel käme sonst in der Vorgabe.
@@ -1716,12 +1730,19 @@ final class PlayerModel {
}
/// Legt die gewählte Skalierung auf die Bildebene.
/// Legt die gewählte Skalierung auf das Bild.
///
/// 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() {
///
/// **Zwei Zeilen, weil es zwei Engines gibt.** Die Bildebene gehört der
/// App und nicht dem Systemplayer — deshalb steht `videoGravity` hier und
/// nicht in `AVPlayerEngine`. VLC zeichnet dagegen selbst und passt sein
/// Bild von sich aus ein; ihm muss man es sagen. Solange nur die erste
/// Zeile hier stand, war der Knopf bei jeder Datei über VLC ohne Wirkung.
func applyVideoFit() {
surface.playerLayer.videoGravity = videoFit == .fill ? .resizeAspectFill : .resizeAspect
engine?.setVideoFit(videoFit)
}
/// Hängt die gehaltene Bildebene an den aktuellen Engine.
@@ -1737,7 +1758,7 @@ final class PlayerModel {
// `.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()
applyVideoFit()
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
if pictureInPicture == nil {