diff --git a/Flixy/Views/Player/PlayerModel.swift b/Flixy/Views/Player/PlayerModel.swift index cd14716..633a6f0 100644 --- a/Flixy/Views/Player/PlayerModel.swift +++ b/Flixy/Views/Player/PlayerModel.swift @@ -210,6 +210,11 @@ final class PlayerModel { private var seekTarget: Duration? /// Ob schon einmal auf VLC ausgewichen wurde. private var hasFallenBack = false + /// Ob der Vorratsspeicher für diese Wiedergabe aufgegeben wurde. + /// + /// Einmal gesetzt, bleibt er aus: ein zweiter Anlauf mit demselben + /// Aufbau scheiterte am selben Grund. + private var prefetchGaveUp = false /// Der Server, von dem der Player holt — und der Vorrat dahinter. private var streamServer: LocalStreamServer? private var streamCache: StreamCache? @@ -744,6 +749,11 @@ final class PlayerModel { // niemand bestellt hat. subtitleStreamIndex: selectedSubtitleStream?.index ?? -1 )) + + diagnostics.note( + "Bestellt — Ton \(selectedAudioStream?.index.map(String.init) ?? "Server")" + + ", Untertitel \(selectedSubtitleStream?.index.map(String.init) ?? "keine")" + ) playbackSession = target.sessionID playbackVersionID = target.versionID @@ -833,7 +843,7 @@ final class PlayerModel { /// Bereichsanfragen, und dort puffert AVPlayer von sich aus weit — dort /// wäre ein Server davor nur eine zusätzliche Fehlerquelle. private func startPrefetching(from url: URL) async -> URL? { - guard url.pathExtension.lowercased() == "m3u8" else { return nil } + guard !prefetchGaveUp, url.pathExtension.lowercased() == "m3u8" else { return nil } await streamServer?.stop() let speicher = StreamCache() @@ -989,6 +999,22 @@ final class PlayerModel { return true } + /// Noch einmal, ohne den eigenen Vorratsspeicher. + /// + /// Gibt `true` zurück, wenn es einen Versuch gab. Genau **einen**: ohne + /// Speicher bleibt nichts mehr wegzunehmen, und ein weiterer Anlauf + /// schickte die App im Kreis. + private func retryWithoutPrefetch() async -> Bool { + guard streamServer != nil, !prefetchGaveUp else { return false } + prefetchGaveUp = true + diagnostics.note("Nichts kam an — noch einmal ohne Vorrat") + prefetchStatus = String(localized: "quality.prefetch.off") + + await restart() + if case .failed = phase { return false } + return true + } + /// Der Rückzug auf VLC, wenn das Umpacken nicht getragen hat. /// /// Genau **einmal** je Wiedergabe: scheitert auch VLC, ist es ein Fehler @@ -1205,7 +1231,16 @@ final class PlayerModel { guard isBuffering else { return } bufferWatchdog = Task { [weak self] in - try? await Task.sleep(for: .seconds(40)) + // **Der Vorrat fliegt früher raus als alles andere.** + // + // Vierzig Sekunden sind die Grenze zum Aufgeben; so lange muss + // niemand warten, um herauszufinden, ob der eigene Speicher im + // Weg steht. Ihn wegzunehmen kostet nur den Vorsprung, und wenn + // es danach läuft, war er die Ursache. + try? await Task.sleep(for: .seconds(12)) + if let self, self.isBuffering, await self.retryWithoutPrefetch() { return } + + try? await Task.sleep(for: .seconds(28)) guard !Task.isCancelled, let self, self.isBuffering else { return } diagnostics.note("Seit 40 s keine Daten") @@ -1223,6 +1258,13 @@ final class PlayerModel { // „nimm, was in den Einstellungen steht". Steht dort „Original", // bittet die App über eine Zugverbindung um eine 4K-Datei und // wartet vierzig Sekunden auf etwas, das nie kommen kann. + // **Erst ohne den Vorratsspeicher.** + // + // Er steht zwischen Player und Server, und was dort schiefgeht, + // sieht von aussen genauso aus wie eine tote Leitung. Ihn + // wegzunehmen ist billig, kostet nur den Vorsprung — und + // unterscheidet die beiden Fälle, statt sie zu verwechseln. + if await self.retryWithoutPrefetch() { return } if await self.retrySmaller() { return } // Den Engine gleich stilllegen: er würde sonst weiter an einem // toten Stream ziehen, und bei Live-TV bliebe der Tuner belegt. @@ -1635,6 +1677,14 @@ final class PlayerModel { case .tracksResolved(let audio, let subtitle): audioTracks = audio subtitleTracks = subtitle + // **Die Zeile, die drei Runden Ratens erspart hätte.** + // Bestellt und angekommen sind zweierlei, und von aussen + // sieht „nicht geliefert" genauso aus wie „geliefert und + // nicht angezeigt". + diagnostics.note( + "Player meldet \(audio.count) Tonspuren, " + + "\(subtitle.count) Untertitel" + ) showOrderedSubtitle() applyPreferredLanguages() } @@ -1655,10 +1705,10 @@ final class PlayerModel { /// **Die erste ist die richtige.** Rechnet der Server um, liegt genau /// eine im Stream — nämlich die bestellte. Es gibt nichts zu verwechseln. private func showOrderedSubtitle() { - guard selectedSubtitleStream != nil, - selectedSubtitleTrack == nil, - let erste = subtitleTracks.first - else { return } + guard selectedSubtitleStream != nil, selectedSubtitleTrack == nil else { return } + guard let erste = subtitleTracks.first else { return } + + diagnostics.note("Untertitel eingeschaltet: \(erste.displayName)") selectSubtitleTrack(erste) } diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Prefetch/StreamCache.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Prefetch/StreamCache.swift index 1e46238..8245b4a 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Prefetch/StreamCache.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Prefetch/StreamCache.swift @@ -56,6 +56,8 @@ public actor StreamCache { private var fetched = 0 private var prefetchTask: Task? + /// Ob gerade eine Anfrage des Players offen ist. + private var isServingPlayer = false public init(policy: Policy = Policy()) { self.policy = policy @@ -152,6 +154,15 @@ public actor StreamCache { } if let ort = stored[address], let daten = try? Data(contentsOf: ort) { return daten } + + // **Der Player hat Vorrang.** Fehlt ihm etwas, wartet er darauf — und + // der Vorrat darf ihm dabei nicht die Leitung wegnehmen. Rechnet der + // Server gerade um, bedient er ohnehin einen Schnipsel nach dem + // anderen: zwei Anfragen gleichzeitig machen die dringende langsamer, + // nicht schneller. Genau so kam es zu „Stream abgerissen", sobald das + // Umrechnen teuer wurde. + isServingPlayer = true + defer { isServingPlayer = false } return await fetch(address) } @@ -173,6 +184,9 @@ public actor StreamCache { /// Gibt `false` zurück, wenn gerade nichts zu tun ist — dann wartet die /// Schleife, statt zu drehen. private func prefetchNext() async -> Bool { + // Solange der Player wartet, wird nicht vorgeladen. + guard !isServingPlayer else { return false } + // **Ist der Player vorausgesprungen, dort weitermachen.** // // Sonst holt der Vorrat brav weiter Schnipsel hinter der