Phase 2: Transcoding, adaptive Bitrate, Spuren, Marker, Sync
Der Decider brauchte keine Änderung — nur eine andere Richtlinie. Genau dafür war er gebaut. Transcoding - TranscodeRequest baut die Parameter für /video/:/transcode/universal. directStream=1 lässt Plex nur umpacken, wenn der Bildcodec passt, statt pauschal neu zu kodieren. - Vor dem Start wird der Server gefragt (decision); sagt er nein, steht der Grund im Klartext da statt eines schwarzen Bildes. - Beim Verlassen wird der Transcoder angehalten. Ohne dieses Signal rechnet der Server minutenlang für niemanden weiter. - Die adaptive Bitrate kommt mit HLS: AVPlayer wechselt sie selbst. Qualitätseinstellungen - Getrennt für WLAN und Mobilfunk, fünf spürbare Stufen. Voreinstellung: zuhause Original, unterwegs 4 Mbit. - NetworkConditions erkennt Mobilfunk über isExpensive — das deckt auch den Hotspot ab, der technisch WLAN ist und trotzdem Volumen kostet. Spuren und Marken - Ton- und Untertitelwahl im Player, bevorzugte Sprache in den Einstellungen, nachsichtiger Sprachvergleich (de/deu/ger/de-DE). - Intro- und Abspann-Marken mit Überspringen-Knopf, optional automatisch. Bereits übersprungene Marken werden gemerkt, sonst käme man nach einem Rücksprung nie ins Intro zurück. - Nächste Folge inkl. Staffelwechsel; Specials in Staffel 0 werden nicht vorgezogen. Fortschritt - Alle zehn Sekunden an den Server, plus Schlussmeldung beim Verlassen. Erst die nimmt den Titel aus „wird gerade abgespielt" heraus. - markWatched/markUnwatched/rate/refreshLibrary stehen bereit. 86 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import PlexKit
|
||||
|
||||
@Suite("Transcode-Anfrage")
|
||||
struct TranscodeRequestTests {
|
||||
private func value(_ request: TranscodeRequest, _ name: String) -> String? {
|
||||
request.queryItems.first { $0.name == name }?.value
|
||||
}
|
||||
|
||||
@Test("Der Pfad zeigt auf den Titel, nicht auf die Datei")
|
||||
func pathPointsAtMetadata() {
|
||||
// Plex erwartet hier die Metadaten-Adresse. Gibt man den Dateipfad an,
|
||||
// antwortet der Transcoder mit 404.
|
||||
let request = TranscodeRequest(ratingKey: "45231", isLocal: true)
|
||||
#expect(value(request, "path") == "/library/metadata/45231")
|
||||
}
|
||||
|
||||
@Test("Umpacken ist erlaubt, DirectPlay ausdrücklich nicht")
|
||||
func allowsDirectStreamButNotDirectPlay() {
|
||||
// directStream=1 lässt Plex nur den Container wechseln, wenn der
|
||||
// Bildcodec passt — das spart die Neukodierung. directPlay=0, weil
|
||||
// diese Frage der PlaybackDecider bereits entschieden hat.
|
||||
let request = TranscodeRequest(ratingKey: "1", isLocal: true)
|
||||
#expect(value(request, "directStream") == "1")
|
||||
#expect(value(request, "directPlay") == "0")
|
||||
}
|
||||
|
||||
@Test("Ohne fastSeek müsste der Transcoder beim Springen von vorn beginnen")
|
||||
func enablesFastSeek() {
|
||||
#expect(value(TranscodeRequest(ratingKey: "1", isLocal: true), "fastSeek") == "1")
|
||||
}
|
||||
|
||||
@Test("Die Bitratengrenze bringt eine passende Qualitätsstufe mit")
|
||||
func bitrateCarriesQuality() {
|
||||
// Ohne videoQuality ignoriert Plex die Grenze bei manchen Fassungen.
|
||||
let request = TranscodeRequest(ratingKey: "1", maximumBitrate: 4_000, isLocal: false)
|
||||
#expect(value(request, "maxVideoBitrate") == "4000")
|
||||
#expect(value(request, "videoQuality") != nil)
|
||||
}
|
||||
|
||||
@Test("Höhere Bitraten ergeben höhere Qualitätsstufen")
|
||||
func qualityRisesWithBitrate() {
|
||||
func quality(_ bitrate: Int) -> Int {
|
||||
Int(value(TranscodeRequest(ratingKey: "1", maximumBitrate: bitrate, isLocal: true), "videoQuality") ?? "0")!
|
||||
}
|
||||
#expect(quality(2_000) < quality(8_000))
|
||||
#expect(quality(8_000) < quality(20_000))
|
||||
#expect(quality(20_000) <= 100)
|
||||
}
|
||||
|
||||
@Test("Ohne Grenze steht keine Bitrate in der Anfrage")
|
||||
func noBitrateWhenUnlimited() {
|
||||
// Ein leerer Wert ist für Plex nicht dasselbe wie ein fehlender — er
|
||||
// würde als Grenze von null gelesen.
|
||||
let request = TranscodeRequest(ratingKey: "1", isLocal: true)
|
||||
#expect(value(request, "maxVideoBitrate") == nil)
|
||||
#expect(value(request, "videoQuality") == nil)
|
||||
}
|
||||
|
||||
@Test("Lokal und außer Haus werden unterschieden")
|
||||
func distinguishesLocation() {
|
||||
#expect(value(TranscodeRequest(ratingKey: "1", isLocal: true), "location") == "lan")
|
||||
#expect(value(TranscodeRequest(ratingKey: "1", isLocal: false), "location") == "wan")
|
||||
}
|
||||
|
||||
@Test("Die Sitzungskennung steht in beiden Feldern")
|
||||
func sessionAppearsTwice() {
|
||||
// Plex erwartet sie sowohl als `session` als auch als
|
||||
// `X-Plex-Session-Identifier`. Fehlt eines, lässt sich der Transcoder
|
||||
// später nicht gezielt anhalten — und er rechnet minutenlang weiter.
|
||||
let request = TranscodeRequest(ratingKey: "1", isLocal: true, sessionIdentifier: "sitzung-1")
|
||||
#expect(value(request, "session") == "sitzung-1")
|
||||
#expect(value(request, "X-Plex-Session-Identifier") == "sitzung-1")
|
||||
}
|
||||
|
||||
@Test("Untertitel werden nur eingebrannt, wenn eine Spur gewählt ist")
|
||||
func subtitlesOnlyWhenRequested() {
|
||||
let without = TranscodeRequest(ratingKey: "1", isLocal: true)
|
||||
#expect(value(without, "subtitles") == nil)
|
||||
|
||||
let with = TranscodeRequest(ratingKey: "1", isLocal: true, subtitleStreamID: 310_024)
|
||||
#expect(value(with, "subtitles") == "burn")
|
||||
#expect(value(with, "subtitleStreamID") == "310024")
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Dauer in Plex-Einheiten")
|
||||
struct DurationConversionTests {
|
||||
@Test("Millisekunden werden ganzzahlig gerechnet")
|
||||
func millisecondsAreWholeNumbers() {
|
||||
#expect(Duration.seconds(90).milliseconds == 90_000)
|
||||
#expect(Duration.milliseconds(4_892_000).milliseconds == 4_892_000)
|
||||
#expect(Duration.zero.milliseconds == 0)
|
||||
}
|
||||
|
||||
@Test("Sekunden als Gleitkommazahl")
|
||||
func secondsAsDouble() {
|
||||
#expect(abs(Duration.milliseconds(1_500).seconds - 1.5) < 0.0001)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user