# TV-Wall

Digitale Anzeigetafel für den Raspberry Pi: Bilder und Videos aus einem Ordner
laufen im Vollbild in Endlosschleife. Neue Inhalte kommen per Webinterface auf
das Gerät — der Pi spannt dafür ein eigenes WLAN auf, in das man sich mit Handy
oder Notebook einwählt.

* **Wiedergabe** — Bilder je 5 Sekunden (einstellbar), Videos komplett, danach
  von vorne. Der Ordner wird alle 10 Sekunden neu eingelesen, neue Dateien
  landen automatisch hinten in der Playliste.
* **Webinterface** — Hochladen per Fingertipp, Reihenfolge per Ziehen ändern,
  Löschen, Pause/Weiter, Anzeigedauer umstellen.
* **Access Point** — eigenes WLAN mit frei wählbarem Namen, damit sich mehrere
  Geräte („TVWall-01“, „TVWall-02“, …) sauber unterscheiden lassen.
* **Autostart** — ein systemd-Dienst, der beim Einschalten von selbst hochfährt.

Ein einziges Go-Programm ohne Fremdbibliotheken, dazu `mpv` als Abspieler.

---

## Schnellstart

### 1. Auf dem Entwicklungsrechner bauen

```bash
make dist
```

Das erzeugt `dist/tvwall-<version>.tar.gz` mit fertigen Programmdateien für
64- und 32-Bit-Raspberry-Pi-Systeme.

### 2. Auf den Pi kopieren

```bash
scp dist/tvwall-*.tar.gz pi@raspberrypi.local:~
ssh pi@raspberrypi.local
tar xzf tvwall-*.tar.gz && cd tvwall-*
```

### 3. Namen vergeben und installieren

```bash
nano config/tvwall.conf      # AP_SSID und HOSTNAME für diesen Pi setzen
sudo ./install.sh
```

Der Installer richtet alles ein: Pakete, Benutzer, Medienordner, systemd-Dienst
und den Access Point. Danach ist das Gerät sofort einsatzbereit und startet bei
jedem Einschalten von allein.

> Alternativ direkt auf dem Pi bauen: Go ≥ 1.21 installieren
> (`sudo apt install golang-go`) und `sudo ./install.sh` im Quellverzeichnis
> aufrufen — der Installer übersetzt dann selbst.

### 4. Benutzen

1. Am Handy mit dem WLAN **TVWall-01** verbinden (Passwort aus `tvwall.conf`).
2. `http://192.168.4.1` bzw. `http://tvwall` im Browser öffnen.
3. Bilder und Videos hochladen, Reihenfolge ziehen — fertig.

---

## Konfiguration

Alle Einstellungen stehen in **`/etc/tvwall/tvwall.conf`**. Nach Änderungen:

```bash
sudo systemctl restart tvwall
```

| Schlüssel | Bedeutung |
|---|---|
| `AP_SSID` | Name des WLANs — **pro Pi eindeutig vergeben** |
| `AP_PASSWORD` | WLAN-Passwort, mindestens 8 Zeichen |
| `AP_COUNTRY` | Ländercode (`DE`); ohne ihn bleibt das WLAN aus |
| `AP_IP` | Adresse des Pi im eigenen Netz (Standard `192.168.4.1`) |
| `AP_CHANNEL` | WLAN-Kanal 1–11 |
| `CAPTIVE_PORTAL` | `yes` = Handys öffnen die Seite von selbst |
| `HOSTNAME` | Gerätename, auch als `http://<name>.local` erreichbar |
| `WEB_PORT` | Port des Webinterface (Standard 80) |
| `WEB_PASSWORD` | gesetzt = Webinterface fragt nach einem Passwort |
| `MAX_UPLOAD_MB` | Obergrenze je Upload-Vorgang |
| `IMAGE_DURATION` | Anzeigedauer für Bilder in Sekunden |
| `MUTE` | `yes` = Videos ohne Ton |
| `SCAN_INTERVAL` | Takt, in dem der Ordner neu eingelesen wird |
| `DISPLAY_MODE` | `auto` (empfohlen), `drm` oder `desktop` |
| `RUN_USER` | Benutzer des Dienstes |
| `MPV_GPU_API` | `opengl` (Standard) oder `vulkan`/`auto` — siehe Fehlersuche |
| `MPV_HWDEC` | `no` (Standard, Software) oder `auto-safe` (Hardware, nur Pi 4) |
| `STALL_TIMEOUT` | Sekunden ohne Fortschritt, bevor ein Video übersprungen wird |
| `MPV_EXTRA_ARGS` | zusätzliche mpv-Optionen, z. B. `--video-rotate=90` |
| `STATE_DIR` | Datenverzeichnis, Medien liegen in `<STATE_DIR>/media` |

### Mehrere Geräte

