- C 93.5%
- Shell 1.7%
- JavaScript 1.3%
- Awk 1.2%
- Makefile 0.9%
- Other 1.4%
Der Puffer Sechzehn Megabyte waren fest verdrahtet, bei den voreingestellten acht Megabit also rund sechzehn Sekunden. Das ueberbrueckt eine Stoerung, aber nicht die Ausfaelle, die eine Wiedergabe wirklich unterbrechen: jemand laedt etwas herunter, die Mikrowelle laeuft, der Router wechselt den Kanal. Die Groesse steht jetzt als Dauer da und die Bytes folgen aus der Bitrate, die die Qualitaetsstufe beim Server bestellt - vier Minuten, gedeckelt auf 160 MB. Das ergibt je nach Stufe zwischen knapp zwei und vier Minuten; 360p bekommt die vollen vier fuer 29 MB, 1080p bei 12 Mbit/s stoesst an den Deckel. Was tatsaechlich belegt wurde, steht im Protokoll, denn ensure_stream_buffer() halbiert sich herunter, bis es passt. Der Vorlauf vor dem ersten Bild bleibt unveraendert kurz: die zwei Sekunden aus PREBUFFER_SECONDS sind unabhaengig davon, wie gross die Reserve dahinter ist. Sie fuellt sich hinter dem laufenden Bild. Damit ein Block dieser Groesse nicht bei jedem Sprung zurueckgegeben und sofort wieder geholt wird - genau der Weg zu einem Heap, der ihn irgendwann nicht mehr liefert, und derselbe Grund, aus dem der Decoder den Sprung ueberlebt - gehoert der Ringpuffer jetzt dem Player statt dem HttpStream. http_stream_open() bekommt ihn geliehen und gibt ihn nicht frei. Er ueberlebt auch player_open(), weil die Vorspann-Intros nacheinander da durchlaufen, und faellt erst in player_close() weg; waehrend des Stoeberns wuerde er nur den Bildcache aushungern. ringbuf_reset() ist dafuer neu: es leert den Puffer und nimmt die Schalter closed und aborted zurueck, sonst haette der zweite Stream in dem Moment geendet, in dem er beginnt. Mit eigenem Test, der genau das nachweist - absichtlich kaputtgemacht faellt er um. Weil eine Prozentzahl von einer minutentiefen Reserve nichts mehr aussagt, zeigt der Wartebildschirm jetzt Sekunden statt Prozent. Ausserdem * draw_toast() rechnete die Box von der rechten Kante nach links auf. Ab etwa hundertzwanzig Zeichen begann sie ausserhalb des Bildschirms. Kein theoretischer Fall: eine Spurbezeichnung kommt als DisplayTitle vom Server und bringt bis zu 127 Byte mit. Breite gedeckelt, Text gekuerzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .github/workflows | ||
| content | ||
| data | ||
| patches | ||
| source | ||
| tests | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
Wii-U-Jellyfin
Ein nativer Jellyfin-Client für die Nintendo Wii U unter der Homebrew-Umgebung
Aroma. Kein Browser, kein Wrapper — eine echte
.wuhb-Anwendung, die mit devkitPPC und wut
gebaut wird, über SDL2 auf die GX2-GPU rendert und Video mit dem
H.264-Hardware-Decoder der Konsole dekodiert.
Jellyfin-Server ──HTTPS──► libcurl ──► MPEG-TS-Demuxer ──┬──► H264DEC (Hardware) ──► NV12 ──► SDL2/GX2
└──► libfaad (AAC) ──► PCM ──► SDL2/AX
Funktionsumfang
Anmeldung
- Quick Connect (Code auf dem Fernseher, Bestätigung in einem anderen Client) — muss auf dem Server unter Dashboard → Allgemein aktiviert sein
- Benutzerliste des Servers oder manuelle Eingabe von Benutzername/Passwort
- Eingabe über die systemeigene Wii-U-Bildschirmtastatur (
nn::swkbd) - Token wird auf der SD-Karte gespeichert, beim nächsten Start geht es direkt weiter
Durchsuchen
- Startseite mit Weiter schauen, Als Nächstes, Bibliotheken und Zuletzt hinzugefügt
- Bibliotheks-Raster mit Nachladen beim Scrollen, Sortierung und Reihenfolge umschaltbar
- Detailseite mit Backdrop, Poster, Beschreibung und Metadaten
- Serien mit Staffelauswahl und Episodenleiste
- Volltextsuche über Filme, Serien und Folgen
- Artwork wird auf Hintergrund-Threads geladen und zwischengespeichert
Wiedergabe
- Progressiver MPEG-TS-Stream vom Server (kein HLS-Playlist-Handling nötig)
- H.264 über den Hardware-Decoder der Wii U, AAC über libfaad
- A/V-Synchronisation über die Audio-Uhr
- Pre-Roll-Intros von Server-Plugins wie Local Intros (siehe unten)
- Ton- und Untertitelspur wählbar während der Wiedergabe (Taste Y)
- Fortsetzen, Springen (±10 s / ±60 s), Pause, OSD mit Fortschrittsbalken
- Untertitel werden serverseitig ins Bild gerendert (
subtitleMethod=Encode) - Wiedergabestatus wird an den Server gemeldet (Start / Fortschritt / Ende), Fortsetzen-Punkte bleiben also über alle Clients synchron
Bedienung
- GamePad, Pro Controller, Wiimote + Classic Controller — alles über SDL2s GameController-Mappings
- Touch auf dem GamePad: Kacheln, Schaltflächen und Einstellungen lassen sich antippen; im Player tippt man auf den Fortschrittsbalken zum Springen
- Die Oberfläche läuft standardmäßig auf Fernseher und GamePad
Installation
- Aroma auf der Wii U einrichten (siehe aroma.foryour.cafe).
jellyfin.wuhbnachsd:/wiiu/apps/kopieren.- Wii U starten, im Wii-U-Menü die Anwendung öffnen (oder über das Aroma Homebrew-Menü).
Beim ersten Start legt die App sd:/wiiu/apps/jellyfin/ an — dort liegen
config.json und jellyfin.log.
HTTPS-Zertifikate
Die Wii U hat keinen Systemzertifikatsspeicher. Das .wuhb enthält deshalb ein
CA-Bundle unter /vol/content/cacert.pem. Alternativ kann eine eigene Datei
nach sd:/wiiu/apps/jellyfin/cacert.pem gelegt werden — praktisch für
selbstsignierte Zertifikate im Heimnetz.
Wird kein Bundle gefunden, verbindet sich die App trotzdem, prüft dann aber die Zertifikate nicht. Die Einstellungsseite zeigt den aktuellen Zustand an.
Bauen
Mit Docker (empfohlen, plattformunabhängig)
./tools/build-docker.sh # Linux / macOS / Git Bash
powershell tools\build-docker.ps1 # Windows PowerShell
Das Skript nutzt das offizielle devkitpro/devkitppc-Image, installiert die
benötigten Portlibs und legt jellyfin.wuhb im Projektverzeichnis ab.
Mit lokaler devkitPro-Installation
dkp-pacman -S --needed wiiu-dev wiiu-sdl2 wiiu-sdl2_image wiiu-sdl2_ttf \
wiiu-curl wiiu-mbedtls ppc-jansson ppc-faad2
make -j$(nproc)
Ergebnis: jellyfin.wuhb.
Die Artwork-Dateien unter data/ werden von tools/gen_assets.mjs erzeugt
(node tools/gen_assets.mjs) und sind eingecheckt — Node wird zum Bauen also
nicht gebraucht.
Steuerung
| Taste | Browsen | Wiedergabe |
|---|---|---|
| A | Öffnen / Auswählen | Pause ⇄ Weiter · im Intro: überspringen |
| B | Zurück | Wiedergabe beenden |
| X | Suche (Startseite) · Sortierung (Bibliothek) | OSD anheften · im Intro: alle überspringen |
| Y | Einstellungen (Startseite) · Reihenfolge (Bibliothek) | Ton-/Untertitelspur wählen |
| L / R | Seitenweise blättern | ±60 Sekunden springen |
| Links / Rechts | Auswahl bewegen | ±10 Sekunden springen |
| + | Neu laden | — |
| HOME | Zurück ins Wii-U-Menü |
Sprünge werden gesammelt und erst ~0,7 s nach der letzten Eingabe ausgeführt — jeder Sprung startet auf dem Server eine neue Transkodierung, und so wird beim Scrubben nur eine einzige angefordert.
Wie die Wiedergabe funktioniert
Die Wii U kann H.264 in Hardware dekodieren (H264DEC* aus wut), aber sonst
nichts. Deshalb fordert die App vom Server einen einzelnen, durchgehenden
MPEG-TS-Stream an:
GET /Videos/{id}/stream.ts?container=ts&videoCodec=h264&audioCodec=aac
&profile=high&level=41&maxWidth=1280&maxAudioChannels=2
&allowVideoStreamCopy=true&allowAudioStreamCopy=true&startTimeTicks=…
allowVideoStreamCopy=true heißt: Jellyfin kopiert die Originalspur, wenn sie
die Vorgaben ohnehin erfüllt, und transkodiert nur sonst. Das schont schwache
Server erheblich.
Der Stream läuft durch:
| Stufe | Datei | Aufgabe |
|---|---|---|
| Download | source/net/http.c |
libcurl-Thread → Ringpuffer (4 MB) |
| Demuxen | source/media/tsdemux.c |
PAT/PMT, PES-Zusammenbau, PTS |
| Video | source/media/h264dec.c |
H264DECExecute → NV12-Bilder |
| Audio | source/media/aacdec.c | ADTS → PCM über libfaad |
| Takt | source/media/player.c | Audio-Uhr, Bildauswahl, Seek |
Die Dekodierung läuft auf Kern 2, das Netzwerk auf Kern 0, die Oberfläche auf dem Hauptkern.
Zwei Fallstricke von h264.rpl
H264DECExecute meldet Erfolg mit 0xE4, nicht mit 0. Der Rest der API
benutzt 0. Wer das übersieht, verwirft jedes dekodierte Bild — nachzulesen im
Referenzdecoder von FFmpeg-wiiu,
der if (res != 0xE4) als Fehlerfall behandelt. Dasselbe gilt für
H264DecodeResult.status.
Threads brauchen mehr Stack. SDL gibt Threads auf der Wii U per
Voreinstellung 32 KB. Ein Aufrufpfad durch h264.rpl, libfaad oder
curl+mbedTLS zurück in eigenen Code sprengt das. Alle Arbeits-Threads werden
deshalb mit SDL_CreateThreadWithStackSize angelegt.
Intros / Pre-Rolls
Vor dem Start eines Films fragt der Client
GET /Items/{id}/Intros?userId=…
ab (mit Fallback auf die alte Route /Users/{uid}/Items/{id}/Intros für
Server vor 10.10). Jedes Plugin, das serverseitig IIntroProvider
implementiert — allen voran das offizielle
Local Intros — liefert
darüber seine Clips. Sie kommen als ganz normale Bibliotheks-Items zurück und
werden über denselben Streaming-Pfad abgespielt wie alles andere.
Verhalten:
- Intros laufen nur beim Start von vorn, nicht beim Fortsetzen aus der Mitte — sonst bekäme man den Vorspann bei jedem Wiederaufnehmen erneut.
- Sie zählen nicht zur Wiedergabehistorie: kein Fortsetzen-Punkt, keine Statusmeldung, kein „wird gerade geschaut".
- A überspringt das laufende Intro, X alle verbleibenden.
- Ein Intro, das sich nicht öffnen lässt oder fehlschlägt, wird still übersprungen. Ein kaputter Vorspann darf den Film nie blockieren.
- Abschaltbar unter Einstellungen → Intros abspielen.
- Maximal 8 Clips werden abgespielt, als Schutz vor Fehlkonfiguration.
Eine Einschränkung, die nicht am Client liegt: Manche Plugins (z. B. die
Episoden-Pre-Rolls von jellyfin-plugin-prerolls) hängen an einem
JavaScript-Hook in Jellyfin Web statt an IIntroProvider. Solche Intros
erreichen keinen nativen Client — weder diesen noch den Jellyfin Media
Player. Was über IIntroProvider läuft, funktioniert hier.
Ton- und Untertitelspuren
Y öffnet während der Wiedergabe die Spurauswahl. Die Liste kommt aus
MediaSources[0].MediaStreams und zeigt Jellyfins eigene DisplayTitle, also
genau das, was auch der Webclient anzeigt — etwa „Deutsch - German - AAC -
Stereo - Default".
Beim Start greifen die Vorgaben des Servers (DefaultAudioStreamIndex,
DefaultSubtitleStreamIndex); ob der Standard-Untertitel gleich an ist,
entscheidet die Einstellung Untertitel einbrennen.
Ein Spurwechsel startet den Stream an der aktuellen Position neu — die Auswahl steckt serverseitig in der Transkodierung, ein Umschalten im laufenden Strom ist nicht möglich. Das kurze Nachladen ist also unvermeidbar.
Verifiziert gegen einen Testserver mit zwei Ton- und zwei Untertitelspuren: die gewählte Tonspur kommt tatsächlich an (Messung der Grundfrequenz: Spur 1 → 442 Hz, Spur 2 → 883 Hz bei 440/880-Hz-Testtönen), und der Untertitel steht sichtbar im Bild.
Warum 720p und nicht 1080p?
Der Hardware-Decoder schafft 1080p — daran liegt es nicht. Der Engpass sitzt hinter dem Decoder: er gibt NV12 aus, die GPU will RGBA, und diese Umwandlung läuft auf der CPU. Ihr Aufwand skaliert linear mit der Pixelzahl, und 1080p sind 2,25-mal so viele Pixel wie 720p. 1080p macht die Wiedergabe also langsamer, nicht besser.
Dazu kommt: die Wii U gibt an den meisten Fernsehern ohnehin 720p aus. Ein 1080p-Stream würde beim Ausgeben wieder herunterskaliert — doppelte Arbeit ohne sichtbaren Gewinn.
1080p steht trotzdem in den Einstellungen, ehrlich beschriftet. Wer flüssige Wiedergabe will, geht in die andere Richtung: 480p hat nur 45 % der Pixel von 720p und ist entsprechend flüssiger.
Wo die CPU-Zeit hingeht — und was noch möglich wäre
Das Dekodieren läuft auf der Hardware, über h264.rpl. Was auf der CPU
liegt, ist die Stufe danach: der Decoder gibt NV12 aus, die GPU will RGBA.
Moonlight Wii U zeigt, wie man auch das noch loswird, und macht dabei zwei Dinge, die dieses Projekt (noch) nicht kann:
- Kein Kopieren. Es legt mehrere GX2-Texturen an und übergibt deren
Speicher direkt an
H264DECExecute— der Decoder schreibt sein Bild also unmittelbar in den Texturspeicher. - Umwandlung auf der GPU. Ein eigener Pixel-Shader
(
src/wiiu/shaders/display.psh) tastet die Y- und die UV-Ebene als zwei Texturen ab (UNORM_R8undUNORM_R8_G8) und rechnet NV12 → RGB beim Zeichnen. Damit kostet die Umwandlung gar keine CPU-Zeit mehr.
Warum hier nicht: Moonlight zeichnet direkt mit GX2 und besitzt seinen Grafikzustand selbst. Dieses Projekt benutzt SDL2s Renderer für die gesamte Oberfläche, und der besitzt den GX2-Kontext und bietet keine Möglichkeit, eigene Shader einzuhängen. Moonlights Weg zu übernehmen hieße, das Videobild mit rohem GX2 neben SDL zu zeichnen und dabei dessen Zustand nicht zu zerstören — machbar, aber ein Eingriff, der sich nur auf echter Hardware erproben lässt.
Übernommen wurde, was sich gefahrlos übernehmen ließ: die Erfolgsprüfung von
H264DECExecute und die genauen Ausrichtungsregeln des Bildspeichers
(Zeilenbreite auf 256, Höhe auf 16 aufgerundet).
Der Weg vom Bild auf den Schirm
Ursprünglich lief das über eine SDL_PIXELFORMAT_NV12-Textur. SDL kann das,
aber nur über einen generischen Software-Pfad: Bild in einen eigenen Puffer
kopieren, dort umwandeln, dann hochladen — drei Durchläufe über jedes Bild.
Auf einem 1,24-GHz-PowerPC waren das ~7 fps bei 1280×704.
Jetzt macht es source/media/nv12.c selbst:
SDL_LockTextureliefert auf der Wii U überGX2RLockSurfaceExeinen direkten Zeiger in den GPU-Speicher — die Umwandlung schreibt also unmittelbar dorthin, ohne Zwischenpuffer.- Die BT.601-Koeffizienten stecken in Nachschlagetabellen, die innere Schleife kommt ohne Multiplikationen aus und verarbeitet zwei Pixel pro Durchgang (beide teilen sich ein Chroma-Sample).
- Die Arbeit wird auf zwei Kerne aufgeteilt: der Renderthread nimmt die obere Bildhälfte, ein Helferthread auf Kern 0 die untere.
- Ein Bild wird nur einmal umgewandelt, auch wenn die Renderschleife es mehrfach zeigt.
Die Farbwerte sind auf dem Host gegen bekannte BT.601-Referenzwerte geprüft
(tools/run-tests.sh), die Geschwindigkeit steht als convert N ms im Log.
Qualitätsstufen
Die Einstellungsseite bietet Automatisch (720p, bis 8 Mbit/s) sowie 720p/480p/360p mit festen Bitratengrenzen. 720p ist die Obergrenze — höher aufgelöstes Material lässt die Konsole zwar dekodieren, aber der Fernseher gibt ohnehin nur 720p aus.
Der Client sendet immer eine videoBitRate. Ohne diese Angabe nimmt
Jellyfin eine sehr vorsichtige Voreinstellung an und skaliert das Bild
entsprechend herunter: ein Testtranskodierung ohne Bitrate kam als 416×234
zurück, mit Bitrate als volle 1280×720.
Ebenso wird immer eine maxHeight mitgeschickt. maxWidth allein reicht
nicht: eine 4:3-Quelle (1440×1080) kommt damit als 1280×960 zurück — mehr
Zeilen, als der für 1280×720 eingerichtete Decoder verarbeiten kann. Mit
Höhenbegrenzung sind es 960×720.
Fehlersuche
Die App schreibt jellyfin.log nach sd:/wiiu/apps/jellyfin/. Dort steht die
Serverversion, die Stream-URL (Token geschwärzt) und der genaue HTTP-Status
jeder fehlgeschlagenen Anfrage.
„Server kann den Stream nicht erzeugen (HTTP 500)"
Das ist ein Serverproblem, kein Client-Problem. Wichtig zu wissen: jede
Anfrage an /Videos/.../stream.ts läuft durch ffmpeg — auch wenn die Datei
schon H.264/AAC ist, muss sie nach MPEG-TS remuxt werden. Fällt ffmpeg aus,
scheitert damit jeder Titel, auch ein zehn Sekunden langes Intro.
Nachgestellt mit einem Jellyfin-Container: nimmt man ffmpeg weg, antwortet der
Server exakt so — HTTP 500, Body Error processing request.
Zu prüfen, in dieser Reihenfolge:
- Dashboard → Wiedergabe → Transkodierung: ffmpeg-Pfad gesetzt und gültig?
- Hardwarebeschleunigung testweise auf keine stellen. Eine falsch konfigurierte VAAPI/QSV/NVENC-Beschleunigung ist die häufigste Ursache.
- Transkodierungs-Pfad beschreibbar und genug Platz frei?
- Das Server-Log ansehen — dort steht der Stacktrace, der die Ursache benennt. Der Client kann nur den Statuscode zeigen.
Gegenprobe: die geloggte Stream-URL im Browser öffnen (Token wieder einsetzen). Kommt dort ebenfalls ein 500, liegt es sicher am Server.
Wiedergabe hängt bei „Puffer 0 %" oder die Konsole stürzt ab
War bis v1.0 ein Fehler im Client, siehe Git-Historie. Falls es weiterhin auftritt, steht die Ursache im Log:
h264: userMemory ist …, erwartet …— die Annahme überH264DECSetParam_USER_MEMORYstimmt nicht; bitte melden.h264: frame needs N bytes, buffer holds M— der Decoder wählt ein größeres Layout als vorgesehen; die Reserve inh264dec.cmuss dann größer werden.Der Videodecoder liefert keine Bilder— es kamen ~15 s Video an, ohne dass ein einziges Bild herauskam.video: N shown, M dropped … (X fps)— läuft alle 5 s mit und zeigt, ob die Pipeline mit der Bildrate mithält.
Quick Connect meldet „Server: aus"
Quick Connect ist in Jellyfin standardmäßig deaktiviert. Einschalten unter
Dashboard → Allgemein → Quick Connect. Der Client fragt vorher
/QuickConnect/Enabled ab und beschriftet die Schaltfläche entsprechend.
Ältere Server (10.8)
Getestet gegen Jellyfin 10.8.13 und 10.11.11. Für 10.8 gibt es
Rückfälle auf die alten Routen (/Users/{id}/Views,
/Users/{id}/Items/{id}/Intros); die 404-Warnungen im Log davor sind normal
und harmlos.
Tests
./tools/run-tests.sh # erzeugt einen TS mit ffmpeg und prüft ihn
./tools/run-tests.sh sample.ts # oder gegen einen echten Server-Mitschnitt
Der Test schickt einen MPEG-TS-Strom durch source/media/tsdemux.c
und prüft, was herauskommt: gültige Annex-B-Startcodes, SPS/PPS/IDR,
ADTS-Synchronisation, monoton laufende PTS. Das ist der Teil der
Medienpipeline, der sich ohne Konsole verifizieren lässt — gegen echten
Jellyfin-Output läuft er sauber durch.
Projektstruktur
source/
├── main.c SDL-Setup und Hauptschleife
├── app/ Zustandsmaschine, Navigation, Konfiguration, Eingabe, Tasks
├── common/ Logging, Stringhilfen, Ringpuffer
├── net/ libcurl-Wrapper und die Jellyfin-API
├── ui/ Zeichenprimitive, Text, Bildcache, Bildschirme
└── media/ Demuxer, Decoder, Player
Grenzen
- Nur Video. Musikbibliotheken werden angezeigt, aber nicht abgespielt.
- Untertitel nur eingebrannt. Die Wii U hat keinen Untertitel-Renderer, der Server zeichnet sie ins Bild. Ein Wechsel kostet deshalb einen Neustart des Streams, und Untertitel erzwingen immer eine Transkodierung.
- Kein Direct Play von HEVC, VP9 oder AV1. Der Server transkodiert diese Formate nach H.264; ohne ausreichend starken Server ruckelt es.
- Die Medienpipeline ist gegen die dokumentierten wut-APIs geschrieben, wurde aber nicht auf echter Hardware gegengetestet — siehe unten.
Status
Auf echter Hardware (Wii U mit Aroma) getestet. Funktioniert dort: Start, Anmeldung mit Benutzer/Passwort, Navigation, Bibliotheks- und Detailansicht mit Artwork. Der H.264-Hardwaredecoder initialisiert sich korrekt (38 MB Arbeitsspeicher für 720p) und das Audiogerät öffnet mit 48 kHz.
Die eigentliche Bildausgabe — H264DECExecute → NV12 → SDL2s YUV-Texturpfad —
konnte noch nicht beobachtet werden, weil der Teststream serverseitig
fehlschlug (siehe Fehlersuche). Der Demuxer davor ist gegen echten
Jellyfin-Output verifiziert.
Lizenz
GPL-3.0-or-later — siehe LICENSE. Die Verlinkung gegen libfaad (GPL-2.0) macht diese Wahl notwendig.
Dieses Projekt steht in keiner Verbindung zu Jellyfin oder Nintendo.