Der Vorrat gibt dem Player Vorrang und weicht, wenn er im Weg steht
Sobald das Umrechnen teuer wird — und das Einbrennen eines Untertitels ist teuer —, bedient Jellyfin einen Schnipsel nach dem anderen. Der Vorrat hat dabei gleichzeitig weitergeholt und dem Player die Leitung weggenommen: zwei Anfragen machen die dringende langsamer, nicht schneller. Herauskam „Es kommen keine Daten mehr\". Jetzt ruht der Vorrat, solange eine Anfrage des Players offen ist. Dazu ein Rueckzugsweg, der gefehlt hat. Der Speicher steht zwischen Player und Server, und was dort schiefgeht, sieht von aussen aus wie eine tote Leitung. Kommt nach zwoelf Sekunden nichts an, wird er weggenommen und es laeuft ohne ihn noch einmal an — das kostet nur den Vorsprung und unterscheidet die beiden Faelle, statt sie zu verwechseln. Erst danach die kleinere Bitrate, erst danach das Aufgeben. Und drei Zeilen ins Protokoll, die drei Runden Ratens erspart haetten: was bestellt wurde, was der Player daraus meldet, und welche Spur eingeschaltet wurde. „Nicht geliefert\" und „geliefert und nicht angezeigt\" sehen von aussen gleich aus — im Protokoll nicht mehr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5e33971452
commit
8e0e8ec246
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user