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
@@ -0,0 +1,28 @@
import SwiftUI
extension EdgeInsets {
/// Dieselben Ränder, aber auf beiden Seiten gleich — der jeweils grössere.
///
/// **Wofür das da ist.** Ein Sicherheitsrand ist selten symmetrisch: im
/// Querformat sitzt die Notch auf **einer** Seite, also zweiundsechzig
/// Punkte links und null rechts; im Hochformat sind es zweiundsechzig oben
/// und vierunddreissig unten. Wer damit ein Bild einrückt, rückt es nicht
/// ein — er **verschiebt** es, und zwar um die halbe Differenz.
///
/// Genau das war zu sehen: einunddreissig Punkte nach rechts im
/// Querformat, vierzehn nach unten im Hochformat. Gemessen an den
/// Bildschirmfotos: vierunddreissig und elfeinhalb.
///
/// **Für Bild, nicht für Knöpfe.** Ein Film soll mittig stehen, auch wenn
/// er dafür auf der freien Seite ein paar Punkte Balken mehr bekommt. Eine
/// Bedienleiste soll dagegen an der Kante kleben — die nimmt weiter die
/// echten, unsymmetrischen Werte.
public var symmetrical: EdgeInsets {
let senkrecht = max(top, bottom)
let waagerecht = max(leading, trailing)
return EdgeInsets(
top: senkrecht, leading: waagerecht,
bottom: senkrecht, trailing: waagerecht
)
}
}
@@ -54,6 +54,18 @@ public enum PlaybackEvent: Sendable, Equatable {
case reachedEnd
}
/// Wo das Bild aufhört.
///
/// **Hier und nicht in der App**, weil es beide Engines angeht. Am Bild
/// einzupassen ist nur zur Hälfte Sache der Ansicht: sie bestimmt die Fläche,
/// die Engine bestimmt, wie das Bild darin liegt.
public enum VideoFit: Sendable {
/// Ganz sichtbar, mit schwarzen Balken.
case fit
/// Bildfüllend, oben und unten beschnitten.
case fill
}
/// Der gemeinsame Nenner von AVPlayer und VLCKit.
///
/// Alles, was die Oberfläche zum Steuern braucht — und nichts darüber hinaus.
@@ -84,6 +96,18 @@ public protocol VideoPlayerEngine: AnyObject {
func seek(to time: Duration) async
func setRate(_ rate: Double)
/// Wie das Bild in seiner Fläche liegt.
///
/// **Muss durch die Engine, nicht nur über die Fläche.** Wer die Fläche
/// auf den ganzen Schirm wachsen lässt, hat damit erst die Hälfte gesagt:
/// VLC zeichnet selbst und passt sein Bild von sich aus ein, also wird
/// der Balken daneben nur breiter. Beschneiden muss man ihm sagen.
///
/// Der Systemplayer braucht es nicht — seine Bildebene gehört der App,
/// und die stellt `videoGravity` selbst. Deshalb steht unten eine
/// Voreinstellung, die nichts tut.
func setVideoFit(_ fit: VideoFit)
var audioTracks: [MediaTrack] { get }
var subtitleTracks: [MediaTrack] { get }
func selectAudioTrack(_ track: MediaTrack?)
@@ -238,4 +262,8 @@ extension VideoPlayerEngine {
/// Wer nichts dazu sagt, kann es nicht.
public var supportsAirPlay: Bool { false }
/// Der Systemplayer regelt es über seine Bildebene, und die hält die App —
/// siehe `PlayerModel.applyVideoFit()`. Hier ist nichts zu tun.
public func setVideoFit(_ fit: VideoFit) {}
}