DIY PVR-Receiver: Docker DVB-S/C Server

Preview DIY PVR-Receiver: Docker DVB-S/C Server

Das Fernsehen hat sich in den letzten Jahren drastisch gewandelt. Mit dem Aufkommen von Streaming-Diensten tritt der klassische TV-Empfang zunehmend in den Hintergrund. Dennoch wollte ich auf das bewährte Fernsehen nicht verzichten, war aber gezwungen meine zentrale Fernsehlösung zu überdenken, da meine Dreambox den Dienst verweigerte.

Zentraler TV-Tuner / (PVR)-Server über WLAN für alle Fernseher.

Lange Zeit hatte ich als Ergänzung für meinen TV einen PVR:Festplattenreceiver, konkret eine Dreambox DM7020HD mit 2 DVB-C-Tunern im Einsatz. Ursprünglich für das automatische Aufnehmen bestimmter Sendungen, damit ich die Werbung überspringen kann, später um rein über WLAN ohne Netzwerk oder Koax-Kabel mit einem FireTV-Stick live fernsehen zu können und die aufgenommenen Sendungen als Stream zu konsumieren: zentral rein über LAN / WLAN für alle Fernseher im Haus. Leider hat die Dreambox nach mehr als 10 Jahren den Geist aufgegeben. Bei der Suche nach einem Ersatz ist mir bewusst geworden, dass neue Geräte mit einem ähnlichen Funktionsumfang wie die Dreambox Mangelware sind. Scheint so als wäre der Markt für Festplattenreceiver in den letzten 10 Jahren dank diverser Streaming-Dienste massiv geschrumpft. Nicht gewillt für eine 7 Jahre alte Hardware mehrere Hundert Euro zu zahlen oder ein Abo für einen IP-Streaming-Dienst abzuschließen, kam mir die Idee meinen bestehenden Mini-Server, einen Beelink 5560U, einfach mit entsprechenden DVB-Tunern auszustatten: [Link Pending] Als Betriebssystem habe ich Ubuntu und Docker installiert, wodurch der Server auch andere Dienste, wie beispielsweise meine Haussteuerung, betreiben kann. Der Beelink 5560U benötigt sehr wenig Strom und ist performant genug um auch noch den PVR-Dienst zu übernehmen. Dies ermöglichte mir den ersatzlosen Abbau der Dreambox, also ein Gerät weniger mit Standby-Stromverbrauch.

Vorhandener Docker Mini-Server ersetzt PVR-Festplatten-Receiver

Nicht nur die Receiver, auch die verfügbare Software sind meist schon etwas länger auf dem Markt und so besitzt der weit verbreitete und kostenlose Service Tvheadend zwar einen genialen Funktionsumfang, dafür aber eine nicht unbedingt zeitgemäße Benutzeroberfläche. Einmal eingerichtet findet der Zugriff ohnehin über einen entsprechenden Client, wie z.B Kodi statt, wodurch sich der direkte Zugriff auf die Benutzeroberfläche von Tvheadend zumindest bei mir voraussichtlich in Grenzen hält. Der Vorteil von TVHeadend im Vergleich zu anderen Services ist das einfache direkte Transcodieren der Aufnahmen in ein anderes Format und die mögliche Unterstützung von OSCAM, um verschlüsselte Sender zu dekodieren.

Software: TVHeadend - Versionen: "latest" nicht immer ausreichend getestet?

Zwar wird TVHeadend noch aktiv weiterentwickelt, mittlerweile aber lediglich als sogenanntes Rolling Release. Jede Änderung fließt in GitHub direkt in "latest" ein, was zur Folge hat, dass bestimmte Änderungen eventuell weniger getestet werden? Zumindest hatte ich mehrfach das Problem, dass nach einem Containerupdate essenzielle Funktionen, wie das Transkodieren von Aufnahmen nur teilweise oder nicht mehr funktionierte, der Ton nicht mehr synchron war oder das Bild einfach dunkel blieb. Einige der Updateprobleme waren dabei einer Änderung an der Konfiguration geschuldet, was durch einen erneuten Aufbau der Einstellungen behoben werden konnte. Aus diesem Grund verwende ich hier nicht "latest" für die in dem Artikel verwendeten Docker-Container, sondern eine Version, die ich bereits länger ohne Problem im Einsatz habe. Sollte ich diese ändern, werde ich neue Versionen nach erfolgreichem Test in diesem Artikel einfliesen lassen.

Hardware USB problematisch? Tuner: DVB-C (USB)

Für den DVB-C TV-Empfang hatte ich folgenden USB-Stick im Einsatz:

auf amazon.de:

Hauppauge WinTV-dualHD TV-Stick mit Fern...

