Self-hosted TV-Streaming-Stack: DVB-Aufnahme + HLS-Live + Watch-Player für mobile Browser. Sitzt auf tvheadend als Tuner/EPG/DVR-Backend und liefert eine eigene Flask-basierte Player-UI.
- tvheadend — Tuner-Driver (DVB-C/T/S oder SAT>IP), EPG-Grabber,
DVR, Autorec. Container-Image z. B.
linuxserver/tvheadend. - hls-gateway (Flask + waitress, Python 3.13, ffmpeg static) — on-demand HLS-Transcoding pro Kanal, EPG-Archiv, Web-UI, Watch- Player mit Scrub/Pause/Von-Anfang/Mediathek-Fallback, Recording- Player mit Werbe-Ausblendung via tv-detect, virtuelle Mediathek-Aufnahmen für ARD/ZDF.
- Caddy (oder beliebiger Reverse-Proxy) — TLS-Terminator. Player funktioniert nur über HTTPS, damit Service-Worker / fullscreen laufen.
- Optional Mac-side daemon (
mac-daemon/tv-thumbs-daemon.py) — übernimmt HLS-Remux, Thumbnail-Generierung und Ad-Detection vom Streaming-Host via HTTP-API. Lokaler.ts-Cache, Letterbox-aware cropdetect pro Detect, Whisper-Post-Processor für Boundary-Refinement. Wenn vorhanden: Streaming-Host-CPU für Recording-Verarbeitung dropt um ~98 %, Re-Detects nach Head-Bumps lesen aus Cache statt LAN (~30× schneller).
Container laufen mit network_mode: host damit DVB-Multicast +
SAT>IP-Discovery direkt funktionieren.
- 2 h DVR-Fenster (timeshift), Swipe zum Kanalwechsel
- Mediathek-Live als Primärquelle für ARD/ZDF — spart Tuner. Fällt automatisch auf DVB zurück, wenn Mediathek-HLS nicht antwortet (8 s Timeout oder hls.js-Error)
- Live-Werbeerkennung (Privatsender, tv-detect rolling, ~12 min Takt; skippt wenn keine neuen Segmente)
- Mux-Dots auf der Kanalübersicht zeigen welche Kanäle sich einen Tuner teilen
- tvheadend-DVR: nach Fertigstellung automatisch zu HLS-VOD umgepackt, Werbeblöcke per tv-detect (MLP2-Head 1290→32→1 mit Channel-One-Hot + Logo + Wall-Clock-Prior; aktuell Block-IoU 0.92 / Test-Acc 98.5 % bei n=35 Test-Recordings über 9 Sender), Serien- Autorec über Titel-Regex
- Eager-Remux + Eager-Thumbs + Eager-Detect: prewarm-loop (alle
120 s) erkennt fertige Aufnahmen ohne HLS/Thumbs/Cutlist, schreibt
.requested-Marker in den jeweiligen rec-dir. Optional Mac-Daemon pollt/api/internal/{thumbs,hls,detect}-pendingalle 5 s, holt .ts via HTTP, läuft ffmpeg/tv-detect lokal, postet Resultate via PUT zurück. User-Klick auf eine fertige Aufnahme = instant playback - Virtuelle Mediathek-Aufnahmen (ARD + ZDF): Long-press auf EPG- Event → Modal bietet „Aus Mediathek speichern", „Jetzt abspielen" oder „DVR (Episode/Serie)". Funktioniert auch für vergangene Sendungen
- Pre-Expiry-Rip: Mediathek-Aufnahmen werden 48 h vor Ablauf
lokal als MP4 gerippt (
-c copy, kein Re-encode), überleben so die Mediathek-Verfügbarkeit - Series-Autorec + Mediathek-Kombi: beim Setzen einer Serien- Aufnahme werden upcoming Episoden automatisch mit Mediathek-Matches ergänzt, damit Tuner frei bleiben
- Whisper-Post-Processor (
WHISPER_ENABLE=1, daemon-side): nach jeder Detect-Run klassifiziert whisper.cpp deutsche Werbe-Sprache und refined die Block-Boundaries
- Netflix-style Show-Tile-Grid (
/bibliothek) als Watch-fokussierte Persona-Split von/recordings. Pro Show genau ein Tile mit Latest- Episode-Preview, Folgenzahl und „vor 3 h"-Zeit - Cross-Channel-Merge nach normalisiertem Titel — selbe Show auf verschiedenen Sendern = ein Tile, Channel-Chip-Filter behält die Info
- Filter + Sortierung: Sender-Chips, Filme/Serien/Alle, Datum/A-Z. Bucketing-Priorität: User-Group → Autorec → (Title, Channel) Orphan
- Movie-vs-Series Klassifikation: User-Group + 🎬 Badge → Film;
Autorec → Serie; sonst TMDB
kind+ ≥80 min Heuristik - Multi-Source Poster-Pipeline: TMDB w342 für Filme (Portrait- Plakat), fernsehserien.de hr2-Banner für deutsche Tagesshows, manuelle URL-Pin-Map für TV-Events ohne Eintrag, Slug-Aliases für ambiguous Cases
- Uniform 2:3 Portrait-Tiles via AppleTV-style blur-bg-Trick: Landscape-Banner kriegen blurred-zoomed Hintergrund + sharp foreground in Native-Aspect, Portraits laufen object-fit:cover
- Detail-Page mit Hero-Backdrop (gleicher Trick, 21:9 desktop / 16:9 mobile), Episodenliste darunter mit direktem Click-zum-Player
- Tag-/Nacht-Modus via
prefers-color-scheme(folgt System-Theme)
- Konfigurierbare Pins (📌 auf Startseite), zusätzlich LRU-Cache für zuletzt geschaute
- Pin-Cap dynamisch nach Tuner-Belegung; Mux-Sharing wird angerechnet (mehrere Kanäle auf gleichem Mux = 1 Tuner)
- Pin-Idle-Timeout: 6 h ohne Viewer → Pin geht schlafen (💤), nächster Tap weckt
- Badge pro Kanal zeigt Puffergröße (grün=LRU, blau=pinned); Klick auf grün stoppt den Tuner
- Header zeigt live
📡 n/N Tuner
- Logo-only-Kanal-Spalte, kompakte Zeitachse, Kurzsendungen mit 2- Zeilen-Wrap
- Long-press öffnet kontextuelles Modal (Episode/Serie/Mediathek je nach Kanal + Vergangenheit)
- Tap auf past event → gleiches Modal (nicht Live-Stream)
- Serien-Gruppierung im Aufnahmen-View mit Episoden-Zähler
- Geteilter CSS/JS-Scaffold, Doppel-Tap links/rechts seek ±10 s
- Hot-Reload ohne Puffer-Verlust:
scp service.pyreicht — File- Watcher triggertos.execv, ffmpeg-Kinder werden per PID-Datei adoptiert. Compile-Check schützt vor kaputtemscp - iOS-Safari-Workarounds:
aria-labelstatttitle(Tap-Tooltip- Bug),forceMseHlsOnAppleDevicesfür hls.js, Mute-autoplay + Unmute-Overlay
Alles unter dem konfigurierten HLS-Root (z. B. /mnt/tv/hls/):
| Pfad | Zweck |
|---|---|
<slug>/ |
Live-HLS-Segmente pro Kanal |
<slug>/.ffmpeg.pid |
Kind-PID + Startzeit für Re-Adoption nach Hot-Reload |
<slug>/.adskip/ |
Live-Werbeerkennung-Artefakte (tv-detect rolling) |
_rec_<uuid>/ |
tvheadend-Aufnahmen umgepackt (HLS-VOD + Cutlist + Thumbs) |
_rec_<uuid>/.{hls,detect}-requested, thumbs/.requested |
Marker-Files für Eager-Pipeline |
_rec_<uuid>/{ads_user,ads,pseudo_labels,bumpers}.json |
User-Edits, Auto-Cutlist, Self-Training-Pseudo-Labels, Bumper-Detector-Output |
_<mt_uuid>/file.mp4 |
gerippte Mediathek-MP4s |
.tvd-models/ |
tv-detect NN-Head (165 KB MLP1 v1: 1290→32→1 ReLU+Sigmoid mit Channel-One-Hot) + MobileNetV2-Backbone + Sidecars (calibration, channel-map, test-set, history, uncertain) |
.tvd-logos/<slug>.logo.txt |
per-Channel Logo-Templates |
.tvd-bumpers/<slug>/{start,end}/*.png |
per-Channel Ad-Bumper-Templates für Boundary-Snap |
.minute_prior_by_channel.json |
Wall-Clock-Prior P(ad | minute, channel) |
.channel-config.json, .detection_learning.json, .block_length_prior*.json |
Per-Channel Tuning-State + Boundary-Drift-Learning + Block-Length-Prior |
.always_warm.json |
Pin-State (persistent) |
.mediathek_recordings.json |
Virtuelle Mediathek-Aufnahmen (Metadaten + HLS-URL + rip-Pfad) |
.live_ads.json |
Erkannte Werbeblöcke pro Kanal (überlebt Hot-Reload) |
.epg_archive.jsonl |
EPG-Archiv mit Auto-Compaction |
.epg_meta.json |
TMDB/fernsehserien Metadaten-Cache für Bibliothek-Tiles |
.usage_stats.json |
Start-Count + Watch-Hours pro Kanal |
.codec_cache.json |
ffprobe-Ergebnisse (H.264 vs MPEG-2) |
Nach Änderung an service.py reicht ein scp auf den Streaming-Host.
Ein File-Watcher im Container erkennt die Änderung und ruft os.execv
auf, um sich an Ort und Stelle durch die neue Version zu ersetzen. Die
laufenden ffmpeg-Prozesse (mit start_new_session=True gestartet und
per PID-Datei getrackt) werden vom neuen Prozess wieder adoptiert —
der 2-h-DVR-Puffer bleibt erhalten. Ein docker restart zerstört
dagegen das cgroup und killt damit auch die ffmpegs.
scp service.py <host>:/path/to/hls-gateway/service.py
# optional explizit: docker kill -s HUP hls-gatewayBeim Bauen des Image (z. B. ffmpeg-Update) — Puffer wird dabei resettet:
ssh <host> 'cd /path/to/hls-gateway && docker compose build && docker compose up -d'| Service | Port | Zweck |
|---|---|---|
| Caddy | 8443 | HTTPS + HTTP/2 |
| tvheadend | 9981 | Web-UI + API |
| tvheadend | 9982 | HTSP |
| hls-gateway | 8080 | Flask (intern) |