Für jeden Pi nur zwei Zeilen anpassen:

```bash
AP_SSID="TVWall-02"
HOSTNAME="tvwall-02"
```

Danach `sudo ./install.sh` — oder auf einem bereits eingerichteten Gerät nach
dem Ändern der Datei:

```bash
sudo /opt/tvwall/scripts/setup-ap.sh
```

Alle Geräte dürfen dieselbe IP `192.168.4.1` verwenden: Man ist immer nur mit
einem WLAN gleichzeitig verbunden.

### Bildausgabe: `auto`, `drm` oder `desktop`

Ein Bildschirm hat immer nur einen Herrn. Läuft ein Desktop, gehört ihm die
Grafikausgabe — mpv kommt dann nur über die Sitzung dieses Desktops daran und
**nur als der Benutzer, dem die Sitzung gehört**. Läuft kein Desktop, malt mpv
direkt über DRM/KMS. Genau darum geht es bei dieser Einstellung.

* **`auto` (empfohlen)** — der Dienst sucht beim Start nach einer erreichbaren
  Wayland- oder X11-Sitzung und nimmt sonst DRM. Der Installer erkennt zusätzlich
  am Boot-Ziel, ob der Pi in den Desktop startet, und trägt bei Bedarf den
  Desktop-Benutzer als `RUN_USER` ein.
* **`drm`** — erzwingt die direkte Ausgabe. Richtig für **Raspberry Pi OS Lite**.
  Der Installer schaltet dafür den Login-Prompt auf `tty1` ab.
* **`desktop`** — erzwingt die Ausgabe über die laufende Desktop-Sitzung. Dann
  muss `RUN_USER` der Desktop-Benutzer sein (meist `pi`) und der Pi sich
  automatisch anmelden: `sudo raspi-config` → *System Options* → *Boot / Auto
  Login* → *Desktop Autologin*.

Der Wayland-Socket heißt je nach Oberfläche `wayland-0` oder `wayland-1` — er
wird gesucht, nicht geraten. Welchen Weg der Dienst tatsächlich gewählt hat,
steht im Protokoll und unten im Webinterface.

---

## Bedienung

### Webinterface

| Bereich | Funktion |
|---|---|
| **Hochladen** | Dateien ziehen oder antippen; mehrere gleichzeitig möglich |
| **Wiedergabe** | Zurück / Pause / Weiter, „Neu laden“ springt sofort in die aktuelle Playliste |
| **Einstellungen** | Anzeigedauer für Bilder, Ton stummschalten |
| **Playliste** | Reihenfolge per Ziehen am Griff `⠿` oder mit ↑ ↓, Löschen per 🗑 |
| **Haken** | Ohne Haken bleibt die Datei liegen, wird auf der Wand aber übersprungen |

Reihenfolge, Haken und Einstellungen bleiben über einen Neustart hinweg
erhalten (`order.json`, `disabled.json`, `settings.json` im Datenverzeichnis).

Ausgeblendete Einträge stehen weiter im Webinterface, sind grau hinterlegt und
mit „ausgeblendet" gekennzeichnet — sie landen nur nicht in der `playlist.m3u`,
die mpv abspielt. Die Änderung greift am nächsten Dateiübergang, ein laufendes
Video wird also nicht unterbrochen.

### Ohne Webinterface

Dateien lassen sich auch direkt ablegen — per USB-Stick, `scp` oder
Netzwerkfreigabe:

```bash
scp urlaub.mp4 pi@tvwall-01.local:/var/lib/tvwall/media/
```

Sie tauchen beim nächsten Durchlauf (Standard: 10 Sekunden) automatisch in der
Playliste auf.

### Unterstützte Formate

* **Bilder:** jpg, jpeg, png, gif, bmp, webp, tif, tiff
* **iPhone-Fotos:** heic und heif werden beim Hochladen automatisch in JPEG
  umgewandelt (dafür wird ffmpeg gebraucht)
* **Videos:** mp4, m4v, mov, mkv, avi, webm, mpg, mpeg, wmv, flv, ts, 3gp, ogv

Alles andere wird beim Hochladen abgelehnt und im Ordner ignoriert.

---

## Betrieb

```bash
sudo /opt/tvwall/scripts/diagnose.sh   # Selbsttest bei Problemen
systemctl status tvwall          # Zustand
journalctl -u tvwall -f          # Protokoll mitlesen
sudo systemctl restart tvwall    # neu starten
sudo systemctl disable tvwall    # Autostart abschalten
```

Aktualisieren: neues Paket auspacken und `sudo ./install.sh` erneut ausführen.
Eine vorhandene `/etc/tvwall/tvwall.conf` bleibt dabei unangetastet; die neue
Fassung landet als `tvwall.conf.neu` daneben.

Entfernen:

