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>
`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>
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>
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>
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>
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>
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>
`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>
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>
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>
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>
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>
Der Anmeldebildschirm kannte nur Plex. JellyfinKit war fertig, aber von der
App aus nicht erreichbar — „ich sehe nix von Jellyfin".
Quick Connect steht vorn: der Server nennt einen sechsstelligen Code, den man
in einem bereits angemeldeten Client bestätigt. Das Passwort erreicht Flixy
dabei nie, dieselbe Zusage wie bei Plex. Benutzername und Passwort sind der
Rückfall für Server, auf denen Quick Connect abgeschaltet ist; das Passwort
wird dann für die Dauer einer Anfrage gehalten und danach weggeworfen.
Die Adresse wird nicht erraten, sondern durchprobiert. „192.168.1.50" kann
alles Mögliche heißen, und die Reihenfolge ist der Kern von JellyfinAddress:
eine IP-Adresse im eigenen Netz spricht fast nie HTTPS, ein Domänenname fast
immer. Andersherum wartet man bei jedem Server ohne Zertifikat erst auf eine
Zeitüberschreitung.
Die Sitzung hält von hier an einen Server hinter dem neutralen Vertrag, nicht
mehr einen Plex-Server. Die Ansichten haben das nicht gemerkt — genau dafür
war MediaKit da. Dabei fällt ab, was Jellyfin kann und Plex nicht: Favoriten
stehen im Aktionsmenü, sobald der Server sie kennt, Sterne nur dort, wo es
sie gibt.
Was noch nicht geht und deshalb auch nicht behauptet wird: Wiedergabe und
Live-TV sprechen weiter unmittelbar mit Plex. Bei Jellyfin verschwindet der
Live-TV-Eintrag aus der Seitenleiste, und der Player sagt, dass die
Wiedergabe der nächste Schritt ist, statt „Server nicht erreichbar" zu
melden — er ist ja erreichbar.
Nebenbei zwei Stellen, die der Übersetzer aus Plex hinterlassen hat:
`catch is CancellationError` griff seit den getippten Fehlern nie mehr, weil
eine abgebrochene Netzanfrage als Übertragungsfehler zurückkommt; und das
Ersatz-Hintergrundbild einer Folge war toter Code, weil die linke Seite des
`??` gar nicht optional war.
TokenStore wandert nach MediaKit — Tokens sind nichts Plex-Eigenes. Die
Konten sind nach Server getrennt, damit ein späteres Umschalten nicht jedes
Mal eine neue Anmeldung kostet; „Abmelden" räumt trotzdem beide ab.
248 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Unterschied zwischen 400 und 200 war ein einziger Parameter:
…&protocol=hls&X-Plex-Token=… → 400
…&protocol=hls&X-Plex-Client-Profile-Name=Chrome&… → 200
Flixy hat immer nur `X-Plex-Client-Profile-Extra` geschickt. Das
**erweitert** ein Profil, es ersetzt keins — ohne Basis gibt es nichts zu
erweitern, und der Server kann keine Entscheidung bilden. Seine Antwort
darauf ist eine nackte 400 ohne jeden Hinweis.
Das betrifft alles, was durch den Transcoder geht: die Qualitätswahl im
Player, komprimierte Downloads und Live-TV. Bei Filmen fiel es nicht auf,
weil dort DirectPlay greift.
„Chrome" als Basis, weil dieses Profil auf jedem Server vorhanden ist und
genau das kann, was AVPlayer kann: HLS mit H.264 und AAC. Die Feinheiten
für iOS stehen weiterhin im Zusatz.
Gefunden wurde es, indem die Anfragen Angabe für Angabe aufgebaut wurden,
bis eine davon umschlug — nicht durch Nachdenken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zehn Runden lang habe ich die Antwort gelesen, ohne sie anzusehen:
<html><head><title>Bad Request</title></head><body><h1>400 Bad
Request</h1></body></html>
So antwortet Plex nicht. Plex schickt XML oder JSON. Das ist die
Standardseite eines Webservers. Dazu passt, dass ping und stop mit 404
antworten, obwohl es sie in jedem Plex gibt.
Die Anfragen erreichen Plex also womöglich gar nicht, sondern werden
davor abgewiesen — vom Router, vom Webserver des NAS oder einem Proxy.
Das erklärt, warum wirklich jede Variante scheitert, auch die minimalste,
und warum eine gewöhnliche Datei genauso betroffen ist.
Der Probe zeigt jetzt alle Wege zum Server und nimmt mit `--local` den
lokalen. Geht es dort, liegt es an dem, was von außen dazwischensteht,
und nicht an Flixy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Server sagt transcoderVideo=1 — er kann umrechnen. Damit ist auch
diese Ausrede weg, und es liegt an meiner Anfrage.
Zwei Stellen, an denen ich es mir zu leicht gemacht habe. Der Titel für
die Gegenprobe kam aus den Startseiten-Reihen, und dort stehen auch
Einträge von plex.tv, deren Kennung kein Schlüssel dieses Servers ist.
Mit so einem Eintrag prüft die Gegenprobe gar nichts — sie kommt jetzt
aus einer echten Mediathek, und vorher wird nachgesehen, ob der Eintrag
überhaupt existiert.
Und die Antwort der Live-Sitzung war bei 700 Zeichen abgeschnitten,
ausgerechnet dort, wo `Part` anfängt — an der einzigen Stelle, die zählt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jede Variante scheitert, auch „nur Pfad und Token". Damit ist es kein
Parameter — es ist der Endpunkt.
Also die Frage eine Ebene höher: `ping` und `stop` gehören zu derselben
Familie und brauchen nur eine Sitzungskennung. Antworten auch sie mit
400, ist nicht die Anfrage falsch, sondern der ganze Zweig nicht da.
Dazu zwei Gegenkontrollen von Dingen, die nachweislich gehen: der
Bild-Transcoder, der denselben `/:/`-Aufbau hat und aus dem die Plakate
kommen, und die Mediathek selbst. Wenn die antworten und der
Video-Transcoder nicht, liegt es nicht an Flixys Adressen.
Und der Probe nimmt jetzt `--channel 002`: bisher hat er immer Sender 001
genommen, auf dem gerade nichts läuft — ein Sender ohne Sendung sagt über
das Einschalten wenig aus. Ohne Angabe nimmt er den ersten, auf dem
etwas läuft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Gegenprobe ist eindeutig:
Gegenprobe — Transcoder mit: Mogli: Legende des Dschungels
→ HTTP 400
Eine gewöhnliche Datei scheitert genauso. Damit ist die Suche der letzten
Tage an der falschen Stelle gelaufen: Transcoding hat in Flixy noch nie
funktioniert, es ist nur nie aufgefallen. Filme laufen über DirectPlay,
da wird der Transcoder gar nicht gefragt — Live-TV ist das Erste, was ihn
zwingend braucht.
Diese Annahme habe ich tagelang mitgeschleppt, ohne sie zu prüfen. Sie zu
prüfen hat zehn Zeilen gekostet.
Jetzt wird richtig halbiert, an einer Datei statt an einem Sender: von
„nur Pfad und Token" aufwärts, Angabe für Angabe, bis eine davon die 400
auslöst. Dazu derselbe Satz an den Entscheider, der in Plex' Format
antwortet statt mit HTML.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Abwarten brachte nichts: der Vorgang meldet auch nach zwölf Sekunden
keine Daten. Und jede Anfrage an den Transcoder endet mit einer nackten
400 — auch die an den Entscheider, der sich sonst erklärt.
Das ist ein Muster, das nicht nach Live-TV aussieht, sondern nach dem
Transcoder selbst. Dass er funktioniert, habe ich die ganze Zeit
vorausgesetzt und nie geprüft: Filme laufen über DirectPlay, da wird er
gar nicht gefragt. Live-TV ist das Erste, was ihn zwingend braucht.
Also die Gegenprobe, vor allem weiteren Suchen: eine gewöhnliche Datei
aus der Mediathek durch den Transcoder. Läuft sie durch, liegt es an
Live-TV. Läuft sie nicht durch, habe ich tagelang am falschen Ende
gesucht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Transcoder antwortet auf jede Form der Adresse mit einer nackten
„400 Bad Request"-Seite — ohne Plex-Meldung, ohne Hinweis. Der Entscheider
bekommt dieselben Angaben und gibt eine Plex-Antwort zurück. Das ist die
letzte Stelle, an der dieser Server noch erklärt, woran es liegt.
Schlägt das Einschalten fehl, zeigt der Probe deshalb jetzt zusätzlich
die Antwort des Entscheiders — in beiden Kodierformen — sowie den
Empfänger, das Gerät und /livetv/sessions im Wortlaut.
Dazu kann der Probe beliebige Adressen abfragen:
`swift run FlixyProbe /livetv/dvrs/2`. Eine Schnittstelle ohne
Beschreibung erkundet man, indem man sie fragt.
Der Token wird aus jeder Ausgabe entfernt, falls der Server ihn
zurückspiegelt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Upload-Kontingent des Tages ist aufgebraucht — verbraucht von meinen
eigenen Vermutungen über eine Schnittstelle, die keine Oberfläche braucht.
Jede Runde ging als TestFlight-Build hinaus und kam als Screenshot zurück.
`swift run FlixyProbe` macht dasselbe in Sekunden: anmelden, Server
suchen, Senderliste und Programm lesen, das Einschalten durchprobieren —
mit derselben Logik wie die App. Der Token kommt über die übliche
Anmeldung und liegt im Schlüsselbund dieses Rechners; gedruckt werden
Pfade, Kennungen und Antworten, keine vollständigen Adressen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>