# Halo > Ein nativer Jellyfin-Client für iPhone, iPad, Mac und Apple TV — dunkel, > mit ausfahrbarer Seitenleiste und Offline-Downloads. Der Name ist ein Platzhalter. Siehe [`Halo/Branding.swift`](Halo/Branding.swift). ## Stand Alle sechs Phasen sind gebaut. Anmeldung, Blättern und Wiedergabe laufen am echten Server; der Rest ist unterschiedlich weit geprüft. Der Fahrplan steht in [`docs/superpowers/specs/2026-09-11-halo-design.md`](docs/superpowers/specs/2026-09-11-halo-design.md). | Phase | Inhalt | Stand | |---|---|---| | 0 | Projektgerüst, Design-Tokens, Navigation | ✅ | | 1 | Anmeldung, Bibliothek, DirectPlay | ✅ am Server geprüft, iPhone und iPad | | 2 | Transcoding, adaptive Bitrate, Spuren, Sync | ✅ Wiedergabe geprüft; Transcoding selbst noch nicht erzwungen | | 3 | Download-Engine und Offline-Wiedergabe | ⚠️ Downloads starten, Offline-Wiedergabe noch nicht bestätigt | | 4 | Download-Intelligenz | ⚠️ komprimierte Downloads ungeprüft | | 5 | Feinschliff und Personalisierung | ✅ gebaut, teilweise geprüft | | 6 | Live-TV mit EPG | ⚠️ Empfänger werden gefunden, Sender noch nicht bestätigt | „Ungeprüft" heißt: die Logik ist von Tests abgedeckt, an einem echten Bestand aber noch nicht gesehen. ### Was der Praxistest gelehrt hat Drei Fehler, die kein Test gefunden hätte, weil sie in Annahmen über fremde Bibliotheken steckten — sie stehen als Warnung im jeweiligen Quelltext: - **`URLSessionDownloadTask` gilt auch bei 404 als erfolgreich** und legt die Fehlerseite als Datei ab. - **Hintergrund-Sitzungen setzen abgerissene Übertragungen still bei null fort**, ohne es zu melden — gemessen wurden sieben Anläufe hintereinander. - **Jellyfins Transcode-Endpunkt bildet eine Tonspur je Sitzung ab.** Ein Download kommt deshalb einspurig an, und welche Spur es ist, entscheidet sich am Server. Der Grund, warum beim Herunterladen gefragt wird. ## Aufbau Die Xcode-Projektdatei wird aus `project.yml` erzeugt und ist **nicht** eingecheckt — das erspart Konflikte in der `pbxproj`. ```bash xcodegen generate open Halo.xcodeproj ``` > **Wichtig: Xcode vorher beenden.** > > `xcodegen generate` schreibt `project.pbxproj` neu. Ein geöffnetes Xcode > liest das nicht nach — es hält seinen eigenen Stand im Speicher und > überschreibt die frisch erzeugte Datei beim nächsten Sichern wieder. Neue > Dateien fehlen dann im Build, ohne dass irgendwo ein Fehler auftaucht. > > Das ist der Preis dafür, die Projektdatei aus Git herauszuhalten. Immer wenn > Dateien dazukommen oder `project.yml` sich ändert: Xcode mit ⌘Q beenden, > `xcodegen generate`, neu öffnen. > > Ob der Build aktuell ist, steht in der App unter **Einstellungen → > Diagnose** als Build-Datum. Der Code liegt zum größten Teil in lokalen Packages unter `Packages/HaloKit`. Das App-Target enthält nur Oberfläche und Navigation. | Modul | Aufgabe | Hängt ab von | |---|---|---| | `MediaKit` | Die serverneutrale Grundschicht: Typen und Rechnungen, die für jeden Server gelten. | — | | `JellyfinKit` | Anmeldung, Endpunkte, Modelle. Kein SwiftUI. | `MediaKit` | | `HaloDesign` | Farben, Typografie, wiederkehrende Bausteine. | — | | `HaloPlayback` | Wiedergabe-Abstraktion und die Entscheidung DirectPlay/Umpacken/Transcode. | `MediaKit` | | `HaloDownloads` | Warteschlange, Hintergrundübertragung, Offline-Bestand. | `MediaKit` | ### Warum umgepackt wird iOS spielt weder Matroska-Dateien noch DTS- oder TrueHD-Ton — und genau das liegt in typischen Beständen. `PlaybackDecider` prüft deshalb pro Datei, ob AVPlayer sie nativ öffnen kann, und lässt sie sonst **vom Server umpacken**: Bild und Ton wandern unverändert in eine MP4, nur die Schachtel wechselt. Das kostet den Server fast nichts — und die Wiedergabe bleibt im Systemplayer, also mit Bild-in-Bild, AirPlay und Systembedienung. Ein zweiter Player stand hier einmal. Er konnte zwar alles öffnen, brachte aber keines dieser drei Dinge mit. Ab Phase 2 kommt der dritte Weg dazu: Server-Transcoding. Dafür ist genau eine Funktion zu erweitern — `PlaybackDecider.decide(media:part:policy:)`. Die Oberfläche bleibt unberührt. ## Tests Die Logik-Tests laufen ohne Simulator, deshalb in Sekunden: ```bash cd Packages/HaloKit && swift test ``` Die wichtigste Testdatei ist `PlaybackDeciderTests` — sie hält fest, welche Datei auf welchem Weg landet, und schlägt an, sobald jemand `DeviceCapabilities` anfasst. Die Antwort-Vorlagen unter `Tests/JellyfinKitTests/Fixtures` sind echte Mitschnitte vom Server. Die Oberfläche wird von Hand im Simulator geprüft. ## Was noch am echten Server zu prüfen ist 1. **Komprimierte Downloads.** Offen: ob der Server bei den umgerechneten Stufen eine `Content-Length` mitschickt. Ohne sie greift beim Prüfen auf Vollständigkeit nur die weiche Regel, und ein Fortschrittsbalken ist eine Schätzung. Ein Fehlschlag erscheint immerhin als solcher — die Plausibilitätsprüfung in `DownloadCoordinator.implausibility` verwirft Fehlerseiten und abgerissene Übertragungen, statt sie als fertig zu verbuchen. 2. **Sprachkennungen nach dem Umpacken.** Jellyfin verliert sie: alle Spuren kommen als `und` an, obwohl die Quelle sie führt. Am fertigen Download gemessen, Ursache noch offen. 3. **Große Dateien im Hintergrund.** Das Verhalten mit einem 40-GB-Remux — vor allem Pausieren und Fortsetzen — ist nur gegen Testdaten geprüft. ## Voraussetzungen - Xcode 26 oder neuer - iOS 26.0 auf dem Zielgerät - XcodeGen (`brew install xcodegen`) ## Fragen stellen, ohne zu bauen Jede Vermutung über die Schnittstelle eines Servers ging bisher als TestFlight-Build hinaus und kam einen halben Tag später als Screenshot zurück. Irgendwann war das Upload-Kontingent des Tages aufgebraucht — für eine Frage, die keine Oberfläche braucht. ```bash cd Packages/HaloKit swift run HaloProbe --jellyfin # Welche Leiter baut der Server? swift run HaloProbe --spuren # Welche Tonspuren hat ein Titel? swift run HaloProbe --playlist # Was steht in dieser HLS-Leiter? ``` `--jellyfin` und `--spuren` fragen nach Benutzername und Passwort und legen den Token im Schlüsselbund dieses Rechners ab. `--playlist` braucht gar keine Anmeldung. Ausgegeben werden Pfade, Kennungen und die Antworten des Servers. Keine vollständigen Adressen, also auch kein Token.