Der Fehlstart im Zug hat den eigentlichen Mangel gezeigt: die ganze neue Regelung misst **ab dem ersten Bild**. Kommt nie eines, misst sie nichts, greift nichts — und „Auto" hat sich als das erwiesen, was es wörtlich heisst: „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. Statt aufzugeben geht der Watchdog jetzt eine Sprosse tiefer und baut neu auf — bei Filmen über `restart()`, bei Sendern über den Tuner-Abbau. Gezählt gegen dasselbe Budget wie die Sicherheitsleine, sonst schickt eine tote Leitung die App endlos im Kreis. Scheitert auch das, nennt die Meldung jetzt den Grund und den Weg statt nur „keine Daten mehr": wer ohne Grenze angefragt hat, soll erfahren, dass es daran liegt und wo es sich ändern lässt. Und „Strom" heisst Stream. Zweiunddreissig Stellen, dazu das Gedächtnis ergänzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
130 lines
4.6 KiB
Swift
130 lines
4.6 KiB
Swift
import Foundation
|
|
|
|
/// Was abgespielt werden soll und wie.
|
|
///
|
|
/// Die Entscheidung *ob* umgerechnet wird, fällt vorher — im
|
|
/// `PlaybackDecider`, aus Profil und Leitung. Hier steht nur noch, was der
|
|
/// Server daraufhin bereitstellen soll. Das ist die Trennung, die es vorher
|
|
/// nicht gab: der Player hat Plex' Transcoder unmittelbar angesprochen und
|
|
/// konnte deshalb mit keinem anderen Server reden.
|
|
public struct PlaybackIntent: Sendable, Equatable {
|
|
public let itemID: String
|
|
/// Welche Fassung — ein Film kann mehrfach vorliegen.
|
|
public let versionIndex: Int
|
|
/// Die Datei innerhalb der Fassung. Plex adressiert Dateien, Jellyfin
|
|
/// Quellen; beide finden sich hierüber wieder.
|
|
public let file: MediaFile
|
|
/// `nil` heißt: unverändert übertragen.
|
|
public let transcode: TranscodeSettings?
|
|
public let startTime: Duration
|
|
/// Ob der Server im selben Netz steht. Plex rechnet dann anders.
|
|
public let isLocalConnection: Bool
|
|
|
|
public init(
|
|
itemID: String,
|
|
versionIndex: Int,
|
|
file: MediaFile,
|
|
transcode: TranscodeSettings? = nil,
|
|
startTime: Duration = .zero,
|
|
isLocalConnection: Bool = false
|
|
) {
|
|
self.itemID = itemID
|
|
self.versionIndex = versionIndex
|
|
self.file = file
|
|
self.transcode = transcode
|
|
self.startTime = startTime
|
|
self.isLocalConnection = isLocalConnection
|
|
}
|
|
}
|
|
|
|
/// Wie weit heruntergerechnet werden soll.
|
|
public struct TranscodeSettings: Sendable, Equatable {
|
|
/// In Kilobit je Sekunde. `nil` heißt „so gut wie möglich".
|
|
public let maximumBitrate: Int?
|
|
/// Etwa „1280x720". `nil` lässt die Auflösung, wie sie ist.
|
|
public let resolution: String?
|
|
/// Ob der Server eine **Leiter** liefern soll statt einer festen Stufe.
|
|
///
|
|
/// Dann enthält die Playlist mehrere Varianten, und der Player wechselt
|
|
/// zwischen ihnen, ohne dass der Stream neu aufgebaut werden muss. Die
|
|
/// angegebene Bitrate bleibt dabei die **oberste** Sprosse; was darunter
|
|
/// liegt, erfindet der Server.
|
|
///
|
|
/// Nur bei `automatic` gesetzt. Wer im Blatt ausdrücklich „720p · 3 Mbps"
|
|
/// antippt, soll das bekommen und nicht „höchstens 3, meistens weniger"
|
|
/// unter unverändertem Etikett.
|
|
public let prefersAdaptive: Bool
|
|
|
|
public init(maximumBitrate: Int?, resolution: String?, prefersAdaptive: Bool = false) {
|
|
self.maximumBitrate = maximumBitrate
|
|
self.resolution = resolution
|
|
self.prefersAdaptive = prefersAdaptive
|
|
}
|
|
}
|
|
|
|
/// Was der Server für diese Wiedergabe bereitgestellt hat.
|
|
public struct PlaybackTarget: Sendable, Equatable {
|
|
public let url: URL
|
|
/// Die Sitzung, die der Server dafür angelegt hat.
|
|
///
|
|
/// Zum Aufräumen: ohne dieses Kennzeichen rechnet der Transcoder weiter,
|
|
/// wenn längst niemand mehr zusieht.
|
|
public let sessionID: String?
|
|
/// Welche Fassung gespielt wird.
|
|
///
|
|
/// Jellyfin will sie bei jeder Fortschrittsmeldung zurückhaben, sonst
|
|
/// ordnet es die Meldung keiner Wiedergabe zu.
|
|
public let versionID: String?
|
|
|
|
public init(url: URL, sessionID: String? = nil, versionID: String? = nil) {
|
|
self.url = url
|
|
self.sessionID = sessionID
|
|
self.versionID = versionID
|
|
}
|
|
}
|
|
|
|
/// In welchem Zustand sich die Wiedergabe befindet.
|
|
public enum PlaybackReportState: String, Sendable {
|
|
/// Der erste Bericht einer Wiedergabe.
|
|
///
|
|
/// Eigener Fall, weil Jellyfin dafür einen eigenen Endpunkt hat: ohne ihn
|
|
/// erscheint der Titel nirgends als „läuft gerade", und die Sitzung, unter
|
|
/// der die weiteren Meldungen laufen, ist dem Server unbekannt. Plex
|
|
/// unterscheidet das nicht und behandelt ihn wie „spielt".
|
|
case started
|
|
case playing
|
|
case paused
|
|
/// Beim Verlassen des Players. Erst dieser Wert nimmt den Titel auf dem
|
|
/// Server aus „wird gerade abgespielt" heraus.
|
|
case stopped
|
|
case buffering
|
|
}
|
|
|
|
/// Wo man gerade steht.
|
|
public struct PlaybackReport: Sendable, Equatable {
|
|
public let itemID: String
|
|
public let sessionID: String?
|
|
public let versionID: String?
|
|
public let state: PlaybackReportState
|
|
public let time: Duration
|
|
public let duration: Duration
|
|
|
|
public init(
|
|
itemID: String,
|
|
sessionID: String? = nil,
|
|
versionID: String? = nil,
|
|
state: PlaybackReportState,
|
|
time: Duration,
|
|
duration: Duration
|
|
) {
|
|
self.itemID = itemID
|
|
self.sessionID = sessionID
|
|
self.versionID = versionID
|
|
self.state = state
|
|
self.time = time
|
|
self.duration = duration
|
|
}
|
|
}
|
|
|
|
extension PlaybackReportState: Equatable {}
|