VLC ist weg — es gibt nur noch einen Spieler
Du hattest recht mit der Erwartung, nur nicht mit dem Stand: entfernt war er nicht. Was wir gemacht haben, war, ihm Arbeit wegzunehmen — DTS, TrueHD und Vorbis gehen seit Kurzem über das Umpacken beim Server. Übrig blieb er an einer Stelle, `PlaybackDecider.fallback`. Jetzt ist er ganz weg, samt dem 920-MB-Binärpaket. **Was an seine Stelle tritt.** Die Entscheidung sagt nicht mehr, *wer* spielt, sondern *was ankommt*: direkt, umgepackt oder umgerechnet. `PlaybackEngineKind` fällt damit weg, `directPlay` trägt keine Engine mehr, und `PlaybackPolicy` kennt kein `allowsFallbackEngine`. **Das Auffangnetz bleibt, nur anders geknüpft.** Scheiterte das Umpacken am Player, sprang bisher VLC ein — der Server entscheidet erst beim Ausliefern, ob er die Ströme wirklich kopieren kann, und manche Antworten nimmt AVPlayer nicht an. Statt der zweiten Engine rechnet der Server jetzt neu (`fallBackToTranscode`). Teurer für ihn, aber es kommt etwas an, statt einer schwarzen Wand mit einer Fehlernummer. **Offline wäre das ein Rückschritt gewesen** — dort gibt es keinen Server, der einspringt. Deshalb ging der vorige Commit voraus: was heruntergeladen wird, kommt schon in einer Hülle, die der Systemplayer öffnet. Was dadurch überall gilt, auch für Dateien, die bisher bei VLC landeten: Bild-in-Bild, AirPlay, Bedienung im Sperrbildschirm — und dasselbe Verhalten auf dem Apple TV, wo es VLC ohnehin nie gab. Die Attrappe heißt ihren Schalter jetzt `--dts` statt `--vlc`: der Fall ist geblieben, sein Ziel nicht. iOS und tvOS bauen, 494 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ef2596e2bb
commit
6b927c21b0
@@ -4,11 +4,6 @@ import MediaKit
|
||||
import Observation
|
||||
import FlixyDownloads
|
||||
import FlixyPlayback
|
||||
// Siehe `PlaybackSettings`: `!os(tvOS)` muss dazu, sonst bricht der
|
||||
// Abhängigkeits-Sucher an VLCKit ab, das für dieses Ziel gar nicht gebaut wird.
|
||||
#if canImport(FlixyVLC) && !os(tvOS)
|
||||
import FlixyVLC
|
||||
#endif
|
||||
import PlexKit
|
||||
|
||||
/// Führt einen Abspielwunsch aus.
|
||||
@@ -162,9 +157,6 @@ final class PlayerModel {
|
||||
/// das Bild zeichnet: auf dem Telefon hängt die gehaltene `AVPlayerLayer`
|
||||
/// daran, auf dem Fernseher ein `AVPlayerViewController`. Beide holen sich
|
||||
/// hier dasselbe ab.
|
||||
///
|
||||
/// `nil` bei VLC — kein Sonderfall, sondern dieselbe Auskunft, die
|
||||
/// `VideoSurface` heute schon auswertet.
|
||||
var systemPlayer: AVPlayer? { (engine as? AVPlayerEngine)?.player }
|
||||
|
||||
/// Wie das Bild in die Fläche gelegt wird.
|
||||
@@ -174,18 +166,6 @@ final class PlayerModel {
|
||||
/// 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".
|
||||
///
|
||||
/// **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
|
||||
|
||||
/// Wo das Bild aufhört.
|
||||
@@ -208,9 +188,6 @@ final class PlayerModel {
|
||||
/// **Beobachtet, nicht abgefragt.** `isPictureInPicturePossible` steht erst
|
||||
/// fest, wenn der Player ein Bild hat — wer beim Zeichnen fragt, bekommt
|
||||
/// `false` und zeichnet den Knopf nie wieder. Also meldet es sich selbst.
|
||||
///
|
||||
/// Bleibt bei VLC dauerhaft falsch: `AVPictureInPictureController` braucht
|
||||
/// eine `AVPlayerLayer`, und VLC zeichnet in eine eigene Ansicht.
|
||||
private(set) var canUsePictureInPicture = false
|
||||
private(set) var isPictureInPictureActive = false
|
||||
/// Was beim Beenden von Bild-in-Bild wieder ins Vollbild führen soll.
|
||||
@@ -269,7 +246,7 @@ final class PlayerModel {
|
||||
/// Ohne das gewönne die letzte Meldung von der alten Stelle gegen den
|
||||
/// gerade ausgelösten Sprung.
|
||||
private var seekTarget: Duration?
|
||||
/// Ob schon einmal auf VLC ausgewichen wurde.
|
||||
/// Ob schon einmal auf den Transcoder ausgewichen wurde.
|
||||
private var hasFallenBack = false
|
||||
/// Ob der Vorratsspeicher für diese Wiedergabe aufgegeben wurde.
|
||||
///
|
||||
@@ -440,9 +417,6 @@ final class PlayerModel {
|
||||
guard let ceiling = automaticCeiling ?? startingCeiling else { return base }
|
||||
return PlaybackPolicy(
|
||||
allowsTranscoding: true,
|
||||
// Ohne Ersatz-Engine: sonst spielte VLC die Datei unverändert ab,
|
||||
// und die abgesenkte Grenze bliebe wirkungslos.
|
||||
allowsFallbackEngine: false,
|
||||
maximumBitrate: min(ceiling, base.maximumBitrate ?? ceiling),
|
||||
forcesTranscoding: true
|
||||
)
|
||||
@@ -759,8 +733,8 @@ final class PlayerModel {
|
||||
videoCodec: offline?.videoCodec,
|
||||
audioCodec: offline?.audioCodec,
|
||||
bitrate: offline?.bitrate,
|
||||
// Was heruntergeladen wurde, ist untersucht. Ohne diese Zusage
|
||||
// ginge jede Offline-Datei vorsichtshalber an VLC.
|
||||
// Was heruntergeladen wurde, ist untersucht — und seit das
|
||||
// Herunterladen bei Bedarf umpackt, auch zu öffnen.
|
||||
hasKnownStreams: offline?.videoCodec != nil
|
||||
),
|
||||
// Lokal gibt es nichts umzurechnen und keine Bandbreitengrenze.
|
||||
@@ -781,10 +755,6 @@ 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.
|
||||
@@ -1208,18 +1178,24 @@ final class PlayerModel {
|
||||
return true
|
||||
}
|
||||
|
||||
/// Der Rückzug auf VLC, wenn das Umpacken nicht getragen hat.
|
||||
/// Der Rückzug auf den Transcoder, wenn das Umpacken nicht getragen hat.
|
||||
///
|
||||
/// Genau **einmal** je Wiedergabe: scheitert auch VLC, ist es ein Fehler
|
||||
/// **Hier stand VLC.** Scheiterte das Umpacken, spielte die zweite Engine
|
||||
/// die Datei unverändert — das gibt es nicht mehr. Statt dessen rechnet der
|
||||
/// Server das Bild neu: teurer für ihn, aber es kommt etwas an, und der
|
||||
/// Nutzer steht nicht vor einer schwarzen Wand mit einer Fehlernummer.
|
||||
///
|
||||
/// Genau **einmal** je Wiedergabe: scheitert auch das, ist es ein Fehler
|
||||
/// des Materials, und ein zweiter Anlauf schickte die App im Kreis.
|
||||
private func fallBackToVLC() async -> Bool {
|
||||
guard !hasFallenBack, policy.allowsFallbackEngine else { return false }
|
||||
private func fallBackToTranscode() async -> Bool {
|
||||
let geltend = effectivePolicy
|
||||
guard !hasFallenBack, geltend.allowsTranscoding else { return false }
|
||||
hasFallenBack = true
|
||||
diagnostics.note("Umpacken abgelehnt — noch einmal mit VLC")
|
||||
diagnostics.note("Umpacken abgelehnt — noch einmal, diesmal umgerechnet")
|
||||
|
||||
await restart(forcing: PlaybackDecision(
|
||||
method: .directPlay(engine: .vlc),
|
||||
rationale: (decision?.rationale ?? []) + [.transcodingUnavailable]
|
||||
method: .transcode(maximumBitrate: geltend.maximumBitrate),
|
||||
rationale: decision?.rationale ?? []
|
||||
))
|
||||
if case .failed = phase { return false }
|
||||
return true
|
||||
@@ -1741,14 +1717,10 @@ final class PlayerModel {
|
||||
/// Eine eigene Funktion, weil es zwei Anlässe gibt: die Wahl des Nutzers
|
||||
/// und jeder Engine-Wechsel. Beide müssen dasselbe tun.
|
||||
///
|
||||
/// **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.
|
||||
/// Die Bildebene gehört der App und nicht dem Systemplayer — deshalb
|
||||
/// steht `videoGravity` hier und nicht in `AVPlayerEngine`.
|
||||
func applyVideoFit() {
|
||||
surface.playerLayer.videoGravity = videoFit == .fill ? .resizeAspectFill : .resizeAspect
|
||||
engine?.setVideoFit(videoFit)
|
||||
}
|
||||
|
||||
/// Hängt die gehaltene Bildebene an den aktuellen Engine.
|
||||
@@ -1758,7 +1730,6 @@ final class PlayerModel {
|
||||
/// weiter auf einen abgeräumten Player und bleibt schwarz.
|
||||
private func attachSurface(to engine: any VideoPlayerEngine) {
|
||||
guard let avEngine = engine as? AVPlayerEngine else { return }
|
||||
|
||||
surface.playerLayer.player = avEngine.player
|
||||
// **Den Zustand lesen, keine Konstante.** Hier stand fest
|
||||
// `.resizeAspect` — und weil diese Zeile nach jedem Neuaufbau erneut
|
||||
@@ -1818,20 +1789,14 @@ final class PlayerModel {
|
||||
restoreFromPictureInPicture = action
|
||||
}
|
||||
|
||||
/// **Es gibt nur noch einen.**
|
||||
///
|
||||
/// Die Entscheidung sagt nicht mehr, *wer* spielt, sondern *was ankommt*:
|
||||
/// direkt, umgepackt oder umgerechnet. Am Ende steht immer der
|
||||
/// Systemplayer — und damit überall Bild-in-Bild, AirPlay und die
|
||||
/// Bedienung im Sperrbildschirm, auch auf dem Apple TV.
|
||||
private func makeEngine(for decision: PlaybackDecision) -> any VideoPlayerEngine {
|
||||
#if canImport(FlixyVLC) && !os(tvOS)
|
||||
switch decision.engine {
|
||||
case .vlc: VLCPlayerEngine()
|
||||
// Auch der Transcode-Weg endet beim Systemplayer: er liefert HLS, und
|
||||
// genau darin wechselt AVPlayer die Bitrate von selbst.
|
||||
case .avPlayer, .none: AVPlayerEngine()
|
||||
}
|
||||
#else
|
||||
// Auf dem Mac gibt es VLC (noch) nicht: das Binärpaket ist auf iOS
|
||||
// festgelegt. Was der Systemplayer nicht öffnen kann, scheitert dort
|
||||
// mit einer Meldung, statt still schwarz zu bleiben.
|
||||
AVPlayerEngine()
|
||||
#endif
|
||||
}
|
||||
|
||||
private func observe(_ engine: any VideoPlayerEngine) {
|
||||
@@ -1853,19 +1818,17 @@ final class PlayerModel {
|
||||
if case .failed(let failure) = state {
|
||||
diagnostics.note("Wiedergabe gescheitert: \(failure)")
|
||||
|
||||
// **Scheitert das Umpacken, bleibt VLC.**
|
||||
// **Scheitert das Umpacken, rechnet der Server um.**
|
||||
//
|
||||
// Der Server entscheidet erst beim Ausliefern, ob er
|
||||
// die Ströme wirklich kopieren kann — und manche
|
||||
// Antworten kann AVPlayer nicht annehmen, auch wenn
|
||||
// sie formal richtig sind. Das hier vorher zu wissen
|
||||
// ist nicht möglich; es hinterher aufzufangen schon.
|
||||
// Er entscheidet erst beim Ausliefern, ob er die
|
||||
// Ströme wirklich kopieren kann — und manche Antworten
|
||||
// kann AVPlayer nicht annehmen, auch wenn sie formal
|
||||
// richtig sind. Das vorher zu wissen ist nicht
|
||||
// möglich; es hinterher aufzufangen schon.
|
||||
//
|
||||
// Ohne diesen Weg stünde der Nutzer vor einer
|
||||
// schwarzen Wand mit einer Fehlernummer, obwohl ein
|
||||
// Player danebensteht, der die Datei anstandslos
|
||||
// spielt.
|
||||
if case .remux = decision?.method, await fallBackToVLC() { return }
|
||||
// schwarzen Wand mit einer Fehlernummer.
|
||||
if case .remux = decision?.method, await fallBackToTranscode() { return }
|
||||
|
||||
diagnostics.finish(outcome: "Wiedergabe gescheitert: \(failure)")
|
||||
// Der Grund gehört auf den Bildschirm, nicht nur ins
|
||||
|
||||
Reference in New Issue
Block a user