import SwiftUI import HaloDesign import HaloPlayback import MediaKit /// Die aufklappbaren technischen Angaben zu einer Datei. /// /// Zeigt nicht nur, was in der Datei steckt, sondern auch, wie Halo sie /// abspielen wird und warum. Bei der Frage „warum ruckelt das" ist das die /// erste Stelle zum Nachsehen — und in Phase 1, wo es noch kein Transcoding /// gibt, erklärt es, warum bei manchen Dateien kein Bild-in-Bild angeboten wird. struct TechnicalDetails: View { let media: MediaVersion let part: MediaFile @Binding var isExpanded: Bool /// Was mit dieser Datei passieren würde. /// /// **Mit Serverhilfe gerechnet, nicht ohne.** Hier stand `.directPlayOnly` /// — damals eine brauchbare Auskunft, weil ohne Serverhilfe die zweite /// Engine übernahm und auch so hiess. Seit es sie nicht mehr gibt, kam /// dort „Direkt (Systemplayer)" heraus, direkt über der Zeile „iOS öffnet /// den Container MKV nicht" — zwei Angaben, die sich widersprechen, und /// die zweite stimmte. /// /// Ohne Bandbreitengrenze, weil sie von der Verbindung abhängt und diese /// Seite sie nicht kennt. Gezeigt wird damit der Fall im WLAN. private var decision: PlaybackDecision { PlaybackDecider().decide( profile: media.profile(file: part), policy: PlaybackPolicy(allowsTranscoding: true) ) } var body: some View { DisclosureGroup(isExpanded: $isExpanded) { VStack(alignment: .leading, spacing: HaloSpacing.snug) { row("technical.playbackMethod", playbackMethodLabel) if let reason = rationaleLabel { row("technical.reason", reason) } Divider().overlay(Color.haloSeparator).padding(.vertical, HaloSpacing.tight) if let container = part.container { row("technical.container", container.uppercased()) } if let codec = media.videoCodec { row("technical.videoCodec", codec.uppercased()) } if let width = media.width, let height = media.height { row("technical.resolution", "\(width) × \(height)") } if let bitrate = media.bitrate { row("technical.bitrate", "\(bitrate / 1000) Mbit/s") } if let size = part.size { row("technical.size", size.formatted(.byteCount(style: .file))) } if !part.audioStreams.isEmpty { Divider().overlay(Color.haloSeparator).padding(.vertical, HaloSpacing.tight) ForEach(part.audioStreams) { stream in row("technical.audio", trackLabel(stream)) } } if !part.subtitleStreams.isEmpty { ForEach(part.subtitleStreams) { stream in row("technical.subtitle", trackLabel(stream)) } } if let file = part.file { Divider().overlay(Color.haloSeparator).padding(.vertical, HaloSpacing.tight) VStack(alignment: .leading, spacing: 2) { Text("technical.file") .font(.haloCaption) .foregroundStyle(Color.haloTextMuted) Text(file) .font(.system(.caption, design: .monospaced)) .foregroundStyle(Color.haloTextSecondary) .textSelection(.enabled) } } } .padding(.top, HaloSpacing.snug) } label: { Text("technical.title") .font(.haloRowHeader) .foregroundStyle(Color.haloTextPrimary) } .tint(Color.haloTextSecondary) } private func row(_ label: LocalizedStringKey, _ value: String) -> some View { HStack(alignment: .top) { Text(label) .font(.haloCaption) .foregroundStyle(Color.haloTextMuted) Spacer() Text(value) .font(.haloCaption) .foregroundStyle(Color.haloTextSecondary) .multilineTextAlignment(.trailing) } } private func trackLabel(_ stream: MediaStream) -> String { stream.displayTitle ?? [stream.language, stream.codec?.uppercased()] .compactMap(\.self) .joined(separator: " · ") } private var playbackMethodLabel: String { switch decision.method { case .directPlay: String(localized: "technical.directPlayNative") // **Zwei Fälle unter einem Namen wären eine Lüge.** Umgepackt bleibt // das Bild unberührt — der Ton nur dann, wenn AVPlayer ihn kennt. case .remux: decision.rationale.contains { if case .audioCodecUnsupported = $0 { true } else { false } } ? String(localized: "technical.remuxAudio") : String(localized: "technical.remux") case .transcode: String(localized: "technical.transcode") } } /// Der erste Grund, der gegen den Systemplayer sprach — mehrere /// aufzuzählen hilft niemandem, es reicht einer. private var rationaleLabel: String? { for reason in decision.rationale { switch reason { case .containerUnsupported(let container): return String(localized: "technical.reason.container \(container.uppercased())") case .videoCodecUnsupported(let codec): return String(localized: "technical.reason.videoCodec \(codec.uppercased())") case .audioCodecUnsupported(let codec): return String(localized: "technical.reason.audioCodec \(codec.uppercased())") case .streamsUnknown: return String(localized: "technical.reason.unanalyzed") case .qualityChosen: return String(localized: "technical.reason.chosen") case .nativelySupported, .bitrateExceedsLimit, .transcodingUnavailable: continue } } return nil } }