Player: die Qualitätsleiter des Servers, Bitrate im Blick, Doppeltipp
Die vier erfundenen Stufen weichen der Leiter, die der Server selbst meldet: 64, 96, 208, 320, 720, 1500, 2000, 3000, 4000, 8000, 10000, 12000, 20000 kbps mit den Auflösungen, die Plex dafür vorsieht. Eine eigene Leiter hieße, Stufen anzubieten, die der Transcoder nicht kennt — er rundet dann still auf die nächste, und die Anzeige log. Angeboten wird nur, was zur Quelle passt. Eine 720p-Datei auf 1080p hochzurechnen bringt kein Bild dazu, und die Stufen sähen alle gleich aus. Oben im Blatt steht, was tatsächlich über die Leitung kommt — gemessene Bitrate, Auflösung, verworfene Bilder. Der Unterschied zur gewählten Stufe ist keine Haarspalterei: die eine sagt, was erlaubt ist, die andere, was ankommt. Ruckelt es trotz hoher Stufe, steht die Antwort dort. „Nur Ton" gibt es, aber ehrlich beschriftet: Plex kennt für Videos keinen reinen Tonstrom, also wird die unterste Sprosse geholt und das Bild ausgeblendet. Dazu Doppeltipp links und rechts für zehn Sekunden zurück und vor. Die Reihenfolge der Gesten ist entscheidend — das einfache Tippen verschluckt sonst das doppelte. Die Kachel hängt nicht mehr in vier Ecken fest, sondern klebt links oder rechts und ist in der Höhe frei. Vier Ecken sind schnell gebaut, aber im Weg: steht sie über einem Knopf, bleiben drei Orte, die ebenso stören. Größer wird sie durch Zusammen- und Auseinanderziehen; der Griff ist weg, er saß zwangsläufig auf dem Bild und stritt sich mit dem Verschieben. Und die Begriffe: „Motor" für engine sagt kein deutscher Entwickler. Fachwörter, die im Deutschen englisch bleiben, bleiben jetzt englisch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c2ddc10871
commit
12bb49615b
@@ -9,8 +9,8 @@ import PlexKit
|
||||
/// Führt einen Abspielwunsch aus.
|
||||
///
|
||||
/// Holt die Metadaten, lässt den `PlaybackDecider` den Weg bestimmen, baut den
|
||||
/// passenden Motor und hält dessen Zustand für die Ansicht bereit. Die Ansicht
|
||||
/// weiß nicht, welcher Motor läuft — bis auf die eine Stelle, an der die
|
||||
/// passendie Engine und hält dessen Zustand für die Ansicht bereit. Die Ansicht
|
||||
/// weiß nicht, welche Engine läuft — bis auf die eine Stelle, an der die
|
||||
/// Bildfläche gebaut wird.
|
||||
@MainActor
|
||||
@Observable
|
||||
@@ -58,6 +58,17 @@ final class PlayerModel {
|
||||
/// Ob gerade auf eine andere Stufe umgestellt wird.
|
||||
private(set) var isChangingQuality = false
|
||||
|
||||
/// Was gerade über die Leitung kommt. Beim Öffnen der Wahl abgefragt.
|
||||
var statistics: PlaybackStatistics? { engine?.statistics }
|
||||
|
||||
/// Die Höhe der Quelldatei — danach richtet sich, welche Stufen überhaupt
|
||||
/// Sinn ergeben. Eine 720p-Datei auf 1080p „hochzurechnen" bringt kein
|
||||
/// Bild dazu.
|
||||
var sourceHeight: Int? { currentMedia?.height }
|
||||
|
||||
/// Ob das Bild gezeigt werden soll.
|
||||
var showsVideo: Bool { quality.showsVideo }
|
||||
|
||||
/// Die Bildebene des Systemplayers.
|
||||
///
|
||||
/// Gehört dem Modell, nicht der Ansicht: das Modell überlebt den Wechsel
|
||||
@@ -181,7 +192,7 @@ final class PlayerModel {
|
||||
/// Stellt auf eine andere Qualitätsstufe um.
|
||||
///
|
||||
/// Bedeutet zwangsläufig einen Neuaufbau: die Stufe steckt in der Adresse
|
||||
/// des Stroms, nicht in einer Einstellung, die sich unterwegs drehen ließe.
|
||||
/// des Streams, nicht in einer Einstellung, die sich unterwegs drehen ließe.
|
||||
/// Die Stelle wird gemerkt und danach wieder angefahren — sonst begänne der
|
||||
/// Film bei jedem Wechsel von vorn.
|
||||
func select(quality newQuality: PlayerQuality) async {
|
||||
@@ -196,7 +207,7 @@ final class PlayerModel {
|
||||
quality = newQuality
|
||||
|
||||
// Den alten Transcoder anhalten, bevor der neue anläuft. Sonst rechnet
|
||||
// der Server zwei Ströme gleichzeitig für einen Zuschauer.
|
||||
// der Server zwei Streams gleichzeitig für einen Zuschauer.
|
||||
if let transcodeSession {
|
||||
await server.stopTranscode(sessionIdentifier: transcodeSession)
|
||||
self.transcodeSession = nil
|
||||
@@ -228,7 +239,7 @@ final class PlayerModel {
|
||||
/// Wiedergabe aus dem Download-Ordner.
|
||||
///
|
||||
/// Ohne Server und ohne Transcoding: die Datei liegt so vor, wie sie auf
|
||||
/// dem Server lag. Welcher Motor sie öffnen kann, steht im Abzug, der beim
|
||||
/// dem Server lag. Welcher Engine sie öffnen kann, steht im Abzug, der beim
|
||||
/// Herunterladen mitgesichert wurde — offline lässt sich nichts nachfragen.
|
||||
/// Live-TV: die Adresse steht schon fest.
|
||||
///
|
||||
@@ -236,7 +247,7 @@ final class PlayerModel {
|
||||
/// Sender steht in keiner Mediathek. Und ohne Fortschrittsmeldung: es
|
||||
/// gibt keine Stelle, an der man ihn später fortsetzen könnte.
|
||||
private func startLiveStream(url: URL) async {
|
||||
diagnostics.note("Live-Strom öffnen")
|
||||
diagnostics.note("Live-Stream öffnen")
|
||||
isLive = true
|
||||
title = request.title
|
||||
// Was gerade läuft, kommt mit dem Wunsch — nachzuschlagen ist es
|
||||
@@ -309,11 +320,11 @@ final class PlayerModel {
|
||||
observe(engine)
|
||||
|
||||
do {
|
||||
diagnostics.note("Motor öffnet Quelle")
|
||||
diagnostics.note("Engine öffnet Quelle")
|
||||
try await engine.load(source)
|
||||
engine.play()
|
||||
phase = .playing
|
||||
diagnostics.note("Motor gestartet")
|
||||
diagnostics.note("Engine gestartet")
|
||||
// Weder offline noch live gibt es einen Fortschritt zu melden.
|
||||
if !isOffline, !isLive { startReporting() }
|
||||
} catch {
|
||||
@@ -322,7 +333,7 @@ final class PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Baut die Quelle — entweder die Datei selbst oder den HLS-Strom.
|
||||
/// Baut die Quelle — entweder die Datei selbst oder den HLS-Stream.
|
||||
private func source(
|
||||
for decision: PlaybackDecision,
|
||||
item: PlexItem,
|
||||
@@ -366,7 +377,7 @@ final class PlayerModel {
|
||||
|
||||
/// Beendet ewiges Laden mit einer Meldung statt mit einem Kreisel.
|
||||
///
|
||||
/// Ein Live-Strom kann sterben, ohne dass der Motor es merkt — der Tuner
|
||||
/// Ein Live-Stream kann sterben, ohne dass die Engine es merkt — der Tuner
|
||||
/// gibt auf, der Transcoder bricht ab, und der Player wartet auf Daten,
|
||||
/// die nie kommen. Ohne diese Grenze dreht sich der Kreisel bis zum
|
||||
/// nächsten Antippen, und niemand weiß, ob noch etwas kommt.
|
||||
@@ -379,8 +390,8 @@ final class PlayerModel {
|
||||
guard !Task.isCancelled, let self, self.isBuffering else { return }
|
||||
|
||||
diagnostics.note("Seit 40 s keine Daten — abgebrochen")
|
||||
// Den Motor gleich stilllegen: er würde sonst weiter an einem
|
||||
// toten Strom ziehen, und bei Live-TV bliebe der Tuner belegt.
|
||||
// Den Engine gleich stilllegen: er würde sonst weiter an einem
|
||||
// toten Stream ziehen, und bei Live-TV bliebe der Tuner belegt.
|
||||
silence()
|
||||
phase = .failed(message: String(localized: "player.stalled"))
|
||||
Task { await self.releaseOnServer() }
|
||||
@@ -451,11 +462,11 @@ final class PlayerModel {
|
||||
selectedSubtitleTrack = track
|
||||
}
|
||||
|
||||
/// Legt den Motor **sofort** still. Kein Netzwerk, kein Warten.
|
||||
/// Legt die Engine **sofort** still. Kein Netzwerk, kein Warten.
|
||||
///
|
||||
/// Getrennt vom Aufräumen am Server, und das ist der ganze Punkt: früher
|
||||
/// stand der Motor erst nach drei Netzwerkaufrufen still. Wurde in der
|
||||
/// Zwischenzeit etwas Neues gestartet, liefen zwei Motoren gleichzeitig,
|
||||
/// stand die Engine erst nach drei Netzwerkaufrufen still. Wurde in der
|
||||
/// Zwischenzeit etwas Neues gestartet, liefen zwei Engines gleichzeitig,
|
||||
/// und der alte hat der neuen Wiedergabe beim Abräumen die Audiositzung
|
||||
/// weggenommen. Was man hörte: der alte Ton lief weiter — auch nachdem
|
||||
/// die App im Hintergrund war —, und das Neue blieb stumm.
|
||||
@@ -574,10 +585,10 @@ final class PlayerModel {
|
||||
return item.media[request.mediaIndex]
|
||||
}
|
||||
|
||||
/// Hängt die gehaltene Bildebene an den aktuellen Motor.
|
||||
/// Hängt die gehaltene Bildebene an den aktuellen Engine.
|
||||
///
|
||||
/// Nach **jedem** Motorwechsel nötig — also auch nach einem
|
||||
/// Qualitätswechsel, der den Motor neu baut. Ohne das zeigt die Ebene
|
||||
/// Nach **jedem** Engine-Wechsel nötig — also auch nach einem
|
||||
/// Qualitätswechsel, der die Engine neu baut. Ohne das zeigt die Ebene
|
||||
/// weiter auf einen abgeräumten Player und bleibt schwarz.
|
||||
private func attachSurface(to engine: any VideoPlayerEngine) {
|
||||
guard let avEngine = engine as? AVPlayerEngine else { return }
|
||||
@@ -628,14 +639,14 @@ final class PlayerModel {
|
||||
isBuffering = state == .buffering || state == .loading
|
||||
watchBuffering(isBuffering)
|
||||
// Jeder Zustandswechsel mit Zeitstempel. Erst daran ist
|
||||
// abzulesen, ob der Motor hängt oder die Leitung.
|
||||
// abzulesen, ob die Engine hängt oder die Leitung.
|
||||
diagnostics.note("Zustand: \(state)")
|
||||
if case .failed(let failure) = state {
|
||||
diagnostics.finish(outcome: "Wiedergabe gescheitert: \(failure)")
|
||||
// Der Grund gehört auf den Bildschirm, nicht nur ins
|
||||
// Protokoll: „die Wiedergabe wurde unterbrochen" allein
|
||||
// ist eine Sackgasse — es sagt weder, ob der Server
|
||||
// ablehnt, noch ob der Motor das Format nicht mag.
|
||||
// ablehnt, noch ob die Engine das Format nicht mag.
|
||||
phase = .failed(message: [
|
||||
String(localized: "player.playbackFailed"),
|
||||
String(describing: failure),
|
||||
|
||||
Reference in New Issue
Block a user