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>
155 lines
5.7 KiB
Swift
155 lines
5.7 KiB
Swift
import Foundation
|
|
|
|
/// Eine im Player wählbare Qualitätsstufe.
|
|
///
|
|
/// Getrennt von den Einstellungen unter „Qualität": dort steht, was
|
|
/// üblicherweise gelten soll; hier greift man für diesen einen Film ein — weil
|
|
/// es gerade ruckelt oder weil man ausnahmsweise das Original will.
|
|
public enum PlayerQuality: Hashable, Sendable, Identifiable {
|
|
/// Was in den Einstellungen für die aktuelle Verbindung hinterlegt ist.
|
|
case automatic
|
|
/// Die Datei unverändert, ohne Umrechnung.
|
|
case original
|
|
/// Eine Stufe der Leiter, die der Server anbietet.
|
|
case limited(Rung)
|
|
/// So wenig Bild wie möglich.
|
|
case audioOnly
|
|
|
|
/// Eine Sprosse: Bitrate und die Auflösung, die Plex dafür vorsieht.
|
|
///
|
|
/// Die Werte stammen nicht aus der Luft, sondern aus dem, was der Server
|
|
/// über sich selbst sagt (`transcoderVideoBitrates`). Eine eigene Leiter
|
|
/// zu erfinden hieße, Stufen anzubieten, die der Transcoder gar nicht
|
|
/// kennt — er rundet dann still auf die nächste, und die Anzeige log.
|
|
public struct Rung: Hashable, Sendable {
|
|
public let bitrate: Int
|
|
public let height: Int
|
|
|
|
public init(bitrate: Int, height: Int) {
|
|
self.bitrate = bitrate
|
|
self.height = height
|
|
}
|
|
|
|
public var width: Int {
|
|
// 16:9, auf gerade Zahlen gerundet — ungerade Kantenlängen mag
|
|
// kein Kodierer.
|
|
let raw = Int((Double(height) * 16.0 / 9.0).rounded())
|
|
return raw % 2 == 0 ? raw : raw + 1
|
|
}
|
|
|
|
public var resolution: String { "\(width)x\(height)" }
|
|
}
|
|
|
|
/// Die Leiter von oben nach unten.
|
|
public static let ladder: [Rung] = [
|
|
Rung(bitrate: 20_000, height: 1080),
|
|
Rung(bitrate: 12_000, height: 1080),
|
|
Rung(bitrate: 10_000, height: 1080),
|
|
Rung(bitrate: 8_000, height: 1080),
|
|
Rung(bitrate: 4_000, height: 1080),
|
|
Rung(bitrate: 3_000, height: 720),
|
|
Rung(bitrate: 2_000, height: 720),
|
|
Rung(bitrate: 1_500, height: 720),
|
|
Rung(bitrate: 720, height: 480),
|
|
Rung(bitrate: 320, height: 360),
|
|
Rung(bitrate: 208, height: 360),
|
|
Rung(bitrate: 96, height: 240),
|
|
Rung(bitrate: 64, height: 160),
|
|
]
|
|
|
|
public static var allCases: [PlayerQuality] {
|
|
[.automatic, .original] + ladder.map(PlayerQuality.limited) + [.audioOnly]
|
|
}
|
|
|
|
public var id: String {
|
|
switch self {
|
|
case .automatic: "automatic"
|
|
case .original: "original"
|
|
case .audioOnly: "audioOnly"
|
|
case .limited(let rung): "limited-\(rung.bitrate)"
|
|
}
|
|
}
|
|
|
|
/// Obergrenze in Kilobit. `nil` bei `automatic` und `original`.
|
|
public var maximumBitrate: Int? {
|
|
switch self {
|
|
case .automatic, .original: nil
|
|
case .limited(let rung): rung.bitrate
|
|
// Plex kennt für Videos keinen reinen Tonstrom. Die unterste Sprosse
|
|
// ist das, was dem am nächsten kommt; das Bild wird dazu im Player
|
|
// ausgeblendet, damit der Schirm dunkel bleiben kann.
|
|
case .audioOnly: Self.ladder.last?.bitrate
|
|
}
|
|
}
|
|
|
|
public var resolution: String? {
|
|
switch self {
|
|
case .automatic, .original: nil
|
|
case .limited(let rung): rung.resolution
|
|
case .audioOnly: Self.ladder.last?.resolution
|
|
}
|
|
}
|
|
|
|
/// Die Höhe in Bildzeilen, sofern die Stufe eine vorgibt.
|
|
public var height: Int? {
|
|
switch self {
|
|
case .automatic, .original: nil
|
|
case .limited(let rung): rung.height
|
|
case .audioOnly: Self.ladder.last?.height
|
|
}
|
|
}
|
|
|
|
/// Ob das Bild gezeigt werden soll.
|
|
public var showsVideo: Bool {
|
|
if case .audioOnly = self { return false }
|
|
return true
|
|
}
|
|
|
|
/// Ob diese Stufe eine Umrechnung erzwingt.
|
|
///
|
|
/// `original` erzwingt das Gegenteil: keine Umrechnung, auch wenn die
|
|
/// Einstellungen eine Grenze vorsehen.
|
|
public var forcesTranscode: Bool {
|
|
switch self {
|
|
case .automatic, .original: false
|
|
case .limited, .audioOnly: true
|
|
}
|
|
}
|
|
|
|
/// Die Richtlinie für diese Stufe.
|
|
///
|
|
/// `automatic` reicht durch, was ohnehin gilt. Die übrigen überschreiben
|
|
/// sie — das ist der Zweck eines Eingriffs im Player.
|
|
public func policy(basedOn base: PlaybackPolicy) -> PlaybackPolicy {
|
|
switch self {
|
|
case .automatic:
|
|
base
|
|
case .original:
|
|
// Keine Grenze und kein Umrechnen: ausdrücklich das Original.
|
|
PlaybackPolicy(allowsTranscoding: false, allowsFallbackEngine: true, maximumBitrate: nil)
|
|
case .limited, .audioOnly:
|
|
// Ohne Ersatzmotor, damit die Entscheidung zwangsläufig beim
|
|
// Transcoder landet — sonst spielte VLC die Datei direkt ab und
|
|
// die gewählte Stufe bliebe wirkungslos.
|
|
PlaybackPolicy(
|
|
allowsTranscoding: true,
|
|
allowsFallbackEngine: false,
|
|
maximumBitrate: maximumBitrate
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Nur die Stufen, die für eine Quelle dieser Höhe sinnvoll sind.
|
|
///
|
|
/// Eine 720p-Datei auf 1080p „hochzurechnen" bringt kein Bild dazu, und
|
|
/// die Stufen sähen alle gleich aus — dieselbe Falle wie beim Herunterladen.
|
|
public static func available(forSourceHeight height: Int?) -> [PlayerQuality] {
|
|
guard let height, height > 0 else { return allCases }
|
|
let fitting = ladder.filter { $0.height <= height }
|
|
// Mindestens die unterste Sprosse anbieten, sonst bliebe bei einer
|
|
// sehr kleinen Quelle nur „Original".
|
|
let rungs = fitting.isEmpty ? Array(ladder.suffix(1)) : fitting
|
|
return [.automatic, .original] + rungs.map(PlayerQuality.limited) + [.audioOnly]
|
|
}
|
|
}
|