Verfügbarkeit: Jetzt
Preis: 69,96 €
zum Stand: 02.01.2026 03:35
Details

Wer bereits ein Docker-Setup am Laufen hat, muss sich um die Abhängigkeiten zu anderen Softwarepaketen keine Gedanken machen. Der PVR-Dienst und dessen Container können einfach im Terminal mit "docker run" gestartet werden: 

Docker Basics

Docker ermöglicht es, Services oder Applikationen per Befehl in einem sogenannten Container zu starten.
Ein Container ist eine vom Betriebssystem (OS) unabhängige isolierte Umgebung:
Beim ersten Start eines Containers, lädt Docker selbstständig alle notwendigen Quellen
aus dem Internet.
Docker kann unter Windows, macOS oder einer Linux-Distribution installiert werden,
siehe auch: Docker

[+]
docker run -d \
  --name=tvheadend \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Europe/Berlin \
  -e RUN_OPTS= `#optional` \
  -p 9981:9981 \
  -p 9982:9982 \
  -v /path/to/tvheadend/data:/config \
  -v /path/to/recordings:/recordings \
  --device /dev/dri:/dev/dri `#optional GPU-Passthrough` \
  --device /dev/dvb:/dev/dvb `#optional USB DVB-Adapter` \
  --restart unless-stopped \
  linuxserver/tvheadend:version-26a14aa3

Der DVB-C USB Stick (Tuner) wird mit --device in den Container gemappt.

Nach dem Start, kann Tvheadend im Browser über die IP-Adresse des Docker-Servers und Port 9981 aufgerufen werden:

Beim Einrichten hatte ich das Problem, dass keine Kanäle gefunden wurden. Erst nachdem ich am Host-Betriebssystem das Paket "linux-firmware-haupauge" hinzugefügt habe, hat auch der Sendersuchlauf funktioniert, daher an dieser Stelle die verwendeten Befehle für das Host-Betriebssystem, in meinem Fall Ubuntu:

sudo add-apt-repository ppa:b-rad/kernel+mediatree+hauppauge
sudo apt-get install linux-firmware-hauppauge

Aufgrund der positiven Erfahrung mit dem Haupauge DVB-C Adapter, wollte ich für ein DVB-S-Setup den Hauppauge WinTV Nova S2 USB-Stick einsetzen. Als Hardware-Version hatte ich Rev. BBH9 bekommen, welche laut der Herstellerseite - Stand Oktober 2025 - nicht unterstützt wird: unterstützt werden nur die Versionen: Rev B6H9, oder älter.

Nicht unterstützt: WinTV-NOVA-S2: BBH9:

 https://www.hauppauge.de/site/support/support_linux.html#tv_tuner

Mit den folgenden Befehlen konnte ich den USB-Stick dann dennoch aktivieren:

cd /lib/firmware/
sudo wget https://hauppauge.s3.us-east-1.amazonaws.com/linux/dvb-demod-m88ds3103c.fw
sudo apt install linux-mediatree

Für bestimmte Kanäle konnte ich den Stick dann auch verwenden, leider aber nicht für alle: bestimmte Muxes wurden nicht gefunden und bei Verwendung eines vorhandenen Mux eines anderen TV-Adapters, konnte der WinTV-NOVA-S2 bei mir dennoch bestimmte Kanäle einfach nicht tunen.

Hier als Vergleich: Mit Network Astra192e hat wurden nach einem "Force Scan" 1336 Services gefunden, mit dem Haupauge-Tuner (Astra192e-Haupauge) funktionieren nur 670 Services.

USB DVB-Adapter erfordern einen Treiber im Host-Betriebssystem, was die Einrichtung erschweren oder mitunter unmöglich machen kann. Die Unterstützung der Hardware und Treiber bei zukünftigen Betriebssystemupdates ist zudem auch nicht garantiert.

Tuner: DVB-S USB vs. SAT>IP

Gestartet mit DVB-C, habe ich mein Setup mittlerweile auch mit DVB-S getestet. Als Hardware kann eine SAT-Schüssel und für TVHeadend einen SAT>IP-Server verwendet werden. SAT>IP ist in Hinblick auf die notwendigen Treiber völlig unproblematisch, da die Treiber intern im SAT>IP-Server geladen werden: Für TVHeadend wird lediglich eine Netzwerkverbindung zum SAT>IP-Server benötigt.

auf amazon.de:

DUR-line 4 Teilnehmer Set - Qualitäts-Al...

Verfügbarkeit: Jetzt
Preis: 89,90 €
zum Stand: 02.01.2026 03:38
Details

[Link Pending]

