Files
Flaro/Flixy/Session/PlaybackDiagnostics.swift
T
ScarriffleandClaude Opus 5 6b927c21b0 VLC ist weg — es gibt nur noch einen Spieler
Du hattest recht mit der Erwartung, nur nicht mit dem Stand: entfernt war er
nicht. Was wir gemacht haben, war, ihm Arbeit wegzunehmen — DTS, TrueHD und
Vorbis gehen seit Kurzem über das Umpacken beim Server. Übrig blieb er an
einer Stelle, `PlaybackDecider.fallback`.

Jetzt ist er ganz weg, samt dem 920-MB-Binärpaket.

**Was an seine Stelle tritt.** Die Entscheidung sagt nicht mehr, *wer* spielt,
sondern *was ankommt*: direkt, umgepackt oder umgerechnet. `PlaybackEngineKind`
fällt damit weg, `directPlay` trägt keine Engine mehr, und `PlaybackPolicy`
kennt kein `allowsFallbackEngine`.

**Das Auffangnetz bleibt, nur anders geknüpft.** Scheiterte das Umpacken am
Player, sprang bisher VLC ein — der Server entscheidet erst beim Ausliefern,
ob er die Ströme wirklich kopieren kann, und manche Antworten nimmt AVPlayer
nicht an. Statt der zweiten Engine rechnet der Server jetzt neu
(`fallBackToTranscode`). Teurer für ihn, aber es kommt etwas an, statt einer
schwarzen Wand mit einer Fehlernummer.

**Offline wäre das ein Rückschritt gewesen** — dort gibt es keinen Server, der
einspringt. Deshalb ging der vorige Commit voraus: was heruntergeladen wird,
kommt schon in einer Hülle, die der Systemplayer öffnet.

Was dadurch überall gilt, auch für Dateien, die bisher bei VLC landeten:
Bild-in-Bild, AirPlay, Bedienung im Sperrbildschirm — und dasselbe Verhalten
auf dem Apple TV, wo es VLC ohnehin nie gab.

Die Attrappe heißt ihren Schalter jetzt `--dts` statt `--vlc`: der Fall ist
geblieben, sein Ziel nicht.

iOS und tvOS bauen, 494 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:04:23 +02:00

88 lines
2.9 KiB
Swift

import Foundation
import Observation
import FlixyPlayback
/// Hält fest, was beim letzten Abspielversuch passiert ist.
///
/// Der Player verschwindet, wenn man ihn schließt — und mit ihm jeder Hinweis
/// darauf, woran es lag. Dieser Mitschnitt überlebt das und steht in den
/// Einstellungen. Aus „es läuft nicht" wird damit „umgepackt, lokale
/// Verbindung, hängt bei Öffnen" — ein Unterschied, der Stunden spart.
@MainActor
@Observable
final class PlaybackDiagnostics {
struct Attempt: Identifiable {
let id = UUID()
let startedAt: Date
var title: String
var method: String
var connection: String
/// Die Adresse ohne Abfrageteil.
///
/// Der Token steckt in der Abfrage. Er hat in einem Protokoll nichts
/// zu suchen — es landet in Screenshots, und wer ihn hat, kommt an die
/// ganze Mediathek.
var sourceDescription: String
var steps: [String] = []
var outcome: String?
}
private(set) var lastAttempt: Attempt?
func begin(title: String, connection: String?) {
lastAttempt = Attempt(
startedAt: Date(),
title: title,
method: "—",
connection: connection ?? "—",
sourceDescription: "—"
)
note("Player geöffnet")
}
func record(decision: PlaybackDecision, source: URL) {
lastAttempt?.method = Self.describe(decision)
lastAttempt?.sourceDescription = Self.describe(source)
note("Quelle gewählt")
}
/// Ein Zwischenschritt, mit Zeitabstand zum Beginn.
///
/// Der Abstand ist die eigentliche Auskunft: steht zwischen „Quelle
/// gewählt" und „spielt" eine Minute, liegt es an der Leitung; kommt
/// „spielt" gar nicht, am Engine.
func note(_ step: String) {
guard var attempt = lastAttempt else { return }
let elapsed = Date().timeIntervalSince(attempt.startedAt)
attempt.steps.append(String(format: "%.1fs %@", elapsed, step))
lastAttempt = attempt
}
func finish(outcome: String) {
lastAttempt?.outcome = outcome
note(outcome)
}
private static func describe(_ decision: PlaybackDecision) -> String {
switch decision.method {
case .directPlay:
"Direkt"
case .remux:
decision.rationale.contains { if case .audioCodecUnsupported = $0 { true } else { false } }
? "Umgepackt · Bild unverändert, Ton umgerechnet"
: "Umgepackt · MP4, ohne Neukodierung"
case .transcode(let limit):
limit.map { "Transcode · max. \($0 / 1000) Mbit" } ?? "Transcode"
}
}
private static func describe(_ url: URL) -> String {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return "—"
}
components.query = nil
return components.string ?? "—"
}
}