```bash
sudo /opt/tvwall/scripts/uninstall.sh          # Medien bleiben erhalten
sudo /opt/tvwall/scripts/uninstall.sh --purge  # alles löschen
```

---

## Wenn etwas klemmt

**Der Bildschirm bleibt schwarz.**
Zuerst den Selbsttest laufen lassen — er prüft Dienst, Benutzer, Gruppen,
Bildschirm, Medien, Webinterface und Access Point und nennt die nötigen Schritte:

```bash
sudo /opt/tvwall/scripts/diagnose.sh
```

Die beiden häufigsten Ursachen:

1. **Ein Desktop läuft, der Dienst aber unter dem falschen Benutzer.** Die
   Desktop-Sitzung von `pi` liegt in `/run/user/1000` und ist für den Benutzer
   `tvwall` schlicht nicht zugänglich. Abhilfe: in `/etc/tvwall/tvwall.conf`
   `RUN_USER="pi"` und `DISPLAY_MODE="desktop"` setzen, dann
   `sudo /opt/tvwall/install.sh --no-ap`.
2. **`DISPLAY_MODE="drm"` auf einem Desktop-System.** Der Compositor hält den
   Bildschirm bereits, mpv kommt nicht daran. Abhilfe: `DISPLAY_MODE="auto"` —
   oder in `raspi-config` auf *Console Autologin* umstellen.

Zum Prüfen, ob mpv überhaupt ein Bild bekommt (Dienst vorher mit
`sudo systemctl stop tvwall` anhalten):

```bash
# mit Desktop
sudo -u pi XDG_RUNTIME_DIR=/run/user/1000 mpv --fullscreen /var/lib/tvwall/media/<datei>
# ohne Desktop
sudo -u tvwall mpv --vo=gpu --gpu-context=drm --fullscreen /var/lib/tvwall/media/<datei>
```

**Das WLAN taucht nicht auf.**

```bash
nmcli connection show --active     # läuft tvwall-ap?
rfkill list                        # Funk gesperrt?
sudo /opt/tvwall/scripts/setup-ap.sh
```

Häufigste Ursache: Der Pi ist noch mit einem anderen WLAN verbunden. Eine
Schnittstelle kann nicht gleichzeitig Client und Access Point sein —
andere WLAN-Verbindungen mit `nmcli connection modify <name> connection.autoconnect no`
abschalten. Zweithäufigste Ursache: fehlender Ländercode (`AP_COUNTRY`).

**Webinterface nicht erreichbar.**
Zuerst `http://192.168.4.1` probieren — Namen wie `tvwall.local` funktionieren
nicht auf allen Handys. Läuft der Dienst? `systemctl status tvwall`.

**Das Bild bleibt nach einigen Wechseln stehen, obwohl das Webinterface
weiterzählt.**
Das ist der Grund für die Voreinstellung `MPV_GPU_API="opengl"`. Über Vulkan
baut libplacebo bei jedem Bildwechsel die Swapchain neu auf und läuft nach rund
neun Wechseln in `VK_ERROR_OUT_OF_HOST_MEMORY`. mpv läuft dann weiter und meldet
auch weiter Dateiwechsel — nur gemalt wird nichts mehr. Im Journal steht:

```
[vo/gpu/libplacebo] vk->CreateSwapchainKHR(...): VK_ERROR_OUT_OF_HOST_MEMORY
[vo/gpu/libplacebo] Failed (re)creating swapchain!
```

Nachgemessen auf einem Pi 5 (Wayland/labwc, mpv 0.40), 20 Bildwechsel:

| Renderer | Swapchain-Fehler |
|---|---|
| `--vo=gpu` (Vulkan) | 24 |
| `--vo=gpu --gpu-api=opengl` | **0** |
| `--vo=gpu-next` (Vulkan) | 84 |

Dasselbe galt für Videos: Mit Vulkan brach die Wiedergabe nach ein bis zwei
Sekunden ab, mit OpenGL lief derselbe Clip vollständig durch. `diagnose.sh`
prüft das mit.

**Ein Video bleibt nach ein paar Sekunden stehen.**
Zuerst `MPV_GPU_API="opengl"` prüfen (siehe oben), dann `MPV_HWDEC="no"`. Der
Raspberry Pi 5 hat *keinen* Hardware-Dekoder für H.264 mehr; mit `auto-safe`
läuft ein Video dort ein bis zwei Sekunden und friert dann ein — genau so lange,
wie der bereits gefüllte Puffer reicht. Zum Gegenprüfen hilft der direkte
Vergleich, weil mpv von sich aus `--hwdec=no` verwendet:

```bash
sudo systemctl stop tvwall
mpv --hwdec=no        --fullscreen /var/lib/tvwall/media/<datei>   # läuft
mpv --hwdec=auto-safe --fullscreen /var/lib/tvwall/media/<datei>   # friert ein
sudo systemctl start tvwall
```

