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))