Der Digibit Twin SAT>IP-Server ermöglicht einen sehr stabilen Betrieb und eine einfachere Installation, da der Server in der Nähe der SAT-Schüssel platziert und von dort mit einem Netzwerkkabel mit dem TVHeadend-Server verbunden werden kann. Einziger kleiner Nachteil: der Digibit Twin hatte bei mir gemessene 3 Watt Standby Verbrauch, auch wenn kein Tuner aktiv ist, obwohl in den Technischen Daten < 0,3 Watt angegeben ist. Mit einem aktiven Tuner liegt der Verbrauch bei ca. 6 Watt und mit 2 Tunern bei ca.7,5 Watt.

Inbetriebnahme SAT>IP Digibit Twin

Nach dem Anschließen ans Netzwerk, holt sich der Digibit Twin eine IP-Adresse vom DHCP. Eingegeben im Browser meldet sich der SAT>IP-Server mit einem Passwort-Dialog:

Das Standard-Passwort des TELESTAR Digibit Twin ist "admin". 
Nach dem Anmelden gibt es genau 3 Menüpunkte: Status, Network und System:

Die Einstellungen beschränken sich also auf die IP-Adresse, das Ändern des Passworts für den Zugriff und der Möglichkeit neue Firmwareversionen einzuspielen.

Der Digibit Twin hat sich in TVHeadend erst gemeldet (Konfiguration / DVB Inputs / TV Adapters), nachdem ich in Docker den Pfad zur description.xml des SAT>IP-Servers unter RUN_OPTS angegeben habe:

docker run -d \
  --name=tvheadend \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Europe/Berlin \
  -e  RUN_OPTS= --satip_xml http://192.168.1.144:38400/description.xml
  --network host \
  -v /path/to/tvheadend/data:/config \
  -v /path/to/recordings:/recordings \
  --restart unless-stopped \
  linuxserver/tvheadend:version-26a14aa3 

Für den Einsatz mehrerer SAT>IP-Server können hier auch mehrere SAT-IP Adressen hintereinander angegeben werden. Zudem musste ich den Network-Mode auf "host" ändern, damit die Kommunikation zu der für SAT>IP notwendigen Port-Range ohne Einschränkung funktioniert.

Irdeto? Smartcard-Reader-Einbindung – Eine technische Machbarkeitsstudie

Wichtiger rechtlicher Hinweis
Dieser Beitrag dient ausschließlich der technischen Dokumentation und der Information über die Funktionsweise.

Die Verwendung von alternativer Hard- oder Software zur Entschlüsselung von Pay-TV- oder Grundverschlüsselungssignalen (wie jenen des ORF) ist in Österreich nur mit den vom Programmanbieter autorisierten Mitteln (z.B. ORF CI+ Modul und registrierte Smartcard) gestattet.

Der Autor distanziert sich ausdrücklich von jeglicher illegalen Nutzung der hier beschriebenen technischen Informationen und übernimmt keine Haftung für Schäden oder Rechtsverstöße, die aus der unsachgemäßen oder unbefugten Anwendung resultieren.

Oscam - EasyMouse

In einer Testumgebung könnte ein Smartcard-Reader über Dienste wie OSCAM, technische Signale von Smartcards auslesen und in ein Backend-System wie TVHeadend einbinden. Die folgende Dokumentation beschreibt die notwendigen technischen Schritte:

Hardware-Setup:

Die EasyMouse meldet sich in diesem Setup am Mini-PC als /dev/ttyUSB0. Die Hardware-Einstellungen am Kartenleser sind typischerweise auf die Anforderungen der Smartcard und des Chipsatzes eingestellt (hier beispielhaft für Smartmouse 2): 5V, Phoenix, 3,579MHz.

Hardware-Beispiel:

auf amazon.de:

EasyMouse Smartmouse 2 USB Premium Progr...

Verfügbarkeit: Jetzt
Preis: 39,90 €
zum Stand: 02.01.2026 03:39
Details

Docker Compose Konfiguration (Beispiel)

Das folgende Beispiel zeigt die Integration des Kartenlesers in den oscam-Container und die Verknüpfung mit tvheadend:

[+]
services:
  tvheadend:
    image:  linuxserver/tvheadend:version-26a14aa3
    container_name: tvheadend
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
      - RUN_OPTS= --satip_xml http://192.168.1.144:38400/description.xml #optional für digibit twin
    volumes:
      - ./data:/config
      -  /mnt/store/daten/recordings:/recordings
      - ./TVHadmin-JS:/usr/share/tvheadend/src/webui/static/TVHadmin-JS
    network_mode: host
    devices:
      - /dev/dri:/dev/dri #optional Grafikkarte GPU
      - /dev/dvb:/dev/dvb #optional für usb-dvb (z.B. Haupauge)
    restart: always
  oscam:
    image: lscr.io/linuxserver/oscam:latest
    container_name: oscam
    group_add:
      - dialout
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
    volumes:
      - ./oscam:/config
    ports:
      - 8888:8888
      - 6666:6666
    devices:
      - /dev/ttyUSB0:/dev/ttyUSB0
    restart: unless-stopped

