import Foundation import MediaKit /// Entscheidet, wie ein Titel abgespielt wird. /// /// Reine Rechnung ohne Netz und ohne Zustand — deshalb vollständig testbar. /// Die zugehörige Testtabelle in `PlaybackDeciderTests` ist die wichtigste im /// Projekt: sie hält fest, welche Datei auf welchem Weg landet, und schlägt /// an, sobald jemand die Fähigkeitenliste anfasst. public struct PlaybackDecider: Sendable { private let capabilities: DeviceCapabilities public init(capabilities: DeviceCapabilities = .avFoundation) { self.capabilities = capabilities } public func decide( profile: MediaProfile, policy: PlaybackPolicy ) -> PlaybackDecision { var rationale: [PlaybackDecision.Rationale] = [] // 0. Hat jemand ausdrücklich eine Stufe gewählt, wird umgerechnet — // auch wenn die Datei die Grenze gar nicht überschreitet. Sonst // bliebe die Wahl folgenlos: für eine 8-Mbit-Datei „12 Mbit" zu // wählen ergäbe DirectPlay, und am Bild änderte sich nichts. if policy.forcesTranscoding, policy.allowsTranscoding { rationale.append(.qualityChosen) return PlaybackDecision( method: .transcode(maximumBitrate: policy.maximumBitrate), rationale: rationale ) } // 1. Reicht die Leitung? Das kommt zuerst, weil es unabhängig vom // Format ist: eine 80-Mbit-Datei über Mobilfunk ist auch dann keine // gute Idee, wenn AVPlayer sie problemlos öffnen könnte. if let limit = policy.maximumBitrate, let bitrate = profile.bitrate, bitrate > limit { rationale.append(.bitrateExceedsLimit(mediaBitrate: bitrate, limit: limit)) if policy.allowsTranscoding { return PlaybackDecision(method: .transcode(maximumBitrate: limit), rationale: rationale) } // Ohne Transcoder bleibt nur, es trotzdem zu versuchen. Das ist // ehrlicher als eine Fehlermeldung: im WLAN klappt es meistens, // und der Grund steht in der Begründung. rationale.append(.transcodingUnavailable) } // 2. Kennt der Server die Datei überhaupt? let container = normalized(profile.container) guard profile.hasKnownStreams, let videoCodec = normalized(profile.videoCodec) else { rationale.append(.streamsUnknown) return fallback(policy: policy, rationale: rationale) } // 3. Kann AVPlayer das? var blockers: [PlaybackDecision.Rationale] = [] if let container, !capabilities.containers.contains(container) { blockers.append(.containerUnsupported(container)) } if !capabilities.videoCodecs.contains(videoCodec) { blockers.append(.videoCodecUnsupported(videoCodec)) } // Geprüft wird die Spur, die tatsächlich klingen soll — welche das // ist, hat der Aufrufer beim Bauen des Profils entschieden. if let audioCodec = normalized(profile.audioCodec), !capabilities.audioCodecs.contains(audioCodec) { blockers.append(.audioCodecUnsupported(audioCodec)) } guard blockers.isEmpty else { rationale.append(contentsOf: blockers) // **Steht nur die Schachtel im Weg, wird umgepackt.** // // Genau das ist der Blu-ray-Rip: Matroska aussen, H.264 oder HEVC // innen, AC3 dazu. AVPlayer könnte jeden dieser Ströme — er kommt // nur nicht an sie heran. Umpacken kostet den Server fast nichts // und verliert nichts, und der Titel landet danach bei AVPlayer // statt bei VLC: mit Bild-in-Bild, AirPlay und einer Datei, die // sich weit vorausladen lässt. // // Stimmt dagegen auch nur ein Codec nicht, hilft Umpacken nicht — // dann fehlt nicht der Zugang, sondern das Können. let nurDieSchachtel = blockers.allSatisfy { if case .containerUnsupported = $0 { return true } return false } if nurDieSchachtel, policy.allowsTranscoding, let container, DeviceCapabilities.remuxableContainers.contains(container) { return PlaybackDecision(method: .remux, rationale: rationale) } return fallback(policy: policy, rationale: rationale) } rationale.append(.nativelySupported) return PlaybackDecision(method: .directPlay(engine: .avPlayer), rationale: rationale) } /// Was passiert, wenn AVPlayer nicht kann. /// /// VLCKit hat Vorrang vor dem Transcoder: die Datei geht unverändert über /// die Leitung, der Server muss nicht rechnen, und die Bildqualität bleibt /// die des Originals. Umgerechnet wird erst, wenn auch VLC ausscheidet. private func fallback( policy: PlaybackPolicy, rationale: [PlaybackDecision.Rationale] ) -> PlaybackDecision { if policy.allowsFallbackEngine { return PlaybackDecision(method: .directPlay(engine: .vlc), rationale: rationale) } if policy.allowsTranscoding { return PlaybackDecision( method: .transcode(maximumBitrate: policy.maximumBitrate), rationale: rationale ) } var rationale = rationale rationale.append(.transcodingUnavailable) return PlaybackDecision(method: .directPlay(engine: .avPlayer), rationale: rationale) } /// Plex schreibt Codecs mal groß, mal klein, und hängt bei Containern /// gelegentlich Punkte an. Ein Vergleich ohne diese Angleichung geht /// sporadisch schief — und zwar nur bei manchen Dateien, was die Suche /// nach dem Grund unangenehm macht. private func normalized(_ value: String?) -> String? { guard let value, !value.isEmpty else { return nil } return value.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: ". ")) } }