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
@@ -17,7 +17,7 @@ import UIKit
public final class VLCPlayerEngine: NSObject, VideoPlayerEngine {
/// Die Fläche, in die VLC zeichnet. Die Ansicht hängt sie in ihren
/// Hierarchiebaum.
public let videoView = UIView()
public let videoView: UIView = VLCSurfaceView()
private let player = VLCMediaPlayer()
@@ -85,6 +85,14 @@ public final class VLCPlayerEngine: NSObject, VideoPlayerEngine {
private var continuation: AsyncStream<PlaybackEvent>.Continuation?
private var pendingStartTime: Duration = .zero
/// Wie das Bild in `videoView` liegen soll.
///
/// **Gemerkt und nicht nur weitergegeben.** Solange VLC keine
/// Bildausgabe hat, gibt es nichts, worauf die Einstellung wirken könnte;
/// vor dem ersten Bild gesetzt, ist sie weg. Deshalb steht sie hier und
/// wird beim Anlaufen erneut gelegt.
private var fit: VideoFit = .fit
public override init() {
super.init()
videoView.backgroundColor = .black
@@ -135,6 +143,20 @@ public final class VLCPlayerEngine: NSObject, VideoPlayerEngine {
player.rate = Float(rate)
}
public func setVideoFit(_ fit: VideoFit) {
self.fit = fit
applyFit()
}
/// Legt die gemerkte Einstellung auf den Player.
///
/// `smaller` heisst bei libvlc ausdrücklich „fit inside", `larger`
/// „fit outside" — also einpassen mit Balken beziehungsweise füllen mit
/// Beschnitt. Die Namen sagen es nicht von selbst; die Kopfdatei tut es.
private func applyFit() {
player.videoFitMode = fit == .fill ? .larger : .smaller
}
public func selectAudioTrack(_ track: MediaTrack?) {
select(track, from: player.audioTracks)
}
@@ -202,6 +224,9 @@ extension VLCPlayerEngine: VLCMediaPlayerDelegate {
state = .playing
applyPendingStartTime()
refreshTracks()
// Siehe `fit`: vor dem ersten Bild wäre sie ins Leere
// gegangen. Hier gibt es eine Ausgabe.
applyFit()
case .paused:
state = .paused
case .stopped, .stopping:
@@ -318,3 +343,33 @@ enum AudioSessionBridge {
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
}
/// Die Fläche, die VLC als Zeichenfläche bekommt.
///
/// **Eine eigene Klasse, weil VLC seine Anzeigefläche nur ein einziges Mal
/// misst.** `VLCDrawable` verlangt vom Aufnehmenden zwei Dinge: `addSubview:`
/// und `bounds`. VLCKit fragt die Grösse beim Anlegen ab, legt seine
/// `VLCVideoUIView` genau so gross an und überlässt sie 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 beim Anlegen ist die Grösse regelmässig falsch — die Fläche entsteht
/// mitten in der Drehung ins Querformat, während SwiftUI sie gerade einblendet.
/// Gemessen: eine Anzeigefläche von 1200×480 Punkt in einer Zeichenfläche von
/// 711×400. Auf dem Bildschirm sah man davon das Stück, das hineinpasste —
/// unten rechts angeschlagen, in falschem Seitenverhältnis. Genau das war
/// „das Bild sitzt nicht mittig" bei allem, was über VLC läuft.
///
/// Hier bekommt die Anzeigefläche bei jedem Layout wieder genau die eigene
/// Grösse. Das ist keine Notlösung, sondern der Teil des Vertrags, den nur der
/// Aufnehmende erfüllen kann: VLC kennt den Grund für eine Grössenänderung
/// nicht, die Ansicht kennt ihn.
final class VLCSurfaceView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
for view in subviews where view.frame != bounds {
view.frame = bounds
}
}
}