OSCAM-Konfiguration (Auszug)

Die folgenden Konfigurationsdateien zeigen die notwendigen technischen Parameter, um die Smartcard auszulesen und den Dienst für TVHeadend bereitzustellen.

Files: oscam.conf

[+]
[global]
logfile                       = stdout

[dvbapi]
enabled                       = 1
au                            = 1
pmt_mode                      = 1
request_mode                  = 1
listen_port                   = 6666
user                          = tvh
boxtype                       = pc

[webif]
httpport                      = 8888
httpuser                      = admin
httppwd                       = [SICHERES PASSWORT]
httpallowed                   = 127.0.0.1,172.16.0.0-172.31.255.255,192.168.0.0-192.168.255.255,10.0.0.0-10.255.255.255,255.255.255.255

oscam.user

[account]
user                          = tvh
au                            = 1
group                         = 1

oscam.server

[reader]
label                         = mouseReader
protocol                      = mouse
device                        = /dev/ttyUSB0
caid                          = 0650
boxkey = [WICHTIG: TECHNISCHER PARAMETER ZUR KARTEN-KOMPATIBILITÄT]
rsakey = [WICHTIG: TECHNISCHER PARAMETER ZUR VERSCHLÜSSELUNG]
detect = cd
group = 1
emmcache = 1,3,2,0

HINWEIS: Die für den Irdeto-Modus notwendigen technischen Parameter boxkey und rsakey dienen der Kompatibilität und Entschlüsselung und können an dieser Stelle aus rechtlichen Gründen nicht veröffentlicht werden.

In TVHeadend wird zunächst in den Einstellungen als Default view level: Expert benötigt:

OSCAM kann wie folgt angebunden werden:

Bekannter Bug in TVHeadend: ORF2 national/regional: Stream Crash

Laut einer aktuellen Diskussion auf der TVHeadend Seite wird der Sender ORF2 unter bestimmten Umständen unbenutzbar: Wie in der Diskussion berichtet, bleibt das Bild vor der täglichen Sendung Bundesland heute (Salzburg heute, Kärnten heute, Wien heute, etc.) stehen und nach der Sendung stürzt der Kanal komplett ab, was erst durch einen Neustart von TVHeadend behoben werden kann, siehe: https://github.com/tvheadend/tvheadend/issues/1973 und https://tvheadend.org/d/9260-orf-2-stream-crash-on-tvh-server.

Ein möglicher Workaround ist der Wechsel auf einen noch funktionierenden Kanal eines anderen Bundeslands nach dem Ende von Bundesland heute, z.B. Salzburg heute.

Für den automatischen Wechsel könnten dem Kanal 2 Services zugewiesen werden: ORF2W HD und ORF2S HD auf einen Kanal, hier "2 ORF2HD" (ORF2S HD könnte in den Kanälen auf ORF2HD umbenannt werden):

Die Idee: nach dem Ende des regionalen Streams soll ORF2W HD anstelle von ORF2S HD verwendet werden.

Bis zum Ende des regionalen Streams soll der regionale Service aktiv bleiben, bis der Stream crasht:

Wenn der Stream nicht mehr funktioniert, kann dieser durch den Wechsel auf ein anderes - noch funktionierendes - Service wieder aktiviert werden:

Damit der Crash in den Logs festgestellt werden kann, hilft ein CA Streamfilter:

Das folgende Bash-File kann die Priorität eines Services ändern oder einen Service deaktivieren oder aktivieren:

Bash-File: changeprio.sh

[+]
#!/bin/bash

# Configuration
TVH_IP="192.168.1.x"
TVH_PORT="9981"
TVH_USER="admin"
TVH_PASS="tvhPassword"

# Usage: changeprio.sh <new_priority> <service_name> [service_index]
NEW_PRIORITY="$1"
SERVICE_NAME="$2"
SERVICE_INDEX="${3:-1}"  # default to 1 if not provided

if [ -z "$NEW_PRIORITY" ] || [ -z "$SERVICE_NAME" ]; then
    echo "$(date +"%d.%m.%Y %H:%M:%S") Usage: $0 <new_priority> <service_name> [service_index]"
    echo "Example: $0 1 'ORF2W HD' 1"
    exit 1
fi

# validate SERVICE_INDEX is a positive integer
if ! [[ "$SERVICE_INDEX" =~ ^[0-9]+$ ]] || [ "$SERVICE_INDEX" -lt 1 ]; then
    echo "❌ Invalid service_index: must be a positive integer."
    exit 1
