From baa542690c07fb1d3f6ccebd675b6556a623e02f Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 27 Sep 2026 15:06:36 +0200 Subject: [PATCH] Vorrat anlegen, und VLC meldet endlich das Stocken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Änderungen, die an der Wiedergabe selbst etwas tun — beide ohne Abhängigkeit davon, ob die Server je eine Leiter liefern. **Vorrat.** Ohne Angabe legt AVPlayer so viel an, wie es für einen zügigen Start braucht, und keinen Meter mehr. Das ist richtig, solange die Leitung trägt, und falsch, sobald sie aussetzt: ein Loch von fünf Sekunden trifft dann auf einen Puffer von fünf Sekunden, und das Bild steht. Fünfundzwanzig Sekunden sind ein Anfangswert, kein Messergebnis. Nur für Filme und Folgen über das Netz. **Nicht für Sender** — dort hieße ein halbminütiger Vorrat eine halbe Minute Verzug und ein zähes Umschalten. Und nicht für Dateien von der Platte, die brauchen keinen. Wer das entscheidet, muss mehr wissen als der Motor, deshalb reist die Zahl mit der Quelle statt dass die Engine rät. **VLC hat das Stocken nie gemeldet.** `VLCMediaPlayerState` kennt keinen Wert dafür — die Aufzählung führt nur `opening`, `playing`, `paused`, `stopped`, `error`. Der Puffer-Fortschritt kommt über `mediaPlayerBufferingChanged:`, und die Methode fehlte. Folge: `.buffering` ging dort **nie** an, der Wachhund im Player löste nie aus, und ein hängender Strom lief stumm ins Nichts — ohne Kreisel, ohne Meldung, ohne Abbruch. Ausgerechnet auf dem Weg, der die großen Remuxe spielt, bei denen eine schwache Leitung am ehesten scheitert. Gewertet wird nur aus dem Spielen heraus: laut Dokumentation kommt vor jeder Wiedergabe einmal 0,0 und einmal 1,0, und dieses anfängliche Füllen ist der Start, kein Rückler. Dazu meldet VLC jetzt überhaupt Statistik. `variantCount` bleibt dabei `nil` und nicht `1` — eine Datei hat keine Leiter, und „gibt es nicht" ist etwas anderes als „gibt es einmal". Bei der Bitrate steht die Annahme im Kommentar statt im Nebel: libvlc führt `inputBitrate` vermutlich in Byte je Sekunde, daher der Faktor acht. Co-Authored-By: Claude Opus 5 --- Flixy/Views/Player/PlayerModel.swift | 20 ++++++- .../FlixyPlayback/Engine/AVPlayerEngine.swift | 18 ++++++ .../Engine/VideoPlayerEngine.swift | 20 ++++++- .../Sources/FlixyVLC/VLCPlayerEngine.swift | 58 +++++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/Flixy/Views/Player/PlayerModel.swift b/Flixy/Views/Player/PlayerModel.swift index 366fbe9..89d2108 100644 --- a/Flixy/Views/Player/PlayerModel.swift +++ b/Flixy/Views/Player/PlayerModel.swift @@ -177,6 +177,19 @@ final class PlayerModel { /// Wacht darüber, dass aus „lädt" nicht „lädt für immer" wird. @ObservationIgnored private var bufferWatchdog: Task? + /// Wie viel Vorrat bei einem Strom über das Netz angelegt wird. + /// + /// **Nur für Filme und Folgen, nicht für Sender und nicht für Dateien.** + /// Ein laufender Sender soll nah am Jetzt bleiben — ein halbminütiger + /// Vorrat hieße dort eine halbe Minute Verzug und ein Umschalten, das + /// sich zäh anfühlt. Eine Datei von der Platte braucht keinen Vorrat. + /// + /// Fünfundzwanzig Sekunden sind ein Anfangswert, kein Messergebnis: genug, + /// um ein Loch von einigen Sekunden zu überbrücken, wenig genug, dass der + /// Start nicht spürbar später kommt. Die richtige Zahl gehört mit einer + /// gedrosselten Leitung ermittelt. + private static let forwardBuffer: Duration = .seconds(25) + /// Das Buch über die Rückler dieser Wiedergabe. @ObservationIgnored private var stallLedger = StallLedger() /// Seit wann tatsächlich gespielt wird — der Nullpunkt für das Buch. @@ -492,7 +505,12 @@ final class PlayerModel { )) playbackSession = target.sessionID playbackVersionID = target.versionID - return PlaybackSource(url: target.url, origin: .remote, startTime: start) + return PlaybackSource( + url: target.url, + origin: .remote, + startTime: start, + forwardBuffer: Self.forwardBuffer + ) } catch { // Die Worte des Servers stehen auf dem Bildschirm, nicht „keine // abspielbare Datei". Sagt er, warum er ablehnt, ist das die einzige diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift index 174d5bf..45c1907 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift @@ -94,6 +94,24 @@ public final class AVPlayerEngine: VideoPlayerEngine { AVURLAssetPreferPreciseDurationAndTimingKey: false, ]) let item = AVPlayerItem(asset: asset) + + // **Vorrat statt Reaktion.** + // + // Ohne diese Angabe legt AVPlayer so viel an, wie es für einen + // zügigen Start braucht — und keinen Meter mehr. Das ist richtig, wenn + // die Leitung trägt, und falsch, wenn sie aussetzt: ein Loch von + // fünf Sekunden trifft dann auf einen Puffer von fünf Sekunden, und + // das Bild steht. + // + // Der Preis ist ehrlich: der Start dauert länger, und wer springt, + // wirft den angelegten Vorrat weg. Beides ist weniger schlimm als ein + // Rückler mitten im Satz. + if let vorrat = source.forwardBuffer { + item.preferredForwardBufferDuration = TimeInterval( + Double(vorrat.components.seconds) + ) + } + player.replaceCurrentItem(with: item) observe(item: item) diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/VideoPlayerEngine.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/VideoPlayerEngine.swift index 98e321c..95d11df 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/VideoPlayerEngine.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/VideoPlayerEngine.swift @@ -14,11 +14,29 @@ public struct PlaybackSource: Sendable, Equatable { /// Ab wo gestartet werden soll. Kommt aus dem zuletzt gemeldeten /// Fortschritt; `.zero` heißt von vorn. public let startTime: Duration + /// Wie viel Vorrat der Player anlegen soll. + /// + /// `nil` überlässt es dem System. Das ist für eine Datei auf der Platte + /// richtig und für einen Sender richtig — bei einem Strom über eine + /// wackelige Leitung ist es zu wenig: der Systemwert ist auf schnellen + /// Start ausgelegt, nicht darauf, einen Aussetzer zu überstehen. + /// + /// **Wer das entscheidet, muss mehr wissen als der Motor.** Ob es ein + /// Sender ist, ob die Datei lokal liegt — das weiß der Player, nicht die + /// Engine. Deshalb reist die Zahl mit der Quelle, statt dass die Engine + /// rät. + public let forwardBuffer: Duration? - public init(url: URL, origin: Origin, startTime: Duration = .zero) { + public init( + url: URL, + origin: Origin, + startTime: Duration = .zero, + forwardBuffer: Duration? = nil + ) { self.url = url self.origin = origin self.startTime = startTime + self.forwardBuffer = forwardBuffer } } diff --git a/Packages/FlixyVLC/Sources/FlixyVLC/VLCPlayerEngine.swift b/Packages/FlixyVLC/Sources/FlixyVLC/VLCPlayerEngine.swift index 64b3709..ee3307f 100644 --- a/Packages/FlixyVLC/Sources/FlixyVLC/VLCPlayerEngine.swift +++ b/Packages/FlixyVLC/Sources/FlixyVLC/VLCPlayerEngine.swift @@ -21,6 +21,40 @@ public final class VLCPlayerEngine: NSObject, VideoPlayerEngine { private let player = VLCMediaPlayer() + /// Was VLC über den laufenden Strom weiß. + /// + /// Bisher meldete dieser Weg gar nichts: die Standardimplementierung gibt + /// `nil` zurück, und die Bitratenanzeige blieb bei VLC dauerhaft leer. + /// + /// **Keine Leiter, und das ist die richtige Aussage.** VLC spielt hier + /// eine Datei, keine Playlist — es gibt keine Stufen, zwischen denen + /// etwas wechseln könnte. `variantCount` bleibt deshalb `nil` und nicht + /// etwa `1`: „gibt es nicht" ist etwas anderes als „gibt es einmal". + /// + /// **Zur Bitrate, ehrlich:** libvlc führt `inputBitrate` als Byte je + /// Sekunde, deshalb der Faktor acht. Das ist der wahrscheinliche, aber + /// nicht belegte Fall — wer es prüfen will, hält den Wert neben die + /// bekannte Bitrate der Datei; liegt er um Faktor acht daneben, steht + /// hier das Falsche. + public var statistics: PlaybackStatistics? { + guard let media = player.media else { return nil } + let werte = media.statistics + + let kilobit = Int((werte.inputBitrate * 8 / 1_000).rounded()) + let größe = player.videoSize + + return PlaybackStatistics( + observedBitrate: kilobit > 0 ? kilobit : nil, + indicatedBitrate: nil, + width: größe.width > 0 ? Int(größe.width) : nil, + height: größe.height > 0 ? Int(größe.height) : nil, + // Nur die wirklich verworfenen. `latePictures` sind zu spät + // gekommen, aber angezeigt worden — sie unter „verworfen" zu + // führen machte die Zahl größer und die Aussage falsch. + droppedFrames: Int(werte.lostPictures) + ) + } + public private(set) var state: PlaybackState = .idle { didSet { guard state != oldValue else { return } @@ -186,6 +220,30 @@ extension VLCPlayerEngine: VLCMediaPlayerDelegate { } } + /// Der Puffer-Fortschritt — **der einzige Weg, auf dem VLC das Stocken + /// überhaupt meldet.** + /// + /// `VLCMediaPlayerState` kennt keinen Wert für „wartet auf Daten": die + /// Aufzählung führt nur `opening`, `playing`, `paused`, `stopped` und + /// `error`. Weil diese Methode bisher fehlte, ging der Zustand + /// `.buffering` auf dem VLC-Weg **nie** an — und damit löste der + /// Wachhund im Player dort nie aus. Ein hängender Strom lief stumm ins + /// Nichts, ohne Kreisel, ohne Meldung, ohne Abbruch. + /// + /// Nur aus dem Spielen heraus gewertet. Die Dokumentation sagt, dass vor + /// jeder erfolgreichen Wiedergabe einmal 0,0 und einmal 1,0 kommt — + /// dieses anfängliche Füllen ist der Start und kein Rückler, und es + /// mitzuzählen gäbe jeder Wiedergabe eine Vorstrafe. + public nonisolated func mediaPlayerBufferingChanged(_ progress: Float) { + Task { @MainActor in + if progress >= 1 { + if state == .buffering { state = .playing } + } else if state == .playing { + state = .buffering + } + } + } + public nonisolated func mediaPlayerTimeChanged(_ aNotification: Notification) { Task { @MainActor in continuation?.yield(.timeChanged(currentTime))