Der Vorrat laeuft jetzt voraus, nicht der Player
AVPlayer entscheidet selbst, wie weit er puffert, und bei einer Abspielliste laesst Apple uns nicht dazwischen: auf eine Anfrage nach einem Schnipsel nimmt er von uns nur eine Weiterleitung an, keine Daten. Der Denkfehler war, seinen Puffer vergroessern zu wollen. Wir muessen aber gar nicht dazwischen — wir koennen die Gegenstelle sein. Ein winziger HTTP-Server auf 127.0.0.1 liefert dem Player die Abspielliste, umgeschrieben auf sich selbst, und holt die Schnipsel zuegiger von Jellyfin, als abgespielt wird. Jedes Segment, das der Player verlangt, liegt dann schon lokal. Faellt die Verbindung im Tunnel aus, merkt er es nicht. Das ist der Unterschied zwischen Puffer und Vorrat: den Puffer bestimmt Apple, den Vorrat bestimmen wir. Streng der Reihe nach, und mit begrenztem Vorsprung. Der Server erzeugt die Schnipsel gerade erst; wer Nummer 40 verlangt, waehrend er bei 12 ist, sieht fuer Jellyfin aus wie jemand, der gesprungen ist — und dann beginnt das Umrechnen von vorn, an der falschen Stelle. Der Lauscher haengt ausdruecklich an 127.0.0.1, nicht an allen Schnittstellen. Ein Server, der den Film ins Netz stellt, waere das Letzte, was eine Mediathek-App tun sollte. Kommt er nicht hoch, spielt es trotzdem — ueber die urspruengliche Adresse, ohne Vorrat. Ein fehlender Vorrat ist ein Verlust an Bequemlichkeit; keine Wiedergabe ist ein Verlust an allem. Das Lesen und Umschreiben der Abspielliste ist ein eigener Typ ohne Netz, mit sieben Tests. Eine falsch umgeschriebene Liste ergibt entweder ein schwarzes Bild oder — schlimmer — eine, die stellenweise am Speicher vorbeigeht und nur manchmal hakt. So etwas findet man im laufenden Film nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
46550fc30b
commit
134dc82927
@@ -200,6 +200,9 @@ final class PlayerModel {
|
||||
private var seekTarget: Duration?
|
||||
/// Ob schon einmal auf VLC ausgewichen wurde.
|
||||
private var hasFallenBack = false
|
||||
/// Der Server, von dem der Player holt — und der Vorrat dahinter.
|
||||
private var streamServer: LocalStreamServer?
|
||||
private var streamCache: StreamCache?
|
||||
private var eventTask: Task<Void, Never>?
|
||||
private var reportTask: Task<Void, Never>?
|
||||
/// Was der Server für diese Wiedergabe angelegt hat.
|
||||
@@ -728,8 +731,22 @@ final class PlayerModel {
|
||||
))
|
||||
playbackSession = target.sessionID
|
||||
playbackVersionID = target.versionID
|
||||
|
||||
// **Der Vorrat läuft voraus, nicht der Player.**
|
||||
//
|
||||
// AVPlayer entscheidet selbst, wie weit er puffert, und bei einer
|
||||
// Abspielliste lässt Apple uns nicht dazwischen. Also stehen wir
|
||||
// davor: ein winziger Server auf diesem Gerät, der die Schnipsel
|
||||
// zügiger holt, als abgespielt wird. Der Player fragt dann uns,
|
||||
// und ob die Leitung gerade steht, merkt er nicht.
|
||||
//
|
||||
// **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
|
||||
|
||||
return PlaybackSource(
|
||||
url: target.url,
|
||||
url: adresse,
|
||||
origin: .remote,
|
||||
startTime: start,
|
||||
forwardBuffer: Self.forwardBuffer(for: decision.method)
|
||||
@@ -775,13 +792,50 @@ final class PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshBuffered() {
|
||||
guard let vorrat = engine?.statistics?.bufferedAhead else {
|
||||
bufferedUntil = nil
|
||||
return
|
||||
/// Stellt den Vorratsspeicher vor den Player.
|
||||
///
|
||||
/// Nur bei Abspiellisten: eine durchgereichte Datei kommt über
|
||||
/// 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 }
|
||||
|
||||
await streamServer?.stop()
|
||||
let speicher = StreamCache()
|
||||
let server = LocalStreamServer(cache: speicher)
|
||||
do {
|
||||
let lokal = try await server.start(upstream: url)
|
||||
streamServer = server
|
||||
streamCache = speicher
|
||||
diagnostics.note("Vorrat läuft voraus")
|
||||
return lokal
|
||||
} catch {
|
||||
diagnostics.note("Vorrat nicht gestartet: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshBuffered() {
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
// **Der eigene Vorrat zählt, nicht der des Players.** Steht ein
|
||||
// Speicher davor, ist dessen Vorsprung die Zahl, die stimmt: der
|
||||
// Player hält davon nur einen Bruchteil, der Rest liegt schon auf
|
||||
// der Platte. Ohne Speicher bleibt die Auskunft des Players.
|
||||
let vorrat: Duration?
|
||||
if let cache = self.streamCache {
|
||||
vorrat = await cache.bufferedAhead
|
||||
} else {
|
||||
vorrat = self.engine?.statistics?.bufferedAhead
|
||||
}
|
||||
|
||||
guard let vorrat else {
|
||||
self.bufferedUntil = nil
|
||||
return
|
||||
}
|
||||
let ende = self.currentTime + vorrat
|
||||
self.bufferedUntil = self.duration.map { min(ende, $0) } ?? ende
|
||||
}
|
||||
let ende = currentTime + vorrat
|
||||
bufferedUntil = duration.map { min(ende, $0) } ?? ende
|
||||
}
|
||||
|
||||
/// Eine Probe nehmen und daraus handeln.
|
||||
@@ -1248,6 +1302,12 @@ final class PlayerModel {
|
||||
|
||||
engine?.teardown()
|
||||
engine = nil
|
||||
// Der Vorrat gehört zu diesem Stream. Stehen zu bleiben hiesse,
|
||||
// Schnipsel für einen Film zu holen, den niemand mehr sieht — und den
|
||||
// Platz dafür zu behalten.
|
||||
if let streamServer { Task { await streamServer.stop() } }
|
||||
streamServer = nil
|
||||
streamCache = nil
|
||||
pictureInPicture = nil
|
||||
pictureInPictureDelegate = nil
|
||||
surface.playerLayer.player = nil
|
||||
|
||||
Reference in New Issue
Block a user