fi

BASE_URL="http://$TVH_IP:$TVH_PORT/api"

echo "Searching for service matches for: $SERVICE_NAME (selecting index: $SERVICE_INDEX)"

# Query service list once and gather matches as "uuid||text" lines
SERVICE_LIST_JSON=$(curl -s --digest --user "$TVH_USER:$TVH_PASS" \
    "$BASE_URL/service/list?enum=0" 2>/dev/null)

if [ -z "$SERVICE_LIST_JSON" ]; then
    echo "❌ Error: Tvheadend API unreachable or curl failed."
    exit 1
fi

# Build lines of "uuid||text" for matching entries
MATCH_LINES=$(echo "$SERVICE_LIST_JSON" | jq -r --arg name "$SERVICE_NAME" \
    '.entries[] | select(.text | contains($name)) | "\(.uuid)||\(.text)"')

# Count matches
MATCH_COUNT=$(echo "$MATCH_LINES" | grep -c . || true)

if [ "$MATCH_COUNT" -eq 0 ]; then
    echo "❌ No services found matching '$SERVICE_NAME'."
    exit 1
fi

# Select the chosen match (1-based)
SELECTED_LINE=$(echo "$MATCH_LINES" | sed -n "${SERVICE_INDEX}p")
SERVICE_UUID="${SELECTED_LINE%%||*}"
SERVICE_TEXT="${SELECTED_LINE#*||}"

if [ -z "$SERVICE_UUID" ]; then
    echo "❌ Failed to select service UUID."
    exit 1
fi

echo "✅ Selected [$SERVICE_INDEX]: $SERVICE_TEXT (UUID: $SERVICE_UUID)"
echo "Setting priority to: $NEW_PRIORITY"

# Prepare payload and send update
if [ "$NEW_PRIORITY" = "disable" ]; then
    # Prepare payload to disable the service
    RAW_JSON_PAYLOAD="{\"uuid\":\"$SERVICE_UUID\",\"enabled\":false}"
else
    # Priority change payload
    RAW_JSON_PAYLOAD="{\"uuid\":\"$SERVICE_UUID\",\"priority\":$NEW_PRIORITY,\"enabled\":true}"
fi
DATA_STRING="node=$RAW_JSON_PAYLOAD"

RESPONSE=$(curl -s --digest --user "$TVH_USER:$TVH_PASS" \
    "$BASE_URL/idnode/save" \
    --data "$DATA_STRING")

CURL_STATUS=$?
if [ $CURL_STATUS -ne 0 ]; then
    echo "❌ CRITICAL ERROR: Curl command failed with status $CURL_STATUS."
    exit 1
fi

# Parse response
if ! echo "$RESPONSE" | jq . >/dev/null 2>&1; then
    echo "❌ JQ ERROR: Failed to parse JSON response. Raw Response below:"
    echo "$RESPONSE"
    exit 1
fi

if echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
    echo "❌ API Error during save. Response from Tvheadend:"
    echo "$RESPONSE" | jq .
    exit 1
fi

echo "✅ SUCCESS! Priority for '$SERVICE_TEXT' is now set to $NEW_PRIORITY."
echo "Response: $(echo "$RESPONSE" | jq .)"

Ein weiteres Bash-Script könnte feststellen, wann der Wechsel vom regionalen auf den nationalen Stream stattfindet und entsprechend die Services anpassen:

Watchdog File: watchdog.sh für den Betrieb in einem Docker-Container:

[+]
#!/bin/bash

# --- Konfiguration ---
CONTAINER_NAME="tvheadend"
SEARCH_STRING="service: esfilter"

# 1. Startzeit erfassen (für die --since-Option von Docker)
START_TIME_EPOCH=$(date +%s)
START_TIME_HUMAN=$(date +"%Y-%m-%d %H:%M:%S")

echo "📺 Tvheadend Real-Time Watchdog gestartet um ${START_TIME_HUMAN}.<br>Monitoring von Logs für Container: ${CONTAINER_NAME}.<br>➡️ Nur Logs **seit** ${START_TIME_HUMAN} werden geprüft."

# 2. Log-Stream überwachen und Counter in AWK verwenden (20 Minuten Timeout)
timeout 20m docker logs -f --since "$START_TIME_EPOCH" "$CONTAINER_NAME" --tail=10 2>&1 | awk -v search="$SEARCH_STRING" '
    # If the search string is found anywhere in the line ($0), exit with success.
    index($0, search) {
        print "!!! AWK MATCH FOUND !!!" > "/dev/stderr";
        exit 0;
    }
    # If input ends without a match, return non-zero so the script can detect timeout/no-match.
    END { exit 1; }
' >> /dev/null

