Download-Knopf tat nichts
DownloadService.plan gab still nil zurück, wenn es keinen Auftrag
bauen konnte, und download() reichte das als nil weiter. Der Knopf sah
aus, als wäre er kaputt, dabei war der Grund jedes Mal bekannt. Es
gibt jetzt einen eigenen Ablehnungsgrund mit vier unterschiedlichen
Begründungen — keine Verbindung, keine Datei, kein abrufbarer Teil,
keine Adresse. Eine Funktion, die kommentarlos nichts tut, ist selbst
der Fehler.
Live-TV galt als „nicht eingerichtet"
Plex legt die Empfängerliste unter dem Schlüssel `Dvr` ab, die Hülle
kannte aber nur Metadata, Directory und Hub. Sie las eine leere Liste
und zog daraus die falsche Schlussfolgerung — auf einem Server, auf
dem ein Empfänger eingerichtet war. Dasselbe galt für die
Anbieterliste unter `MediaProvider`, ohne die es keinen
Programmführer gibt.
Beim Umbau wäre beinahe die Fehlerdiagnose verloren gegangen: mit
`try?` über alle Schlüssel verschwände auch ein echter Dekodierfehler
und ergäbe stillschweigend eine leere Liste. Aus „ein Feld heißt jetzt
anders" würde „die Mediathek ist leer". Jetzt wird erst geprüft, ob
der Schlüssel da ist, und dann ohne try? dekodiert. Fünf Tests halten
beides fest.
Qualität im Player wählbar
Automatisch, Original, 1080p, 720p, 480p. Der Eingriff wird genau dann
gebraucht, wenn es ruckelt — dafür will niemand den Film verlassen.
Ein Wechsel baut den Strom neu auf (die Stufe steckt in der Adresse),
merkt sich die Stelle und fährt sie wieder an. Der alte Transcoder
wird vorher angehalten, sonst rechnet der Server zwei Ströme für einen
Zuschauer.
AVPlayer-Anzeige entfernt
Sie war eine Prüfhilfe aus Phase 1. Dieselbe Angabe steht weiterhin
unter Einstellungen → Diagnose, wo sie hingehört.
137 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Crashlog vom Gerät:
_dispatch_assert_queue_fail
_swift_task_checkIsolatedSwift
MainActor.assumeIsolated
VLCPlayerEngine.mediaPlayerStateChanged(_:)
VLCKit input_thread_Events
Im Code stand als Begründung für assumeIsolated, VLCKit stelle seine
Rückrufe über die Hauptwarteschlange zu. Das ist falsch — es ruft den
Delegaten von seinem eigenen Input-Thread. Die App brach deshalb hart
ab, sobald VLC den ersten Zustandswechsel meldete, also unmittelbar
beim Drücken auf Play.
Alle drei Delegat-Methoden springen jetzt mit Task { @MainActor in }
sauber hinüber. Die Reihenfolge bleibt erhalten: die Aufgaben werden aus
demselben Thread nacheinander eingereiht.
Dieselbe Annahme steckte im AVPlayer-Motor in den KVO-Beobachtern —
KVO meldet auf dem Thread, der die Änderung ausgelöst hat, und AVPlayer
ändert seinen Zustand auch aus internen Threads. Ebenfalls umgestellt.
Die beiden Rückrufe mit ausdrücklichem queue: .main behalten
assumeIsolated: dort ist die Annahme belegt statt geraten, und der
Zeitbeobachter feuert viermal je Sekunde.
Dazu die Anmeldung: Plex führt den Rücksprung über forwardUrl nicht
zuverlässig aus. Auf dem iPad landet der Browser nach dem Anmelden auf
watch.plex.tv und bleibt stehen — der Token liegt bereit, aber die App
wartete auf einen Rückruf, der nie kam. Das Fenster wird jetzt geöffnet,
ohne darauf zu warten; parallel wird im Sekundentakt beim Server
nachgefragt. Der Rücksprung ist damit eine Annehmlichkeit, keine
Voraussetzung.
132 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kein Video lief jemals an
AVPlayerEngine.load() wartete auf asset.load(.duration) und die
Spurenliste, BEVOR es zurückkehrte — und erst danach rief das Modell
play(). AVFoundation muss für die Dauer aber so viel von der Datei
holen, bis es den Index findet, und der sitzt bei MP4 wie MKV oft am
Ende. Über eine langsame Leitung sind das Minuten, in denen nur ein
Ladekreis zu sehen war, obwohl der Player längst hätte anfangen können.
Jetzt kehrt load() zurück, sobald das Item hängt; Dauer und Spuren
kommen nach und melden sich über den Ereignisstrom.
Dazu AVURLAssetPreferPreciseDurationAndTimingKey: false — sonst
durchsucht AVFoundation bei manchen Formaten die ganze Datei.
Zweiter Fund an derselben Stelle: VLCMedia(url:) ist nullable. Ging es
schief, blieb media still nil, play() tat nichts und es gab keinen
Fehler — nur einen Ladekreis ohne Ende. Wird jetzt gemeldet.
Kein Querformat im Player
requestGeometryUpdate allein dreht nicht: das System dreht sofort
zurück, solange die erlaubten Ausrichtungen es nicht hergeben. Die
Erlaubnis kommt nur über den App-Delegaten, deshalb OrientationLock
als Brücke. Player dreht ins Querformat, beim Verlassen zurück. iPad
bleibt frei — dort ist Querformat beim Blättern der Normalfall.
Uhrzeit über dem Film
Die Statusleiste war nur ausgeblendet, solange die Bedienelemente weg
waren. Jetzt immer.
Sehr langer Start
Zwei Ursachen. Erstens legte der Initialisierer der App die
Hintergrund-URLSession an — die spricht über XPC mit nsurlsessiond und
verzögerte den allerersten Bildaufbau. Jetzt erst bei Bedarf, und wer
keine Downloads hat, braucht sie nie. Zweitens fragte jeder Start erst
plex.tv nach der Serverliste und suchte die Verbindung neu. Die zuletzt
funktionierende wird jetzt gemerkt (Server-Token im Schlüsselbund) und
mit kurzer Zeitgrenze zuerst probiert; die vollständige Suche greift
nur noch, wenn sie nicht mehr antwortet — etwa wenn man von zuhause
weggeht.
Dazu ein Hinweis im Player, wenn die Verbindung über die gedrosselte
Plex-Vermittlung läuft. Sonst sucht man den Fehler in der App.
132 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Decider brauchte keine Änderung — nur eine andere Richtlinie.
Genau dafür war er gebaut.
Transcoding
- TranscodeRequest baut die Parameter für /video/:/transcode/universal.
directStream=1 lässt Plex nur umpacken, wenn der Bildcodec passt,
statt pauschal neu zu kodieren.
- Vor dem Start wird der Server gefragt (decision); sagt er nein, steht
der Grund im Klartext da statt eines schwarzen Bildes.
- Beim Verlassen wird der Transcoder angehalten. Ohne dieses Signal
rechnet der Server minutenlang für niemanden weiter.
- Die adaptive Bitrate kommt mit HLS: AVPlayer wechselt sie selbst.
Qualitätseinstellungen
- Getrennt für WLAN und Mobilfunk, fünf spürbare Stufen.
Voreinstellung: zuhause Original, unterwegs 4 Mbit.
- NetworkConditions erkennt Mobilfunk über isExpensive — das deckt auch
den Hotspot ab, der technisch WLAN ist und trotzdem Volumen kostet.
Spuren und Marken
- Ton- und Untertitelwahl im Player, bevorzugte Sprache in den
Einstellungen, nachsichtiger Sprachvergleich (de/deu/ger/de-DE).
- Intro- und Abspann-Marken mit Überspringen-Knopf, optional
automatisch. Bereits übersprungene Marken werden gemerkt, sonst käme
man nach einem Rücksprung nie ins Intro zurück.
- Nächste Folge inkl. Staffelwechsel; Specials in Staffel 0 werden
nicht vorgezogen.
Fortschritt
- Alle zehn Sekunden an den Server, plus Schlussmeldung beim Verlassen.
Erst die nimmt den Titel aus „wird gerade abgespielt" heraus.
- markWatched/markUnwatched/rate/refreshLibrary stehen bereit.
86 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Anmelde-Adresse wurde doppelt kodiert
URLComponents.fragment kodiert beim Setzen erneut, aus %5B wurde
%255B. Plex antwortete mit „Diese Anfrage konnte nicht abgeschlossen
werden". Die Adresse wird jetzt von Hand zusammengesetzt.
Verbindungssuche hing bis zu anderthalb Minuten
probeTimeout war deklariert, aber nirgends angewendet — es galt der
URLSession-Standard von 60 Sekunden. Zusammen mit der Prüfung Rang
für Rang kostete eine schweigende lokale Adresse die volle Zeit,
bevor der direkte Weg überhaupt begann. Jetzt laufen alle Adressen
gleichzeitig, mit Zeitgrenze, und die Rangordnung entscheidet unter
den erreichbaren.
Kein Zurück während des Ladens
Die Kopfzeile mit dem Schließen-Knopf wurde nur im Zustand .playing
gezeichnet. Bei langsamer Verbindung saß man ohne Ausweg vor einem
Ladekreis. Sie liegt jetzt als eigene Ebene darüber und erscheint
auch beim Vorbereiten — samt Titel, damit erkennbar bleibt, worauf
gewartet wird.
Sechs neue Tests, darunter drei, die die Zeitgrenze der
Verbindungssuche an einer schweigenden Adresse nachstellen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Datenschicht:
- plex.tv-Anmeldung über das PIN-Verfahren, Token im Schlüsselbund
- Serversuche und Verbindungsauswahl nach Rang (lokal vor direkt vor
Vermittlung), mit Abgleich der Server-Kennung gegen veraltete
plex.direct-Adressen
- PlexServer als einzige Stelle, an der die App Endpunkte kennt:
Mediatheken, Hubs, seitenweises Blättern mit Filtern, Metadaten,
Staffeln/Folgen, Suche
Die Oberfläche:
- Startseite mit Aufmacher und den Reihen vom Server
- Mediathek als Raster oder Liste, mit Filter- und Sortierblatt
- Detailseiten für Filme, Folgen, Serien und Staffeln inkl. Besetzung,
Fassungsauswahl und aufklappbaren technischen Angaben
- Suche mit gemerkten Anfragen
- Vollbildplayer mit eigenem Chrome
Der Player:
- AVPlayerEngine und VLCPlayerEngine hinter einem gemeinsamen Protokoll
- VLCKit liegt in einem eigenen Package (FlixyVLC), damit `swift test`
in FlixyKit nicht das 920-MB-Binärpaket mitzieht
Am echten plex.tv geprüft: der Anmeldecode kommt an. Dabei fiel auf,
dass `strong=true` einen dreißigstelligen Code liefert, der sich bei
plex.tv/link nicht eintippen lässt — jetzt der kurze.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Legt das Fundament für den Plex-Client an:
- XcodeGen-Projekt nach dem Muster von Onyx (com.scarriffleservices,
strict concurrency, Projektdatei nicht eingecheckt)
- FlixyKit mit vier Modulen: PlexKit, FlixyDesign, FlixyPlayback,
FlixyDownloads. macOS steht als Plattform mit drin, damit die
Logik-Tests ohne Simulator laufen.
- FlixyDesign: dunkle Farb-Tokens, Schriftskala, Poster- und
Querformat-Kacheln, Aufmacher-Bild. Die Akzentfarbe fließt über die
Umgebung und wird per WCAG-Rechnung lesbar gehalten.
- PlexKit: Kopfzeilen-Identität, Transport-Protokoll, Fehlerarten und
die Medienmodelle samt nachsichtigem Decoder.
- FlixyPlayback: VideoPlayerEngine-Protokoll und PlaybackDecider — die
eine Funktion, an der Phase 2 das Transcoding andockt.
- Ausfahrbare Seitenleiste auf dem iPhone, Split-View auf dem iPad.
- 45 Tests, alle grün.
VLCKit 4.0.0-a24 ist vorab geprüft: baut über SPM unter Swift 6 mit
strict concurrency. Die iOS-Slice ist mit 239 MB allerdings gewichtig,
deshalb bekommt sie in Phase 1 ein eigenes Target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>