Files
Flaro/Packages/FlixyKit/Tests/PlexKitTests/TranscodeRequestTests.swift
T
GuidoandClaude Opus 5 55bcfe2fbf 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>
2026-09-11 17:24:17 +02:00

102 lines
4.3 KiB
Swift

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)
}
}