Bleibt es dabei, misst dieses Skript jede Variante einzeln durch und nennt die
verantwortliche Option — statt zu raten:

```bash
sudo /opt/tvwall/scripts/bisect-mpv.sh /var/lib/tvwall/media/<datei>
```

Was in der Datei steckt, zeigt `diagnose.sh` oder direkt:

```bash
ffprobe -hide_banner /var/lib/tvwall/media/<datei>
```

Ist das Material breiter als 1920 Pixel (Handyvideos sind oft 4K), hilft nur
Herunterrechnen — am besten auf dem Rechner, nicht auf dem Pi:

```bash
ffmpeg -i gross.mov -vf scale=1920:-2 -c:v libx264 -preset slow -crf 23 \
       -c:a aac -movflags +faststart klein.mp4
```

Die Wiedergabe bleibt dabei nicht hängen: Läuft ein Video `STALL_TIMEOUT`
Sekunden (Standard 30) nicht weiter, wird es übersprungen und die Schleife
läuft weiter. Im Webinterface steht dann, welche Datei betroffen war.

**Kein Ton.**
Ausgabegerät prüfen (`mpv --audio-device=help`) und passend in
`MPV_EXTRA_ARGS="--audio-device=..."` eintragen. Bei HDMI-Ton hilft oft
`--audio-device=alsa/sysdefault:CARD=vc4hdmi0`.

---

## Aufbau

```
cmd/tvwall/main.go        Start, Signalbehandlung, periodischer Ordner-Durchlauf
internal/config/          tvwall.conf lesen, Laufzeit-Einstellungen speichern
internal/playlist/        Ordner einlesen, Reihenfolge merken, playlist.m3u schreiben
internal/mpv/             JSON-IPC-Verbindung zu mpv
internal/player/          mpv starten, überwachen, Dateien einzeln weiterschalten
internal/web/             HTTP-Schnittstelle und Oberfläche (eingebettet)
systemd/                  Vorlagen für den Dienst (drm / desktop)
internal/player/display.go  Wayland-/X11-/DRM-Erkennung
scripts/setup-ap.sh       Access Point per nmcli
scripts/diagnose.sh       Selbsttest bei Problemen
scripts/console-mode.sh   Desktop abschalten, direkt in die Anzeige booten
scripts/set-splash.sh     eigenes Logo als Startbildschirm setzen
scripts/silent-boot.sh    Textkonsole beim Starten unterdruecken
scripts/bisect-mpv.sh     misst, welche mpv-Option einen Haenger ausloest
install.sh                Installation auf dem Pi
```

Ein paar Entscheidungen, die vielleicht nicht offensichtlich sind:

* **mpv spielt Bilder und Videos aus einer gemeinsamen Playliste.** Kein
  Umschalten zwischen zwei Programmen, keine Lücke am Übergang.
* **Die Playliste wird zwischen zwei Dateien getauscht**, nicht mittendrin: Ein
  Upload unterbricht kein laufendes Video. Wer nicht warten will, drückt
  „Neu laden“.
* **Die Oberfläche kommt ohne externe Bibliotheken aus** und ist ins Programm
  eingebettet. Am Access Point gibt es kein Internet — alles, was von einem CDN
  nachgeladen werden müsste, wäre dort tot.
* **mpv rendert über OpenGL, nicht über Vulkan.** Auf dem Raspberry Pi 5 baut
  die Vulkan-Anbindung von libplacebo bei jedem Bildwechsel die Swapchain neu
  auf und läuft dabei nach rund neun Wechseln aus dem Speicher — der Bildschirm
  friert ein, während intern alles weiterläuft. Siehe Fehlersuche.
* **Uploads landen erst in einer temporären Datei** und werden nach vollem
  Empfang umbenannt. So gerät keine halbe Datei in die Wiedergabe.

### Web-Schnittstelle

| Weg | Methode | Zweck |
|---|---|---|
| `/api/state` | GET | Playliste, Zustand, Einstellungen |
| `/api/upload` | POST | Dateien hochladen (multipart) |
| `/api/order` | POST | `{"order":["a.jpg","b.mp4"]}` |
| `/api/delete` | POST | `{"name":"a.jpg"}` |
| `/api/enabled` | POST | `{"name":"a.jpg","enabled":false}` — aus der Wiedergabe nehmen |
| `/api/settings` | POST | `{"image_duration":5,"mute":false}` |
| `/api/control` | POST | `{"action":"next\|prev\|pause\|reload\|restart\|rescan"}` |
| `/thumb/<name>` | GET | Vorschaubild (per ffmpeg erzeugt, zwischengespeichert) |
| `/media/<name>` | GET | Originaldatei |

### Lokal ausprobieren

Ohne Pi, nur die Oberfläche (kein mpv nötig):

```bash
make run     # http://localhost:8080
```
