Der Klick in den schwarzen Rand trifft jetzt den schwarzen Rand

Er hat weiterhin angehalten statt ein- und auszublenden, und der Grund
steckte in der Rechnung: der schwarze Rand wurde aus der Fenstergrösse
ausgerechnet, das Bild liegt aber in einer Fläche mit `ignoresSafeArea()`.
Auf dem Mac ist der Einzug der Fensterleiste genau der Unterschied
zwischen beiden — die Rechnung lag um diesen Betrag daneben.

Jetzt wird die Bildfläche gemessen statt angenommen, und beide Zahlen
stehen in einem gemeinsamen, benannten Koordinatenraum. Vor allem aber
wird zuerst `AVPlayerLayer.videoRect` gefragt: der Player weiss selbst,
wo Bild steht. Die eigene Rechnung bleibt für den Moment vor dem ersten
Bild und für VLC, das keine solche Ebene hat.

Dazu zwei Worte: „Nachschub" heisst jetzt „Nachladen", und „Blatt" heisst
wieder „Sheet" — das ist ein Fachbegriff und bleibt englisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-28 06:49:15 +02:00
co-authored by Claude Opus 5
parent f47c6385e3
commit 18958aadbc
17 changed files with 4234 additions and 4190 deletions
+56 -21
View File
@@ -45,8 +45,19 @@ struct PlayerView: View {
/// Wie gross die Player-Fläche gerade ist — für die Frage, wo das Bild
/// aufhört und das Schwarze anfängt.
@State private var playerSize: CGSize = .zero
/// Wo die Bildfläche im Player liegt — gemessen, nicht angenommen.
///
/// **Der Unterschied zur Fenstergrösse.** Die Bildfläche hat
/// `ignoresSafeArea()`, der Player selbst nicht. Auf dem Mac liegt oben
/// die Fensterleiste als Einzug: die beiden Flächen sind gegeneinander
/// verschoben. Wer den schwarzen Rand aus der Fenstergrösse ausrechnet,
/// rechnet ihn um genau diesen Einzug falsch — und trifft daneben.
@State private var videoFrame: CGRect = .zero
/// Ob Zeiger und Tastatur gelten. Einmal beim Start ermittelt.
private var isMac: Bool { FlixyPlatform.isMac }
/// Der gemeinsame Raum für Messen und Tippen.
private static let playerSpace = "flixy.player"
/// Wann der Zeiger zuletzt die Bedienelemente geholt hat.
@State private var lastPointerReveal: Date = .distantPast
@@ -64,6 +75,9 @@ struct PlayerView: View {
isActive: playback.presentation == .full
)
.ignoresSafeArea()
.onGeometryChange(for: CGRect.self) {
$0.frame(in: .named(Self.playerSpace))
} action: { videoFrame = $0 }
}
// **Unter** den Bedienelementen, nicht darüber. Als Überzug
@@ -162,7 +176,11 @@ struct PlayerView: View {
// Für alles außerhalb der Sprungflächen. Mit Ort, weil es einen
// Unterschied macht, **wo** geklickt wurde — siehe `tap(at:)`.
.onGeometryChange(for: CGSize.self) { $0.size } action: { playerSize = $0 }
.onTapGesture(coordinateSpace: .local) { tap(at: $0) }
// **Ein benannter Raum, kein `.local`.** Gemessen wird die Bildfläche,
// getippt wird auf den Player — zwei verschiedene Ansichten. Nur in
// einem gemeinsamen Raum sind die beiden Zahlen vergleichbar.
.coordinateSpace(.named(Self.playerSpace))
.onTapGesture(coordinateSpace: .named(Self.playerSpace)) { tap(at: $0) }
.onReceive(
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
) { _ in
@@ -196,7 +214,7 @@ struct PlayerView: View {
}
// **Die Umgebung wird den Blättern mitgegeben, nicht vererbt.**
//
// Ein Blatt hängt an einem eigenen Darstellungsbaum. Auf dem iPad kam
// Ein Sheet hängt an einem eigenen Darstellungsbaum. Auf dem iPad kam
// die Umgebung des Players dort bisher an, auf dem Mac nicht — und
// `@Environment(PlaybackSettings.self)` ist kein Wert mit
// Ersatzangabe, sondern eine Behauptung: fehlt der Gegenstand, hält
@@ -288,7 +306,7 @@ struct PlayerView: View {
// Getrennte Knöpfe, weil es zwei verschiedene Fragen sind: die
// Tonspur stellt man einmal am Anfang um, die Untertitel mitten im
// Film. Ein gemeinsames Blatt bedeutete jedes Mal Scrollen an der
// Film. Ein gemeinsames Sheet bedeutete jedes Mal Scrollen an der
// anderen Liste vorbei.
if !model.audioTracks.isEmpty {
trackButton(
@@ -679,41 +697,58 @@ struct PlayerView: View {
/// Wo das Bild tatsächlich liegt.
///
/// Der Player füllt das ganze Fenster, das Bild aber nur den Teil, der
/// zum Seitenverhältnis passt — der Rest ist schwarz. Die Maße kommen
/// aus `presentationSize`, also von dem, was wirklich ankommt, nicht aus
/// den Angaben der Datei.
/// **Zuerst die Ebene fragen.** `AVPlayerLayer.videoRect` ist die Fläche,
/// auf der wirklich Bild steht — vom Player selbst ausgerechnet, aus dem,
/// was gerade ankommt, und einschliesslich dessen, was die Füllart damit
/// macht. Jede eigene Rechnung ist daneben eine Nachbildung, die
/// irgendwann abweicht.
///
/// Ohne Maße die ganze Fläche: dann verhält es sich wie bisher, statt
/// **Dann die eigene Rechnung.** Solange die Ebene noch nichts zeigt —
/// und bei VLC, das keine hat — bleibt das Seitenverhältnis aus den
/// Messwerten. Es wird auf `videoFrame` angewandt, nicht auf die
/// Fenstergrösse: die beiden unterscheiden sich um den Einzug der
/// Fensterleiste, und **das** war der Fehler, wegen dem ein Klick in den
/// schwarzen Rand bisher angehalten hat statt einzublenden.
///
/// Ohne beides die ganze Fläche: dann verhält es sich wie früher, statt
/// aus Unkenntnis überall das Falsche zu tun.
private var videoRect: CGRect {
let ganzes = CGRect(origin: .zero, size: playerSize)
let fläche = videoFrame.isEmpty ? ganzes : videoFrame
guard fläche.width > 0, fläche.height > 0 else { return ganzes }
// Die Ebene liegt in der Bildfläche; ihre Angabe ist relativ zu deren
// Ecke und muss dorthin verschoben werden.
let ausDerEbene = model.surface.videoRect
if ausDerEbene.width > 1, ausDerEbene.height > 1 {
return ausDerEbene.offsetBy(dx: fläche.minX, dy: fläche.minY)
}
guard let breite = model.statistics?.width,
let höhe = model.statistics?.height,
breite > 0, höhe > 0,
playerSize.width > 0, playerSize.height > 0
breite > 0, höhe > 0
else { return ganzes }
let verhältnisBild = CGFloat(breite) / CGFloat(höhe)
let verhältnisFläche = playerSize.width / playerSize.height
let verhältnisFläche = fläche.width / fläche.height
if verhältnisBild > verhältnisFläche {
// Breiter als das Fenster: Balken oben und unten.
let sichtbar = playerSize.width / verhältnisBild
// Breiter als die Fläche: Balken oben und unten.
let sichtbar = fläche.width / verhältnisBild
return CGRect(
x: 0,
y: (playerSize.height - sichtbar) / 2,
width: playerSize.width,
x: fläche.minX,
y: fläche.minY + (fläche.height - sichtbar) / 2,
width: fläche.width,
height: sichtbar
)
}
// Höher als das Fenster: Balken links und rechts.
let sichtbar = playerSize.height * verhältnisBild
// Höher als die Fläche: Balken links und rechts.
let sichtbar = fläche.height * verhältnisBild
return CGRect(
x: (playerSize.width - sichtbar) / 2,
y: 0,
x: fläche.minX + (fläche.width - sichtbar) / 2,
y: fläche.minY,
width: sichtbar,
height: playerSize.height
height: fläche.height
)
}