Bis hierher konnte man sich bei Jellyfin anmelden und stöbern, aber nichts abspielen. Der Player war die letzte große Stelle, die Plex unmittelbar kannte. Drei Mitglieder kommen dafür in den Vertrag: eine Wiedergabe vorbereiten, sie beenden, den Fortschritt melden. Die Entscheidung **ob** umgerechnet wird, fällt weiterhin vorher im `PlaybackDecider` — der Server führt sie nur aus. Genau diese Trennung fehlte: der Player hat Plex' Transcoder angesprochen und konnte deshalb mit nichts anderem reden. `MediaBackendError` bekommt einen siebten Fall: `refused(reason:)`. Der Server hat verstanden und sagt nein, in eigenen Worten. Bei Plex war genau dieser Satz die einzige brauchbare Spur, und er ging tagelang verloren, weil es dafür keinen Fall gab. Bei Jellyfin kommt die Adresse einer umgerechneten Fassung vom Server selbst (`PlaybackInfo` → `TranscodingUrl`), nicht von uns. Er weiß, welche Spuren er kopieren kann und welche er neu rechnen muss. Drei Dinge, die beim Hinsehen auffielen: - **`startTimeTicks` gehört nicht in die Anfrage.** Sonst entscheiden zwei Stellen über den Startpunkt: der Server ließe den Strom dort beginnen, und der Player spränge noch einmal um dieselbe Strecke vor. Bei einundzwanzig Minuten Fortschritt landet man bei zweiundvierzig. - **Der erste Bericht braucht einen eigenen Endpunkt.** Ohne ihn erscheint der Titel bei Jellyfin nirgends als „läuft gerade". Er fiel zunächst aus, weil er die Laufzeit verlangte — und die steht erst Sekunden nach dem Öffnen fest. Jetzt tut es für die erste Meldung die des Servers. - **In der Download-Adresse standen Titel und Fassung vertauscht.** Bei einem Film mit einer Fassung sind beide Kennungen dieselbe und nichts fällt auf; bei zweien schon. `MediaFile` sagt jetzt, zu welchem Titel sie gehört. Live-TV bleibt bei Plex und wird als Einziges noch unmittelbar durchgereicht. Ein Tuner ist ein Stück Hardware, und die fünfhundert Zeilen, die es gekostet hat, ihn zu bändigen, gehören in keinen gemeinsamen Vertrag. Nachgesehen, nicht nur gebaut: gegen die Attrappe läuft ein echtes Video, der Beginn wird gemeldet, der Fortschritt kommt in Ticks mit Sitzung und Fassung an. Was die Attrappe **nicht** beantworten kann, ist, ob ein echter Jellyfin das Geräteprofil annimmt und eine Umrechnung anbietet — das entscheidet erst ein richtiger Server. 257 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
3.9 KiB
Swift
118 lines
3.9 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?
|
|
|
|
public init(maximumBitrate: Int?, resolution: String?) {
|
|
self.maximumBitrate = maximumBitrate
|
|
self.resolution = resolution
|
|
}
|
|
}
|
|
|
|
/// 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 {}
|