# Capture exit statuses of the pipeline components:
# PIPESTATUS[0] = exit code of `timeout docker logs ...`
# PIPESTATUS[1] = exit code of `awk ...`
ps0=${PIPESTATUS[0]:-0}
ps1=${PIPESTATUS[1]:-0}

# Use awk's exit code to determine whether a match was found; if awk didn't match and timeout returned 124, treat as timeout.
if [ "$ps1" -eq 0 ]; then
    EXIT_STATUS=0
elif [ "$ps0" -eq 124 ]; then
    EXIT_STATUS=124
else
    EXIT_STATUS=$ps1
fi

if [ $EXIT_STATUS -eq 0 ]; then
    MATCH_TIME=$(date +"%Y-%m-%d %H:%M:%S")
    echo "Führe Kommandos aus: ORF2W HD aktivieren, ORF2S HD Aufnahme stoppen, ORF2S HD deaktivieren. (Match-Zeit: ${MATCH_TIME})"
    OUTPUT=$("/var/web/tvheadend/changeprio.sh" 5 "ORF2W HD")
    echo "Output from command to switch to ORF2W HD: $OUTPUT"
    sleep 1
    #OUTPUT=$("/var/web/tvheadend/stoprecording.sh" "ORF2 HD")
#   echo "Output from stop recording for ORF2 HD: $OUTPUT"
#    sleep 1
    OUTPUT=$("/var/web/tvheadend/changeprio.sh" disable "ORF2S HD")
    echo "Output from command to disable ORF2S HD: $OUTPUT"
else
    echo "⚠️ Ein Fehler trat während der Überwachung der Logs auf (Exit Code ${EXIT_STATUS})."
fi
echo "finished monitoring logs for container: ${CONTAINER_NAME}. Exiting watchdog."
exit 0

Anpassungen: im File watchdog.sh der Kontainername: CONTAINER_NAME und im File: changeprio.sh die TVHeadend IP: TVH_IP, Port: TVH_PORT der Username und das Passwort. Zudem der Pfad, hier: "/var/web/tvheadend/"

Das Aktivieren des Watchdog-Scripts könnte über einen Eintrag in Linux Crontab erfolgen: (sudo crontab -e)

12 19 * * * /var/web/tvheadend/watchdog.sh

Zusätzlich könnte TVHeadend jede Nacht neu gestartet und dabei die Kanäle wieder zurückgesetzt werden. Der Neustart könnte zu einem Zeitpunkt erfolgen, bei dem TVHeadend nicht in Verwendung ist.

Bash-Script: restart.sh

[+]
#!/bin/bash
set -euo pipefail

THRESHOLD=0.3
SLEEP=5
TIMEOUT=$((3*60*60))  # 3 hours in seconds

start_time=$(date +%s)

echo "started: restart.sh"

# wait until tvheadend container CPU% is below THRESHOLD or timeout
while true; do
    cpu=$(docker stats --no-stream --format '{{.CPUPerc}}' tvheadend 2>/dev/null || true)
    cpu=${cpu%\%}            # strip trailing %
    cpu=${cpu/,/.}          # handle locales using comma

    # check timeout
    elapsed=$(( $(date +%s) - start_time ))
    if [ "$elapsed" -ge "$TIMEOUT" ]; then
        logaction "Timeout reached after $TIMEOUT seconds, proceeding"        break
    fi

    [ -z "$cpu" ] && sleep "$SLEEP" && continue

    # compare using awk (avoids requiring bc)
    if awk -v c="$cpu" -v t="$THRESHOLD" 'BEGIN{exit !(c < t)}'; then
        break
    fi

    sleep "$SLEEP"
done
echo "correct the configuration files"
NEXT_COMMAND="/var/web/tvheadend/correct-config.sh"
OUTPUT=$("${NEXT_COMMAND}")
echo "Output from command correct Config: $OUTPUT"

echo "/usr/bin/docker restart tvheadend"
/usr/bin/docker restart tvheadend
sleep 20
echo "Führe Kommandos aus: Wechsel auf ORF2S HD und Deaktivierung von ORF2W HD"
NEXT_COMMAND="/var/web/tvheadend/changeprio.sh"
OUTPUT=$("${NEXT_COMMAND}" 0 "ORF2S HD")
echo "Output from command to enable ORF2S HD: $OUTPUT"
sleep 30
OUTPUT=$("${NEXT_COMMAND}" disable "ORF2W HD")
echo "Output from command to disable ORF2W HD: $OUTPUT"
echo "finished"

Auch hier ist wieder ein bestimmter Pfad in Verwendung, hier: "/var/web/tvheadend/" und auch der Kontainername: "tvheadend" ist nicht als Variable ausgeführt.

