c37087fd3453afdb8e6b64e72da68e1ea1ff609d
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c37087fd34 |
Auf dem Telefon stand ein Plakat im Aufmacher — zweimal derselbe Name
Der Aufmacher auf dem iPhone hat nicht das Hintergrundbild geholt,
sondern das **Plakat**. Dafür gab es einen Grund, und er stimmte: ein
16:9-Bild in einem 2:3-Rahmen ist ein Streifen aus der Mitte.
Nur war der Rahmen das Problem, nicht das Bild. Ein Plakat trägt den
Titel selbst, und darüber legt der Aufmacher noch den freigestellten
Schriftzug — derselbe Name zweimal, einmal gesetzt und einmal gemalt.
**Jetzt ein Querbild in einem 4:3-Rahmen.** 16:9 ginge nicht: das sind
auf 440 Punkten Breite 248 Punkte Höhe, und darin sollen Schriftzug,
Zeile, ein Satz Beschreibung und zwei Knöpfe stehen. Sie standen schon
einmal übereinander, das ist der dokumentierte Grund für das Hochformat.
4:3 gibt 330 Punkte — gemessen passt der Text hinein; dafür fehlt dem
Bild links und rechts zusammen ein Viertel.
**Die Detailseite bleibt hochkant.** Dort hat man sich schon entschieden;
das Plakat ist das Bild dieses Films, und du wolltest den Kopf der Seite
ausdrücklich höher. Beide Seiten lasen bisher aus **derselben**
Funktion — eine Änderung für den Aufmacher hätte die Detailseite von 660
auf 330 Punkte gestutzt, ohne dass irgendwo ein Fehler aufgetaucht wäre.
`HeroArtwork` kennt deshalb jetzt den Verwendungszweck:
HeroArtwork.aspectRatio(.detailHeader, compact: …) // 2:3
HeroArtwork.aspectRatio(compact: …) // 4:3 bzw. 16:9
Maße, Verhältnis und Reihenfolge der Bildpfade hängen alle an **einer**
privaten Abfrage — sie müssen zusammenpassen, sonst beschneidet der
Server anders als der Rahmen.
Die Maße schmal: 1200×900 statt 1920×1080. Der Rahmen ist dort höchstens
1320 Bildpunkte breit; das spart auf einer Mobilfunkverbindung ein
Drittel.
Gemessen im Simulator (iPhone 17 Pro Max): Aufmacher 4:3, Text passt,
Detailseite unverändert hochkant. iOS und tvOS bauen, 476 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2ab05ee8f6 |
Der Schriftzug gehörte zum vorigen Film — ein Zustand ohne Adresse
„Werner Beinhart" über dem Bild von John Wick. Von mir eingebaut, im
Commit, der die Überblendung umgestellt hat.
**Der Grund ist der Umbau selbst.** Vorher bekam der Aufmacher je Titel
eine neue Identität; sein Zustand begann damit jedes Mal von vorn. Seit
er bleibt und nur seine Angaben wechseln, überlebt auch der geladene
Schriftzug den Wechsel — und `availableLogo` gab ihn heraus, ohne zu
prüfen, ob er überhaupt zum aktuellen Titel gehört:
if let logo { return logo } // ← zu irgendeinem Film
Jetzt trägt der Zustand die Adresse mit, zu der er gehört, und wird nur
herausgegeben, wenn sie zur aktuellen passt. Passt sie nicht, gilt der
Zwischenspeicher — und wenn auch der nichts hat, der gesetzte Titel. Nie
der falsche Schriftzug.
**Dieselbe Falle steckte in `FlixyArtwork`**, nur unauffälliger: auch
dort hielt `loaded` ein Bild ohne Angabe, wozu es gehört, und `load()`
brach deswegen früh ab („ist ja schon eins da"). Solange jede Kachel ihre
eigene Identität hat, fällt das nicht auf. Der Aufmacher hat seit dem
Umbau keine mehr.
Die Regel dahinter gilt allgemein: **ein Zustand, der etwas
zwischenspeichert, muss sagen, wozu es gehört** — sonst gilt er für
alles.
Die Attrappe liefert jetzt Schriftzüge (`Logo`-Bildmarke). Ohne sie fällt
der Aufmacher auf den gesetzten Titel zurück, und dieser ganze Weg wäre
im Simulator gar nicht zu sehen — genau deshalb ist der Fehler erst beim
Nutzer aufgefallen.
iOS und tvOS bauen, 476 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
37bb153dd8 |
Der Schriftzug wandert wieder mit — über dem Verlauf statt in ihm
Im Commit davor blendete nur noch das Bild über; Schriftzug, Zeile und
Knöpfe standen still und wechselten schlagartig. Das war zu viel
weggenommen: die Bewegung gehört dazu.
Jetzt wandert der Text wieder mit — aber an der richtigen Stelle im
Stapel. Der Aufmacher zeichnet seinen Inhalt während eines Wechsels
zweimal: den weichenden deckend, den kommenden mit demselben Einblenden
und demselben Hereinrutschen wie das Bild darunter. Beides liegt **über**
dem Verlauf, und dort kostet es ihn nichts.
Der Unterschied zum alten Aufbau ist genau diese Reihenfolge. Vorher
lagen zwei **vollständige** Aufmacher übereinander, jeder mit eigenem
Verlauf — und zwei halbdurchsichtige Verläufe ergeben zusammen keinen
ganzen. Jetzt gibt es einen Verlauf, einmal, in voller Stärke.
Nachgemessen über einen auf sechs Sekunden gestreckten Wechsel, an
derselben Stelle im Bild:
ursprünglich 68 .. 154
jetzt 63 .. 75
Dafür ist der Textsatz in `HeroBanner` eine eigene Funktion geworden —
es gibt ihn während eines Wechsels zweimal — und `HeroSection.metadata`
ist `static`, weil die Zeile auch für den weichenden Titel gebraucht
wird. `titleView` und `plainTitle` sind darin aufgegangen.
iOS und tvOS bauen, 476 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
63c664e5a9 |
Der Aufmacher hellte beim Wechsel auf — zwei Verläufe ergeben keinen
Beim Wechsel im Karussell war der Verlauf nach unten für einen Moment
weg: das Bild stand bis unten hell da, und erst nach dem Übergang kam das
Abdunkeln zurück.
**Der Grund steckt in der Form der Überblendung.** Das Karussell legte
zwei vollständige Aufmacher übereinander und blendete den oberen ein. Nur
bringt jeder von beiden seinen eigenen Verlauf mit — und zwei
halbdurchsichtige Verläufe ergeben zusammen keinen ganzen.
Gemessen, an derselben Stelle im Bild, über einen auf sechs Sekunden
gestreckten Wechsel:
Ruhezustand 68 .. 75
Mitte des Wechsels 137 ← fast doppelt so hell
Zwei naheliegende Reparaturen habe ich davor versucht und verworfen,
weil die Messung sie widerlegt hat: dem weichenden Aufmacher die
Animation zu nehmen (wurde schlechter, 152) und beide Ebenen
gegenläufig zu blenden (brachte einen dunklen Einbruch auf 35 dazu).
Beide behandelten das Symptom, nicht die Form.
**Jetzt wechselt nur das Bild.** `HeroBanner` nimmt zusätzlich die
weichende Bildadresse entgegen und blendet **innen** über; Verlauf,
Schriftzug und Knöpfe liegen einmal darüber und ändern sich dabei nicht.
Damit kann der Verlauf gar nicht mehr schwächer werden — nicht weil es
jetzt richtig gerechnet wird, sondern weil es nur noch einen gibt.
Nachgemessen, derselbe Wechsel:
vorher 68 .. 154
jetzt 63 .. 75
Die Aufmacher-Ansicht behält dabei ihre Identität, statt je Titel eine
neue zu bekommen: eine neue hiesse Einfügen und Entfernen, und SwiftUI
blendete sie dann zusätzlich ein — noch eine Überblendung über der
gemeinten.
iOS und tvOS bauen, 476 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
a665d79e6d |
Antippen spielt ab — und vier Dinge, die daneben lagen
**Antippen startet, langes Drücken führt auf die Detailseite.** Am Telefon und am iPad; am Mac bleibt der Klick die Seite, denn dort gibt es den Abspielknopf beim Überfahren und langes Drücken ist keine übliche Geste. Die Hürde war nicht die Geste, sondern das Schieben: ein `NavigationLink` schiebt selbst, ein Menüeintrag kann es nicht. Der Stapel ist deshalb ein eigener Typ mit Pfad (`MediaStack`) und reicht eine Schiebefunktion über die Umgebung nach innen — denselben Weg nimmt schon die Meldung, dass sich am Server etwas geändert hat. **Zwei Knöpfe übereinander auf der Detailseite.** Das Monogramm hing als Überzug an `mediaStack` — also über **jedem** Bildschirm dieses Stapels, auch den geschobenen, und dort über deren Drei-Punkte-Menü. Es gilt jetzt für die Startseite und endet mit ihr. **Der Kopf der Detailseite war viel zu flach.** Dort stand kein Seitenverhältnis, also galt die Vorgabe 16∶9 — während `HeroArtwork` auf einem schmalen Gerät ein **Plakat** anfordert, also hochkant. Zu sehen war ein Streifen aus der Mitte. Der Aufmacher auf der Startseite macht es seit Langem richtig; die beiden Detailseiten nicht. **Die Mediathekenleiste stand linksbündig in einer halbleeren Kapsel.** Jetzt ist sie so breit wie ihr Inhalt und mittig. `ViewThatFits` nimmt die ungescrollte Reihe, solange sie passt, sonst die scrollbare — damit steht nirgends eine Zahl, ab wann es kippt. **Und der Grund, warum die Mediathekenauswahl beim Nutzer nie griff:** Plex. Dessen Hubs sagten nicht, aus welcher Mediathek sie stammen, also warf der Filter jede Reihe weg und fiel auf „alles" zurück — ein Schalter, der nichts tut. `PlexHub` liest jetzt `librarySectionID`, als Zahl oder als Zeichenkette, je nachdem was der Server liefert. Vier Tests dazu. Das ist **kein** Wiederaufnehmen der Plex-Arbeit: es repariert eine Funktion, die dort gelogen hat. Neues kommt weiterhin nicht dazu. iOS und tvOS bauen, 476 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1fa8011094 |
Eine Mediathek fehlte ganz — „Heimvideos" lagen bei den Fotos
Jellyfin nennt „Heimvideos & Fotos" `collectionType: homevideos`. Die Zuordnung legte das zusammen mit `photos` auf `.photo` — und damit fiel die ganze Mediathek unter `MediaLibrary.isSupported` heraus: keine Reihe auf der Startseite, kein Eintrag in der Mediathekenleiste, nicht einmal eine Zeile in der Auswahl für den Aufmacher. Aus Sicht der App gab es sie nicht. Dass Fotos im Namen stehen, macht den Inhalt nicht zu Fotos. `photos` bleibt bei den Fotos, `homevideos` ist jetzt `.other` — wie alles, was der Server nicht einsortiert. Die Zuordnung steht dafür nicht mehr mitten in `libraries()`, sondern als eigene Funktion mit Tests. Sie entscheidet mehr, als sie aussieht: was hier als `.photo` oder `.music` landet, ist in der App unsichtbar, und das sieht aus wie ein Server, der nichts liefert — nicht wie eine Zuordnung, die zu scharf ist. Drei Tests halten es fest, darunter der Fall ohne Angabe: eine gemischte Mediathek meldet gar keine Art und muss trotzdem bleiben. iOS und tvOS bauen, 472 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0a59b8e82b |
Die Attrappe unterscheidet die Mediatheken — und der Aufmacher sagt, wenn eine Auswahl ins Leere greift
Gemessen, weil „es tut nichts" und „es tut etwas anderes" von aussen
gleich aussehen. Gegen die Attrappe, mit laufendem Mitschnitt:
Auswahl ["lib-shows","lib-movies"] → übrig [latest.lib-movies, latest.lib-shows]
Auswahl ["lib-shows"] → übrig [latest.lib-shows]
Und der Aufmacher wechselte dabei **sofort** von „Nachtzug nach Genf"
(Film) zu „Küstenwache" (Serie) — ohne Neustart, mit offener App. Der
Weg stimmt also; was gemeldet wurde, muss eine andere Ursache haben.
Zwei Dinge, die diese Messung erst möglich gemacht haben:
**Die Attrappe hatte in jeder Mediathek dasselbe stehen.** „Neu in Filme",
„Neu in Serien" und „Neu in Gemischtes" lieferten dieselben Titel — damit
sieht ein wirkungsloser Filter genauso aus wie ein wirksamer. Jetzt gibt
es zwei Titel, die es **nur** in „Gemischtes" gibt, und jede Mediathek
führt, was zu ihr gehört.
**Ein Zwischenergebnis, das ich fast für den Fehler gehalten hätte:** ein
Neuinstallieren über `simctl install` löscht die Voreinstellungen nicht.
Die vermeintlich frische Auswahl war in Wahrheit die alte plus die neue —
zwei Messungen lang.
**Und der Punkt, der dem gemeldeten Verhalten am nächsten kommt:** trifft
die Auswahl keine einzige Reihe, gilt sie seit gestern als wirkungslos und
der Aufmacher zeigt alles. Das verhindert einen leeren Aufmacher, sieht
aber genau aus wie ein Schalter, der nichts tut. Jetzt steht es dort, wo
man es einstellt: ein Hinweis unter der Auswahl, sobald für die
angehakten Mediatheken keine eigene Reihe auf der Startseite existiert.
Welche Mediatheken überhaupt eine Reihe stellen, wird dafür aus den
geladenen Reihen gelesen, nicht angenommen.
iOS und tvOS bauen, 469 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7f380e5f4d |
Die Bibliothek beginnt jetzt in einer Mediathek
Vorher stand dort eine Liste aller Mediatheken: auf „Bibliothek" tippen, dann auf „Filme", dann endlich ein Raster. In eine andere wechseln hiess zurück und wieder hinein. Der Zwischenbildschirm hat nie etwas erzählt, was eine Leiste nicht auch zeigen kann — er war ein Platzproblem, kein Entwurf. Jetzt steht man sofort in einer Mediathek, und darüber der unteren Leiste liegt eine zweite mit den Mediatheken und Live-TV. Auf dem iPad und am Mac ändert sich nichts: dort steht jede Mediathek einzeln in der Seitenleiste, auf dem Fernseher nebeneinander in der oberen Leiste — es ist dieselbe Idee in der Form, die ein Telefon hergibt. **Warum eine zweite Leiste und nicht Plätze in der unteren.** Die hat fünf, und alles darüber schiebt das System in ein „Mehr"-Verzeichnis, dessen Einträge zwei Navigationsleisten übereinander tragen. Genau daran ist der Zwischenbildschirm seinerzeit entstanden. Eine selbst gezeichnete Leiste kennt diese Grenze nicht — und sie sieht aus wie die Insel auf dem iPad, weil sie dieselbe Fläche benutzt: `FlixySidebar.surface(in:)` ist ein Bauteil, kein nachgebautes Aussehen, und wird jetzt zum dritten Mal gerufen. Als Sicherheitsrand und nicht als Überzug, damit die Liste über der Leiste endet statt dahinter zu verschwinden. Bei mehr als vier Mediatheken wird sie waagerecht scrollbar. **Das Monogramm steht jetzt in der Werkzeugleiste**, am rechten Ende, auf derselben Höhe wie Suche, Filter und Ansicht — die rücken dafür nach links. Als Überzug lag es über dem Inhalt, auf einem Raster also über einem Plakat. Dass es dabei an den Anfang der Reihe rutschte, hat einen Grund, der im Quelltext steht: SwiftUI reiht Werkzeugleisten von aussen nach innen, ein von aussen angehängter Eintrag steht deshalb **vor** den übrigen. Wo in der Reihe etwas steht, kann nur der entscheiden, der die Reihe schreibt. Auf der Startseite bleibt es ein Überzug — dort gibt es keine Navigationsleiste. Dazu: die Zuordnung Mediathekenart → Symbol stand dreimal im Projekt, eine davon falsch (jede Mediathek bekam „film", auch eine mit Serien). Jetzt einmal, neben den übrigen Ableitungen aus den Modellen. Geprüft im Simulator: „Bibliothek" öffnet sofort das Raster, die Leiste wechselt zwischen Filme, Serien, Gemischtes und Live-TV, und das Monogramm steht ganz rechts in der Werkzeugleiste. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0aa11e1c25 |
Die Einstellungen verlassen die Leiste — oben rechts steht jetzt das Konto
Auf dem iPhone belegten die Einstellungen einen von fünf Plätzen in der unteren Leiste, gleichrangig neben Start und Bibliothek. Das unterstellt, dass man zwischen ihnen hin und her wechselt, und das tut niemand. Jetzt stehen dort drei Einträge — Start, Bibliothek, Downloads — und der Weg in die Einstellungen führt über das Monogramm oben rechts. Auf dem iPad und am Mac bleibt alles, wie es war: dort hat die Seitenleiste Platz, und den Weg über das Konto gibt es ohnehin schon. Drei Entscheidungen, die nicht offensichtlich sind: **Ein Überzug, kein Werkzeugleisten-Knopf.** Die Startseite schaltet ihre Navigationsleiste ganz ab — sie war leer und belegte trotzdem Höhe —, ein `ToolbarItem` erschiene dort gar nicht. Der Knopf liegt deshalb einmal über allen Bildschirmen statt dreimal in deren Leisten und sitzt überall an derselben Stelle. Beim Scrollen blendet er sich aus, wie die Insel auf dem iPad: über einem Film, den man gerade sucht, hat ein Kontoknopf nichts verloren. **Ein Blatt, kein Bereich.** `SettingsView` ist ein Formular aus geschobenen Unterbildschirmen und braucht nur einen Navigationsstapel um sich; die Medienziele aus `mediaStack` ruft es nie auf. Beim Schliessen steht man wieder dort, wo man war. **Ein Buchstabe, kein Bild.** Den Avatar gab es schon — er steckte in der Kontozeile der Seitenleiste und wird jetzt von beiden benutzt. Ein Serverbild führt das Konto heute nirgends mit, weder bei Jellyfin noch bei Plex; eine graue Silhouette, die bei jedem gleich aussieht, sagt weniger als der Anfangsbuchstabe. Dazu gehört `.settings` in die Rückfallliste beim Grössenwechsel: wer am iPad in den Einstellungen steht und das Fenster schmal zieht, stünde sonst vor einem Bereich, den die Leiste gar nicht mehr führt. Geprüft im Simulator: drei Einträge unten, Monogramm oben rechts, Tippen öffnet die Einstellungen, Schliessen führt zurück. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
19c9dc843c |
„Aus Weiterschauen entfernen" tut jetzt etwas — und man kann es sehen
Der Knopf war wirkungslos, und zwar aus zwei Gründen, die beide auf
dasselbe hinauslaufen: er war nie geprüft worden.
**Der Weg war falsch.** Die Methode las erst ein Benutzerdatenblatt über
`GET /Users/{id}/Items/{id}/UserData`, änderte es und schrieb es zurück.
Diese Stelle war die **einzige im ganzen Projekt**, die ein solches Blatt
eigens liest — überall sonst kommt es am Titel mit. Es gab also kein
zweites Beispiel, an dem der Weg je funktioniert hätte, und ein 404
beendete die Sache, bevor überhaupt etwas geschrieben wurde.
Jetzt wird das Blatt **gebaut**, nicht erfragt: „gesehen", „Favorit" und
die Zählung stehen am Titel, den der Aufrufer ohnehin in der Hand hat —
deshalb nimmt `removeFromContinueWatching` jetzt den Titel statt nur
seiner Kennung. Gelesen wird trotzdem noch, aber unverbindlich: gelingt
es, bleiben auch Felder erhalten, die diese App gar nicht kennt. Und
geschrieben wird über beide Wege — erst der gebündelte
`/UserItems/{id}/UserData`, bei einem 404 der ältere. Welcher gilt, hängt
an der Serverfassung.
**Und man konnte es nicht sehen.** Die Attrappe kannte die Route nicht;
ihr Auffangzweig antwortete auf jeden Schreibversuch mit Status 200, und
die Weiterschauen-Reihe war fest verdrahtet. Der Aufruf gelang also immer
und änderte nie etwas — genau das Bild eines kaputten Knopfes, nur aus
einem anderen Grund. Genau deshalb ist der Fehler durchgerutscht.
Sie führt jetzt echte Benutzerdatenblätter, beantwortet beide Wege und
liefert unter „Weiterschauen" nur noch, was eine gemerkte Stelle grösser
null hat. Unter `/__userdata` steht, was geschrieben wurde und wohin.
Gemessen im Simulator, nach einem Tipp auf den Eintrag:
/UserItems/m2/UserData
{"Played":false,"IsFavorite":false,"PlaybackPositionTicks":0,…}
Danach ist „Weiterschauen" leer und die Reihe verschwindet von der
Startseite.
**Zwei Dinge, die danach trotzdem auffallen können**, und beide sind
keine Fehler: Jellyfins „Als Nächstes" führt angefangene Folgen mit, eine
entfernte Folge kann dort wieder auftauchen. Und bei zusammengelegten
Reihen stehen auch Folgen ohne gemerkte Stelle — dort gibt es nichts zu
entfernen, und der Eintrag fehlt.
Plex bleibt unberührt: der Weg dort war in Ordnung und bekommt nur die
neue Form.
iOS und tvOS bauen, 469 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ac62e8bf19 |
Der Aufmacher war weg, weil die Mediathek unterwegs verlorenging
Zwei Schalter, die im selben Bildschirm untereinander stehen, ergaben zusammen gar keinen Aufmacher mehr: „Gesehenes aus Neuzugängen ausblenden" und eine angehakte Mediathek. Der Grund steht in `HomeFeed.prepared`. Der Zweig für das Ausblenden baut die betroffenen Reihen neu — und baute sie **ohne `libraryID`** neu, weil der neue Parameter dort nicht mitgegeben war. Getroffen hat es dabei ausgerechnet die Reihen `latest.<mediathek>`, die als einzige überhaupt eine tragen. Danach warf der Filter des Aufmachers jede Reihe ohne Mediathek weg, und übrig blieb nichts. Dasselbe an zweiter Stelle: `combiningContinueRows` verlor `libraryID` **und** `moreKey` — Letzteres ein eigener, älterer Fehler, durch den bei zusammengelegten Weiterschauen-Reihen „Alle anzeigen" wegfiel. **Dazu eine Regel, die ich zurücknehme.** Gestern stand hier: trifft die Auswahl nichts, bleibt nichts. Das war als Ehrlichkeit gedacht und ist in der Praxis der schlechtere Fehler — ein Aufmacher, der lautlos verschwindet, wird überall gesucht, nur nicht in einer Einstellung zwei Bildschirme weiter. Trifft die Auswahl keine einzige Reihe, gilt sie jetzt als wirkungslos. Das deckt auch den Fall ab, dass eine gespeicherte Kennung nach einem Serverwechsel ins Leere zeigt. Und damit solche Kennungen nicht unsichtbar weiterwirken, werden sie beim Öffnen des Bildschirms gegen die vorhandenen Mediatheken abgeglichen — vorher standen sie in keiner Zeile und liessen sich nicht abwählen. Geprüft im Simulator: „Gesehenes ausblenden" an, „Filme" angehakt — der Aufmacher ist da und zeigt einen Film. Vorher: nichts. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3a6a6c4242 |
Intro und Abspann bei Jellyfin, und drei Dinge aus dem Gebrauch
**Intro überspringen gab es für Jellyfin nie.** Der Knopf, die Marken,
das automatische Überspringen, die Einstellungen dazu — alles stand seit
Langem im Player und auf dem Fernseher. Nur kam bei Jellyfin nie eine
Marke an: `JellyfinItem+Media.swift` setzte `markers: []`, weil Jellyfin
sie nicht am Titel liefert.
Seit 10.10 gibt es `/MediaSegments/{itemId}`, gefüllt von einem Anbieter
im Server wie dem Intro-Skipper. Die Anfrage läuft **neben** dem Abruf
des Titels, nicht danach — sonst wartete die Detailseite zweimal.
Zwei Dinge, die sonst eine Stunde kosten: `includeSegmentTypes` muss
**zweimal** stehen, nicht einmal mit Komma (mit Komma antwortet Jellyfin
mit null Treffern), und die Abfrage wirft nicht. Ein Server ohne Anbieter
antwortet leer, einer vor 10.10 mit einem 404 — und ein Film ohne Intro
ist kein Fehler, sondern der Normalfall.
Geprüft im Simulator: die Attrappe meldet jetzt Segmente, und im Player
steht „Intro überspringen".
---
**„Aus Weiterschauen entfernen".** Eine halbe Stunde gesehen und keine
Lust mehr — der Titel ist weder gesehen noch ungesehen, er soll nur nicht
ewig oben stehen. Beide Server können es, auf verschiedenen Wegen:
- **Plex** hat einen eigenen: `PUT /actions/removeFromContinueWatching`.
**Nicht** `/:/unscrobble` — das setzt zugleich auf ungesehen.
- **Jellyfin** hat keinen. Was es gibt, ist das Benutzerdatenblatt; die
Liste führt, was eine gemerkte Stelle grösser null hat. Also wird das
Blatt gelesen, bekommt eine Null und geht so, wie es war, zurück.
**Erst lesen, dann schreiben**, weil dort auch „gesehen", „Favorit" und
die Bewertung stehen — wer seinen Favoriten verliert, weil er einen
Film aus einer Liste genommen hat, sucht den Grund nie dort.
Der Eintrag erscheint nur, wenn es eine gemerkte Stelle gibt.
---
**Die Mediatheken für den Aufmacher taten zu wenig.** Die Regel „Reihen
ohne Mediathek bleiben immer drin" war freundlich gemeint und falsch: bei
„nur Filme" stand weiter eine Serie oben, weil sie aus „Weiterschauen"
kam. Gemessen und nicht vermutet — es lag nicht am Zeitfenster der
Auswahl, wie zuerst angenommen.
Jetzt gilt: ist etwas gewählt, speist sich der Aufmacher **nur** daraus.
Wer Weiterschauen oben haben will, wählt keine Mediathek. Der Hinweis
unter der Auswahl sagt das auch.
---
**Der „Bearbeiten"-Knopf stand über allem und tat dort nichts.** Er gilt
nur für die Reihen; jetzt steht er an deren Überschrift.
---
Die Attrappe kann drei Dinge mehr, sonst wäre nichts davon prüfbar:
Segmente, `parentId` bei den Neuzugängen (vorher stand in jeder Mediathek
dasselbe) und damit überhaupt eine Mediathekenauswahl, die man ansehen
kann.
iOS und tvOS bauen, 468 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
dc93d6e22e |
Der Aufmacher lässt sich auf Mediatheken einschränken
Mehrfachauswahl in den Einstellungen unter „Startseite": welche Mediatheken den Aufmacher speisen dürfen. Haken statt Auswahlliste — eine Liste könnte nur eine, und „keine" wäre dort ein Eintrag, den man versehentlich trifft. Dafür musste eine Reihe sagen können, woher sie kommt: `MediaRowGroup` trägt jetzt eine `libraryID`. Jellyfin setzt sie an den „Neu in …"-Reihen — die baut die App ohnehin je Mediathek. Zwei Regeln, und beide sind Entscheidungen, keine Nebenwirkungen: **Leer heisst alle.** Wer nichts ankreuzt, meint „mir ist es gleich", nicht „nimm nichts". Ein leerer Aufmacher wäre der Fehler, den niemand mit dieser Einstellung in Verbindung brächte. **Reihen ohne Mediathek bleiben immer drin.** Weiterschauen, Als Nächstes und Favoriten gehen quer durch alle, und aus welcher Mediathek eine angefangene Folge stammt, sagt der Server an keiner Stelle. Sie wegzuwerfen hiesse, jemandem das Angefangene zu nehmen, weil er „Musikvideos" abgewählt hat. Der Hinweis unter der Auswahl sagt das auch dem, der nicht in den Quelltext sieht. Eingeschränkt wird **nur der Aufmacher**, nicht die Startseite: die Reihe selbst bleibt stehen. Wer sie ganz loswerden will, blendet sie aus — das ist eine andere Frage und hat ihren eigenen Schalter. Fünf Tests an `HeroSelection.rows(_:limitedTo:)` halten die Regeln fest, darunter der Fall einer gespeicherten Kennung, die es nicht mehr gibt. **Plex bleibt aussen vor:** dort kommen die Reihen als fertige Hubs vom Server, und `librarySectionID` wird in `PlexHub` nicht gelesen. Die Auswahl schränkt dort also nichts ein. Nachzuholen, wenn Plex wieder dran ist. iOS und tvOS bauen, 468 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3f680c7426 |
Gesehen und Favorit direkt an der Kachel — und die Pinch-Geste greift wieder
Drei Dinge aus dem Gebrauch.
**Das Kontextmenü einer Kachel trug genau einen Eintrag.** „Abspielen",
sonst nichts; der Kommentar daneben nannte es seit Längerem den
natürlichen Ort für „Als gesehen markieren". Jetzt steht es dort —
zusammen mit Favorit und Sternen, soweit der Server sie kennt. Es sind
dieselben Einträge wie im Menü der Detailseite: `ItemActionEntries` wird
von beiden benutzt, statt sie zweimal zu beschreiben.
Zwei Dinge daran sind nicht offensichtlich. Der Inhalt eines
Kontextmenüs lebt nur, solange das Menü offen ist — ein `@State` darin
könnte nichts anzeigen, was nach dem Tippen passiert. Ausgeführt wird
deshalb von der Kachel, die länger lebt, und dort hängt auch die
Fehlermeldung. Und danach muss die Seite neu laden: eine abgehakte Folge
gehört nicht mehr in „Weiterschauen", und der Fortschrittsbalken stimmte
sonst nicht mehr. Dafür gibt es `flixyItemChanged` in der Umgebung —
sonst müsste der Rückweg durch jede Zwischenebene gereicht werden.
**Die Pinch-Geste griff fast nie.** Sie hing an der Bildfläche, und über
der liegen die Sprungflächen — und sobald man einmal getippt hat, die
ganze Bedienung. Eine Zwei-Finger-Geste bildet sich aber nur, wenn
**beide** Finger auf der Ansicht landen, die sie trägt; in dem schmalen
Streifen dazwischen gelang das kaum. Sie hängt jetzt als
`simultaneousGesture` am ganzen Player: sie erkennt mit, statt
dazwischenzugehen, und Tippen, Doppeltippen und Scrubber bleiben
unberührt.
Gemessen im Simulator, Querformat auf einem Pro Max, mit einer echten
Zwei-Finger-Geste:
auseinander 956 × 440 pt, Rand 0,0 / 0,0
zusammen 782 × 440 pt, Rand 87,0 / 87,0
**Der Drehknopf zeigte ein Telefon.** Das las sich als Zustandsanzeige —
„so liegt es" — statt als Knopf. Zwei Pfeile im Kreis sagen, was
passiert.
iOS und tvOS bauen, 463 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
46b0117984 |
Füllend heisst füllend — der schwarze Streifen unten ist weg
Im Füllmodus wurde die Bildebene unten um den Sicherheitsrand eingerückt,
damit die Untertitel nicht unter die Bildschirmkante geraten. Das stand
dort unbedingt — auch wenn gar kein Untertitel lief. Übrig blieb dann ein
schwarzer Streifen unter einem Bild, das gerade füllen soll: genau das
Gegenteil dessen, wofür der Knopf da ist.
Der Einzug hängt jetzt daran, ob überhaupt eine Untertitelspur gewählt
ist. Ohne Untertitel reicht das Bild bis an alle vier Kanten, mit
Untertiteln bleibt die Schrift gerettet.
Gemessen auf einem Pro Max, füllend, ohne Untertitel:
hochkant 440 × 956 pt Rand 0,0 / 0,0 / 0,0 / 0,0
quer 956 × 440 pt Rand 0,0 / 0,0 / 0,0 / 0,0
`PlayerModel.subtitleBottomInset` fällt weg. Die Eigenschaft beschrieb
diese Rechnung, las sie aber niemand — die Ansicht rechnete selbst. Eine
Beschreibung, die nichts beschreibt, zeigt beim nächsten Mal in die
falsche Richtung.
**Nicht geprüft:** wie es mit eingeschalteten Untertiteln aussieht. Die
Attrappe liefert keine Untertitelspur, und ohne eine lässt sich der Fall
im Simulator nicht herstellen. Der Weg ist unverändert geblieben, aber
belegt ist er damit nicht.
iOS und tvOS bauen, 463 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
9e7f3393b4 |
DTS, TrueHD und Vorbis gehen jetzt an den Server statt an VLC
Bisher entschied der Decider: steht nur die Schachtel im Weg, wird
umgepackt — stimmt ein Codec nicht, geht es an VLC. Das war beim Ton zu
grob gedacht.
Beim Umpacken nennt das Geräteprofil dem Server, was ankommen darf:
"VideoCodec": "h264,hevc"
"AudioCodec": "aac,ac3,eac3,mp3"
Trifft ein Strom eine dieser Angaben, kopiert der Server ihn; trifft er
sie nicht, rechnet er **genau diesen einen** um. Eine Matroska mit HEVC
und DTS braucht also kein VLC: das Bild geht Byte für Byte durch, und nur
die Tonspur wird nach AC3 oder AAC kodiert. Das kostet Tonqualität und
sonst nichts.
Die Bedingung heisst deshalb nicht mehr „nur die Schachtel", sondern
„das Bild bleibt": Container und Ton dürfen im Weg stehen, der
Videocodec nicht. Dazu musste die Prüfung der Schachtel aufhören, sich
immer zu stellen — eine MP4 mit DTS stand mit einem reinen Tonhindernis
da und fiel trotzdem durch die Liste umpackbarer Container.
**Der grössere Gewinn liegt auf dem Apple TV.** Dort gibt es VLC nicht,
und bisher landete eine Datei mit DTS-Ton beim vollen Neurechnen — Bild
inbegriffen, obwohl mit dem Bild nichts war. Jetzt bleibt das Bild
unberührt. Auf iPhone, iPad und Mac verhält sich derselbe Film jetzt
gleich, und zwar über AVPlayer: mit Bild-in-Bild, AirPlay und
Sperrbildschirm, die es auf dem VLC-Weg nie gab.
Geprüft im Simulator gegen die Attrappe mit `--vlc` (meldet DTS): der
Film läuft, und in der Leiste steht jetzt der AirPlay-Knopf, der vorher
fehlte — der sichtbarste Beleg dafür, dass AVPlayer spielt und nicht VLC.
„Umgepackt" und „umgepackt, Ton umgerechnet" sind in den technischen
Infos und in der Diagnose auseinandergehalten. Ein Weg, der den Ton neu
kodiert, darf nicht „ohne Neukodierung" heissen.
Was bleibt bei VLC: nicht unterstützte **Bild**codecs, allen voran AV1,
und Dateien, die der Server noch nicht untersucht hat. Dafür gäbe es nur
das volle Neurechnen — eine eigene Entscheidung, nicht diese.
iOS und tvOS bauen, 463 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3ed682dd00 |
Der Sicherheitsrand am Bild war falsch — er schützte vor nichts
Eingepasst bekam das Bild ein Polster in Grösse des Sicherheitsrandes,
damit es nicht unter die Dynamic Island läuft. Nachgerechnet hält das
nicht.
Querformat auf einem Pro Max: 956 × 440 Punkt. Ein Bild in 16∶9 wird bei
voller Höhe 782 Punkt breit — es bleiben 87 Punkte Balken auf jeder
Seite, und die Island braucht 62. Das Bild kommt ihr von selbst nicht
nahe. Das Polster hat also nichts abgewehrt und dabei vierzig Punkte
Höhe gekostet, also jedes zehnte Bildpunktband:
vorher 695,7 × 400 pt Balken 130,0 / 130,3
jetzt 765,3 × 440 pt Balken 95,3 / 95,3
Beides über VLC gemessen, dieselbe Datei, dieselbe Lage.
Erst was breiter ist als der Schirm (2,17∶1) läuft an die Kanten — bei
21∶9 über die ganze Breite, und dann liegt ein Streifen hinter der
Island. Dafür ist der Zoomknopf da. Ein Film, der nie an den Rand darf,
wäre der schlechtere Tausch: er kostet bei *jeder* Datei Höhe, um bei
wenigen einen Streifen zu retten.
`EdgeInsets.symmetrical` fällt damit weg, samt seiner fünf Tests. Es
hatte genau eine Aufgabe — ein unsymmetrisches Polster mittig zu halten
—, und das Polster gibt es nicht mehr. Eine geprüfte Funktion, die
niemand ruft, ist trotzdem nur Ballast.
iOS und tvOS bauen, 460 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b0db4f8f5f |
Bei VLC sass das Bild nie richtig — und der Knopf wirkte nie
Zwei Fehler, die dieselbe Ursache haben: alles, was über VLC läuft, ging
an der Bildebene vorbei, an der die App gedreht hat.
**Das Bild sass nicht mittig.** Nicht ein paar Punkte daneben — gemessen
eine Anzeigefläche von 1200×480 Punkt in einer Zeichenfläche von 711×400,
unten rechts angeschlagen, im falschen Seitenverhältnis. Der Grund steht
im Vertrag von `VLCDrawable`: VLCKit fragt die Grösse **einmal** beim
Anlegen ab und überlässt seine Anzeigefläche danach der
Autoresizing-Maske. Eine Maske hält aber den Unterschied fest, nicht das
Verhältnis — was beim Anlegen zu gross war, bleibt für immer um denselben
Betrag zu gross. Und angelegt wird sie mitten in der Drehung ins
Querformat, während SwiftUI sie einblendet: also regelmässig falsch.
Die Zeichenfläche ist jetzt eine eigene Klasse, die ihrer Anzeigefläche
bei jedem Layout wieder die eigene Grösse gibt. Danach gemessen: 695,7 ×
400 Punkt, Rand links 130,0, rechts 130,3 — mittig.
**Der Knopf für einpassen/füllen tat nichts.** Er schrieb `videoGravity`
auf eine `AVPlayerLayer`, die bei VLC gar nicht im Baum hängt. Die
Einstellung geht deshalb jetzt durch die Engine: `setVideoFit` am
Protokoll, beim Systemplayer eine Voreinstellung, die nichts tut — dort
macht es die Bildebene —, bei VLC `videoFitMode`. `smaller` heisst bei
libvlc „fit inside", `larger` „fit outside".
Gemessen, im Querformat auf einem Pro Max, über VLC:
eingepasst 695,7 × 400 pt Rand l/r 130,0/130,3 o/u 20,0/20,0
füllend 935,3 × 440 pt Rand l/r 10,3/ 10,3 o/u 0,0/ 0,0
**Zurückgenommen:** im Kommentar an `videoFit` stand, ein `didSet` auf
einer `@Observable`-Klasse werde nicht gemeldet. Das stimmt nicht —
`HomeLayoutSettings` lebt davon. Ein Kommentar, der einen falschen
Mechanismus behauptet, schickt den nächsten in die falsche Richtung;
jetzt steht der belegte Grund dort.
Dazu, damit dieser Weg überhaupt prüfbar ist: die Attrappe meldet mit
`--vlc` DTS-Ton. AVFoundation kann das nicht, also fällt die App auf
VLCKit zurück. Die Beispieldatei bleibt dieselbe — geprüft wird der Weg,
nicht der Ton. Ohne diesen Schalter ist der VLC-Zweig im Simulator gar
nicht zu erreichen, und genau deshalb lagen beide Fehler so lange da.
Ebenfalls dabei: `EdgeInsets.symmetrical` mit fünf Tests und das Polster
im Player, das damit nicht mehr verschiebt statt einrückt.
iOS und tvOS bauen, 465 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
51abcabb27 |
Die Rechnung nachgetragen — und eine falsche Erklärung zurückgenommen
Eine unabhängige Gegenprüfung (zweiunddreissig Agenten, jede Behauptung
von Skeptikern angegriffen) bestätigt die Ursache und rechnet alle vier
gemessenen Zahlen nach:
scrollbarer Teil 421,0 (8 Zeilen à 36,33 + Überschrift 22,33
+ 8 Zwischenräume à 12 + 12 unten)
starrer Rest 137,6 (Kopfzeile 48,33 + Trenner 1
+ Kontozeile 64,27 + 2 × 12 aussen)
zusammen 558,6 gemessen 558,67
zentriert (420 − 558,67) / 2 = −69,335 gemessen −69,33
Die Rechnung steht jetzt im Quelltext, damit beim nächsten Eintrag in der
Leiste jemand nachsehen kann, wie viel Luft noch ist: starr bleiben 137,6
von 420.
**Und eine Berichtigung.** Ich hatte den Zwischenwert 620,67 einer
Rückkopplung zugeschrieben — gemessener Rand geht über `topInset` in
einen `safeAreaInset` und verändert den Rand. Das stimmt nicht. Die
Differenz von zweiundsechzig kommt schlicht aus
`.padding(.top, statusBarHeight)` an der Seitenleiste, das während der
Drehung noch den Hochformatwert trug.
Die Umstellung auf den Fensterwert bleibt richtig und nützlich — sie
spart eine Layoutrunde. Sie war nur nicht die Reparatur, und der
Kommentar sagte etwas anderes. Ein Kommentar, der einen falschen
Mechanismus behauptet, ist schlimmer als keiner: er schickt den nächsten
in die falsche Richtung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5f635c699a |
Die Seitenleiste war zu hoch — und der Player ging deshalb hinaus
Dreimal habe ich an den Rändern geschraubt, und dreimal war es nicht das.
Der Fehler sass in der Navigation und zeigte sich im Player.
**Gemessen statt vermutet.** Auf einem iPhone Pro Max im Querformat:
Fenster 956×440, Rand 0,62,62,20 → erwartet 832×420
Wurzel 832×558,67 @62,−69,33 → 138,67 zu hoch
Der Wurzelbaum ist zu gross für den Vorschlag und wird darin zentriert;
(420 − 558,67) / 2 ergibt exakt −69,33. Der Player liegt als Überlagerung
darin und erbt den Kasten — oben fielen neunundsechzig Punkte heraus,
unten neunundvierzig. Genau dort sitzen Kopfzeile und Zeitangaben.
**Die Ursache:** `FlixySidebar` war eine `VStack` ohne Bildlaufansicht,
mit dem Kommentar „die Einträge sind wenige und passen". Auf einem iPad
und im Hochformat stimmt das. Ein grosses iPhone ist quer
vierhundertvierzig Punkte hoch, und die Leiste braucht mit Kopf, Gruppen,
Trenner und Kontozeile gut fünfhundertdreissig. Eine `VStack` schrumpft
dann nicht — sie überragt, der `HStack` nimmt das Maximum, und alles
darüber wächst mit.
Deshalb war es nur dort zu sehen: nur auf einem grossen iPhone ist die
Anordnung quer `regular`, und nur dann gibt es überhaupt eine
Seitenleiste. Auf einem 17 Pro ist quer `compact` — dort stimmte jede
Messung, und genau deshalb konnte ich es dreimal nicht nachstellen.
Jetzt scrollt die Leiste, wenn sie muss (`basedOnSize`). Wo die Einträge
passen, sieht man keinen Unterschied. Nachgemessen: Wurzel 832×420,
Rahmen des Players 956×440 @0,0 — deckungsgleich mit dem Fenster.
**Mitgenommen:** `statusBarHeight` kam aus `proxy.safeAreaInsets.top` und
ging über `topInset` in einen `safeAreaInset` am selben Baum zurück. Das
Layout lief dadurch in Runden auf die richtige Grösse zu, jede Runde
zweiundsechzig Punkte. Der Wert kommt jetzt aus dem Fenster, das ihn ohne
Rückkopplung kennt — eine Runde statt drei.
`ScreenSafeArea` liegt dafür nicht mehr beim Player, sondern bei den
Modellen: es beantwortet jetzt für zwei Stellen dieselbe Frage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7bcef03c25 |
Eingepasst heisst ganz zu sehen — und der Rahmen wird mitgemessen
**Das Bild bleibt im sicheren Bereich, wenn es eingepasst ist.** Vorher lief es bis an die Bildschirmkante und damit unter die Dynamic Island — im Querformat frisst sie eine Ecke des Films. „Einpassen" und „ein Stück fehlt" schliessen sich aus. Das kostet quer gut hundert Punkte Breite; wem das zu klein ist, legt mit zwei Fingern auf **füllen** um, wo es bewusst bis an die Kanten läuft und nur der Untertitel ausgespart bleibt. **Und die Diagnosezeile misst jetzt den Rahmen mit.** Das ist die Frage, an der alles hängt, und ich habe sie zweimal übersprungen: ist der Rahmen grösser als der Bildschirm, liegt es nicht an den Rändern — dann wird der Player in einen zu grossen Kasten gelegt und mittig darin ausgerichtet, und oben wie unten fällt gleich viel heraus. Genau so sieht es aus, wenn die Kopfzeile fehlt **und** die Zeitangaben fehlen, während die Knöpfe in der Mitte stehen. Der Verdacht, warum ich es nie nachstellen konnte: die Bildschirmfotos sind 2796×1290, also ein Pro Max. Der ist im Querformat `regular` und nimmt damit in `RootView` einen **anderen Zweig** — Seitenleiste statt Leiste unten, mit `safeAreaInset` und `ignoresSafeArea(.container)`. Mein Simulator ist ein 17 Pro und läuft quer über den anderen Zweig. Belegt ist das noch nicht; die Zeile wird es sagen. Im Simulator steht dort „Rahmen 874×402 @0,0 · Schirm 874×402" — gleich gross, Ursprung in der Ecke, alles liegt drin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c7b901b103 |
Der Player sagt jetzt, was er über seinen Platz weiss
Zweimal habe ich behauptet, die Ränder seien behoben, und zweimal sassen die Knöpfe weiter ausserhalb. Im Simulator steht dort „SA 0,62,62,20 · Schirm 874×402" und alles liegt drin — nachstellen lässt es sich hier also nicht. Damit ist Raten der falsche Weg. **Eine Zeile im Entwicklerbau**, unter dem Schieberegler: die Ränder, wie sie ankommen, die Bildschirmgrösse daneben und der Skalierungsmodus. Sitzen die Knöpfe daneben, gibt es genau drei Möglichkeiten — die Ränder kommen als null an, sie kommen falsch an, oder sie stimmen und der Rahmen ist grösser als der Schirm. Von aussen sehen alle drei gleich aus. Die Zeile unterscheidet sie. **Und eine Härtung, die beim Nachlesen auffiel.** `ScreenSafeArea` suchte nur die Szene, die `foregroundActive` ist. Genau die gibt es in zwei Momenten nicht: während einer Drehung und kurz nach dem Zurückholen der App. Wer dann fragt, bekommt `nil`, daraus werden Ränder von null — und die bleiben stehen, bis sich die Fläche wieder ändert. Die Knöpfe sitzen dann dauerhaft im Home-Indikator, obwohl die Rechnung stimmt. Ein Fenster mit den richtigen Rändern ist in dem Moment trotzdem da, es ist nur nicht „aktiv". Jetzt der Reihe nach: aktiv, sonst sichtbar, sonst irgendeines. Dazu wird beim Zurückholen der App nachgefasst — eine Drehung im Hintergrund ändert die Fläche, ohne dass die Ansicht davon etwas merkt. Ob das der Fehler war, weiss ich nicht. Die Zeile wird es sagen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
41719595b9 |
Mediatheken nach oben — und das Regal sagt endlich, was es sieht
**Die Mediatheken sind Reiter geworden.** Sie standen unten auf der Startseite, und dorthin musste man erst scrollen — an allen Vorschlägen vorbei, obwohl „ich will in die Filme" genau der Fall ist, in dem einem die Vorschläge egal sind. Damit fällt die Reihe unten weg: zwei Wege zum selben Raster sind auf einem Fernseher kein Komfort, sondern eine Frage, welcher der richtige ist. Die Leiste liest sich jetzt „Start · Filme · Serien · Gemischtes · Suchen" und wird von tvOS mittig gehalten, egal wie viele Mediatheken dazukommen. Die Stelle der Suche bestimmt Apple, nicht diese Datei: `role: .search` setzt sie dorthin, wo sie in jeder anderen App auch steht, und gibt ihr die Lupe. **Und eine Statuszeile fürs Regal.** Bleibt es leer, gibt es genau drei Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da, oder sie ist da und das System fragt trotzdem nicht. Von aussen sehen alle drei gleich aus — nämlich nach dem grossen Plakat aus dem Asset-Katalog. Nach zwei Runden Vermuten steht unten links jetzt, welcher Fall es ist: „3 Reihen, 9 Einträge, zuletzt 21:55" oder eben „Keine App-Gruppe". Nur im Entwicklerbau (`#if DEBUG`) und als Überlagerung, nicht als letzte Zeile der Liste: dorthin müsste man erst an allen Reihen vorbeiscrollen, und wer nachsieht, warum das Regal leer ist, soll es sofort sehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
08514eb564 |
Und iOS wieder gebaut — TVServices gibt es dort nicht
Mein Fehler: `TopShelfWriter` liegt in `Flixy/Session/`, und den Ordner übersetzen beide Ziele. Mit `import TVServices` brach damit das iPhone-Ziel ab. Die ganze Datei steht jetzt in `#if os(tvOS)`. Ein `#if` um den einen Import hätte nicht genügt: der Abhängigkeits-Sucher von Clang prüft ihn trotzdem und bricht mit „Unable to resolve module dependency: TVServices" ab, ohne den Fernseher zu erwähnen — dieselbe Falle wie vorhin bei VLCKit. Committet hatte ich vorher, ohne iOS zu bauen. Das war der eigentliche Fehler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
47c93d89bb |
Niemand hat dem System gesagt, dass es neu fragen soll
Das Regal zeigte weiter das grosse Plakat aus dem Asset-Katalog, obwohl die Datei daneben längst drei Reihen enthielt. Der Grund steht in Apples eigenem Header, zwei Zeilen über der Methode, die wir überschreiben: > If the completion handler is not called in a timely manner or is called > with empty content the system will display the static image instead. Genau das war zu sehen. Und darunter steht, was fehlte: > + (void)topShelfContentDidChange; > Can be called from an application or extension to tell the system that > your Top Shelf content has changed. Die Erweiterung kann sich nicht selbst anstossen — sie läuft nur, wenn sie gefragt wird. Gefragt wird sie, wenn die App es sagt. Die App sagt es jetzt, unmittelbar nachdem sie den Abzug geschrieben hat. Ausgeschlossen, bevor ich hier landete, und zwar nachgemessen statt vermutet: das System kennt die Erweiterung (`pluginkit` listet sie unter `com.apple.tv-top-shelf`), die Hauptklasse ist im Binärcode auffindbar (`_TtC13FlixyTopShelf16TopShelfProvider`), die Gerätefamilie stimmt, und die Datei liegt mit drei Reihen in der App-Gruppe. Nicht nachgewiesen: dass es jetzt zeichnet. Die Erweiterung startet nur, wenn das System sie fragt, und das tut es nur, wenn die App im Menü oben steht und fokussiert ist — beides kann ich im Simulator nicht auslösen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a1576921fd |
Das Regal hatte seine eigene Transportsicherheit
Die Plakate blieben weg, und der Grund stand nicht im Code, sondern daneben: eine App-Erweiterung hat ihre **eigene** Info.plist, und damit ihre eigene Transportsicherheit. Die Ausnahme fürs lokale Netz, die in der Info.plist der App seit jeher steht, gilt dort nicht. Ein Jellyfin im Heimnetz antwortet auf blankem HTTP. Jede Bildanfrage der Erweiterung lief damit gegen ATS — lautlos, ohne Fehler, ohne Eintrag. Das Regal blieb leer und sah aus wie „geht halt nicht". Dazu die richtige Plakatgrösse: 666×1000 statt 404×606, Apples Mass für die doppelte Auflösung. Für beide Stufen dasselbe Bild — ein Apple TV zeichnet das Regal ohnehin doppelt aufgelöst, und zwei Adressen abzulegen hiesse, dem Server zwei Grössen desselben Plakats abzuverlangen, damit eine davon nie gebraucht wird. Nicht nachgewiesen: dass es jetzt zeichnet. Das Regal erscheint nur, wenn die App in der **obersten** Reihe des Menüs steht und fokussiert ist, und den Fokus kann ich im Simulator nicht bewegen — tvOS nimmt keine Touch-Eingaben, und an die Simulator-App komme ich aus dieser Umgebung nicht heran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6373566d1c |
Suchen am Fernseher — und die Leiste, die es dafür braucht
Das Backend konnte die Suche längst; es fehlte die Oberfläche. **`.searchable`, keine eigene Tastatur.** tvOS macht daraus einen eigenen Bildschirm mit Rastertastatur oben und Ergebnissen darunter — die Bedienung, die jeder von Apples Apps kennt. Etwas Eigenes zu bauen hiesse, sie nachzuahmen und dabei die Diktierfunktion zu verlieren. **Herausgelöst statt kopiert**, wie schon bei Start und Mediathek: `SearchFeed` trägt das Entprellen, die Mindestlänge und die zuletzt gesuchten Begriffe. Drei Stellen, an denen es nicht offensichtlich ist — und die dritte ist die wichtigste: geprüft wird auf `Task.isCancelled` und nicht auf `CancellationError`, weil eine abgebrochene Netzanfrage als Übertragungsfehler zurückkommt. Die naheliegende Prüfung lief ins Leere, und jeder Tastendruck leerte kurz die Liste. **Und erst jetzt eine Reiterleiste.** Bis hierher gab es genau einen Bildschirm, und eine Leiste mit einem Eintrag wäre Zierrat gewesen. Der Suchreiter trägt `role: .search`: tvOS stellt ihn dadurch ans Ende und gibt ihm die Lupe, dieselbe Stelle wie überall sonst. Keinen dritten Reiter für die Mediatheken — die stehen als Reihe auf der Startseite, und ein Reiter verspräche einen eigenen Bereich, hinter dem dasselbe Raster läge. Der offene Reiter wird ausdrücklich gehalten, nicht dem System überlassen: ein Link aus dem Regal über der App führt zur Startseite, und stünde gerade die Suche offen, passierte sichtbar nichts. Nebenbei bekommt die iOS-Suche einen Knopf zum Vergessen des Verlaufs. Wischen zum Löschen gab es dort, am Fernseher gibt es das nicht — und eine Liste, die sich nur auf einer Plattform leeren lässt, ist keine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5ee7bb4e6c |
Lila statt Mint — die Farbe der Knöpfe passt zum Icon
Die Akzentfarbe war Mint. Eine hübsche Farbe, die mit der Marke nur nichts zu tun hatte: das Plakat auf dem Homescreen ist violett, und die Knöpfe darin waren grün. `lilac` (#9B8AD4) ist die entsättigte Schwester desselben Violetts. Auf dem dunklen Grund erreicht sie 6,5:1 — die Palette verlangt 4,5:1, und ein Test hält das fest. Die Farbe steht in der Palette, nicht nur als Vorgabe: wer lieber Mint mag, findet es weiterhin in den Einstellungen. Geändert wird damit nur, womit eine frische Installation anfängt. Der Test, der die Vorgabe festhält, ist mitgeändert — genau dafür steht er da: ein Umsortieren der Palette soll sie nicht still verändern, ein bewusster Wechsel schon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e801502c7a |
Ein Regal über der App — und ein Weg aus dem Player heraus
**Das Regal.** Wer im tvOS-Menü über der App steht, sah bisher das Icon noch einmal gross. Jetzt stehen dort „Weiterschauen", „Als Nächstes" und die Neuzugänge als Plakate, mit Fortschrittsbalken; ein Druck spielt ab, ein Wisch nach unten öffnet die Detailseite. **Warum es dafür eine App-Gruppe braucht.** Das Regal zeichnet eine eigene Erweiterung, also ein anderer Prozess mit eigener Sandbox — und tvOS ruft sie auf, während die App **nicht** läuft, mit weniger als einer Sekunde Zeit. Kein Netzabruf, keine Anmeldung, kein Blättern. Also legt die App beim Laden der Startseite hin, was das Regal braucht, und sonst nichts: Titel, Bildadresse, Kennung, Fortschritt. Eine Datei, ein `JSONDecoder`, fertig. Die Bildadressen tragen den Zugangsschlüssel, weil Jellyfin ihn für Bilder verlangt und die Erweiterung ihn sonst nirgends herbekäme. Die Datei liegt in der App-Gruppe — erreichbar für die App und ihre Erweiterungen, für sonst niemanden. Der Schlüsselbund bleibt, wo er ist. **Und der Fehler, den du gefunden hast.** Solange der Stream noch ausgehandelt wurde, gab es keinen Weg zurück: schwarzes Bild, Kreisel, und der Menü-Knopf tat nichts. `AVPlayerViewController` fängt ihn nämlich selbst ab — aber erst, wenn er einen Player mit einem Item hat, und genau den gibt es in dem Moment noch nicht. Der Griff liegt jetzt eine Ebene darüber, wo es ihn vom ersten Bild an gibt. Doppelt ist er nicht: greift Apples Player zu, kommt der Befehl dort gar nicht mehr an. **Was am Gerät noch offen ist.** Die App-Gruppe muss im Entwicklerkonto eingetragen sein, sonst bleibt das Regal leer — im Simulator legt das System sie von selbst an, auf einem echten Apple TV nicht. Geprüft ist, dass die App die Datei schreibt (drei Reihen, richtige Adressen) und dass tvOS das Schema kennt. Dass das Regal beim Darüberstehen auch zeichnet, lässt sich ohne Fernbedienung nicht auslösen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e2714e058e |
Ein Untertitelmenü statt zweier — und Staffel statt S
Zwei Sachen, die am laufenden Player auffielen. **Zwei Sprechblasen nebeneinander.** Eine war Apples eingebautes Untertitelmenü, eine unseres — und welche was tut, sah man erst nach dem Drücken. Schlimmer als die Doppelung war, was dahinterlag: Apples Menü verstellt die **Systemeinstellung** für Untertitel, während unsere Engine `AVTextStyleRule`s aus `SubtitleAppearance` setzt. Zwei Stilregler, von denen nur einer wirkt. Apples Knopf ist jetzt abgeschaltet — `allowedSubtitleOptionLanguages` auf eine leere Liste, der einzige dokumentierte Hebel dafür; die Auswahl von Hand bleibt unberührt. Unser Menü trägt die Spuren oben und darunter Schriftgröße und Darstellung als Untermenüs. Tiefer geht es nicht: die Transportleiste zeigt genau eine Ebene. **Und das Tonmenü verschwindet, wenn Apple es besser kann.** Wird die Datei unverändert gespielt und stimmt die Spurzahl, liegen alle Spuren im Stream — dann zeigt Apple sie selbst, und zwei Knöpfe wären dasselbe zweimal. Nur beim Umrechnen liegt im Stream genau eine, und dann führt allein unseres weiter. **„S7 E2" heisst jetzt „Staffel 7 · Episode 2"**, wo Platz ist: in der Titelzeile des Players und im Aufmacher am Fernseher. Auf einer Kachel bleibt die kurze Form — dort ist eine Zeile breit, und ausgeschrieben verdrängte sie den Titel. Mitgewandert in die geteilte Schicht sind die Namen der Stilstufen: sie standen bei der Einstellungsansicht und wurden damit nur vom Telefon übersetzt. Zwei Listen gleich zu halten ist genau die Art Fehler, die erst auffällt, wenn jemand „Gelb auf Schwarz" wählt und „Weiss auf Schwarz" bekommt. Der Stil wirkt sofort und ohne Neuaufbau, und die Haken im Menü wandern mit: dafür steht er in der Momentaufnahme, obwohl der Player selbst ihn nicht braucht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
12bfc9e530 |
Abspielen am Fernseher — mit Apples Player und unseren Spuren
Schritt sieben. Wiedergabe, Spurwahl, Titelzeile, „Intro überspringen". **Die schwierige Stelle war die Spurwahl.** Unsere Ton- und Untertitelwahl geht als `audioStreamIndex` an Jellyfin und verlangt bei einem Transcode einen **neuen Stream**. Apples eingebauter Spurwähler zeigt dagegen die Auswahlgruppen des **laufenden** Streams — beim Transcode genau eine. Er ist die richtige Oberfläche für die falsche Liste. Unsere Menüs sitzen deshalb über `transportBarCustomMenuItems` daneben; `customInfoViewControllers` wäre falsch, denn die Tonspur ist ein Bedienelement der Wiedergabe und kein Begleitmaterial. **Der Neuaufbau wird angesagt, nicht erklärt.** Jeder Eintrag, der Sekunden mit schwarzem Bild kostet, trägt das als Untertitel — vorher, nicht hinterher. Wer drückt, sieht den Haken sofort wandern, weil das Menü sich schliesst und darin kein Platz für einen Kreisel ist. Das schwarze Bild deckt eine Überlagerung in `contentOverlayView` zu, die hinter den Bedienelementen liegt und keinen Fokus nimmt — `customOverlayViewController` täte beides nicht und machte die Fernbedienung währenddessen tot. Und solange umgebaut wird, sind alle Einträge gesperrt: sonst räumt `restart()` eine Engine ab, die eine andere gerade baut. **Die Regel dahinter ist jetzt prüfbar.** „Wann kostet ein Spurwechsel einen Neuaufbau" lag bisher mitten im Vollzug von `selectAudioStream` und liess sich nur an einem echten Server mit mehreren Tonspuren auslösen. Als `TrackMenu` steht sie neben den übrigen Entscheidungsregeln, mit sieben Tests — darunter der Fall, auf den der Kommentar dort seit jeher hinweist: stimmt die Spurzahl nicht überein, ist die Zuordnung über die Reihenfolge falsch, und es muss neu aufgebaut werden. **Zwei stille Fehler nebenbei.** Die Tonsitzung stand hinter `#if os(iOS)` und wäre auf tvOS nie gesetzt worden — verfügbar ist sie dort seit 9.0, man hätte es nur daran gemerkt, dass sich die Wiedergabe gegen anderen Ton nicht durchsetzt. Und `PlayerModel.downloads` ist jetzt optional statt eines leeren Koordinators: ein leerer bräuchte einen `ModelContainer`, und der ist das Einzige, wofür diese App SwiftData benutzt. **Belegt statt vermutet.** Im Simulator lief die Wiedergabe gegen die Attrappe, und die Titelzeile zeigte „Unbekannt". Statt an AVKit zu drehen, hat ein Protokolleintrag die Frage beantwortet: `externalMetadata` wird gesetzt, der Wert stimmt, und „Unbekannt" ist wörtlich das, was die Attrappe für unbekannte Kennungen liefert. Geblieben ist davon nur die Erkenntnis, dass die Beschriftung auf das Item warten muss — beim Austausch des Players gibt es noch keines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
87f23afe90 |
Die Startseite am Fernseher — und die Ladeschicht nur einmal
Aufmacher, Reihen, Mediatheken, Raster. Der sichtbare Teil ist neu; der Teil darunter ist derselbe wie am Telefon, und das war die eigentliche Arbeit. **Herausgelöst statt kopiert.** `HomeFeed` und `LibraryFeed` tragen jetzt, was vorher in `HomeView` und `LibraryView` stand: laden, ordnen, den Aufmacher auslosen, den Zwischenspeicher abziehen, entdoppeln, nach einem Fehlschlag sperren. Das zu kopieren wäre der schlimmste Ausgang dieses Vorhabens gewesen — an mindestens drei dieser Stellen klebte schon je ein Fehler, der sich als „es sind nicht alle Filme drin" äusserte und sonst nichts. Einer davon, das Los des Aufmachers an der Position statt am Titel, wäre in einer Kopie ein zweites Mal zu finden gewesen. Mitgewandert sind `rowTitle`, `rowArtworkPath`, `posterArtworkPath`, `runtimeLabel` und `HeroArtwork`: reine Ableitungen, die in einer Ansichtsdatei lagen und deshalb nur das Telefon übersetzte. **Nachgeladen wird am Fokus, nicht am Erscheinen.** Die alte Regel hiess „die letzten fünf" und hing daran, dass eine Kachel sichtbar wird. Beim ersten Zeichnen erscheinen aber alle sechzig, auch die letzten fünf — das Nachladen lief sofort los statt beim Blättern. Auf einem Raster aus sechs Spalten sind fünf Einträge ausserdem nicht einmal eine Zeile. `PagingWindow` rechnet deshalb in Zeilen; sechs Tests halten es fest, darunter der Fall, der die alte Regel entlarvt. **Der Fokus setzt den Aufmacher.** Wer durch „Zuletzt hinzugefügt" wandert, sieht oben, was er gerade anschaut. Kein Wechsel im Sieben- sekundentakt wie am Telefon: vor einem Fernseher wäre ein Bild, das sich von allein austauscht, während man es liest, eine Zumutung. **Drei Dinge, die erst der Simulator zeigte.** Die Seiten müssen bildfüllend laufen, weil ein Aufmacher, der achtzig Punkte vor der Bildschirmkante aufhört, nach Fehler aussieht — also trägt `screenMargin` auf tvOS den Überscan mit. Die Karten brauchen einen Fokus-Rückruf, weil `\.isFocused` den nächsten fokussierbaren Vorfahren meint und das in einer Reihe aus Karten nicht verlässlich die Karte ist. Und der Bau scheiterte an „Unable to resolve module dependency: VLCKit" — der Abhängigkeits- Sucher prüft `canImport(FlixyVLC)` auch für ein Ziel, das es nie verlinkt. Ein `&& !os(tvOS)` genügt; das Paket selbst bleibt unverändert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ab4fcfb3b8 |
Dasselbe Icon, nur breit
Die Markenbilder für den Fernseher waren nachempfunden: ein Dreieck, das ungefähr aussah wie das im App-Icon, mit Farben aus icon.json geraten. Jetzt sind es dieselben. **Die Form aus play.svg gerechnet, nicht nachgezeichnet.** Der Pfad beschreibt ein gleichseitiges Dreieck mit 592,76 Punkten Seite und 54,7 Punkten Eckenradius. Aus diesen Zahlen entsteht die Silhouette — und zwar über ein um den Radius eingerücktes Dreieck mit rundem Strich, was nicht eine Annäherung an die Bögen ist, sondern genau sie. **Die Farben gemessen, nicht geschätzt.** In icon.json stehen Ausgangsfarben für einen automatischen Verlauf, und wie Icon Composer den aufspannt, steht nirgends. Also aus dem gerenderten iOS-Icon abgelesen: Grund 160,112,255 nach 120,69,255, Zeichen 224,152,255 nach 170,97,255, dazu der helle Saum und der weiche Schatten aus der Gruppe. **Zwei Dinge, die erst im Vergleich auffielen.** Den optischen Versatz nach rechts hatte ich auf die Leinwandbreite bezogen — auf dem quadratischen iOS-Icon dasselbe, auf der breiten Fernseherleinwand sichtbar zu viel. Er bezieht sich jetzt auf das Zeichen. Und der helle Saum fehlte ganz; ohne ihn wirkt die Fläche aufgeklebt statt aufliegend. Das Skript liegt als Tools/TVBrandAssets.py bei. Ändert sich die Marke, läuft es einmal und alle fünf Grössen stimmen wieder — von Hand wären es zehn Dateien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1552e5f2b9 |
Am Fernseher steht der Code vorn, nicht das Passwort
Schritt drei: anmelden. Die Oberfläche ist neu, das Modell dahinter ist dasselbe wie am Telefon — `JellyfinSignInModel`, unverändert bis auf einen Wert im Initialisierer. **Die eine Verhaltensänderung.** Nach gefundenem Server sprang der Weg bisher immer zu Benutzername und Passwort, mit der Begründung, ein Passwortspeicher fülle beide Felder in einer Sekunde. Vor einem Fernseher stimmt daran kein Wort: es gibt keinen Passwortspeicher, man tippt mit dem Steuerkreuz, und das Gerät, auf dem man den Code bestätigt, liegt sowieso auf dem Sofa. Also `preferring: .quickConnect` — ein Wert im Initialisierer und keine Plattformbedingung mitten in `findServer()`. Welcher Fall gilt, weiß die Oberfläche; das Modell nicht. Der Vorgabewert hält iOS unverändert. Der sechsstellige Code steht in 96 Punkt mit Sperrung: er wird aus drei Metern abgelesen und in ein Telefon getippt. In Fliesstextgrösse wäre das eine Zumutung. Beide Wege bleiben erreichbar — für Server, auf denen Quick Connect abgeschaltet ist, und für den Fall, dass gerade kein zweites Gerät zur Hand ist. **Und der Beweis, dass die geteilte Schicht trägt.** Gegen die Jellyfin-Attrappe im Simulator: die App verbindet sich, lädt die drei Mediatheken und zeigt sie. FlixySession, JellyfinBackend, die Zustandsmaschine der Anmeldung — alles unverändert, alles über Pfadeinträge geteilt, keine Zeile kopiert. Die Startseite ist noch eine Liste von Namen. Sie kommt in Schritt vier; hier steht sie, damit die Wurzel vollständig ist und die Anmeldung für sich prüfbar bleibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
22f824b000 |
Die Ränder kommen jetzt von der Stelle, die sie kennt
Mein voriger Versuch war an der Quelle falsch, nicht an der Rechnung. **Was ich gebaut hatte:** die Sicherheitsränder in `RootView` messen und über die Umgebung an den Player reichen. Gemessen in einer Ansicht, angewendet in einer anderen, über eine Drehung hinweg — drei Gelegenheiten, daneben zu liegen. Im Simulator ging es gut, auf dem Gerät nicht: dort standen im Querformat links 75 und rechts 28 Punkt Rand, und der Untertitel wurde von der Bildschirmkante abgeschnitten. **Warum SwiftUI hier nicht hilft.** Der Sicherheitsrand ist dort kein Wert, den man erfragt, sondern einer, den man verbraucht: wer ihn respektiert, sieht null, und wer ihn mit `ignoresSafeArea` überschreitet, sieht danach ebenfalls null. Beide Auswege habe ich durchgespielt und beide gemessen. Nimmt man der Ansicht das `ignoresSafeArea`, sitzen die Knöpfe richtig — aber die Bildfläche verliert es mit, und das Bild ist im Querformat um die Notch-Breite schmaler. Schiebt man einen `GeometryReader` dazwischen, passiert dasselbe. **Das Fenster weiß es immer**, in der richtigen Ausrichtung und in dem Moment, in dem gefragt wird. `ScreenSafeArea` ist die eine Stelle in dieser App, die so fragt, und die eine, an der es keinen SwiftUI-Weg gibt. Nachgeholt wird der Wert bei jeder Änderung der Fläche — das ist genau der Moment des Drehens. Nachgemessen im Simulator gegen die Jellyfin-Attrappe: im Querformat meldet das Fenster 0/62/62/20, und damit stehen Kopfzeile, Schieberegler und Zeitangaben vollständig im sichtbaren Bereich, während das Bild bis an die Kanten reicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6326c8c24c |
Ein Apple-TV-Target, und es zeigt schon etwas
Das leere Gerüst: eigene Wurzel FlixyTVApp, eigenes Target FlixyTV in project.yml, eigener Asset-Katalog. Es beweist Target, Icon, Signierung und Paketauflösung auf einen Schlag — bevor eine Zeile Logik daran hängt. **Das Icon von Hand gebaut.** Icon Composer deckt iOS, macOS und watchOS ab, tvOS nicht: der Fernseher verlangt gestapelte Bilder, damit die Parallaxe beim Wischen etwas zu bewegen hat. Zwei Ebenen aus derselben Marke — Verlauf hinten, Dreieck vorn —, dazu App-Store-Icon und beide Top-Shelf-Bilder. Ohne das breite lehnt App Store Connect den Upload mit 90513 ab. **Zwei Annahmen aus dem Plan haben sich beim Bauen erledigt.** PlexWebAuthenticator übersetzt nicht: ASWebAuthenticationSession steht zwar im tvOS-SDK, aber presentationContextProvider, cancel() und der Rest sind dort nicht verfügbar. Der Typ bleibt trotzdem stehen — als Attrappe, die sofort „ging nicht" meldet. Ihn herauszuschneiden hiesse, die Zustandsmaschine der Anmeldung für eine Plattform zu spalten, die diesen Zweig nie betritt. Dieselbe Bauform wie bei OrientationLock. NowPlaying wartet auf den Player in Schritt sieben. **Und eine Messung statt einer Vermutung.** Der Plan führte zwei offene Annahmen über den Sicherheitsrand. Der Simulator beantwortet beide: SwiftUI legt ihn von selbst an, und er misst sechzig oben und unten, achtzig seitlich — nicht neunzig. Nutzfläche 1760×960. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6010229b80 |
Die Pakete wissen jetzt, was ein Fernseher ist
Schritt eins zur Apple-TV-Fassung: nichts davon ist sichtbar, aber ohne das lässt sich für tvOS nicht einmal übersetzen. **Der stille Fehler zuerst.** `FlixyPlatform.isMac` hatte einen `#else`-Zweig, der `true` zurückgab — eine Antwort auf eine Frage, die niemand gestellt hatte. Auf tvOS fiel er dort hindurch: kein Übersetzungsfehler, keine Warnung, nur ein Fernseher, der sich für einen Mac hielt. Jetzt steht dort `#error`. Die nächste Plattform bricht den Bau ab und verlangt eine Entscheidung, statt sich eine auszusuchen. Ein Test hält fest, dass nicht beides zugleich gelten darf; er hätte genau diesen Fehler gefangen. **Ein echter Übersetzungsfehler**, und er lag an der Reihenfolge: `UIPasteboard` steht hinter `canImport(UIKit)`, und das ist auf tvOS wahr. Der tvOS-Zweig steht deshalb jetzt davor — ein Fernseher hat ohnehin nichts, wohin man etwas einfügen könnte. **Drei Vermutungen waren falsch**, nachgeschlagen im tvOS-SDK statt geraten: `presentationDetents` gibt es dort seit tvOS 16 (übersetzt, bewirkt nur nichts — jetzt steht es ausdrücklich da), `.topBarTrailing` seit 14.0, und `isMac` hatte acht Fundstellen in fünf Dateien, nicht fünfzig in siebenundzwanzig. **Schrift und Abstände** bekommen einen eigenen Zweig statt durchzufallen. Nicht feste Grössen: Dynamic Type gibt es auf tvOS auch, und dass `body` dort grösser ausfällt, ist der Sinn der Sache. Falsch war nur die Zuordnung der Rollen — die Skala ist anders gespreizt, und eine Beschriftung in `subheadline` unter einem Plakat ist aus drei Metern Zierrat. Der Aufmacher geht auf `.title`, weil UIKit `largeTitle` auf tvOS gar nicht kennt und nirgends steht, was SwiftUI daraus macht. `screenMargin` steht auf 48 und nicht auf 90: die neunzig Punkte des Überscan-Bereichs liefert das System als Sicherheitsrand, und wer sie hier aufschlägt, hat hundertachtzig. **Zwei Dinge, die sich erst beim Bauen zeigten.** `FlixyDownloads` übersetzte nicht — `volumeAvailableCapacityForImportantUsage` gibt es auf tvOS nicht. Das Paket wird dort trotzdem mitverlinkt, damit `PlayerModel` den Koordinator nennen darf, ohne dass zweitausend Zeilen Wiedergabelogik von `#if` durchzogen werden; angelegt wird nie einer. Und `FlixyProbe` liess das ganze Paket scheitern: ein Kommandozeilenwerkzeug mit `getpass` und `Host`, das auf einem Fernseher nie laufen sollte, hielt den Bau für alle anderen auf. Es ist jetzt ausdrücklich macOS-only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4576eac67c |
Das Bild füllt den Schirm, und die Schrift bleibt darin
Vier Dinge am iPhone-Player, die zusammengehören, weil sie alle davon handeln, wo der Bildschirm aufhört. **Einpassen oder füllen.** `videoGravity` stand fest auf `.resizeAspect`, und zwar in einer Zeile, die nach jedem Neuaufbau erneut läuft — eine Wahl hätte dort also jeden Qualitätswechsel nicht überlebt. Jetzt steht sie im Modell, und die Zeile liest sie. Umgelegt wird mit zwei Fingern oder über einen Knopf; stufenlos wäre falsch, weil alles zwischen den zwei Zuständen ein Bild ist, das weder ganz zu sehen ist noch den Schirm füllt. Auf dem Mac erscheint der Knopf nicht: dort steht der Vollbildknopf darüber, und zwei Knöpfe, die beide „grösser" bedeuten, sind eine Zumutung. **Untertitel im Füllmodus.** Dort reicht das Bild über die untere Bildschirmkante hinaus, und genau dort zeichnet AVFoundation die Untertitel. Statt sie anzuschneiden, wird die Bildebene um den Sicherheitsrand eingerückt — rund zwanzig Punkt Bild gegen eine Zeile, die man lesen kann. Dafür bekommt die Bildfläche echtes Layout: eine `autoresizingMask` hält Ränder verhältnismässig, und ein fester Einzug von zwanzig Punkten wäre im Querformat plötzlich vierzig gewesen. **Die Bedienleiste kannte ihre Ränder nicht mehr.** Der Player bekommt `ignoresSafeArea()` von der Wurzel und sieht danach überall null — Schieberegler und Zeitangaben sassen im Home-Indikator, der Zurück-Pfeil hinter der Dynamic Island, wo ihn niemand drücken konnte. Gemessen wird jetzt eine Ebene weiter aussen, wo die Ränder noch gelten, und über die Umgebung nach innen gereicht. Dieselbe Bauart wie der Akzent. Die Zeile steht ganz am Ende der Kette: stünde sie vor dem Overlay, umschlösse das Overlay sie, und ausgerechnet der Player ginge leer aus. **Gedreht wird einmal, gesperrt wird nicht.** Die Maske blieb auf `.landscape` stehen — wer im Bett auf der Seite lag, konnte nicht mehr ins Hochformat drehen. Ein Film beginnt quer, weil er quer ist; danach gehört die Entscheidung dem Nutzer. Dazu ein Drehknopf für den Fall, dass die Ausrichtungssperre des Systems an ist, und ein Fehlerbehandler an `requestGeometryUpdate`: eine abgelehnte Drehung ist dann eine Einstellung und keine Panne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1ff65e823a |
Ein Vollbildknopf im Player — wenn der Mac ihn annimmt
Fuer eine „fuer iPad angepasste\" App gibt es dafuer keine oeffentliche Schnittstelle. UIKit kennt `isFullScreen` nur lesend und nur fuer Catalyst; einen Umschalter gibt es nirgends. Das Fenster gehoert AppKit, und an AppKit kommt diese App nicht heran. Was bleibt, ist die Antwortkette: `toggleFullScreen:` ist dieselbe Aktion, die der gruene Knopf und Strg-Cmd-F ausloesen, und UIKit reicht unbeantwortete Aktionen an das tragende AppKit-Fenster weiter. Ob es das tut, ist nirgends zugesagt. Deshalb wird vorher gefragt statt hinterher gehofft: `target(forAction:)` sagt, ob die Aktion jemand entgegennimmt, **ohne sie auszuloesen**. Erst wenn es einen Empfaenger gibt, erscheint der Knopf. Ein Knopf, der nichts tut, ist schlimmer als keiner — er laesst den Nutzer an sich zweifeln statt an der Plattform. Das Symbol zaehlt den Zustand selbst mit, weil er sich nicht abfragen laesst. Wer mit dem gruenen Knopf umschaltet, bringt es aus dem Tritt; das Umschalten funktioniert trotzdem, es zeigt nur kurz das falsche Bild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4323c47db6 |
Schneller anfangen, dann hinaufgehen
Es dauerte eine halbe Minute bis zum ersten Bild. Der Grund war nicht die Leitung, sondern der Server: „Auto\" begann mit acht Megabit, und das heisst 1080p von der ersten Sekunde an. So lange braucht er, um das zu rechnen. Jetzt wird mit drei Megabit begonnen — also 720p. Das laeuft in wenigen Sekunden an, sieht einen Augenblick lang weicher aus, und von dort geht es hinauf, sobald sich zeigt, dass die Leitung mehr traegt. Dabei steckt eine Falle, die den ganzen Gewinn aufgezehrt haette: der Server baut seine Leiter aus der Bestellung. Wer mit drei Megabit anfaengt, bekommt eine Leiter, deren oberste Sprosse drei Megabit ist — und der billige Hebel bewegt sich nur **innerhalb** dieser Leiter. Aus dem schnellen Start waere ein Film geworden, der den ganzen Abend in 720p bleibt. Hoeher geht es deshalb ueber einen Neuaufbau, und der braucht Regeln: nicht zu frueh, nicht nach einem Ruckler, nicht ohne Vorrat, nicht oefter als zweimal. Sie stehen in `UpgradeRule` — reiner Typ, dreizehn Tests, wie beim Absenken. Und deutlich zurueckhaltender als dort: ein zu spaetes Absenken bedeutet Ruckeln, ein zu spaetes Hinaufschalten ein etwas weicheres Bild. Das erste ist ein Fehler, das zweite eine Geschmacksfrage. Bei Live-TV wird nicht hinaufgegangen: dort kostet jeder Neuaufbau den Tuner, und ein schaerferes Bild ist diesen Einsatz nicht wert. `StallLedger` kann jetzt sagen, wann der letzte Ruckler war — das brauchte die Regel, und „noch nie gestockt\" ist etwas anderes als „vor langer Zeit\". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
57170b20ea |
Der Untertitelstil wirkt jetzt sofort
`textStyleRules` wird nur gelesen, wenn die Untertitelspur ausgewaehlt wird — eine laufende Einblendung zeichnet AVFoundation davon nicht neu. Genau das war der Eindruck: man stellt um, es passiert nichts, und erst wer die Untertitel aus- und wieder einschaltet, sieht die Aenderung. Dieses Aus- und Einschalten **ist** die Neuwahl. Also machen wir sie selbst, gleich nachdem der Stil gesetzt wurde. Abgewaehlt und wieder angewaehlt wird dabei dieselbe Option, nicht die erste: bei mehreren Spuren waere das ein stiller Sprachwechsel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c681d69fcc |
Fünf Fehler auf einem Weg — warum nie ein Untertitel kam
Sechs Reparaturversuche haben nichts bewirkt, weil jeder an einer Stelle
ansetzte, die gar nicht die Ursache war. Eine systematische Untersuchung —
45 Hypothesen, jede von drei Skeptikern gegengeprüft — hat fünf voneinander
unabhängige Defekte auf demselben Weg gefunden. Jeder einzelne genügt.
**1. Es wurde eine Playlist bestellt, die keine Untertitelspur tragen kann.**
Jellyfins TranscodingUrl lautet `main.m3u8` — eine Media Playlist, also eine
Liste von Schnipseln. `#EXT-X-MEDIA`, die Zeile, mit der ein Server eine
Untertitelspur ankündigt, darf nach RFC 8216 nur in einer Master Playlist
stehen. Der Wechsel main→master hing aber an der Leiter, und die wird beim
Umpacken und bei jeder von Hand gewählten Stufe nicht bestellt. Ein Matroska
geht immer über das Umpacken — die halbe Sammlung konnte strukturell keine
Untertitel haben.
Der Kommentar daneben war überholt und hat den Fehler verdeckt: er stammte
aus der Zeit, als Umpacken eine fortlaufende MP4 war. Seit es über HLS läuft,
ist die Master-Liste genau richtig. Pfadwechsel und Leiter sind jetzt zwei
Fragen, nicht eine.
**2. Der lokale Server etikettierte jeden Schnipsel als video/mp4.** Auch die
WebVTT-Schnipsel der Untertitelspur. So angekündigt gehen sie bei
AVFoundation an den falschen Parser, finden keine Box, und es entsteht
lautlos keine Einblendung — die Spur steht im Blatt, ist ausgewählt, bleibt
leer. Der Vorrat behält jetzt die Sorte der Gegenstelle; die Endung ist nur
noch der Notnagel.
**3. Nach dem ersten Untertitel wählte niemand mehr etwas aus.**
`selectedSubtitleTrack` wurde beim Neuaufbau nie zurückgesetzt, und beide
Wähler prüfen genau darauf. Eine frische Engine hat aber nichts ausgewählt —
also blieb sie für den Rest des Films stumm, während das Symbol „ein" zeigte.
Auch das Nachfassen im Sekundentakt war damit stillgelegt. Die Bestellung
(`selectedSubtitleStream`) bleibt, die Merker fallen.
**4. Die Schutzmarke war nach 250 ms tot.** `restart()` setzte sie als
Allererstes — und wartete danach auf einen echten Netzaufruf mit fünf
Sekunden Zeitgrenze, bevor es den alten Ereignisstrom abbestellte. In diesem
Fenster meldete der alte Player weiter seine Stelle, und die Prüfung „sind
wir angekommen" traf sofort zu. Die Marke war gelöscht, bevor der neue Stream
bestellt war; danach setzte jede Meldung des anlaufenden Streams die Stelle
auf null, und der nächste Neuaufbau merkte sich den Filmanfang. Das war der
Sprung — und der Grund, warum mein Fix von
|
||
|
|
8e0e8ec246 |
Der Vorrat gibt dem Player Vorrang und weicht, wenn er im Weg steht
Sobald das Umrechnen teuer wird — und das Einbrennen eines Untertitels ist teuer —, bedient Jellyfin einen Schnipsel nach dem anderen. Der Vorrat hat dabei gleichzeitig weitergeholt und dem Player die Leitung weggenommen: zwei Anfragen machen die dringende langsamer, nicht schneller. Herauskam „Es kommen keine Daten mehr\". Jetzt ruht der Vorrat, solange eine Anfrage des Players offen ist. Dazu ein Rueckzugsweg, der gefehlt hat. Der Speicher steht zwischen Player und Server, und was dort schiefgeht, sieht von aussen aus wie eine tote Leitung. Kommt nach zwoelf Sekunden nichts an, wird er weggenommen und es laeuft ohne ihn noch einmal an — das kostet nur den Vorsprung und unterscheidet die beiden Faelle, statt sie zu verwechseln. Erst danach die kleinere Bitrate, erst danach das Aufgeben. Und drei Zeilen ins Protokoll, die drei Runden Ratens erspart haetten: was bestellt wurde, was der Player daraus meldet, und welche Spur eingeschaltet wurde. „Nicht geliefert\" und „geliefert und nicht angezeigt\" sehen von aussen gleich aus — im Protokoll nicht mehr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5e33971452 |
AVPlayer hat die Untertitel wieder abgeschaltet
Der Server lieferte sie, die Spur lag im Stream, ausgewaehlt war sie auch — und angezeigt wurde trotzdem nichts. Der Grund ist eine Eigenschaft, die von Haus aus auf `true` steht: `appliesMediaSelectionCriteriaAutomatically`. Der Player richtet sich damit nach den Untertiteleinstellungen des Systems, und die stehen in aller Regel auf „aus\". Er nahm also jede Wahl, die wir trafen, wieder zurueck — lautlos, und erst nachdem wir sie gesetzt hatten. Beim Ton aendert das Abschalten nichts: welche Spur klingt, ist laengst beim Aushandeln entschieden, und bei einem umgerechneten Stream kommt ohnehin nur eine an. Dazu eine zweite Absicherung: bei einer Abspielliste laedt AVFoundation die Nebenspuren nachtraeglich. Meldet sie beim ersten Mal noch keine, ginge der bestellte Untertitel verloren — deshalb wird im Sekundentakt nachgefasst, solange nichts ausgewaehlt ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cff0c06745 |
Untertitel brachten den Vorratsspeicher auf 404
Untertitel stehen in der Abspielliste als eigene Spur unter EXT-X-MEDIA — eine Anweisung mit einer Adresse darin. Umgeschrieben habe ich bisher nur EXT-X-MAP, also blieb diese relativ stehen und zeigte danach auf den lokalen Server, wo es sie nicht gibt. Zu sehen war der Fehler erst, als jemand Untertitel einschaltete: HTTP 404, mitten im Film. Jetzt sagt der Umschreiber, **was** fuer eine Adresse er gerade in der Hand hat: eine Qualitaetsstufe, eine Nebenspur, ein Kopfstueck oder ein Schnipsel. Das ist noetig, weil sie Verschiedenes wollen — hinter einer Stufe liegt Bild und die soll vorgeladen werden, hinter einer Untertitel- spur nicht. Sonst lueden wir Untertitel statt Bild: der Film bliebe stehen, waehrend die Anzeige reichlich Vorrat meldet. Dazu wandern alle Adressen im lokalen Server auf die **vollstaendige** Form. Eine relative gilt je nach Liste gegen eine andere Grundadresse — die Untertitelspur liegt woanders als das Bild —, und wer das erst beim Abholen aufloest, loest es irgendwann gegen die falsche auf. Das waere der naechste Fehler derselben Art gewesen. Drei Tests halten die Unterscheidung fest, darunter die Liste mit Untertitelspur, die den 404 ausgeloest hat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
88ba1a540c |
Untertitel werden auch gezeigt, und die Stelle geht nicht mehr verloren
Zwei Fehler, die beide beim Umstellen im laufenden Film auftraten. Der Server lieferte den gewaehlten Untertitel brav mit — nur sagte danach niemand dem Player, dass er ihn **zeigen** soll. Gewaehlt wurde allein in `applyPreferredLanguages()`, und das greift nur, wenn in den Einstellungen eine Untertitelsprache steht. Wer keine eingestellt hatte, schaltete die Untertitel ein und sah nichts. Jetzt wird die bestellte Spur nach dem Neuaufbau angezeigt; bei einem umgerechneten Stream liegt genau eine drin, naemlich die bestellte, also gibt es nichts zu verwechseln. Und der Sprung an den Filmanfang, der „teilweise\" passierte: ein frisch geoeffneter Stream meldet erst einmal null, eins, zwei — er springt erst danach an die gemerkte Stelle. Wer in diesem Augenblick `currentTime` liest, liest zwei Sekunden statt einundzwanzig Minuten. Genau das tut ein zweiter Neuaufbau, wenn man zuegig noch etwas umstellt: er merkt sich die falsche Stelle. Daher trat es nicht immer auf, sondern nur manchmal. Waehrend auf die gemerkte Stelle gewartet wird, gelten die Meldungen des anlaufenden Streams jetzt nicht mehr — dasselbe Prinzip wie beim Spulen, wo die Nachzuegler der alten Stelle verworfen werden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0815432efd |
Untertitel lassen sich jetzt einstellen
Groesse in vier Stufen, Schrift und Hintergrund als fuenf feste Paare — Weiss, Weiss auf Schwarz, Schwarz auf Weiss, Gelb, Gelb auf Schwarz. Feste Paare statt zweier Regler, weil unabhaengige Achsen unlesbare Mischungen zulassen: Schwarz auf durchsichtig ueber einer Nachtszene ist eine gueltige Einstellung und eine wertlose. Die Uebersetzung nach AVFoundation ist eine Tabelle aus Schluesseln, bei der jeder Fehler lautlos ist — ein falscher Schluessel laesst den Initialisierer `nil` zurueckgeben, und dann gibt es gar keine Regel. Auf dem Bildschirm sieht „keine Regel\" genauso aus wie „Regel ohne Wirkung\". Zehn Tests decken das ab, darunter die drei Fallen: Alpha zuerst in der Farbe, Prozentwert statt Punktgroesse, und der Hintergrund auch fuer „durchsichtig\" ausdruecklich gesetzt statt weggelassen — weggelassen gilt sonst, was Jellyfins WebVTT selbst mitbringt, und das ist oft ein schwarzer Kasten. Als Protokollmethode, nicht als Feld an `PlaybackSource`: die wird einmal beim Oeffnen gelesen, und eine Einstellung, die erst beim naechsten Film wirkt, haelt jeder fuer kaputt. Angewandt wird an zwei Stellen — am laufenden Gegenstand und am frisch angelegten. Nur das erste zu tun hiesse, dass die naechste Folge wieder in der Vorgabe erscheint. VLC erbt die leere Vorgabe. Die Einstellungsseite sagt beides: dass es fuer den Standardplayer gilt, und dass eingebrannte Bilduntertitel von Blu-rays sich nicht gestalten lassen, weil es dort keinen Text gibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a1ff5cb7bb |
Von der Kachel direkt abspielen, auch bei Serien
Wer weiss, was er sehen will, klickte bisher auf die Kachel, wartete auf die Detailseite und drueckte dort „Abspielen\". Zwei Schritte fuer eine Absicht. Jetzt gibt es oben rechts einen Knopf — auf dem Mac beim Darueberfahren, auf iPad und iPhone ueber langes Druecken. Bei einer Serie ist die Absicht nicht „spiel die Serie\", sondern „mach da weiter, wo ich war\". Welche Folge das ist, entscheidet `EpisodeSelection`: die aelteste ungesehene, fortgesetzt falls angefangen, und von vorn, wenn alles gesehen ist. Ein reiner Typ mit achtzehn Tests — im App-Ziel gibt es kein Unit-Test-Ziel, und „er hat die falsche Folge gestartet\" ist sonst ein Befund, den man nicht nachstellen kann. Ausdruecklich die aelteste ungesehene, nicht die am weitesten fortgeschrittene: wer sich eine Folge aufgehoben hat, soll sie nicht dadurch verlieren, dass er weiter vorn hineingeschaut hat. Dazu `episodes(inSeries:)` im Vertrag — bei Jellyfin eine Anfrage statt dreizehn, fuer alle anderen die Vorgabe ueber `children(of:)`. Nicht `/Shows/NextUp`: der braucht eine Seriennummer, die eine Serie selbst nicht hat, versteckt vollstaendig gesehene Serien und bringt eine eigene Zeitgrenze mit. Welche Folge dran ist, soll eine Regel entscheiden, nicht der Server. Nebenbei behoben: der grosse Abspielknopf im Aufmacher scheiterte bisher mit „keine abspielbare Datei\", sobald dort eine Serie stand — und Jellyfins „Zuletzt hinzugefuegt\" liefert neue Folgen als ihre Serie. Der Knopf liegt als Ueberzug auf dem NavigationLink, nicht darin: ein Button im Label eines Links loest auf iOS nie aus, und `.plain` macht das Versagen lautlos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2a2afa8573 |
Der Aufmacher bleibt stehen, und Folgen bekommen ein Bild
Drei Sachen am Karussell. Die Vorschlaege wechselten bei jedem Start, obwohl es dafuer laengst ein Zeitfenster von sechs Stunden gab. Der Grund: das Los hing an der Position in der Liste, nicht am Titel. Kam einer dazu, verschwand einer, oder aenderte sich ein Fortschritt, bekamen alle dahinter ein anderes Los — und „Weiterschauen\" waechst und schrumpft staendig. Jetzt haengt das Los an der Kennung; wer dazukommt, stoert niemanden. Als eigener Typ mit elf Tests, weil „es wechselt zu oft\" kein Befund ist, den man debuggen kann. Folgen zeigten kein Bild. Die Kette Folge → Serie gab es zwar, aber sie hoerte beim ersten Glied auf, das sich **zusammensetzen** liess — und ob es das Bild dahinter gibt, stellt sich erst beim Abruf heraus. Die Reihenfolge wandert deshalb ins Laden: Folge, Staffel, Serie, der Reihe nach, bis eine Adresse antwortet. Und auf dem Mac gibt es jetzt Pfeile links und rechts, sobald der Zeiger ueber dem Aufmacher liegt, sowie Punkte, die unter dem Zeiger wachsen und auf die man springen kann. Nur dort: auf dem iPad und dem iPhone wischt man ohnehin, und zwei Knoepfe staenden nur im Bild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4b084bc926 |
Ein Sprung in den Vorrat laeuft jetzt sofort weiter
Drei Gruende, und der erste erklaert das ganze Aergernis. AVPlayer besteht nach einem Sprung darauf, erst seinen eigenen Puffer zu fuellen. Der stand auf sechzig Sekunden — richtig, solange er selbst vorladen sollte. Mit einem Speicher davor ist es das Gegenteil: springt man mitten in den Vorrat, liegt dahinter vielleicht noch eine halbe Minute, zu wenig fuer seine sechzig, und er wartet auf den Server, obwohl alles da ist. Der Abstand liegt jetzt bei uns; seiner darf klein sein. Der Vorrat holte ausserdem weiter Schnipsel hinter der Abspielstelle, wenn der Player vorausgesprungen war — und der eine, auf den gerade gewartet wurde, kam als letzter. Er macht jetzt dort weiter, wo gesprungen wurde. Und dieselbe Liste noch einmal zu holen setzte den Vorrat zurueck. Der Player holt sie staendig: nach einem Sprung, beim Stufenwechsel, von sich aus. Zurueckgesetzt wird nur noch bei einem echten Wechsel der Quelle. Dazu ein latenter Fehler, der beim Umpacken ein Schwarzbild gegeben haette: in EXT-X-MAP steckt die Adresse des Kopfstuecks einer fragmentierten MP4, ohne das kein einziger Schnipsel zu dekodieren ist. Sie blieb stehen und zeigte damit relativ auf den lokalen Server. Der alte Test hielt genau dieses Verhalten fest — er hat mitgeaendert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bbd1c3b572 |
Der Vorrat steigt jetzt auch eine Ebene tiefer
Bei „Auto\" bestellen wir eine Liste von Stufen; die Schnipsel stehen eine Ebene darunter. Der Speicher erkannte das zwar, stieg dann aber aus und reichte die Liste mit relativen Adressen durch — die zeigten danach auf den lokalen Server, wo es sie nicht gibt. Es wurde nichts vorgeholt, und beinahe waere auch nichts mehr gelaufen. Jetzt wird auch die Stufenliste umgeschrieben. Holt der Player eine Stufe, kommt er wieder bei uns an, und dort beginnt das Vorauslaufen — bei der Stufe, fuer die er sich entschieden hat. Dazu eine Zeile im Qualitaetsblatt, die sagt, ob der Vorrat laeuft und wie weit er voraus ist. Von aussen ist das nicht zu sehen: laeuft er nicht, spielt es trotzdem, nur ohne Vorsprung — und genau so sieht auch ein Vorrat aus, der laeuft und nichts bekommt. Ohne diese Zeile sind die beiden Faelle nicht zu unterscheiden, und ich haette wieder geraten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
134dc82927 |
Der Vorrat laeuft jetzt voraus, nicht der Player
AVPlayer entscheidet selbst, wie weit er puffert, und bei einer Abspielliste laesst Apple uns nicht dazwischen: auf eine Anfrage nach einem Schnipsel nimmt er von uns nur eine Weiterleitung an, keine Daten. Der Denkfehler war, seinen Puffer vergroessern zu wollen. Wir muessen aber gar nicht dazwischen — wir koennen die Gegenstelle sein. Ein winziger HTTP-Server auf 127.0.0.1 liefert dem Player die Abspielliste, umgeschrieben auf sich selbst, und holt die Schnipsel zuegiger von Jellyfin, als abgespielt wird. Jedes Segment, das der Player verlangt, liegt dann schon lokal. Faellt die Verbindung im Tunnel aus, merkt er es nicht. Das ist der Unterschied zwischen Puffer und Vorrat: den Puffer bestimmt Apple, den Vorrat bestimmen wir. Streng der Reihe nach, und mit begrenztem Vorsprung. Der Server erzeugt die Schnipsel gerade erst; wer Nummer 40 verlangt, waehrend er bei 12 ist, sieht fuer Jellyfin aus wie jemand, der gesprungen ist — und dann beginnt das Umrechnen von vorn, an der falschen Stelle. Der Lauscher haengt ausdruecklich an 127.0.0.1, nicht an allen Schnittstellen. Ein Server, der den Film ins Netz stellt, waere das Letzte, was eine Mediathek-App tun sollte. Kommt er nicht hoch, spielt es trotzdem — ueber die urspruengliche Adresse, ohne Vorrat. Ein fehlender Vorrat ist ein Verlust an Bequemlichkeit; keine Wiedergabe ist ein Verlust an allem. Das Lesen und Umschreiben der Abspielliste ist ein eigener Typ ohne Netz, mit sieben Tests. Eine falsch umgeschriebene Liste ergibt entweder ein schwarzes Bild oder — schlimmer — eine, die stellenweise am Speicher vorbeigeht und nur manchmal hakt. So etwas findet man im laufenden Film nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
46550fc30b |
Umgepackt wird als HLS, und ein Fehlschlag faellt auf VLC zurueck
Der Versuch mit einer fortlaufenden MP4 ist am Geraet gescheitert: AVErrorServerIncorrectlyConfigured (-11850), sofort. Der Grund ist einleuchtend, sobald man ihn kennt — AVPlayer verlangt bei einer fortlaufenden Datei Bereichsanfragen, er will hineinspringen koennen. Eine MP4, die ffmpeg gerade erst erzeugt, kann das nicht: sie hat weder Laenge noch ein Inhaltsverzeichnis am Anfang. Das war eine Annahme, die ich haette messen sollen, bevor ich darauf baue. Vom Ziel bleibt damit das Umpacken, nicht das weite Vorausladen — und das ist immer noch der groessere Teil: ein Matroska-Rip mit HEVC und AC3 landet bei AVPlayer statt bei VLC, mit Bild-in-Bild und AirPlay, ohne dass ein Bild neu gerechnet wird. `Container: mp4` statt `ts`, weil HLS als fragmentierte MP4 auch HEVC traegt; in MPEG-TS waere dafuer eine Neukodierung noetig, also genau das, was hier vermieden werden soll. Dazu ein Rueckzugsweg, der vorher gefehlt hat. Ob der Server die Stroeme wirklich kopieren kann, entscheidet sich erst beim Ausliefern, und manche Antworten nimmt AVPlayer nicht an. Das vorher zu wissen ist nicht moeglich; es hinterher aufzufangen schon. Scheitert das Umpacken, laeuft derselbe Titel einmalig mit VLC an, statt den Nutzer vor eine schwarze Wand mit einer Fehlernummer zu stellen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
62691fc2bc |
Matroska geht jetzt an AVPlayer, nicht mehr an VLC
Ein Blu-ray-Rip liegt als Matroska vor, und AVFoundation oeffnet Matroska nicht — obwohl das Bild darin H.264 oder HEVC ist und der Ton AC3, also alles, was AVPlayer bestens kann. Im Weg stand allein die Verpackung. Dafuer gibt es jetzt einen dritten Weg neben Durchreichen und Neurechnen: umpacken. Dieselben Stroeme, Byte fuer Byte, in eine MP4. Kein Bild wird angefasst, es geht nichts an Qualitaet verloren, und den Server kostet es fast nichts. Entscheidend ist dabei `Protocol: http` statt `hls`. Eine Abspielliste laesst sich nicht vorausladen — AVPlayer holt Segment fuer Segment, und Apple laesst uns dazwischen nicht eingreifen. Eine fortlaufende Datei kommt ueber gewoehnliche Bereichsanfragen, und die kann man so weit vorausholen, wie man will. Der Vorratswunsch steht dort deshalb bei zehn Minuten statt bei einer. Der Gewinn ist gross: solche Titel bekommen Bild-in-Bild, AirPlay und die Systembedienung, die es bei VLC nicht gibt. Umgepackt wird nur, wenn **allein** die Schachtel im Weg steht. Stimmt auch nur ein Codec nicht — DTS, TrueHD, AV1 —, fehlt nicht der Zugang zur Datei, sondern das Koennen; dann bleibt es bei VLC. Sechs Tests halten die Grenze fest, in beide Richtungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
806eb381c7 |
Beim Durchreichen darf viel weiter vorgeladen werden
Es sind zwei verschiedene Lagen, und sie hatten bisher dieselbe Zahl. Rechnet der Server um, existieren die Schnipsel noch gar nicht — ffmpeg erzeugt sie gerade, und weiter voraus als bis dorthin kommt niemand. Eine Minute ist dort reichlich; ein groesserer Wunsch liefe ins Leere. Wird die Datei unveraendert durchgereicht, liegt sie fertig auf der Platte und kommt ueber gewoehnliche Bereichsanfragen — dieselbe Lage wie bei einem Video im Netz, das man beliebig weit vorausladen kann. Dort bei einer Minute aufzuhoeren war schlicht zu schuechtern; jetzt sind es zehn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f48e018fa3 |
Der Regler springt sofort, und man sieht, bis wohin geladen ist
Zwei Dinge, die beide dasselbe Missverständnis erzeugt haben. Der Regler blieb beim Spulen liegen, wo er war, und rückte erst nach, wenn das erste Bild von der neuen Stelle da war — über das Netz Sekunden später. Das liest sich als „der Sprung ist nicht angekommen", und man spult noch einmal. Der Sprung ist aber angekommen, in dem Moment, in dem man ihn auslöst; alles Weitere ist Warten auf Daten, und dafür gibt es den Kreisel. Die Stelle steht jetzt sofort. Nachzügler von der alten Stelle werden verworfen, sonst zöge die letzte Meldung den Regler wieder zurück. Und bis wohin geladen ist, stand nirgends. Dafür ist der `Slider` gewichen: er hat genau eine Spur, und die gehört dem Fortschritt. Etwas dahinter zu zeichnen und auf gleiche Innenabstände zu hoffen ginge so lange gut, bis Apple sie ändert. Der neue Regler zeichnet drei Schichten aus derselben Rechnung — Grund, Geladen, Gespielt — und hat einen Griff, den ein Finger trifft. Der Vorrat wird dabei abgelegt statt ausgerechnet. Eine berechnete Eigenschaft wird nur gelesen, wenn ohnehin neu gezeichnet wird, und das geschieht, weil die Abspielstelle weiterläuft. Im pausierten Zustand läuft sie nicht — und ausgerechnet da will man zusehen, wie der Vorrat wächst. Die Schleife, die ihn nachführt, lief bisher ausserdem nur bei „automatisch": wer eine Stufe von Hand gewählt hatte, hätte nie gesehen, wie weit geladen ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
67c679006f |
Untertitel kommen aus der Datei, und Fortsetzen geht wieder
Im Spurenblatt stand „CC" und sonst nichts — derselbe Fehler wie beim Ton: das ist AVFoundations Name für den einen Untertitel, den ein umgerechneter Stream mitbringt. Zur Auswahl steht jetzt, was in der Datei liegt, und die Nummer geht als `subtitleStreamIndex` mit in die Anfrage. `-1` heisst dabei ausdrücklich „keine". Das ist nicht dasselbe wie nichts zu schicken: dann sucht der Server sich einen aus, und es steht plötzlich ein Untertitel im Bild, den niemand bestellt hat. Und der Klick ins Bild: Anhalten ging, Fortsetzen nicht. Der Abdunkler hinter der Bedienung deckt das ganze Fenster ab und lag über dem Film — solange die Bedienung zu sehen war, landete jeder Klick auf ihm. Auffallen konnte das nur an einer Stelle: nach dem Pausieren bleibt die Bedienung stehen, also traf der Klick, der wieder starten sollte, nie das Bild. Der Abdunkler ist jetzt unantastbar; er dunkelt ab, mehr nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
60194c2d2a |
Die Bedienung blieb stehen, weil der Zeiger sich nie bewegt hat
Seit die Bedienung dauerhaft montiert ist, zeichnet der Scrubber darin im Sekundentakt neu. `onContinuousHover` meldet nicht nur bei Bewegung, sondern auch bei jedem Neuzeichnen — also setzte die Ausblendfrist sich endlos selbst zurück. Die Bedienung blieb für immer stehen, und die Überblendung, die dadurch nie stattfand, sah aus wie eine kaputte Animation. Sie war nie das Problem; das war sie schon beim letzten Anlauf nicht. Jetzt gilt der Zeiger nur als bewegt, wenn er an einer anderen Stelle ist als zuletzt. Und der Klick in den schwarzen Rand: ich habe zweimal ausgerechnet, wo das Bild aufhört, und lag zweimal daneben. Jetzt rechnet das Layout. Die Bildfläche ist auf ihr Seitenverhältnis zugeschnitten und hat ihren eigenen Griff; der schwarze Rand ist schlicht der Hintergrund dahinter. Was am Player ankommt, ist damit per Definition daneben gegangen — es muss niemand mehr feststellen. Damit fallen `videoRect`, `videoFrame`, `playerSize`, der benannte Koordinatenraum und die Abfrage der `AVPlayerLayer` ersatzlos weg. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
983015b9ad |
Die Sprache lässt sich jetzt auch je Serie und je Mediathek sagen
Das Regelwerk stand schon und wurde abgefragt — es gab nur keinen Ort, an dem man etwas hineinschreiben konnte. Jetzt steht die Wahl auf der Serienseite und im Filter-Sheet der Mediathek, beide über denselben Auswahlknopf: zwei mit demselben Inhalt liefen unweigerlich auseinander. Bei der Mediathek in einem eigenen Abschnitt, nicht zwischen den Filtern. Ein Filter schränkt ein, was zu sehen ist; das hier bestimmt, was zu hören ist — dazwischengestellt läse es sich als „nur Titel mit deutscher Tonspur zeigen". Die Rangfolge Serie → Mediathek → allgemein ist dabei von den Einstellungen zu `AudioTrackChooser.resolve` gewandert. Sie ist eine Regel, keine Ablage, und im App-Ziel gibt es kein Unit-Test-Ziel — dort liesse sie sich nicht durchspielen. Sechs Tests decken sie jetzt ab, darunter der Fall, an dem man sonst lange sucht: eine leere Zeichenkette steht in den Einstellungen für „soll der Server entscheiden" und darf nicht als Sprache gelten, sonst schaltet sie die Ebene darüber stillschweigend ab. Nebenbei bekommt das Filter-Sheet die Mediathek selbst statt nur ihrer Art. Die Genres wurden vorher über irgendeine Mediathek dieser Art nachgeschlagen — bei zwei Filmmediatheken also über die falsche. Und der Puffer steht auf einer Minute statt auf 25 Sekunden. Es ist ein Mindestwunsch, keine Obergrenze; der harte Deckel liegt ohnehin woanders, nämlich darin, wie weit der Transcoder schon produziert hat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
596a143545 |
Die Tonspur wird aus der Datei gewählt, nicht aus dem Stream
Im Spurenblatt stand eine einzige Zeile: „Unbekannt". Das ist der Name, den AVFoundation einer namenlosen Spur gibt — und bei einem umgerechneten Stream liefert der Server genau eine. Der Player hatte nichts zur Auswahl, weil die Wahl längst gefallen war, bevor er gefragt wurde. Gewählt wird jetzt aus den Spuren, die **in der Datei** stehen, und zwar bevor der Stream ausgehandelt wird. Die Nummer geht als `audioStreamIndex` mit hinaus; wer mitten im Film umstellt, bekommt den Stream neu — ausser bei unveränderter Wiedergabe, wo der Player ohne Unterbrechung umschaltet. Die Abwägung selbst macht `AudioTrackChooser`: gemerkte Spur, dann Wunschsprache, dann Rangliste, dann Standardspur — und Audiodeskription mit deutlichem Abzug statt Ausschluss, damit eine Sprache, die es nur so gibt, immer noch besser ist als Stille. Drei Ebenen in `LanguagePreferences`: Serie, Mediathek, allgemein. Gefragt wird von unten nach oben. Die Mediathek wird nur nachgeschlagen, wenn überhaupt jemand eine Sprache je Mediathek gesetzt hat — sonst kostete jede Wiedergabe eine Anfrage für eine Ebene, die niemand benutzt. Wer von Hand umstellt, dessen Wahl wird für die Serie gemerkt, ohne Nachfrage. Der Dialog „für diese Serie merken?" ist genau die Art, die man nach dem dritten Mal wegklickt. Sichtbar ist es trotzdem: das Spurenblatt zeigt es und lässt es löschen. Dazu die Rangliste in den Einstellungen, ziehbar, und der Schalter „Audiodeskription meiden". Und die Player-Oberfläche blendet endlich als Ganzes über. Die Kopfzeile hatte gar keine Überblendung — sie sprang herein, während die Bedienung darunter verblasste. Jetzt steht die Animation an einer Stelle statt an sechs, und sie läuft über Deckkraft statt über Einfügen und Entfernen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2dbe260901 |
Mediatheken stehen sofort da statt sich jedes Mal neu zu laden
Jeder Aufruf einer Mediathek war ein Serverabruf mit Ladekreis davor. Die Plakate lagen längst auf der Platte; was fehlte, war die Liste, die sagt, welche davon hierhergehören. Jetzt liegt die erste Seite als Abzug daneben: beim Aufrufen steht sie sofort da, der Server wird still nachgefragt, und man sieht in aller Regel nichts davon. Antwortet er nicht, bleibt der Abzug stehen — eine Liste von gestern ist im Zug brauchbarer als ein Fehlerbildschirm. Bewusst nur die erste Seite. Stünden 240 gespeicherte Titel da und der Server antwortet mit 60, müsste entweder die Liste vor den Augen schrumpfen oder der Versatz fürs Nachladen wäre falsch. Gespeichert wird ausserdem nur, was eine Kachel zeigt. Ein Titel aus einer Listenabfrage bringt jede Spur jeder Datei mit — im Raster steht davon nichts, und ausgepackt werden müsste es bei jedem Aufrufen, also genau da, wo der Abzug Zeit sparen soll. Die Mediathekenliste selbst wird nur als Rettungsanker gemerkt, nicht als Beschleuniger: sie sofort zu zeigen hiesse, eine Sitzung für fertig zu erklären, bevor feststeht, dass der Token noch gilt. Abmelden räumt alles weg. Titel, Zusammenfassungen und Sehfortschritt sind persönlich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
18958aadbc |
Der Klick in den schwarzen Rand trifft jetzt den schwarzen Rand
Er hat weiterhin angehalten statt ein- und auszublenden, und der Grund steckte in der Rechnung: der schwarze Rand wurde aus der Fenstergrösse ausgerechnet, das Bild liegt aber in einer Fläche mit `ignoresSafeArea()`. Auf dem Mac ist der Einzug der Fensterleiste genau der Unterschied zwischen beiden — die Rechnung lag um diesen Betrag daneben. Jetzt wird die Bildfläche gemessen statt angenommen, und beide Zahlen stehen in einem gemeinsamen, benannten Koordinatenraum. Vor allem aber wird zuerst `AVPlayerLayer.videoRect` gefragt: der Player weiss selbst, wo Bild steht. Die eigene Rechnung bleibt für den Moment vor dem ersten Bild und für VLC, das keine solche Ebene hat. Dazu zwei Worte: „Nachschub" heisst jetzt „Nachladen", und „Blatt" heisst wieder „Sheet" — das ist ein Fachbegriff und bleibt englisch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f47c6385e3 |
Auflösungen heissen jetzt, wie Menschen sie nennen
Auf der Detailseite stand „1080" — die Zeichenkette des Servers in Grossbuchstaben. Weder eine Zahl mit Bedeutung noch ein Name, den jemand benutzt. Jetzt steht dort „Full HD", „4K", „HD", „SD". Die Einstufung geht neu über die **Breite**, nicht über die Höhe. Ein Kinofilm in 2,39:1 misst 1920 × 804; nach der Höhe war das bisher „720p", und zwar ausgerechnet bei den grossen Filmen. Die Höhe bleibt als Notnagel für Server, die keine Breite melden, und der Name des Servers als letzter. Nebenbei fällt damit der 4K-Filter bei Jellyfin nicht mehr durch, wenn ein breitwandiges 4K nur 1608 Zeilen hat, und die doppelte Einstufung in PlexMedia verschwindet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e61cb0403d |
Die Tonspur wird jetzt bestellt, nicht abgewartet
Der Befund, der die ganze Umsetzung bestimmt: `AudioStreamIndex` kam in der PlaybackInfo-Anfrage **nicht vor**. Der Server wählte, wir nahmen was kam — und bei einem umgerechneten Stream kommt danach nur noch diese eine Tonspur an. `selectAudioTrack` schob seither ins Leere, weil im Player eine Liste mit einem Eintrag stand. Damit ist auch erklärt, warum die Spracheinstellung beim ZDF-Fall nichts ausrichten konnte: sie lief auf einer Auswahl, die es gar nicht gab. `PlaybackIntent` trägt die Spurnummer jetzt mit, und die Aushandlung nennt sie ausdrücklich. Zwei Tests halten fest, dass sie ankommt — und dass sie ohne Wunsch weggelassen wird, statt eine Null zu schicken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
afb4a63655 |
Der Kern der Sprachwahl: Kürzel, die nicht lügen, und Abwägen statt Filtern
**`LanguageCode`** bringt Kürzel auf eine vergleichbare Form. Der Anlass ist ein stiller Fehler: verglichen wurden die ersten zwei Zeichen, also `ge` gegen `de` — und `ger` ist die ISO-639-2/B-Form von Deutsch, die ffmpeg in viele Dateien schreibt. Bei solchen Titeln wirkte die Spracheinstellung überhaupt nicht. `gsw` bleibt `gsw`. Schweizerdeutsch ist keine Schreibweise von Deutsch, und es gleichzusetzen hiesse, jemandem Schweizerdeutsch unterzuschieben, der Hochdeutsch wollte. Es gehört in die Rangliste, nicht in eine Gleichsetzung. **`AudioTrackChooser`** wägt ab, statt zu filtern: jede Spur bekommt Punkte, die höchste gewinnt. Damit gibt es immer eine Antwort, und Audiodeskription wird gemieden, ohne unmöglich zu werden — gibt es eine Sprache nur mit Audiodeskription, ist sie besser als Stille. Gemerkt wird nicht eine Nummer, sondern eine Beschreibung der Spur: Folgen derselben Serie haben unterschiedlich sortierte Spuren, und „Spur 2" bedeutet in der nächsten Folge etwas anderes. Siebzehn Tests, darunter der ZDF-Fall wörtlich — deutsche Spur mit Audiodeskription als Standard, englisch betitelte deutsche Spur daneben — und zwar in beide Richtungen: mit Meiden gewinnt die richtige, ohne Meiden gewinnt das Kürzel. Sonst stünde der Schalter nur dabei. Dazu trägt `MediaStream` jetzt `title`, `isDefault`, `isHearingImpaired` und `isOriginal`. Der Name aus der Datei ist das zweite Signal neben dem Kürzel und manchmal das einzige richtige. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
946de29893 |
Nachsehen, was in den Tonspuren wirklich steht
Die Sprachwahl vergleicht Kürzel, und Kürzel lügen — beim ZDF-Fall ist die als deutsch ausgezeichnete Spur die mit Audiodeskription. Ob der **Name** der Spur das rettet, sieht man nur, wenn man beides nebeneinander legt. Alles andere wäre geraten, und davon hatten wir heute genug. `--spuren <host> <suchbegriff>` sucht einen Titel und druckt zu jeder Tonspur: Kürzel, Name aus der Datei, wie Jellyfin es zusammensetzt, Codec, Kanäle und die Kennzeichen. Dafür holt der Decoder jetzt auch `Title`, `IsHearingImpaired` und `IsOriginal`. `Title` ist das zweite Signal neben dem Kürzel und manchmal das einzige richtige: wo „eng" steht, kann im Namen „Deutsch" stehen. Wir haben es bisher weggeworfen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
41cd7331a9 |
Entwurf für Standardsprachen der Tonspur
Drei Ebenen (allgemein → Mediathek → Serie), eine globale Rangliste als Boden darunter, und die Wahl als prüfbares Punktesystem. Zwei Funde bestimmen den Entwurf: **Jellyfin hat kein Kennzeichen für Audiodeskription.** `MediaStream` führt `IsDefault`, `IsForced`, `IsHearingImpaired`, `IsOriginal` — nichts für Sehschädigung; die `visual_impaired`-Disposition reicht der Server nicht durch. Sie ist nur am Text zu erkennen, und deshalb muss die Erkennung nachsichtig sein und sich überstimmen lassen. **Der Sprachvergleich greift heute bei `ger` nicht.** Verglichen werden die ersten zwei Zeichen, also `ge` gegen `de` — und `ger` ist die ISO-639-2/B-Form von Deutsch, die ffmpeg in viele Dateien schreibt. Bei solchen Titeln wirkt die Spracheinstellung lautlos gar nicht. Gemessen, nicht vermutet. Auf Serienebene wird nicht eine Sprache gespeichert, sondern die Signatur der tatsächlich gewählten Spur. Nur so ist der ZDF-Fall lösbar, bei dem die deutsche Spur die mit Audiodeskription ist und die als englisch ausgezeichnete die richtige. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
10caa5fdd1 |
Platzhalter statt Ratespiel, und zwei Griffe im Player
**Das Raster sagt jetzt, ob noch etwas kommt.** Eine Mediathek lädt seitenweise nach, und wer unten ankam, sah ein Raster, das aufhört — ohne unterscheiden zu können, ob das alles war oder gerade nachgeladen wird. Beides sah gleich aus. Beide Server nennen die Gesamtzahl (Jellyfin `TotalRecordCount`, Plex `totalSize`), und wir lesen sie längst. Also stehen die nächsten Kacheln schon da, in der Form, die sie haben werden — gedeckelt auf ein Dutzend, weil zweihundert Platzhalter niemandem helfen. Ist wirklich alles da, steht es am Ende ausdrücklich: „Alle 252 Titel geladen". Gegen die Attrappe mit 252 Titeln durchgeblättert und nachgesehen: die Zeile steht da, und alle 252 sind angekommen. **Herein sanft, hinaus zügig.** Das Einblenden der Bedienung begleitet eine Bewegung des Zeigers und soll wirken, als käme sie der Hand entgegen; zwei Zehntel wirkten wie ein Aufblitzen. Das Ausblenden dagegen ist ein Aufräumen, und dabei muss niemand zusehen. **Ein Klick ins Schwarze blendet um, statt anzuhalten.** Ein Film füllt das Fenster fast nie ganz aus — die Balken daneben gehören nicht zum Film, und dort zu klicken ist die natürliche Geste für „zeig mir die Bedienung". Wo das Bild aufhört, wird aus `presentationSize` gerechnet, also aus dem, was wirklich ankommt. Fehlen die Maße, verhält es sich wie bisher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
279d8e36ae |
Zwei Fehler um dieselbe Bildfläche
**Die Wiedergabe sprang von selbst wieder an.** Auf dem Mac pausiert ein Klick ins Bild — und der Klick, mit dem man ein Fenster nach vorn holt, kommt ebenfalls dort an. Man aktiviert das Fenster und drückt dabei auf Play. Klicks innerhalb einer Viertelsekunde nach dem Aktivieren zählen deshalb nicht: länger braucht niemand bis zum ersten gemeinten Klick, kürzer erwischt den Fall nicht. **Schwarzbild mit Ton nach dem Vergrössern.** Kachel und Vollbild bestehen beim Wechsel kurz gleichzeitig, und beide holten sich dieselbe Bildfläche in ihre Hülle. Eine Ansicht hat aber nur einen Vorgesetzten — wer zuletzt aktualisiert, gewinnt, und das war nicht verlässlich die richtige. Blieb die Fläche bei der Kachel, die gleich darauf verschwand, lief im Vollbild der Ton weiter und das Bild blieb schwarz. Die Hülle allein genügte nicht: sie verhindert, dass das Abräumen der einen Ansicht der anderen etwas wegnimmt, entscheidet aber nicht, wer die Fläche bekommen **soll**. Das sagt jetzt `isActive` — abgeleitet aus `playback.presentation`, also aus derselben Quelle, die auch bestimmt, welche Ansicht überhaupt zu sehen ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
348ec96282 |
Eine misslungene Seite beendete das Nachladen für immer
Der Grund für „es sind wieder nicht alle Filme drin": im Fehlerfall stand hier `page = nil`. Eine einzige misslungene Anfrage auf einer wackeligen Leitung, und die Mediathek blieb endgültig bei sechzig Titeln stehen — lautlos, ohne dass irgendetwas darauf hindeutete. Im Zug passiert das zuverlässig. Der Kommentar daneben begründete das Schweigen, und das war auch richtig: ein Fehlerblatt mitten im Scrollen wäre störend. Nur hat er das eigentliche Problem verdeckt — nicht die fehlende Meldung war schlimm, sondern dass es danach keinen Weg zurück gab. Jetzt bleibt die Seite erhalten, und unten erscheint eine Zeile zum Weiterladen. Von selbst wird nach einem Fehlschlag **nicht** mehr nachgefasst: sonst liefe bei jedem Scrollen eine Anfrage in dieselbe tote Leitung. Dazu der Knopf zum Neuladen in der Leiste, wie gewünscht. Ziehen von oben gibt es auf dem Mac nicht, und in einem Raster ist es ohnehin kaum zu treffen — wer den Verdacht hat, dass etwas fehlt, braucht einen Knopf und keine Geste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
24d7423f9c |
Bei Jellyfin galt zuhause als unterwegs
Beinahe-Regression, gefunden bevor sie jemanden traf: `isLocalConnection` kam aus `session.server?.connection.local`, und `server` ist ein `PlexServer` — bei Jellyfin immer `nil`. Jede Jellyfin-Wiedergabe galt damit als entfernt, auch zuhause am Kabel. Der neue Deckel hätte dort ein 67-Mbit-Remux ohne Not auf acht Megabit heruntergerechnet. Jetzt wird gefragt statt geraten: `/System/Endpoint` beantwortet es mit **demselben** Aufruf, mit dem Jellyfin auch selbst entscheidet, ob es eine Bitratenleiter ausliefert. Ein Blick auf die Adresse hätte nicht genügt — wer über einen Reverse Proxy geht, erreicht seinen Server auch von der Couch aus unter einem öffentlichen Namen, und über ein VPN kommt eine private Adresse von weit her. Gefragt wird bei beiden Wegen, beim Anmelden und beim Wiederherstellen: dasselbe Konto kann heute zuhause und morgen im Zug aufwachen. Im Zweifel `false` — lieber einmal zu vorsichtig gedeckelt als eine Wiedergabe, die über eine Fernverbindung gar nicht erst anläuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fd11a9eebe |
„Auto" misst jetzt die Verbindung, statt die Einstellung abzulesen
Der Denkfehler, und er war meiner: `automatic` hiess wörtlich „nimm, was in den Einstellungen steht". Stand dort „Original", bat die App über eine Zugverbindung um eine 4K-Datei und wartete vierzig Sekunden auf etwas, das nie kommen konnte. Das ist keine Automatik, das ist eine Durchreiche mit einem irreführenden Namen. **Die Einstellung ist jetzt die Obergrenze, nicht das Ziel** — „bis hierhin, wenn die Leitung es trägt". Im eigenen Netz bleibt alles wie bisher: dort gibt es keine Grenze, weil die Leitung das Original trägt und ein Deckel das Bild ohne Grund verschlechterte. Die Unterscheidung steht in `isLocalConnection` und ist nicht geraten. Unterwegs beginnt „Auto" bei acht Megabit — genug für ordentliches 1080p, wenig genug, dass es fast überall anläuft — und tastet sich von dort hinauf, solange es gut geht, bis höchstens zur eingestellten Decke. Hinunter geht es sofort, sobald es hakt. **Und die Auflösung geht mit.** Bitrate allein genügt nicht: 4K mit zwei Megabit sieht schlechter aus als 720p mit zwei Megabit — dieselbe Datenmenge, auf viermal so viele Bildpunkte verteilt. Wer die Bitrate senkt, muss die Auflösung mitnehmen, sonst spart er Daten und verschenkt das Bild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
efed8717a3 |
„Auto" gibt nicht mehr auf, es versucht es kleiner
Der Fehlstart im Zug hat den eigentlichen Mangel gezeigt: die ganze neue Regelung misst **ab dem ersten Bild**. Kommt nie eines, misst sie nichts, greift nichts — und „Auto" hat sich als das erwiesen, was es wörtlich heisst: „nimm, was in den Einstellungen steht". Steht dort „Original", bittet die App über eine Zugverbindung um eine 4K-Datei und wartet vierzig Sekunden auf etwas, das nie kommen kann. Statt aufzugeben geht der Watchdog jetzt eine Sprosse tiefer und baut neu auf — bei Filmen über `restart()`, bei Sendern über den Tuner-Abbau. Gezählt gegen dasselbe Budget wie die Sicherheitsleine, sonst schickt eine tote Leitung die App endlos im Kreis. Scheitert auch das, nennt die Meldung jetzt den Grund und den Weg statt nur „keine Daten mehr": wer ohne Grenze angefragt hat, soll erfahren, dass es daran liegt und wo es sich ändern lässt. Und „Strom" heisst Stream. Zweiunddreissig Stellen, dazu das Gedächtnis ergänzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4abce1c8b9 |
Name und Passwort zuerst, und Blätter in Fenstergrösse
**Die Anmeldung.** Sobald ein Jellyfin-Server Quick Connect anbietet, sprang der Weg von selbst in den Code-Fluss. Gut gemeint — dabei läuft kein Passwort durch diese App —, aber am Alltag vorbei: der Code will in einem zweiten Gerät bestätigt werden, und wer im Zug sitzt, hat das nicht offen. Ein Passwortspeicher füllt dagegen beide Felder in einer Sekunde. Der Code bleibt als Knopf darunter erreichbar. **Die Blätter.** Wieder `#if os(macOS)`, das auf dem Mac nie greift, weil dort der iOS-Bau läuft. Filter, Spurwahl und Qualitätsblatt bekamen deshalb eine Blattkante in iPhone-Höhe — ein schmaler Streifen, der in einem grossen Fenster unten im Nichts klebt. Jetzt zur Laufzeit entschieden: auf dem Mac volle Höhe, sonst wie bisher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8ff74ed4fa |
Der Hinweis bei „ohne Grenze" — aber nur mit Beleg
Das letzte offene Stück aus dem Plan. Du wolltest die WLAN-Voreinstellung nicht verschoben haben, sondern einen Hinweis — und einen, der sich auf etwas stützt. Ohne Grenze kommt die Entscheidung oft bei direkter Wiedergabe heraus, und dann gibt es keine Stufe, auf die irgendetwas ausweichen könnte. Beim Testserver heisst „Original" konkret: 67 Mbit, 4K, HEVC. Über eine Fernverbindung trägt das keine Leitung. Der Hinweis erscheint deshalb nur, wenn beides zutrifft: die Einstellung steht auf „ohne Grenze" **und** die letzte Wiedergabe hat tatsächlich gehakt — gemessen am gleitenden Fenster, nicht geraten. Eine Warnung, die immer dasteht, liest nach dem zweiten Mal niemand mehr. Der Befund überlebt die Wiedergabe, weil der Player längst zu ist, wenn jemand in die Einstellungen schaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c334fca665 |
Den grössten Titel messen, und die Quelle dazusagen
Die Reihe lieferte viermal dasselbe — 480p bei 1,1 Mbit, gleichgültig ob mit `videoResolution` oder mit 20 Mbit. Das sieht nach einem Fehler in der Anfrage aus und ist wahrscheinlich keiner: gemessen wurde „2 Fast 2 Furious", der erste Titel im Alphabet, und vermutlich ein 540p-Rip. Plex rechnet nichts hoch. Damit misst die Reihe die **Datei** statt die Einstellungen — und aus einer Messung, die immer dasselbe sagt, liest man leicht das Falsche heraus. Die Probe nimmt jetzt den Titel mit der höchsten Bitrate aus einer Stichprobe von sechzig und druckt die Quelle dazu: Auflösung, Bitrate, Codec. Damit ist auf einen Blick zu sehen, ob eine kleine Antwort eine kleine Quelle ist oder ein echter Deckel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d2237c29cd |
Fragen statt starten — die Probe überfuhr den Server
Beim letzten Lauf scheiterte auch die Anfrage, die vorher funktioniert hatte. Kein neuer Befund, sondern mein Messfehler: die Probe warf sechs Transcode-Sitzungen in Folge an, und danach lehnt Plex alles mit nackten 400ern ab — auch das, was vorher ging. Jede Anfrage an `start.m3u8` startet einen Transcoder. Für eine Parameterreihe ist das das falsche Werkzeug. Der **Entscheider** sagt, was der Server täte, ohne etwas zu tun: Auflösung, Bitrate, und ob umgerechnet oder umgepackt wird. Die Reihe läuft jetzt darüber. Die zwei verbleibenden echten Sitzungen bekommen Abstand voneinander. Der Wert wird aus der Antwort gesucht statt geparst — Plex antwortet je nach Kopfzeile als XML oder JSON, und es geht um eine Anzeige, nicht um Daten, mit denen weitergerechnet wird. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b861ca0599 |
Prüfen, woran die kleine Plex-Auflösung hängt
Die Servereinstellungen sind es nicht: „Begrenzung der Stream-Bitrate übers Internet" steht auf Original, keine Upload-Grenze, Hardware- Transcoding an. Die Vermutung ist damit widerlegt. Bleibt die eigene Anfrage. Bei „automatisch" schickt die App `maxVideoBitrate` und `videoQuality`, aber **kein** `videoResolution` — dort steht `nil`, weil die Stufe keine vorgibt. Verdacht: Plex nimmt dann seine Voreinstellung für die Qualitätsstufe, und die ist 540p. Wäre das so, bekäme jeder Plex-Nutzer bei „automatisch" über eine Fernverbindung stillschweigend 960x540 bei 1,1 Mbit, auch auf einer schnellen Leitung. Die Probe fährt deshalb vier Kombinationen ab und hält jeden Transcoder einzeln wieder an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7ecb16d4eb |
autoAdjustQuality bricht die Plex-Wiedergabe — wieder raus
Gemessen, nicht vermutet: zwei Anfragen an denselben Titel, getrennte Sitzungskennungen, einzige Abweichung der Wert. Ohne ihn kommt eine Playlist, mit ihm eine nackte **HTTP 400**. Die Zeile hätte also nicht die Qualität verbessert, sondern die Wiedergabe verhindert. Der Plan hat vor genau diesem Verhalten gewarnt — nur habe ich es am Entscheider vermutet und ihn dort ausgespart, während die Start-Adresse ihn ebenso ablehnt. Plex' eigene Clients schicken den Wert; welche weitere Angabe sie dabei mitführen, ist offen. Bis das gemessen ist, bleibt er draußen: eine Leiter, die es vielleicht gibt, ist keinen Strom wert, der sicher nicht läuft. `TranscodeRequest.adaptive` bleibt samt seinen Tests bestehen. Das Gerüst steht, es fehlt die richtige Beschwörungsformel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
165a82acd0 |
Der Plex-Vergleich verglich zwei Mal dasselbe
`askingForLadder()` behält die Sitzungskennung bei — richtig für die App, weil sonst beim Aufräumen ein Transcoder stehen bliebe. Für eine Gegenüberstellung ist es tödlich: die zweite Anfrage bekam die laufende Sitzung der ersten zurück, und die Antwort war zwangsläufig identisch. Im Wortlaut stand zweimal dieselbe Sitzungs-GUID — der Beweis, dass gar nichts gemessen wurde. Jetzt zwei eigenständige Anfragen mit getrennten Kennungen, beide Kennungen werden gedruckt, und beide Transcoder werden hinterher angehalten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e324a474fd |
Watchdog heisst Watchdog, Engine heisst Engine
Eingedeutschte Fachbegriffe in den Kommentaren — „Wachhund" für Watchdog, „Motor" für Engine. Das liest sich albern und ist unklarer als das Original; niemand sagt Wachhund, wenn er einen Watchdog meint. Acht Stellen berichtigt. Gewöhnliche Sprache bleibt deutsch, der Stil der Kommentare ändert sich nicht. Dazu die Plex-Leiter messbar gemacht: `--leiter` baut dieselbe Anfrage zweimal, mit und ohne `autoAdjustQuality=1`, und zählt die Stufen. Die Zeile steht seit dem Umbau im Code und war nie geprüft — genau die Art ungeprüfter Annahme über Serververhalten, die bei Jellyfin vier Runden gekostet hat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
314c1a3bfc |
Die Schnipsel wiegen, nicht die Playlist zählen
Berechtigter Einwand: eine Playlist zu bekommen beweist gar nichts. Sie ist Text und kostet ein Kilobyte; die Datenrate steckt in den Schnipseln. Die vorige Fassung zählte `#EXTINF`-Zeilen und hätte „funktioniert" gemeldet, auch wenn der Server bei jeder Stufe denselben Strom ausliefert. Jetzt lädt die Probe je Stufe den **ersten Schnipsel** und rechnet aus, was er an Datenrate bedeutet — inklusive der unveränderten Stufe als Vergleichsmass. Werden sie kleiner, trägt die Idee. Bleiben sie gleich gross, rechnet der Server trotz anderer Zahl dasselbe, und ein selbstgebautes Menü brächte nichts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f48eb55f35 |
Prüfen, ob wir uns die Leiter selbst bauen können
Der Wortlaut der Master-Playlist zeigt, woraus Jellyfins eigene Leiter bestünde: dieselbe `main.m3u8`-Adresse, mehrfach, mit anderem `VideoBitrate`. Eine Master-Playlist ist nur ein Menü. Liefert der Server diese Unter-Playlists auch dann, wenn **wir** die Zahl ändern, brauchen wir seine Zustimmung gar nicht — dann schreiben wir das Menü selbst, und die Frage, warum er es verweigert, ist gegenstandslos. Die Probe versucht deshalb drei Stufen von Hand: halbe Bitrate, ein Viertel, und ein Viertel zusammen mit `maxHeight=480`. Das letzte ist der interessante Fall — Jellyfins eigene Leiter lässt die Auflösung stehen und dreht nur an der Bitrate. Eine Stufe, die auch kleiner rechnet, sieht bei wenig Bandbreite deutlich besser aus als dieselbe Auflösung mit zu wenig Daten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6e18fda4aa |
Die Probe zeigt Fassung und Wortlaut
Der Proxy ist gelöst — Jellyfin hält die Verbindung jetzt für ausserhalb —, und es kommt trotzdem nur eine Stufe. Damit sind alle Hürden widerlegt, die im Quelltext von `master` stehen. Also stimmt etwas an der Annahme nicht, und am wahrscheinlichsten ist es die Fassung: ich lese womöglich den Quelltext einer anderen Version als der, die dort läuft. Deshalb meldet die Probe jetzt `Jellyfin <Version>` — und druckt bei nur einer Stufe die Master-Playlist **im Wortlaut**. Ab da hilft keine Zusammenfassung mehr: was fehlt, sieht man nur im Original. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e4e6cc6cbd |
Die Probe liest zurück, was der Server wirklich eingetragen hat
Ein Eintrag unter „Bekannte Proxys", der gespeichert, aber nicht übernommen wurde, sieht von außen genauso aus wie gar keiner — und bei der Fehlersuche unterscheidet das alles. `GET /System/Configuration/network` führt `KnownProxies`. Zusammen mit `IsLocal` und `IsInNetwork` steht damit in drei Zeilen, woran es liegt: kein Eintrag, der falsche Eintrag, oder fehlender Neustart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
539ea0aacb |
Die Probe zeigt beide Werte, nicht nur das Urteil
`IsLocal` und `IsInNetwork` beantworten verschiedene Fragen, und der Unterschied benennt die Ursache: `IsLocal` heißt, die Anfrage kommt über 127.0.0.1 — dann steht der Proxy auf demselben Host, und ein Eintrag unter „Bekannte Proxys" mit der LAN-Adresse geht ins Leere, weil Jellyfin diese Adresse nie zu sehen bekommt. Vorher stand nur „LOKAL" da, und damit blieb offen, ob der Eintrag fehlt oder der falsche ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a79d562642 |
Die Sicherheitsleine — der teure Weg, mit engen Regeln
Wenn der billige Hebel nichts ausrichtet, weil die Playlist nur eine Stufe hat oder schon die unterste läuft, bleibt nur der Neuaufbau mit kleinerer Bitrate: Transcoder anhalten, Motor abreißen, neu aushandeln, zurückspringen. Sekunden mit schwarzem Bild. **Ein Notnagel, keine Regelschleife.** Zu oft ausgelöst erzeugt er genau das Ruckeln, das er beheben soll. `DowngradeRule` ist deshalb eine Liste von Bedingungen, die alle gelten müssen, und keine Schwelle: nur bei „automatisch", nur wenn der billige Weg erschöpft ist, nur ab drei Rücklern in anderthalb Minuten oder einem einzelnen über fünfzehn Sekunden, nicht in den ersten zehn Sekunden, nicht binnen zehn Sekunden nach einem Sprung, höchstens alle drei Minuten, höchstens zweimal. Ein Sprung leert den Puffer **immer**. Ihn als Beleg für eine schlechte Leitung zu nehmen hieße, sich für das eigene Tun zu bestrafen. Das Urteil ist eine Aufzählung und kein `Bool`: „es hakt, aber wir tun nichts" ist ohne Grund von einem Fehler nicht zu unterscheiden. Vierzehn Tests schreiben jede Bedingung einzeln hin. Ist das Budget verbraucht, **wird es gesagt** statt lautlos weiter abgesenkt. Still bis zur untersten Sprosse zu gehen ist schlimmer, als einmal zuzugeben, dass die Leitung es nicht trägt. **Live-TV ist dabei**, wie gewünscht — aber mit Tuner-Abbau vorweg und Budget eins statt zwei. Er ist ein Stück Hardware, vier Stück gibt es, und ein doppelt belegter ist schlimmer als ein Stottern. Bekannte Grenze: `endLiveSession` meldet nicht, **ob** die Freigabe geklappt hat — das Protokoll gibt nichts zurück. Deshalb der eine Versuch und jeder Schritt im Protokoll, damit ein verlorener Tuner nachvollziehbar bleibt. Nebenbei aufgeräumt: die Wahl von Hand und die Sicherheitsleine teilen sich jetzt denselben Neuaufbau. Was sie unterscheidet, steht in `effectivePolicy` — die Wahl setzt `quality`, die Automatik `automaticCeiling`, und beides fließt dort zusammen. Die Automatik schreibt dabei nie `rememberedQuality` fort: eine schlechte Zugfahrt soll nicht dauerhaft das Wohnzimmer verschlechtern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d878289929 |
Der Server sagt jetzt selbst, ob er uns für lokal hält
Die Probe endete bisher mit einem Konjunktiv: „Kam dieser Aufruf von zuhause, ist Punkt 1 die Erklärung." Das ist eine Vermutung an genau der Stelle, an der die ganze Messung hängt. `GET /System/Endpoint` beantwortet `IsInNetwork` mit **demselben** Aufruf, mit dem auch die ABR-Sperre entscheidet: `_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())`. Damit steht als Erstes im Bericht, ob eine Leiter überhaupt möglich wäre — und ob das Folgende eine Aussage über unterwegs enthält oder keine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
52d6ebba43 |
„Automatisch" misst jetzt, statt die Einstellung durchzureichen
Bisher hieß `PlayerQuality.automatic` wörtlich: nimm die Zahl, die für WLAN oder Mobilfunk hinterlegt ist. Keine Messung, kein Nachregeln — die Entscheidung fiel einmal beim Start und galt bis zum Ende. `StreamHealth` beurteilt den Strom jetzt fortlaufend aus Proben im Sekundentakt. Ein reiner Typ: kein AVFoundation, keine Uhr, kein Netz. Damit lässt sich eine schlechte Zugfahrt als Zahlenreihe hinschreiben und nachsehen, was sie auslöst — zehn Tests tun genau das. **Welches Signal wann meldet**, ist der eigentliche Inhalt. Der Pufferverlauf ist der einzige echte Frühwarner: schrumpft er schneller, als die Zeit vergeht, kommt weniger herein als hinausgeht, und das sieht man zehn bis zwanzig Sekunden vor dem Rückler. `isLikelyToKeepUp` ist früh, aber sprunghaft — es zählt erst, wenn es anhält. Der Rücklerzähler meldet zu spät zum Regeln und bleibt trotzdem der Massstab, an dem sich hinterher ablesen lässt, ob es besser wurde. Die **gemessene** Bitrate steht bewusst nicht drin: auf einer kleinen Sprosse holt der Player wenig, ohne dass die Leitung schlecht wäre. Ausdrücklich kein Bandbreitentest. Der kostet Daten, ist beim Vorliegen veraltet und misst die Vergangenheit. Der Pufferverlauf ist die bessere Messung, weil er die einzige Größe misst, auf die es ankommt: lädt es schneller, als es läuft. Gehandelt wird über `preferredPeakBitRate` — der billige Hebel, der ohne Neuaufbau wirkt. Gezielt wird dabei auf eine Sprosse, **die es in `AVAsset.variants` wirklich gibt**: ein Wert zwischen zweien wählt dieselbe wie die untere und machte jede Anzeige darüber zur Lüge. Absichtlich unsymmetrisch — runter sofort, hinauf frühestens nach einer Minute Ruhe. Und getrennt von `quality`: die Automatik ruft **nicht** `select(quality:)`. Die Methode schreibt `rememberedQuality` fort und setzte eine manuelle Stufe — womit die Automatik sich selbst abschaltete und eine schlechte Zugfahrt dauerhaft das Wohnzimmer verschlechterte. Hat die Playlist nur eine Stufe, greift der Hebel ins Leere. Das ist kein Fehler, sondern der Fall, für den die Sicherheitsleine gebaut wird — bis dahin steht er wenigstens im Protokoll. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cdff53fa0d |
Eine Leiter bestellen statt einer einzelnen Stufe
Der Kern des Ruckelproblems: die App bestellte bei beiden Servern eine HLS-Liste mit genau einer Variante. AVPlayer kann nur zwischen Stufen wechseln, die in der Liste stehen — bei einer gibt es nichts zu wechseln, und wenn die Leitung einbricht, kann nichts nachgeben. **Jellyfin.** `enableAdaptiveBitrateStreaming` wird gesetzt, und zwar ersetzend: die Voreinstellung im Server ist `false`, und bei zwei gleichnamigen Werten gilt beim Server der erste — dieselbe Falle wie bei `maxHeight` eine Funktion weiter oben. Dazu wird `main.m3u8` zu `master.m3u8`, falls der Server die Schnipselliste nennt; nur auf der Stufenliste kann der Parameter wirken. Am Geräteprofil ändert sich **nichts**. Mehrere `TranscodingProfiles` wären der naheliegende Fehlgriff — die beschreiben Formate, nicht Bitraten. `MaxStreamingBitrate` bleibt die oberste Sprosse; was darunter liegt, erfindet der Server. **Plex.** `autoAdjustQuality=1`, als Eigenschaft der Anfrage statt als feste Zeile: `queryItems` speist auch die Adresse zum Herunterladen, und dort ergäbe eine Leiter keinen Sinn. Bewusst **nicht** am Entscheider — der antwortet auf Unbekanntes mit nackten 400ern, wofür es hier eigens `transcoderBisect` gibt. Er beantwortet ohnehin nur „darf das gespielt werden". Stünde der Wert dort und nähme der Server ihn übel, wäre nicht die Leiter kaputt, sondern die Wiedergabe. Beides nur bei „automatisch". Wer im Blatt ausdrücklich „720p · 3 Mbps" antippt, bekommt das und nicht „höchstens 3, meistens weniger" unter unverändertem Etikett. **Was das nicht kann, und das gehört dazugesagt:** Jellyfin verweigert die Leiter im lokalen Netz grundsätzlich — „Within the local network this will likely do more harm than good." Ein schwaches WLAN zählt dort mit, und vom Client aus ist das nicht zu überstimmen. Ebenso fällt sie weg, wenn Bild oder Ton nur kopiert statt gerechnet werden. Ob am Ende wirklich mehrere Stufen ankommen, steht deshalb nicht im Code, sondern in `PlaybackStatistics.variantCount`. Elf neue Tests, darunter der doppelte Parameter, die andere Schreibweise, und dass die Sitzungskennung beim Umbau dieselbe bleibt — sonst bliebe beim Aufräumen ein Transcoder stehen. Die Probe zeigt jetzt außerdem, ob kopiert statt gerechnet wird. Das ist die zweite Hürde, und sie ist von zuhause aus nicht sichtbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2d3c4adbba |
Die Probe räumt auf und sagt, woran es liegt
Sie hat am Server eine Transcode-Sitzung angelegt und nicht wieder beendet — ein Messwerkzeug, das Arbeit hinterlässt, ist ein schlechtes. Jetzt läuft `endPlayback` am Ende. Und sie erklärt das Ergebnis, statt es nur hinzustellen. Kommt nur eine Stufe zurück, nennt sie die Hürden aus Jellyfins eigenem Quelltext (`DynamicHlsHelper.EnableAdaptiveBitrateStreaming`) — allen voran die erste und überraschendste: im lokalen Netz liefert Jellyfin **grundsätzlich** keine Leiter. Wörtlich dort: „Within the local network this will likely do more harm than good." Ein schwaches WLAN ist für Jellyfin auch nur lokales Netz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ab597ee2f0 |
Stillstand ohne Absicht ist kein Pausieren
`timeControlStatus` meldet zwei verschiedene Dinge als `.paused`: das Anhalten durch den Nutzer und den Fall, dass `play()` gerufen wurde, der Player aber nicht losläuft — weil die Quelle nicht bereit ist oder weil nichts ankommt. Die Engine hat beides als „pausiert" weitergegeben. Sichtbar war das als Player mit Abspieldreieck und Scrubber auf 0:00, während in Wahrheit auf Daten gewartet wurde. Wer dann tippte, drückte auf Play bei etwas, das längst laufen sollte. Die Engine führt jetzt mit, ob überhaupt jemand spielen wollte. Stillstand mit Absicht ist Pause, Stillstand ohne Absicht ist Warten. Dazu die Anzeigeregel, wie gewünscht: **der Kreisel gilt immer**, mit oder ohne eingeblendete Bedienung. Er ist keine Verzierung der Knöpfe, sondern die Auskunft „es kommt gleich weiter" — und die braucht man gerade dann, wenn nichts eingeblendet ist und man vor einem stehenden Bild sitzt. Das Play-Pause-Symbol erscheint dagegen nur mit der übrigen Bedienung, und an seiner Stelle bleibt beim Laden eine Lücke, damit der Kreisel hindurchscheint statt zwei Anzeigen übereinanderzulegen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
baa542690c |
Vorrat anlegen, und VLC meldet endlich das Stocken
Zwei Änderungen, die an der Wiedergabe selbst etwas tun — beide ohne Abhängigkeit davon, ob die Server je eine Leiter liefern. **Vorrat.** Ohne Angabe legt AVPlayer so viel an, wie es für einen zügigen Start braucht, und keinen Meter mehr. Das ist richtig, solange die Leitung trägt, und falsch, sobald sie aussetzt: ein Loch von fünf Sekunden trifft dann auf einen Puffer von fünf Sekunden, und das Bild steht. Fünfundzwanzig Sekunden sind ein Anfangswert, kein Messergebnis. Nur für Filme und Folgen über das Netz. **Nicht für Sender** — dort hieße ein halbminütiger Vorrat eine halbe Minute Verzug und ein zähes Umschalten. Und nicht für Dateien von der Platte, die brauchen keinen. Wer das entscheidet, muss mehr wissen als der Motor, deshalb reist die Zahl mit der Quelle statt dass die Engine rät. **VLC hat das Stocken nie gemeldet.** `VLCMediaPlayerState` kennt keinen Wert dafür — die Aufzählung führt nur `opening`, `playing`, `paused`, `stopped`, `error`. Der Puffer-Fortschritt kommt über `mediaPlayerBufferingChanged:`, und die Methode fehlte. Folge: `.buffering` ging dort **nie** an, der Wachhund im Player löste nie aus, und ein hängender Strom lief stumm ins Nichts — ohne Kreisel, ohne Meldung, ohne Abbruch. Ausgerechnet auf dem Weg, der die großen Remuxe spielt, bei denen eine schwache Leitung am ehesten scheitert. Gewertet wird nur aus dem Spielen heraus: laut Dokumentation kommt vor jeder Wiedergabe einmal 0,0 und einmal 1,0, und dieses anfängliche Füllen ist der Start, kein Rückler. Dazu meldet VLC jetzt überhaupt Statistik. `variantCount` bleibt dabei `nil` und nicht `1` — eine Datei hat keine Leiter, und „gibt es nicht" ist etwas anderes als „gibt es einmal". Bei der Bitrate steht die Annahme im Kommentar statt im Nebel: libvlc führt `inputBitrate` vermutlich in Byte je Sekunde, daher der Faktor acht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
afab93f53a |
FlixyProbe kann die Leiter zählen
Die Frage, von der alles Weitere abhängt, ist eine Tatsachenfrage: Liefert der Server eine HLS-Liste mit mehreren Stufen oder mit einer? Sie über einen Build aufs Gerät zu beantworten kostet einen halben Tag; über HTTP kostet sie Sekunden. Genau dafür gibt es dieses Werkzeug. `--playlist <adresse>` holt eine Playlist und zählt die `EXT-X-STREAM-INF`-Zeilen mitsamt Bandbreite und Auflösung. Es unterscheidet dabei die drei Fälle, die man auseinanderhalten muss: eine Master-Playlist mit mehreren Stufen, eine mit genau einer — und eine Varianten-Playlist, die gar keine Stufen führt, sondern Schnipsel. Die letzte Verwechslung ist die naheliegende: `main.m3u8` sieht aus wie eine Playlist und ist die falsche. `--jellyfin <host>` meldet sich an, holt einen Film, lässt den Server eine Transcode-Adresse aushandeln und prüft sie dreimal: unverändert, als `master.m3u8`, und als `master.m3u8` mit `enableAdaptiveBitrateStreaming=true`. Damit steht danach fest, ob der Umbau in JellyfinBackend überhaupt etwas bringen kann. Das Passwort wird eingetippt statt übergeben, ohne Echo und ohne Ablage. Gedruckt werden Pfad und die **Namen** der Abfragewerte, nie die vollständige Adresse mit Schlüssel — dieselbe Regel, nach der das Werkzeug schon vorher gebaut war. Beide Modi laufen vor der Plex-Anmeldung: die eine Frage hat mit Plex nichts zu tun, die andere bringt ihre eigene Anmeldung mit. Der Leser selbst ist gegen drei erfundene Playlists geprüft — mehrstufig, einstufig, und eine Schnipselliste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
903bfe5f63 |
Der Player weiß jetzt, wie es ihm geht
Bisher gab es über eine laufende Wiedergabe zwei Zahlen — gemessene Bitrate und verworfene Bilder —, und die standen nur zur Anzeige da. Die Frage „trägt die Leitung das gerade" war nicht zu beantworten, weder für den Nutzer noch für die App. **Die wichtigste neue Zahl ist `variantCount`.** Sie kommt aus `AVAsset.variants`, also vom Parser des Players, und beantwortet die Frage, ob es überhaupt etwas zum Umschalten gibt. Steht dort 1, greift jedes Nachregeln ins Leere — und genau das ist heute der Fall, weil die App bei beiden Servern eine Playlist mit einer Stufe bestellt. Dazu der Pufferstand aus `loadedTimeRanges`: der einzige echte Frühwarner. Schrumpft er schneller, als die Zeit vergeht, kommt weniger herein als hinausgeht — zehn bis zwanzig Sekunden, bevor das Bild steht. Alles andere meldet sich erst danach. **Verworfene Bilder werden jetzt über alle Protokolleinträge summiert.** Das Zugriffsprotokoll legt bei jedem Stufenwechsel einen neuen an, dessen Zähler bei null beginnt; wer nur `last` liest, hält eine Wiedergabe für makellos, die schon zwanzigmal Bilder verworfen hat. **Der Wachhund war blind für den häufigsten Fall.** Vierzig Sekunden am Stück ohne Daten brechen ab — aber er setzte sich bei jedem kurzen Erholen zurück, und zehn Rückler à dreißig Sekunden lösten ihn nie aus. `StallLedger` zählt daneben mit, wie viel Zeit im gleitenden Fenster fürs Warten draufging. Es bricht nichts ab, es stellt nur fest — und ist ein reiner Typ ohne Uhr und ohne AVFoundation, damit sich eine Folge von Rücklern als Zahlenreihe hinschreiben und das Urteil prüfen lässt. Sieben Tests, darunter genau der Fall, an dem der alte Wachhund vorbeisah. Gezählt wird nur der Weg vom Spielen ins Warten. Das anfängliche Puffern ist der Start, kein Rückler; würde es mitgezählt, hätte jede Wiedergabe eine Vorstrafe. Und zwei Kommentare sind berichtigt. Sie behaupteten, AVPlayer wechsle die Bitrate von selbst und das sei „die zugesagte automatische Qualitätsanpassung". Umschalten kann er nur zwischen Varianten, die in der Liste stehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9c66727aca |
Mediatheken ohne bekannte Art verschwanden wortlos
`isSupported` war `movie || show`. Alles Übrige fiel heraus — auch eine Mediathek mit gemischtem Inhalt, die Jellyfin ohne `CollectionType` meldet. Wer Filme und Serien nicht getrennt hat, sah sie nirgends: nicht in der Seitenleiste, nicht auf der Startseite, und nirgends eine Meldung. Eine unbekannte Art ist kein Grund, etwas zu verschweigen. Musik, Fotos und Live-TV bleiben draußen, und das aus jeweils eigenem Grund — jetzt aufgezählt statt stillschweigend. Die Attrappe blättert außerdem richtig: vorher kam immer alles auf einmal und `TotalRecordCount` war die Länge der Antwort, womit eine App, die richtig blättert, genauso aussah wie eine, die es gar nicht versucht. Damit ist belegt, dass die Mediathek alle 252 Titel holt — in fünf Seiten, bis StartIndex 240. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
682049b45a |
Staffeln ohne eigenes Plakat blieben grau
Die Bildadresse entstand aus der Kennung, nicht aus einer Antwort des Servers: jeder Titel ohne eigenes Plakat trug eine Adresse, die mit „nicht gefunden" antwortet. Und weil ein 404 zu Recht nicht wiederholt wird, blieb die Kachel für immer grau. Betroffen waren vor allem Staffeln — die haben bei Jellyfin oft kein eigenes Bild, sondern leihen das der Serie. Genau diesen Rückfall gab es nur für Folgen, nicht für Staffeln. Beides behoben: kein Bild, keine Adresse — und eine Staffel darf sich nehmen, was die Serie hat. Dieselbe Vorsicht stand eine Zeile tiefer schon beim Hintergrundbild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
00db1bbab9 |
Der Folgen-Schlüssel hieß beim Auslesen anders als beim Nachschlagen
`Text("episode.number \(episode)")` trug Xcode beim Auslesen als
`episode.number %@` in den Katalog ein, während die Laufzeit
`episode.number %lld` nachschlägt. Zwischen zwei verschiedenen
Schlüsseln findet man nie einen — und auf dem Bildschirm stand weiter
der Schlüssel selbst.
Jetzt über `String(localized:)` wie die Staffel zwei Blöcke darüber.
Damit ist der Platzhalter eindeutig, und die übersetzte Tabelle im
gebauten Bündel führt genau die beiden Schlüssel, die der Code sucht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5d09836686 |
Die Medientasten bedienen jetzt Flixy
Die App hat sich beim System nie als „läuft gerade" gemeldet — das MediaPlayer-Framework kam im ganzen Projekt nicht vor. macOS schickt die Medientasten aber genau dorthin: an das Programm, das sich gemeldet hat. Meldet sich keines, gewinnt irgendein anderes, und man pausiert etwas, das man gar nicht sieht. `NowPlaying` meldet Titel, Untertitel, Länge, Stelle und Tempo und nimmt entgegen: Abspielen, Anhalten, Umschalten, Vor- und Zurückspulen mit den **eingestellten** Weiten, und das Ziehen am Balken. Damit tun Medientasten, Kontrollzentrum, Sperrbildschirm und Kopfhörerknöpfe dasselbe wie die Knöpfe im Player — es ist eine Sache, nicht vier. Eine Anmeldung für die ganze App, nicht eine je Player: das System kennt ohnehin nur einen laufenden Titel. Abgemeldet wird nur vom Player, der auch angemeldet ist — sonst räumte einer, der spät zu Ende geht, dem längst gestarteten nächsten die Tasten weg. Die Rückrufe springen auf den Hauptakteur, statt ihn anzunehmen. Dass sie dort ankommen, ist nirgends zugesagt, und dieselbe Annahme hat die App im VLC-Engine schon einmal zum Absturz gebracht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fc812f5a25 |
Zwei Schlüssel ohne Platzhalter, zwei Kreisel übereinander
**Die Schlüssel.** Im Quelltext steht `"season.number \(season)"`, der Schlüssel heißt damit `season.number %lld`. Im Katalog stand er ohne Platzhalter — also traf er nie, und die App zeigte den Schlüssel selbst: „The 50 · season.number 1 · episode.number 1". Beide umbenannt. Ein Überbleibsel `episode.number %@` ohne eine einzige Übersetzung ist mitgegangen, und aus „Episode" wurde „Folge", wie überall sonst im Haus. **Die Kreisel.** Die Abdeckung beim Wiederherstellen der Stelle ist eine ganze Seite, kein Schleier — nur lag das Übrige weiter darüber. Zu sehen waren Bedienelemente mitten im Satz, ein Kreisel in der Abdeckung und ein zweiter im Abspielknopf, den man in dem Moment ohnehin nicht drücken soll. Jetzt ist die Abdeckung ausschließlich. Die Kopfzeile bleibt: sie ist der Weg zurück. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |