Files
Flaro/Packages/FlixyKit/Tests/FlixyPlaybackTests/PlaybackDeciderTests.swift
T
ScarriffleandClaude Opus 5 f628819765 Player: eine gewählte Stufe wurde gar nicht umgerechnet
Der Decider rechnet nur um, wenn die Datei die Grenze überschreitet. Für
den Normalfall ist das richtig — für eine ausdrückliche Wahl im Player
nicht: wer für eine 8-Mbit-Datei „12 Mbit" wählt, bekam DirectPlay, weil
die Datei ja darunter liegt. Gemeint ist aber „rechne es mir auf diese
Stufe um", nicht „gib mir die Datei, falls sie so vorliegt".

Die Richtlinie kann eine Umrechnung jetzt erzwingen, und die Wahl im
Player tut das.

Dazu verschluckte `select(quality:)` den eigentlichen Grund: lehnte der
Server ab und sagte im Klartext warum, wurde diese Meldung danach durch
das allgemeine „keine abspielbare Datei" ersetzt — der eine brauchbare
Hinweis war damit weg. Jetzt bleibt stehen, was der Server gesagt hat.

Und das Menü war zu viel. Vorn steht die kurze Liste — automatisch,
Original, je eine brauchbare Stufe pro Auflösung, nur Ton. Wer eine
bestimmte Bitrate erzwingen muss, klappt „Alle Stufen" auf; im Alltag
will man „eine Stufe kleiner" und nicht die Wahl zwischen zehn und zwölf
Megabit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:05:27 +02:00

231 lines
9.5 KiB
Swift

import Testing
import PlexKit
@testable import FlixyPlayback
/// Die Tabelle, die festhält, welche Datei auf welchem Weg landet.
///
/// Sie ist der Grund, warum `DeviceCapabilities` nicht beiläufig angefasst
/// werden kann: jede Änderung dort schlägt hier sichtbar durch.
@Suite("Wiedergabe-Entscheidung")
struct PlaybackDeciderTests {
/// Baut eine Datei mit den angegebenen Eigenschaften.
private func media(
container: String,
video: String,
audio: String,
bitrate: Int? = nil
) -> (PlexMedia, PlexPart) {
let streams = [
PlexStream(id: 1, streamType: 1, codec: video),
PlexStream(id: 2, streamType: 2, codec: audio, selected: true),
]
let part = PlexPart(id: 10, key: "/library/parts/10/1/file", container: container, streams: streams)
let media = PlexMedia(
id: 100,
container: container,
videoCodec: video,
audioCodec: audio,
bitrate: bitrate,
parts: [part]
)
return (media, part)
}
// MARK: - Was AVPlayer nativ kann
@Test(
"Native Formate gehen an AVPlayer",
arguments: [
("mp4", "h264", "aac"),
("mp4", "hevc", "eac3"),
("mov", "h264", "ac3"),
("m4v", "hevc", "alac"),
// Groß-/Kleinschreibung darf keinen Unterschied machen — Plex ist
// darin nicht einheitlich.
("MP4", "H264", "AAC"),
]
)
func nativeFormatsUseAVPlayer(container: String, video: String, audio: String) {
let (media, part) = media(container: container, video: video, audio: audio)
let decision = PlaybackDecider().decide(media: media, part: part, policy: .directPlayOnly)
#expect(decision.method == .directPlay(engine: .avPlayer))
#expect(decision.rationale.contains(.nativelySupported))
}
// MARK: - Was VLC übernehmen muss
@Test("Matroska geht an VLC, auch bei nativen Codecs")
func matroskaFallsBackToVLC() {
let (media, part) = media(container: "mkv", video: "h264", audio: "aac")
let decision = PlaybackDecider().decide(media: media, part: part, policy: .directPlayOnly)
#expect(decision.method == .directPlay(engine: .vlc))
#expect(decision.rationale.contains(.containerUnsupported("mkv")))
}
@Test(
"Nicht unterstützter Ton geht an VLC",
arguments: ["dts", "dca", "truehd", "vorbis", "opus"]
)
func unsupportedAudioFallsBackToVLC(codec: String) {
let (media, part) = media(container: "mp4", video: "h264", audio: codec)
let decision = PlaybackDecider().decide(media: media, part: part, policy: .directPlayOnly)
#expect(decision.method == .directPlay(engine: .vlc))
#expect(decision.rationale.contains(.audioCodecUnsupported(codec)))
}
@Test(
"Nicht unterstütztes Bild geht an VLC",
arguments: ["av1", "vp9", "mpeg2video", "vc1", "wmv3"]
)
func unsupportedVideoFallsBackToVLC(codec: String) {
let (media, part) = media(container: "mp4", video: codec, audio: "aac")
let decision = PlaybackDecider().decide(media: media, part: part, policy: .directPlayOnly)
#expect(decision.method == .directPlay(engine: .vlc))
#expect(decision.rationale.contains(.videoCodecUnsupported(codec)))
}
@Test("Die gewählte Tonspur zählt, nicht die erste beste")
func decisionFollowsSelectedAudioTrack() {
// Deutsches AAC ist ausgewählt, englisches DTS liegt daneben. Würde
// der Decider nur die erste Tonspur ansehen, ginge diese Datei
// unnötig an VLC — und damit ohne Bild-in-Bild.
let streams = [
PlexStream(id: 1, streamType: 1, codec: "h264"),
PlexStream(id: 2, streamType: 2, codec: "dts", language: "English"),
PlexStream(id: 3, streamType: 2, codec: "aac", language: "Deutsch", selected: true),
]
let part = PlexPart(id: 10, key: "/p", container: "mp4", streams: streams)
let media = PlexMedia(id: 100, container: "mp4", videoCodec: "h264", audioCodec: "dts", parts: [part])
let decision = PlaybackDecider().decide(media: media, part: part, policy: .directPlayOnly)
#expect(decision.method == .directPlay(engine: .avPlayer))
}
@Test("Unanalysierte Dateien gehen an den toleranteren Engine")
func unknownStreamsFallBackToVLC() {
let part = PlexPart(id: 10, key: "/p", container: "mp4", streams: [])
let media = PlexMedia(id: 100, container: "mp4", parts: [part])
let decision = PlaybackDecider().decide(media: media, part: part, policy: .directPlayOnly)
#expect(decision.method == .directPlay(engine: .vlc))
#expect(decision.rationale.contains(.streamsUnknown))
}
// MARK: - Die Bandbreitengrenze
@Test("Über der Grenze wird umgerechnet, sobald das erlaubt ist")
func exceedingBitrateTranscodesWhenAllowed() {
let (media, part) = media(container: "mp4", video: "h264", audio: "aac", bitrate: 80_000)
let policy = PlaybackPolicy(allowsTranscoding: true, maximumBitrate: 8_000)
let decision = PlaybackDecider().decide(media: media, part: part, policy: policy)
#expect(decision.method == .transcode(maximumBitrate: 8_000))
#expect(decision.rationale.contains(.bitrateExceedsLimit(mediaBitrate: 80_000, limit: 8_000)))
}
@Test("Ohne Transcoder wird es trotzdem versucht — und der Grund vermerkt")
func exceedingBitrateStillPlaysInPhaseOne() {
// Das ist die Lage in Phase 1. Abbrechen wäre falsch: im WLAN läuft
// auch ein 80-Mbit-Remux, die Grenze ist nur eine Vorsichtsmaßnahme.
let (media, part) = media(container: "mp4", video: "h264", audio: "aac", bitrate: 80_000)
let policy = PlaybackPolicy(allowsTranscoding: false, maximumBitrate: 8_000)
let decision = PlaybackDecider().decide(media: media, part: part, policy: policy)
#expect(decision.isDirectPlay)
#expect(decision.rationale.contains(.transcodingUnavailable))
}
@Test("Unter der Grenze bleibt alles beim Direktabspielen")
func withinBitrateLimitPlaysDirectly() {
let (media, part) = media(container: "mp4", video: "h264", audio: "aac", bitrate: 4_000)
let policy = PlaybackPolicy(allowsTranscoding: true, maximumBitrate: 8_000)
let decision = PlaybackDecider().decide(media: media, part: part, policy: policy)
#expect(decision.method == .directPlay(engine: .avPlayer))
}
@Test("Ohne VLC übernimmt der Transcoder")
func withoutFallbackEngineTranscodingTakesOver() {
let (media, part) = media(container: "mkv", video: "h264", audio: "dts")
let policy = PlaybackPolicy(allowsTranscoding: true, allowsFallbackEngine: false)
let decision = PlaybackDecider().decide(media: media, part: part, policy: policy)
#expect(decision.method == .transcode(maximumBitrate: nil))
}
@Test("Der Transcode-Weg läuft über AVPlayer")
func transcodingUsesAVPlayer() {
// HLS ist genau die Kost, für die AVPlayer gebaut ist — inklusive
// automatischem Bitratenwechsel. Deshalb darf VLC hier nicht greifen.
let (media, part) = media(container: "mkv", video: "h264", audio: "dts")
let policy = PlaybackPolicy(allowsTranscoding: true, allowsFallbackEngine: false)
let decision = PlaybackDecider().decide(media: media, part: part, policy: policy)
#expect(decision.engine == .avPlayer)
}
@Test("Eine gewählte Stufe wird umgerechnet, auch wenn die Datei darunter liegt")
func honoursAnExplicitChoice() {
// Ohne das verpuffte die Wahl im Player: wer für eine 8-Mbit-Datei
// „12 Mbit" wählt, bekam DirectPlay, weil die Datei ja unter der
// Grenze liegt. Gemeint ist aber „rechne es mir auf diese Stufe um".
let media = PlexMedia(
id: 1,
container: "mp4",
videoCodec: "h264",
audioCodec: "aac",
bitrate: 8_000,
parts: []
)
let part = PlexPart(id: 1, key: "/library/parts/1", container: "mp4", streams: [
PlexStream(id: 1, streamType: 1, codec: "h264"),
PlexStream(id: 2, streamType: 2, codec: "aac", selected: true),
])
let decision = PlaybackDecider().decide(
media: media,
part: part,
policy: PlaybackPolicy(
allowsTranscoding: true,
allowsFallbackEngine: false,
maximumBitrate: 12_000,
forcesTranscoding: true
)
)
#expect(decision.method == .transcode(maximumBitrate: 12_000))
#expect(decision.rationale.contains(.qualityChosen))
}
@Test("Ohne ausdrückliche Wahl bleibt es bei DirectPlay")
func leavesAutomaticAlone() {
let media = PlexMedia(
id: 1,
container: "mp4",
videoCodec: "h264",
audioCodec: "aac",
bitrate: 8_000,
parts: []
)
let part = PlexPart(id: 1, key: "/library/parts/1", container: "mp4", streams: [
PlexStream(id: 1, streamType: 1, codec: "h264"),
PlexStream(id: 2, streamType: 2, codec: "aac", selected: true),
])
let decision = PlaybackDecider().decide(
media: media,
part: part,
policy: PlaybackPolicy(allowsTranscoding: true, maximumBitrate: 12_000)
)
#expect(decision.method == .directPlay(engine: .avPlayer))
}
}