Für einen Neustart könnte eine weitere Zeile in Linux Crontab hinzugefügt werden: (sudo crontab -e)

20 4 * * * /var/web/tvheadend/restart.sh

Hier ein paar Screenshots meiner Einstellungen in TVheadend für DVB-C

Zunächst habe ich unter Konfiguration / DVB-Inputs / Netzwerk ein Netzwerk angelegt:

Das Netzwerk kann dann den angeschlossenen TV-Adaptern, in meinem Fall den beiden Adaptern des Hauppauge WinTV-dualHD TV-Stick zugewiesen und nach Kanälen gesucht werden:

 

Die TV-Kanäle sollten nach einiger Zeit unter "Services" auftauchen.

Kanalsuche DVB-S:  TV Adapter, Netzwerk

Voraussetzung für den Betrieb ist natürlich, dass sich die TV-Adapter in TVHeadend melden:

Für den Sendersuchlauf wird ein Netzwerk benötigt, hier für Astra 19.2E:

Das Netzwerk muss dann noch mindestens einem TV-Adapter zugewiesen werden:

Force Scan startet die Suche nach Muxes und fügt die gefundenen Sender als Services hinzu:

Direktes transkodieren nach H.265 (HEVC)

Wer einen halbwegs performanten Mini-PC einsetzt, kann die Aufnahmen on the fly als H.264 oder noch besser mit H.265 transkodieren, wodurch diese sehr wenig Speicher benötigen und dennoch eine sehr gute Qualität aufweisen. Für HD-Qualität wird der Stream mit H.265 von etwa 10 kB/s auf 2,5 kB/s reduziert. Dabei erreicht die CPU meines Mini-PCs pro Stream eine Auslastung von etwa 25%, beim Einsatz von bis zu 3 Tunern: kein Problem. 

 

Als Container habe ich "MPEG-TS/av-lib" verwendet, da ansonsten das Wechseln zu einem anderen Zeitpunkt (Skip) nur sehr verzögert funktioniert. "Hardware acceleration" habe ich deaktiviert, da ich festgestellt habe, dass die Aufnahmen dadurch eine besser Qualität aufweisen. Der Ursache bin ich bis jetzt noch nicht auf den Grund gegangen.

Da der Mini-PC 32GB-Ram besitzt, habe ich die Timeshift auf "Nur RAM" geändert, um die SSD-Festplatte zu schonen.

Aufnahmeprofil: meine Anpassungen

Im Aufnahmeprofil kann das vorab angelegte Streamprofil verwendet werden:

Die Einstellungen  "Benutze EPG Laufend Status" führt bei bestimmten Kanälen dazu, dass die Aufnahmen abgeschnitten werden. Die Einstellung kann pro Kanal deaktiviert werden, wodurch der "Vor-Aufnahme-Polster" und "Nach-Aufnahme-Polster" des Aufnahme-Profils verwendet wird:

Für bestimmte "Auto-Aufnahmen" habe ich dann jeweils die Dublettenbehandlung und DVR Datei Speicherzeit angepasst:

Falsche Tonspur: z.B. akustische Bildbeschreibung

Nach dem Umstieg von DVB-C auf DVB-S hat sich bei bestimmten Fernsehsendern eine andere Tonspur als Standard eingeschlichen: Bestimmte Sendungen waren dadurch per Default auf Original Englisch, oder mit einer akustischen Bildbeschreibung versehen. Das Verhalten kann einfach mit einem "Stream Filter" angepasst werden:

Mit IGNORE können bestimmte Audio-Streams ignoriert werden. Mit Action: "EMPTY" kann der Audiostream dennoch verwendet werden, wenn kein anderer Stream verfügbar ist. Folgendes Beispiel: 
Sprache German (ger) soll verwendet werden: USE, "mis" soll nicht verwendet werden: IGNORE.
Der letzte Filter: EMPTY ist ein Fallback, falls aufgrund der ersten beiden Filter kein Audio Stream vorhanden sein sollte: z.B. ein Stream der nicht "ger" ist. ("mis" wäre im Fallback dann auch möglich: Der Filter für "mis" müsste demnach nicht sein ..) 

Älteste Aufnahmen zuerst löschen

TV-Headend stoppt die Aufnahmen, wenn die Limits im Aufnahmeprofil überschritten wurden: "Verwalte freien Speicher in MiB" und "Verwalte genutzten Speicher in MiB". 

Damit die ältesten Aufnahmen gelöscht werden und um neue aufnehmen zu können, habe ich am Host-Betriebssystem ein kleines Bash-Script am Laufen:

[+]
#!/bin/bash

RECORDINGS_DIR="/mnt/store/daten/recordings"
STORE_MOUNT="/mnt/store"
MIN_FREE_GB=60
MAX_USED_GB=300

