9e7f3393b42c903eb5d0156c500f3cec0bc6dae0
78
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
8a63f815cd |
Das Qualitätsblatt hat die App angehalten
Kein Hänger, sondern ein Absturz, der nicht durchkam. Der Stapel des eingefrorenen Prozesses endet in `_assertionFailure` beim Lesen eines `@Environment`-Werts — mitten in `SheetBridge.present`, also während das Blatt aufgebaut wird. Weil der Player in diesem Moment in einem Layoutdurchlauf steckt, erschien nie eine Meldung: das Fenster stand still. `@Environment(PlaybackSettings.self)` ohne Fragezeichen ist keine Bitte, sondern eine Behauptung — fehlt der Gegenstand, hält Swift die Anwendung an. Und ein Blatt hängt an einem eigenen Darstellungsbaum. Auf dem iPad kam die Umgebung des Players dort bisher an, auf dem Mac nicht. Zwei Änderungen, beide nötig: - Der Player reicht `PlaybackSettings` und den Akzent ausdrücklich in seine Blätter hinein, statt sich auf Vererbung zu verlassen. - `QualitySheet` liest die Einstellungen als Optional. Ein Vergessen kostet künftig eine fehlende Zeile im Blatt statt der ganzen App. Dazu ein Test, der genau diesen Weg geht: abspielen, Qualitätsknopf, Blatt da? Er lief schon vor der Korrektur durch — der Unterschied zwischen iPad und Mac ist mit einem Simulator nicht zu fassen. Er hält trotzdem fest, dass der Weg überhaupt gegangen werden kann. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f3ea1ea891 |
Die Fensterleiste schluckte die halbe Kopfzeile
Der Player deckt das ganze Fenster ab, und auf dem Mac zeichnet das System seine Ampelknöpfe darüber. Die Kopfzeile lag damit nicht nur optisch unter ihnen — der Streifen gehört der Fensterleiste, und die nimmt jeden Klick für sich. Verkleinern, Schließen, Tonspur, Untertitel und die Qualitätsstufe waren dort tot, und ein Doppelklick auf dieselbe Stelle legte das Fenster ins Dock. Jetzt beginnt die Kopfzeile auf dem Mac achtundzwanzig Punkte tiefer. Dazu: Tabulator springt in der Jellyfin-Anmeldung vom Namen zum Passwort und zurück. „Name, Tab, Passwort, Eingabe" ist der Weg, den jeder mit einem Passwortspeicher nimmt; SwiftUI schiebt den Fokus dort nicht von selbst weiter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
37a6c2648f |
Der Mac ist nicht immer Catalyst
Die Bedienung mit Zeiger und Tastatur hing an `#if targetEnvironment(macCatalyst)`. Das ist in dem Bau, der tatsächlich benutzt wird, **falsch**: „für iPad angepasst" ist buchstäblich der iOS-Bau, der auf Apple Silicon startet, und dort ist die Abfrage nicht erfüllt. Klick-zum-Anhalten, Leertaste und die Pfeiltasten waren damit genau dort abgeschaltet, wo sie gebraucht werden. Jetzt beantwortet `FlixyPlatform.isMac` die Frage zur Laufzeit und kennt beide Wege. Die Ansicht bleibt dabei eine einzige: `focusable(_:)` nimmt den Schalter entgegen, die Tastenhandler geben sonst `.ignored` zurück. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9a4b6b1e06 |
Ein Kreisel ist kein Pausenknopf
Während der Player auf Daten wartete, stand im mittleren Knopf das Abspieldreieck und der Ladekreis lag dahinter. Das las sich als „angehalten": man drückte auf Abspielen, und es geschah nichts — es lief ja bereits, es kam nur nichts an. Jetzt sitzt der Kreisel **im** Knopf, dort wo sonst das Symbol steht. Der freistehende Kreisel in der Bildmitte bleibt für den Fall, dass die Bedienelemente ausgeblendet sind. Zwei übereinander gibt es nicht mehr. `togglePlayPause` zählt das Warten jetzt zum Laufen. Vorher führte ein Tipp in diesem Moment zu `play()` auf etwas, das schon spielte. Dazu auf dem Mac, wie im Browser auch: - Ein Klick ins Bild hält an, statt erst die Bedienelemente zu holen. Die kommen dort von allein, sobald sich der Zeiger regt. - Leertaste hält an, Pfeil links und rechts springen. Mit dem Finger bleibt es beim Alten: ohne Zeiger gibt es keine Bewegung ohne Berührung, und ein Tipp, der sofort anhält, nähme den einzigen Weg zu Scrubber, Tonspur und Qualität. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
eebe9280cb |
Die Oberfläche weiß nicht mehr, dass es Plex gibt
Keine Datei unter Views/LiveTV importiert noch PlexKit. Sender, Programm und Einschalten laufen über den neutralen Vertrag, und supportsLiveTV fragt endlich die Fähigkeit ab statt "server != nil" — womit die Zusage in JellyfinBackend.capabilities zum ersten Mal stimmt. Der Player holt die laufende Sendung jetzt für genau einen Sender nach. Vorher ging dafür das Raster über alle Sender hinaus, weil der Plex-Weg keinen schmaleren kannte; im neutralen Vertrag ist die Senderliste ein Parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d54954b9c2 |
macOS: die App läuft jetzt auf dem Mac
Und dein Argument dafür war das beste: auf dem Mac kann ich selbst hinsehen. Kein Simulator-Tunnel, der mitten im Prüfen abstürzt — `screencapture` auf das Fenster der App, und zwar nur auf ihres, nicht auf den Bildschirm. **Ein eigenes Ziel, nicht ein gemeinsames.** Die Info.plist der iOS-App ist voller iOS-Angaben: Startbildschirm, erlaubte Ausrichtungen, Live Activities. Die alle bedingt zu machen hieße, dass jede spätere Änderung an der iOS-Seite über eine Stelle geht, die auch den Mac betrifft. Zwei Ziele sind ein paar Zeilen mehr und eine Sorge weniger. **Was den Unterschied ausmacht, steht an einer Stelle.** Ein neuer `PlatformShims` in FlixyDesign übersetzt, was auf beiden Seiten verschieden heißt: schmale Titelzeile, halbhohes Blatt, Eingabefelder ohne Großschreibung, die Plätze in der Werkzeugleiste, die Zwischenablage. Zwanzig Ansichten benutzen ihn, statt zwanzigmal `#if os(iOS)` zu tragen — sonst wandert die Fallunterscheidung in jede neue Datei und wird beim übernächsten Bildschirm vergessen. Wirklich verschieden sind nur fünf Stellen: die Bildfläche (`UIView` gegen `NSView`), der AirPlay-Knopf, die Ausrichtungssperre (auf dem Mac gibt es nichts zu sperren), der App-Delegat für Hintergrund-Downloads und das Anmeldefenster, das ein anderes Fenster als Anker braucht. **Das Karussell blättert jetzt mit einem einrastenden Streifen** statt mit `TabView(.page)` — den Seitenstil gibt es auf dem Mac nicht. Das ist nebenbei die bessere Lösung: das verschachtelte TabView hatte die Eigenschaft, beim Wechsel kurz zwei Seiten gleichzeitig zu zeigen. **Was auf dem Mac fehlt und warum:** VLC. Das Binärpaket ist auf iOS festgelegt; sein xcframework enthält zwar Mac-Teile, aber das ist eine eigene Runde. Solange fällt die Wiedergabe dort auf den Systemplayer zurück — MKV und DTS gehen also noch nicht. Live Activities gibt es auf dem Mac nicht, Bild-in- Bild startet von Hand statt beim Verlassen der App. Gesehen: die App startet, der Anmeldebildschirm steht, beide Wege sind da. Angemeldet habe ich mich auf dem Mac **nicht** — das ginge nur, indem ich einen Token in deinen Schlüsselbund schreibe, und dann fragt macOS dich beim ersten Zugriff nach dem Passwort. Das machst du mit einem Klick selbst. iOS baut unverändert, 263 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
21c5a8b46b |
Player: vier Knöpfe, die vorher fehlten oder nichts sagten
**Eine Taste zum Vergrößern** unten rechts auf der Kachel. Das Antippen der Fläche vergrößert weiterhin, aber auf derselben Fläche liegen schon das Ziehen und das Aufziehen — und was dabei danebengeht, sieht aus wie „reagiert nicht". Ein Knopf geht nicht daneben. **Ton und Untertitel getrennt.** Es sind zwei verschiedene Fragen: die Tonspur stellt man einmal am Anfang um, die Untertitel mitten im Film. In einem gemeinsamen Blatt scrollte man jedes Mal an der anderen Liste vorbei. Das Untertitel-Symbol ist gefüllt, solange welche laufen — so sieht man es, ohne das Blatt zu öffnen. Und eine Auswahl schließt das Blatt jetzt selbst. **AirPlay.** Nicht nachgebaut, sondern die Systemstelle: sie kennt Geräte, die eine App nie aufzählen könnte. Der Knopf erscheint nur beim Systemplayer — VLC zeichnet selbst und kann kein AirPlay, und ein Knopf, der dort ins Leere führte, wäre schlimmer als keiner. **Die Qualität steht als Text da**, nicht als drei Schieberegler: „1080p", „720p", „Auto". Bei „automatisch" steht dort, was gerade tatsächlich ankommt — das weiß nur der Player, und ihn zu fragen ist ehrlicher, als „Auto" hinzuschreiben und die Antwort schuldig zu bleiben. Dazu die Einstellungen mit farbigen Symbolen und einer Kopfkarte je Bereich. Das ist nicht nur Zierde: in einer Liste aus zehn gleich aussehenden Zeilen findet das Auge nichts wieder, und „das mit dem blauen Zahnrad" ist eine brauchbare Beschreibung. Die Farben sind fest und ausdrücklich **nicht** die Akzentfarbe — trüge alles dieselbe, unterschiede sie nichts mehr. Und der Aufmacher läuft jetzt unter der Seitenleiste weiter (`backgroundExtensionEffect`): die Ränder werden gespiegelt und weich fortgeschrieben, während Titel und Knöpfe stehen bleiben. Vorher endete das Bild an der Kante der Leiste, und beim Weiterwandern schob sich das nächste an einer harten Linie ins Bild. 263 Tests grün. Player-Knöpfe und Einstellungen angesehen; den Aufmacher hinter der Leiste konnte ich an den einfarbigen Flächen der Attrappe nicht sicher beurteilen — der Simulator ist dabei erneut abgestürzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dace5d1543 |
Einstellungen: sechs Bildschirme statt einer Liste — und was sie steuern
Deine Einstellungs-Bilder sind eine lange Liste. Ich habe sie durchgegangen
und getrennt: was etwas steuert, ist gebaut; was eine Einstellung für eine
Sache wäre, die es nicht gibt, ist nicht gebaut. Ein Schalter, der nichts
bewirkt, ist schlimmer als kein Schalter.
**Die Einstellungen sind jetzt eine Übersicht.** Vorher stand alles
untereinander auf einem Bildschirm — Serverangaben, Qualität, Wiedergabe, drei
Verweise, Downloads, Version, Abmelden. Das war beim Bauen bequem und beim
Suchen anstrengend. Jetzt führt jede Zeile in einen Bildschirm, der zu einer
Frage gehört: Server, Erscheinungsbild, Startseite, Wiedergabe, Ton &
Untertitel, Downloads, Diagnose.
**Neu, und jedes davon greift irgendwo:**
- Zurück- und Vorspulen getrennt einstellbar. Die Knöpfe im Player und die
Doppeltipp-Flächen tragen danach auch das passende Symbol.
- Startgeschwindigkeit — die Engine kann es längst, gefragt wurde nie.
- „Im Querformat sperren" abschaltbar. Für die allermeisten Filme richtig, für
den, der im Bett auf der Seite liegt, das Gegenteil.
- „Gewählte Qualität merken": die Stufe gilt dann auch für den nächsten Titel.
Aufbewahrt wird eine **Zahl**, keine Sprosse — welche Sprossen es gibt, hängt
von der Quelle ab, und eine 720p-Datei kennt keine Zwanzig-Megabit-Stufe.
Dann kommt die nächstgelegene, nicht „automatisch".
- „Bitrate im Qualitätsmenü zeigen".
- Gleichzeitige Downloads. Der Wert gab es schon in der Engine, nur ohne
Bedienung und ohne dass er einen Neustart überlebte.
- Aufmacher: wie viele Titel, und ob sie aus Angefangenem oder nur aus
Neuzugängen kommen.
- „Weiterschauen und Als Nächstes zusammenlegen" und „Gesehenes aus
Neuzugängen ausblenden" — beides in der Ansicht, nicht im Server: das sind
Fragen der Darstellung, und so gelten sie für Plex und Jellyfin gleich.
**Verkleinert herunterladen geht jetzt auch bei Jellyfin.** Da hatte ich zu
früh aufgegeben: ich hatte nur nach der Adresse zum *Abspielen* gesucht, und
die ist eine Abspielliste mit hunderten Schnipseln — als Datei unbrauchbar.
`/Videos/{id}/stream.mp4` gibt dieselbe Umrechnung als eine Datei heraus. Die
Größe bleibt eine Schätzung; eine Umrechnung, die erst beim Abruf beginnt,
kennt ihre eigene Länge nicht.
**Die Kontozeile ist jetzt eine Karte, die nach oben aufklappt**, kein
Systemmenü mehr. Ein Menü legt sich als eigene Ebene über alles und springt
dorthin, wo Platz ist; die Karte bleibt, wo sie ist, und lässt die Zeile mit
dem Namen stehen — man sieht also weiter, um wessen Konto es geht.
**Nicht gebaut, mit Grund:** Trailer im Karussell, Poster- und Sammlungsstil,
Untertitel-Aussehen, sekundäre Untertitel, lokale Intros, Modular Home,
App-Symbol. Für all das fehlt das, was die Einstellung steuern würde — es gibt
in Flixy weder Trailer noch Sammlungen, und der Player zeichnet Untertitel
nicht selbst.
263 Tests grün. **Nicht angesehen**: der Simulator-Tunnel ist weiterhin
abgestürzt und ließ sich nicht wieder aufbauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|