Files
Flaro/Packages/FlixyKit/Tests/FlixyPlaybackTests/PlaybackDeciderTests.swift
T
ScarriffleandClaude Opus 5 12bb49615b Player: die Qualitätsleiter des Servers, Bitrate im Blick, Doppeltipp
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>
2026-09-12 21:50:39 +02:00

174 lines
7.4 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)
}
}