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 }