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:
co-authored by
Claude Opus 5
parent
51abcabb27
commit
b0db4f8f5f
@@ -173,9 +173,20 @@ final class PlayerModel {
|
|||||||
/// Bildebene selbst: der Wechsel zwischen Kachel und Vollbild und jeder
|
/// Bildebene selbst: der Wechsel zwischen Kachel und Vollbild und jeder
|
||||||
/// Neuaufbau der Engine gehen an der Ansicht vorbei. Läge die Wahl dort,
|
/// 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".
|
/// 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.
|
/// Wie viel unten ausgespart bleibt, damit Untertitel sichtbar sind.
|
||||||
///
|
///
|
||||||
@@ -187,12 +198,11 @@ final class PlayerModel {
|
|||||||
var subtitleBottomInset: CGFloat = 0
|
var subtitleBottomInset: CGFloat = 0
|
||||||
|
|
||||||
/// Wo das Bild aufhört.
|
/// Wo das Bild aufhört.
|
||||||
public enum VideoFit: Sendable {
|
///
|
||||||
/// Ganz sichtbar, mit schwarzen Balken.
|
/// Liegt bei `FlixyPlayback`, weil beide Engines es brauchen. Der Name
|
||||||
case fit
|
/// hier bleibt, damit die Ansicht weiter `PlayerModel.VideoFit` schreiben
|
||||||
/// Bildfüllend, oben und unten beschnitten.
|
/// kann.
|
||||||
case fill
|
typealias VideoFit = FlixyPlayback.VideoFit
|
||||||
}
|
|
||||||
|
|
||||||
/// Bild-in-Bild hängt an genau dieser Ebene und wandert deshalb mit.
|
/// 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)
|
let engine = makeEngine(for: decision)
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
attachSurface(to: 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)
|
observe(engine)
|
||||||
// **Vor dem Öffnen.** Ein frisch angelegter Gegenstand ist
|
// **Vor dem Öffnen.** Ein frisch angelegter Gegenstand ist
|
||||||
// ungestaltet, und der erste Untertitel käme sonst in der Vorgabe.
|
// 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
|
/// Eine eigene Funktion, weil es zwei Anlässe gibt: die Wahl des Nutzers
|
||||||
/// und jeder Engine-Wechsel. Beide müssen dasselbe tun.
|
/// 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
|
surface.playerLayer.videoGravity = videoFit == .fill ? .resizeAspectFill : .resizeAspect
|
||||||
|
engine?.setVideoFit(videoFit)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hängt die gehaltene Bildebene an den aktuellen Engine.
|
/// 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
|
// `.resizeAspect` — und weil diese Zeile nach jedem Neuaufbau erneut
|
||||||
// läuft, fiel ein auf formatfüllend gestelltes Bild bei jedem
|
// läuft, fiel ein auf formatfüllend gestelltes Bild bei jedem
|
||||||
// Qualitätswechsel klammheimlich zurück.
|
// Qualitätswechsel klammheimlich zurück.
|
||||||
applyVideoGravity()
|
applyVideoFit()
|
||||||
|
|
||||||
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
|
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
|
||||||
if pictureInPicture == nil {
|
if pictureInPicture == nil {
|
||||||
|
|||||||
@@ -848,6 +848,10 @@ struct PlayerView: View {
|
|||||||
// **Nur im Füllmodus.** Eingepasst liegt das Bild seit
|
// **Nur im Füllmodus.** Eingepasst liegt das Bild seit
|
||||||
// Neuestem ohnehin im sicheren Bereich — dort kann kein
|
// Neuestem ohnehin im sicheren Bereich — dort kann kein
|
||||||
// Untertitel mehr unter die Kante geraten.
|
// Untertitel mehr unter die Kante geraten.
|
||||||
|
// Im Füllmodus läuft das Bild bis an die Kanten; damit der
|
||||||
|
// Untertitel nicht darunter gerät, wird die **Ebene** unten
|
||||||
|
// eingerückt. Der rohe Wert genügt hier — es geht um die
|
||||||
|
// Bildschirmkante, nicht um die Mitte.
|
||||||
bottomInset: model.videoFit == .fill ? safeArea.bottom : 0
|
bottomInset: model.videoFit == .fill ? safeArea.bottom : 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -877,7 +881,13 @@ struct PlayerView: View {
|
|||||||
// damit, statt hinterher beschnitten zu werden.
|
// damit, statt hinterher beschnitten zu werden.
|
||||||
fläche
|
fläche
|
||||||
.aspectRatio(CGFloat(breite) / CGFloat(höhe), contentMode: .fit)
|
.aspectRatio(CGFloat(breite) / CGFloat(höhe), contentMode: .fit)
|
||||||
.padding(safeArea)
|
// **Gleichgemacht, nicht roh.** Der Sicherheitsrand ist
|
||||||
|
// unsymmetrisch — die Notch sitzt auf einer Seite. Mit den
|
||||||
|
// rohen Werten rückt man das Bild nicht ein, man
|
||||||
|
// **verschiebt** es um die halbe Differenz; gemessen waren
|
||||||
|
// das vierunddreissig Punkte nach rechts und elfeinhalb nach
|
||||||
|
// unten. Siehe `EdgeInsets.symmetrical`.
|
||||||
|
.padding(safeArea.symmetrical)
|
||||||
} else {
|
} else {
|
||||||
fläche
|
fläche
|
||||||
}
|
}
|
||||||
@@ -903,6 +913,9 @@ struct PlayerView: View {
|
|||||||
private func setFit(_ neu: PlayerModel.VideoFit, on model: PlayerModel) {
|
private func setFit(_ neu: PlayerModel.VideoFit, on model: PlayerModel) {
|
||||||
guard model.videoFit != neu else { return }
|
guard model.videoFit != neu else { return }
|
||||||
model.videoFit = neu
|
model.videoFit = neu
|
||||||
|
// Ausdrücklich: die Ansicht baut ihr Seitenverhältnis von selbst ab,
|
||||||
|
// aber das Bild in der Engine liegt weiter, wie es lag.
|
||||||
|
model.applyVideoFit()
|
||||||
revealControls()
|
revealControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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.
|
/// Der gemeinsame Nenner von AVPlayer und VLCKit.
|
||||||
///
|
///
|
||||||
/// Alles, was die Oberfläche zum Steuern braucht — und nichts darüber hinaus.
|
/// 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 seek(to time: Duration) async
|
||||||
func setRate(_ rate: Double)
|
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 audioTracks: [MediaTrack] { get }
|
||||||
var subtitleTracks: [MediaTrack] { get }
|
var subtitleTracks: [MediaTrack] { get }
|
||||||
func selectAudioTrack(_ track: MediaTrack?)
|
func selectAudioTrack(_ track: MediaTrack?)
|
||||||
@@ -238,4 +262,8 @@ extension VideoPlayerEngine {
|
|||||||
|
|
||||||
/// Wer nichts dazu sagt, kann es nicht.
|
/// Wer nichts dazu sagt, kann es nicht.
|
||||||
public var supportsAirPlay: Bool { false }
|
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) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Testing
|
||||||
|
@testable import FlixyDesign
|
||||||
|
|
||||||
|
@Suite("Ränder gleichmachen")
|
||||||
|
struct SymmetricInsetsTests {
|
||||||
|
|
||||||
|
/// **Der Fall aus dem Querformat.** Notch links, nichts rechts — mit den
|
||||||
|
/// rohen Werten stünde das Bild einunddreissig Punkte zu weit rechts.
|
||||||
|
@Test("Die Notch-Seite bestimmt beide Seiten")
|
||||||
|
func landscapeNotch() {
|
||||||
|
let roh = EdgeInsets(top: 0, leading: 62, bottom: 21, trailing: 0)
|
||||||
|
let gleich = roh.symmetrical
|
||||||
|
#expect(gleich.leading == 62)
|
||||||
|
#expect(gleich.trailing == 62)
|
||||||
|
#expect(gleich.top == 21)
|
||||||
|
#expect(gleich.bottom == 21)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Und aus dem Hochformat: oben zweiundsechzig, unten vierunddreissig.
|
||||||
|
@Test("Oben und unten wird der grössere genommen")
|
||||||
|
func portrait() {
|
||||||
|
let gleich = EdgeInsets(top: 62, leading: 0, bottom: 34, trailing: 0).symmetrical
|
||||||
|
#expect(gleich.top == 62)
|
||||||
|
#expect(gleich.bottom == 62)
|
||||||
|
#expect(gleich.leading == 0)
|
||||||
|
#expect(gleich.trailing == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Was schon gleich ist, bleibt gleich — sonst würde die Rechnung auf
|
||||||
|
/// einem iPad ohne Notch Balken erfinden, wo keine hingehören.
|
||||||
|
@Test("Symmetrisches bleibt unverändert")
|
||||||
|
func alreadySymmetric() {
|
||||||
|
let roh = EdgeInsets(top: 20, leading: 50, bottom: 20, trailing: 50)
|
||||||
|
#expect(roh.symmetrical == roh)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Ohne Ränder bleibt es ohne")
|
||||||
|
func zero() {
|
||||||
|
let gleich = EdgeInsets().symmetrical
|
||||||
|
#expect(gleich == EdgeInsets())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Die Zusage, auf die es ankommt.** Egal wie schief der Rand ankommt —
|
||||||
|
/// danach ist die Mitte des verbleibenden Platzes die Mitte des Schirms.
|
||||||
|
@Test("Die Mitte bleibt die Mitte")
|
||||||
|
func centreStaysCentred() {
|
||||||
|
for roh in [
|
||||||
|
EdgeInsets(top: 0, leading: 62, bottom: 21, trailing: 0),
|
||||||
|
EdgeInsets(top: 62, leading: 0, bottom: 34, trailing: 0),
|
||||||
|
EdgeInsets(top: 5, leading: 1, bottom: 99, trailing: 40),
|
||||||
|
] {
|
||||||
|
let g = roh.symmetrical
|
||||||
|
#expect(g.leading - g.trailing == 0)
|
||||||
|
#expect(g.top - g.bottom == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import UIKit
|
|||||||
public final class VLCPlayerEngine: NSObject, VideoPlayerEngine {
|
public final class VLCPlayerEngine: NSObject, VideoPlayerEngine {
|
||||||
/// Die Fläche, in die VLC zeichnet. Die Ansicht hängt sie in ihren
|
/// Die Fläche, in die VLC zeichnet. Die Ansicht hängt sie in ihren
|
||||||
/// Hierarchiebaum.
|
/// Hierarchiebaum.
|
||||||
public let videoView = UIView()
|
public let videoView: UIView = VLCSurfaceView()
|
||||||
|
|
||||||
private let player = VLCMediaPlayer()
|
private let player = VLCMediaPlayer()
|
||||||
|
|
||||||
@@ -85,6 +85,14 @@ public final class VLCPlayerEngine: NSObject, VideoPlayerEngine {
|
|||||||
private var continuation: AsyncStream<PlaybackEvent>.Continuation?
|
private var continuation: AsyncStream<PlaybackEvent>.Continuation?
|
||||||
private var pendingStartTime: Duration = .zero
|
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() {
|
public override init() {
|
||||||
super.init()
|
super.init()
|
||||||
videoView.backgroundColor = .black
|
videoView.backgroundColor = .black
|
||||||
@@ -135,6 +143,20 @@ public final class VLCPlayerEngine: NSObject, VideoPlayerEngine {
|
|||||||
player.rate = Float(rate)
|
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?) {
|
public func selectAudioTrack(_ track: MediaTrack?) {
|
||||||
select(track, from: player.audioTracks)
|
select(track, from: player.audioTracks)
|
||||||
}
|
}
|
||||||
@@ -202,6 +224,9 @@ extension VLCPlayerEngine: VLCMediaPlayerDelegate {
|
|||||||
state = .playing
|
state = .playing
|
||||||
applyPendingStartTime()
|
applyPendingStartTime()
|
||||||
refreshTracks()
|
refreshTracks()
|
||||||
|
// Siehe `fit`: vor dem ersten Bild wäre sie ins Leere
|
||||||
|
// gegangen. Hier gibt es eine Ausgabe.
|
||||||
|
applyFit()
|
||||||
case .paused:
|
case .paused:
|
||||||
state = .paused
|
state = .paused
|
||||||
case .stopped, .stopping:
|
case .stopped, .stopping:
|
||||||
@@ -318,3 +343,33 @@ enum AudioSessionBridge {
|
|||||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,5 +30,10 @@ abgeräumt wird.
|
|||||||
Mit `--unanalysed` melden die Quellen keine Spuren. Dann entscheidet die App
|
Mit `--unanalysed` melden die Quellen keine Spuren. Dann entscheidet die App
|
||||||
wie bei einem Server, der eine Datei noch nicht untersucht hat.
|
wie bei einem Server, der eine Datei noch nicht untersucht hat.
|
||||||
|
|
||||||
|
Mit `--vlc` melden sie DTS-Ton. AVFoundation kann das nicht, also läuft die
|
||||||
|
Wiedergabe über VLCKit — der einzige Weg, diesen Zweig mit der Beispieldatei
|
||||||
|
überhaupt zu erreichen. Die Datei bleibt dieselbe; VLC spielt sie ebenso, und
|
||||||
|
geprüft wird der Weg, nicht der Ton.
|
||||||
|
|
||||||
Kein echter Server wird dabei angefasst, und es gehen keine Zugangsdaten über
|
Kein echter Server wird dabei angefasst, und es gehen keine Zugangsdaten über
|
||||||
die Leitung.
|
die Leitung.
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ REPORTS = []
|
|||||||
# Mit `--unanalysed` melden die Quellen keine Spuren. Dann entscheidet die App
|
# Mit `--unanalysed` melden die Quellen keine Spuren. Dann entscheidet die App
|
||||||
# auf Umrechnen, und der Weg über PlaybackInfo lässt sich prüfen.
|
# auf Umrechnen, und der Weg über PlaybackInfo lässt sich prüfen.
|
||||||
UNANALYSED = "--unanalysed" in os.sys.argv
|
UNANALYSED = "--unanalysed" in os.sys.argv
|
||||||
|
# Mit `--vlc` melden die Quellen DTS-Ton. AVFoundation kann das nicht, also
|
||||||
|
# faellt die App auf VLCKit zurueck — und dieser Weg ist sonst mit der
|
||||||
|
# Beispieldatei gar nicht erreichbar. Die Datei selbst bleibt dieselbe; VLC
|
||||||
|
# spielt sie ebenso, und geprueft wird der Weg, nicht der Ton.
|
||||||
|
AS_VLC = "--vlc" in os.sys.argv
|
||||||
|
AUDIO_CODEC = "dts" if AS_VLC else "aac"
|
||||||
|
|
||||||
TITLES = [
|
TITLES = [
|
||||||
("m1", "Der weite Himmel", "Movie", "Ein Mann, ein Berg, zwei Stunden Schweigen."),
|
("m1", "Der weite Himmel", "Movie", "Ein Mann, ein Berg, zwei Stunden Schweigen."),
|
||||||
@@ -49,7 +55,7 @@ def item(key, name, kind, overview, *, resume=None, series=None):
|
|||||||
# Oberfläche zählt, was der Server meldet.
|
# Oberfläche zählt, was der Server meldet.
|
||||||
{"Index": 0, "Type": "Video", "Codec": "h264",
|
{"Index": 0, "Type": "Video", "Codec": "h264",
|
||||||
"Width": 1920, "Height": 1080},
|
"Width": 1920, "Height": 1080},
|
||||||
{"Index": 1, "Type": "Audio", "Codec": "aac", "Channels": 2,
|
{"Index": 1, "Type": "Audio", "Codec": AUDIO_CODEC, "Channels": 2,
|
||||||
"Language": "ger", "DisplayTitle": "Deutsch", "IsDefault": True},
|
"Language": "ger", "DisplayTitle": "Deutsch", "IsDefault": True},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
|
|||||||
Reference in New Issue
Block a user