From 4b084bc926c44d4e6412958a3ec07f7043d0b974 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 28 Sep 2026 20:36:33 +0200 Subject: [PATCH] Ein Sprung in den Vorrat laeuft jetzt sofort weiter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drei Gruende, und der erste erklaert das ganze Aergernis. AVPlayer besteht nach einem Sprung darauf, erst seinen eigenen Puffer zu fuellen. Der stand auf sechzig Sekunden — richtig, solange er selbst vorladen sollte. Mit einem Speicher davor ist es das Gegenteil: springt man mitten in den Vorrat, liegt dahinter vielleicht noch eine halbe Minute, zu wenig fuer seine sechzig, und er wartet auf den Server, obwohl alles da ist. Der Abstand liegt jetzt bei uns; seiner darf klein sein. Der Vorrat holte ausserdem weiter Schnipsel hinter der Abspielstelle, wenn der Player vorausgesprungen war — und der eine, auf den gerade gewartet wurde, kam als letzter. Er macht jetzt dort weiter, wo gesprungen wurde. Und dieselbe Liste noch einmal zu holen setzte den Vorrat zurueck. Der Player holt sie staendig: nach einem Sprung, beim Stufenwechsel, von sich aus. Zurueckgesetzt wird nur noch bei einem echten Wechsel der Quelle. Dazu ein latenter Fehler, der beim Umpacken ein Schwarzbild gegeben haette: in EXT-X-MAP steckt die Adresse des Kopfstuecks einer fragmentierten MP4, ohne das kein einziger Schnipsel zu dekodieren ist. Sie blieb stehen und zeigte damit relativ auf den lokalen Server. Der alte Test hielt genau dieses Verhalten fest — er hat mitgeaendert. Co-Authored-By: Claude Opus 5 --- Flixy/Views/Player/PlayerModel.swift | 19 ++++++++++-- .../Prefetch/MediaPlaylist.swift | 31 ++++++++++++++++++- .../FlixyPlayback/Prefetch/StreamCache.swift | 19 ++++++++++-- .../MediaPlaylistTests.swift | 24 ++++++++++++-- 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/Flixy/Views/Player/PlayerModel.swift b/Flixy/Views/Player/PlayerModel.swift index 612e18e..80a73ea 100644 --- a/Flixy/Views/Player/PlayerModel.swift +++ b/Flixy/Views/Player/PlayerModel.swift @@ -752,13 +752,28 @@ final class PlayerModel { // **Und wenn es nicht hochkommt, spielt es trotzdem.** Ein // fehlender Vorrat ist ein Verlust an Bequemlichkeit; keine // Wiedergabe ist ein Verlust an allem. - let adresse = await startPrefetching(from: target.url) ?? target.url + let lokal = await startPrefetching(from: target.url) + let adresse = lokal ?? target.url return PlaybackSource( url: adresse, origin: .remote, startTime: start, - forwardBuffer: Self.forwardBuffer(for: decision.method) + // **Mit eigenem Vorrat ein kleiner Puffer.** + // + // Er bestimmt, wie viel AVPlayer beisammenhaben will, bevor + // er nach einem Sprung weiterspielt. Sechzig Sekunden waren + // richtig, solange er selbst vorladen sollte; mit einem + // Speicher davor sind sie das Gegenteil: springt man mitten + // in den Vorrat, liegt dahinter vielleicht noch ein halbe + // Minute — zu wenig für seine sechzig, und er wartet auf den + // Server, obwohl alles da ist. + // + // Der Abstand liegt jetzt bei uns. Seiner darf so klein sein, + // dass er sofort loslegt. + forwardBuffer: lokal == nil + ? Self.forwardBuffer(for: decision.method) + : .seconds(8) ) } catch { // Die Worte des Servers stehen auf dem Bildschirm, nicht „keine diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Prefetch/MediaPlaylist.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Prefetch/MediaPlaylist.swift index d6546c0..0facb37 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Prefetch/MediaPlaylist.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Prefetch/MediaPlaylist.swift @@ -72,7 +72,18 @@ public struct MediaPlaylist: Sendable, Equatable { for zeile in lines { let getrimmt = zeile.trimmingCharacters(in: .whitespaces) - if getrimmt.isEmpty || getrimmt.hasPrefix("#") { + if getrimmt.isEmpty { + ergebnis.append(zeile) + } else if getrimmt.hasPrefix("#EXT-X-MAP:") { + // **Die eine Anweisung, in der eine Adresse steckt.** + // + // Sie zeigt auf das Kopfstück einer fragmentierten MP4 — ohne + // das ist kein einziger Schnipsel zu dekodieren. Bliebe sie + // stehen, zeigte sie relativ auf den lokalen Server, und dort + // gibt es sie nicht: schwarzes Bild, ohne dass an den + // Schnipseln irgendetwas falsch wäre. + ergebnis.append(Self.rewritingMapURI(zeile, transform)) + } else if getrimmt.hasPrefix("#") { ergebnis.append(zeile) } else { ergebnis.append(transform(getrimmt)) @@ -81,6 +92,24 @@ public struct MediaPlaylist: Sendable, Equatable { return ergebnis.joined(separator: "\n") } + /// Tauscht die Adresse in `#EXT-X-MAP:URI="…"` aus, sonst nichts. + private static func rewritingMapURI( + _ zeile: String, + _ transform: (String) -> String + ) -> String { + let marke = "URI=\"" + guard let start = zeile.range(of: marke), + let ende = zeile[start.upperBound...].firstIndex(of: "\"") + else { return zeile } + let ende2 = ende.. String { + // **Nur bei einem echten Wechsel von vorn.** Der Player holt dieselbe + // Liste immer wieder — nach einem Sprung, beim Stufenwechsel, von + // sich aus. Jedes Mal zurückzusetzen hiesse, den Vorrat jedes Mal zu + // vergessen und von Schnipsel null neu zu zählen. + let neueQuelle = upstream != url upstream = url segments = liste.segments if let dauer = liste.targetDuration { targetDuration = dauer } - lastRequested = 0 - fetched = 0 + + if neueQuelle { + lastRequested = 0 + fetched = 0 + } beginPrefetching() return liste.rewritingReferences { LocalStreamServer.localPath(for: $0) } } @@ -162,6 +170,13 @@ 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 { + // **Ist der Player vorausgesprungen, dort weitermachen.** + // + // Sonst holt der Vorrat brav weiter Schnipsel hinter der + // Abspielstelle — und der eine, auf den gerade gewartet wird, kommt + // als letzter. Das ist genau der Fall, in dem es nach einem Sprung + // lange lädt, obwohl vorn alles bereitliegt. + if lastRequested > fetched { fetched = lastRequested } guard fetched < segments.count else { return false } // Der Vorsprung in Schnipseln, aus dem Vorsprung in Sekunden. diff --git a/Packages/FlixyKit/Tests/FlixyPlaybackTests/MediaPlaylistTests.swift b/Packages/FlixyKit/Tests/FlixyPlaybackTests/MediaPlaylistTests.swift index d5b1cbe..e85cd70 100644 --- a/Packages/FlixyKit/Tests/FlixyPlaybackTests/MediaPlaylistTests.swift +++ b/Packages/FlixyKit/Tests/FlixyPlaybackTests/MediaPlaylistTests.swift @@ -53,14 +53,32 @@ struct MediaPlaylistTests { #expect(neu.contains("http://127.0.0.1:1234/s/hls1/main/0.mp4")) #expect(neu.contains("http://127.0.0.1:1234/s/hls1/main/2.mp4")) - // Anweisungen bleiben, wie sie waren — auch die mit einer Adresse - // **darin**, die wir nicht verstehen. + // Anweisungen bleiben, wie sie waren. #expect(neu.contains("#EXT-X-TARGETDURATION:6")) - #expect(neu.contains("#EXT-X-MAP:URI=\"hls1/main/-1.mp4\"")) + // **Mit einer Ausnahme**, und die hat mich ein Schwarzbild gekostet: + // in `EXT-X-MAP` steckt eine Adresse, und die muss mit. Siehe unten. + #expect(neu.contains("#EXT-X-MAP:URI=\"http://127.0.0.1:1234/s/hls1/main/-1.mp4\"")) #expect(neu.contains("#EXT-X-ENDLIST")) #expect(neu.hasPrefix("#EXTM3U")) } + /// Ohne das Kopfstück ist kein einziger Schnipsel einer fragmentierten + /// MP4 zu dekodieren. Bliebe die Adresse stehen, zeigte sie relativ auf + /// den lokalen Server — schwarzes Bild, ohne dass an den Schnipseln + /// irgendetwas falsch wäre. + @Test("Das Kopfstück wandert mit") + func rewritesInitSegment() { + let neu = MediaPlaylist(medien).rewritingReferences { "/s/\($0)" } + #expect(neu.contains("#EXT-X-MAP:URI=\"/s/hls1/main/-1.mp4\"")) + } + + @Test("Eine Anweisung ohne Adresse bleibt unberührt") + func leavesOtherTagsAlone() { + let neu = MediaPlaylist(medien).rewritingReferences { "/s/\($0)" } + #expect(neu.contains("#EXT-X-VERSION:7")) + #expect(neu.contains("#EXTINF:6.000000,")) + } + @Test("Die Zeilenzahl bleibt gleich") func keepsStructure() { let neu = MediaPlaylist(medien).rewritingReferences { $0 }