Er hat weiterhin angehalten statt ein- und auszublenden, und der Grund steckte in der Rechnung: der schwarze Rand wurde aus der Fenstergrösse ausgerechnet, das Bild liegt aber in einer Fläche mit `ignoresSafeArea()`. Auf dem Mac ist der Einzug der Fensterleiste genau der Unterschied zwischen beiden — die Rechnung lag um diesen Betrag daneben. Jetzt wird die Bildfläche gemessen statt angenommen, und beide Zahlen stehen in einem gemeinsamen, benannten Koordinatenraum. Vor allem aber wird zuerst `AVPlayerLayer.videoRect` gefragt: der Player weiss selbst, wo Bild steht. Die eigene Rechnung bleibt für den Moment vor dem ersten Bild und für VLC, das keine solche Ebene hat. Dazu zwei Worte: „Nachschub" heisst jetzt „Nachladen", und „Blatt" heisst wieder „Sheet" — das ist ein Fachbegriff und bleibt englisch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
140 lines
5.1 KiB
Swift
140 lines
5.1 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
|
|
/// Welche Tonspur gespielt werden soll.
|
|
///
|
|
/// **Ohne diese Angabe wählt der Server.** Bei einem umgerechneten Stream
|
|
/// gibt es danach nur noch *eine* Tonspur — was hier nicht gesagt wird,
|
|
/// lässt sich später nicht mehr umstellen, weil im Player schlicht keine
|
|
/// zweite ankommt. Genau daran scheiterte die Sprachwahl bisher: sie lief
|
|
/// auf einer Liste mit einem Eintrag.
|
|
public let audioStreamIndex: Int?
|
|
|
|
public init(
|
|
itemID: String,
|
|
versionIndex: Int,
|
|
file: MediaFile,
|
|
transcode: TranscodeSettings? = nil,
|
|
startTime: Duration = .zero,
|
|
isLocalConnection: Bool = false,
|
|
audioStreamIndex: Int? = nil
|
|
) {
|
|
self.itemID = itemID
|
|
self.versionIndex = versionIndex
|
|
self.file = file
|
|
self.transcode = transcode
|
|
self.startTime = startTime
|
|
self.isLocalConnection = isLocalConnection
|
|
self.audioStreamIndex = audioStreamIndex
|
|
}
|
|
}
|
|
|
|
/// 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 Sheet 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 {}
|