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