get_free_gb() {
    df -BG --output=avail "$STORE_MOUNT" | tail -1 | tr -dc '0-9'
}

get_used_gb() {
    du -sBG "$RECORDINGS_DIR" | awk '{print $1}' | tr -dc '0-9'
}

while true; do
    free_gb=$(get_free_gb)
    used_gb=$(get_used_gb)

    if [[ $free_gb -ge $MIN_FREE_GB && $used_gb -le $MAX_USED_GB ]]; then
        break
    fi

    # Find the oldest file (null-terminated)
    oldest_file=$(find "$RECORDINGS_DIR" -type f -printf '%T@ %p\0' | sort -z -n | head -zn 1 | cut -z -d' ' -f2-)

    if [[ -z "$oldest_file" ]]; then
        echo "No more files to delete."
        break
    fi

    # Remove the file safely
    printf 'Deleting %s to free up space...\n' "$oldest_file"
    rm -f -- "$oldest_file"
done

Das Script überwacht den Aufnahmeordner, in meinem Fall: /mnt/store/daten/recordings und den freien Speicherplatz von /mnt/store und löschte alte Aufnahmen, wenn weniger als 60GB frei ist, oder die Aufnahmen mehr als 200GB in Anspruch nehmen.

Über einen Eintrag in Cron (sudo crontab -e) werden die Aufnahmen täglich bereinigt:

3 3 * * * sudo /scripts/cleanup-recordings.sh > /dev/null 2>&1

Zugriff von Unterwegs über das Internet: Sicher und ohne VPN

Um bestimmte Sendungen von unterwegs programmieren zu können oder Sendungen live auf das Smartphone zu streamen, habe ich den Standardbenutzer in Tvheadend mit einem Benutzernamen und Passwort versehen. Ein zusätzlicher DNS-Eintrag im Internet und das bestehende Port-Forwarding inklusive Reverse-Proxy ermöglicht mir den Zugriff über das Internet, ganz ohne VPN, siehe: sichere https Verbindung: Traefik Reverse Proxy + Let´s Encrypt

Hier meine verwendete Docker-Compose-Konfig:

/conf/tvheadend.yml -Datei Remote-Zugriff

http:
  routers:
    tvh:
      entrypoints: "websecure"
      service: "tvh@file"
      rule: "Host(`tv.domain.tld`)"
      tls:
        certresolver: "myresolver"
  services:
    tvh:
      loadbalancer:
        servers:
          - url: "http://192.168.1.5:9981"

Einbinden über:

version: "3.3"
services:
  traefik:
    image: "traefik"
    container_name: "traefik"
    command:
...
      - "--providers.file.directory=/etc/traefik/conf"
...
    volumes:
...
      - "./conf:/etc/traefik/conf"

Client-Zugriff: Fernseher und mobile Geräte

Für den Zugriff im lokalen Netzwerk nutze ich in den Fernsehern jeweils einen FireTV-Stick. Zudem gibt es Kodi auch als Android-App, wodurch das Handy oder Tablet zum Fernseher wird. Die Android-App stürzt bei mir beim Starten immer wieder mal ab. Wenn die App nicht mehr startet, hilft das Löschen des Cache in den App-Settings. Alternativ kann auch VLC verwendet werden: In VLC Stream: http://tvheadendip/playlist/channels.m3u?profile=pass (Voraussetzung ist das Erlauben von Basic Authentication in TVHeadend)

Über das Internet greife ich derzeit über die Weboberfläche von Tvheadend zu. Da die Weboberfläche nicht für mobile Geräte optimiert ist, teste ich gerade TVHadmin-JS und parallel Jellyfin mit TVHeadend-Plugin.

Fazit

Der Umstieg von einem klassischen PVR-Festplatten-Receiver auf ein Mini-Server Setup mit Docker und DVB-Tunern hat sich als praktikable Lösung erwiesen. Ein Mini-PC wir der Beelink 5560U und Tvheadend unter Docker bietet eine solide Grundlage für TV-Empfang und Streaming für mehrere TV-Geräte: ganz ohne Koax-Kabel: rein über Ethernet oder WLAN. Das Setup ermöglicht das direkte Transkodieren der Aufnahmen nach H265 und den Zugriff über das Internet.

positive Bewertung({{pro_count}})
Beitrag bewerten:
{{percentage}} % positiv
negative Bewertung({{con_count}})

DANKE für deine Bewertung!

Beitrag erstellt von Bernhard | Veröffentlicht: 27.02.2025 | Aktualisiert: 22.12.2025 | Translation English |🔔 | Kommentare:0

Fragen / Kommentare


 
Durch die weitere Nutzung der Seite stimmst du der Verwendung von Cookies zu Mehr Details