Hallo Claus & Klaus,
danke für eure schnellen Antworten. Das Wiki bezieht sich auf Hardware-Grabber. Ich habe aber inzwischen eine schlanke Software-Lösung für mein System gefunden:
Bei mir läuft MLD 6.5 auf einem ASRock J3455M mit interner Intel-Grafik 500 und Ausgabe über softhddevice mit va-api HW-Beschleunigung, installiert über das Image "X86 PC mit Intel GPU".
Das Paket hyperion-ng setzt einen Podman-Container auf, hyperiond startete aber erst, nachdem ich in /usr/share/hyperion/docker-compose.yml ganz unten card0 durch card1 ersetzt hatte, also:
- /dev/dri/card1:/dev/dri/card1Bitte auch gleich das # vor privileged: true entfernen.
Das Hyperion UI war dann nach einem Reboot unter mld.fritz.box:8090 erreichbar.
Eigentlich sollte man dort unter "Aufnahme Hardware" die Grafik-Schnittstelle auswählen können. Das hat aber nicht funktioniert. Daher habe ich ein Python Script erstellt, das den Bildschirminhalt in einer niedrigen Auflösung abgreift und im Flatbuffer-Format an Hyperion schickt. Das funktioniert hier tadellos. Im Hyperion UI unter "Netzwerk" muss "Flatbuffer Server" aktiviert und der Port 19400 eingetragen sein. Speichern nicht vergessen!
Das Script habe ich als /usr/share/hyperion/vdr-kmsgrab-flatbuf.py abgelegt.
Inhalt:
#!/usr/bin/env python3
import subprocess
import socket
import struct
import time
import signal
import select
import flatbuffers
# ============================================================
# Konfiguration
# ============================================================
WIDTH = 160
HEIGHT = 90
FPS = 25
HYPERION_HOST = "127.0.0.1"
HYPERION_PORT = 19400
PRIORITY = 100
ORIGIN = "VDR-KMSgrab"
FRAME_SIZE = WIDTH * HEIGHT * 3
running = True
# ============================================================
# Signal handling
# ============================================================
def signal_handler(signum, frame):
global running
running = False
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# ============================================================
# FlatBuffer
# ============================================================
def create_register(builder):
origin = builder.CreateString(ORIGIN)
builder.StartObject(2)
builder.PrependUOffsetTRelativeSlot(
0, origin, 0
)
builder.PrependInt32Slot(
1, PRIORITY, 0
)
return builder.EndObject()
def create_raw_image(builder, frame):
data = builder.CreateByteVector(frame)
builder.StartObject(3)
builder.PrependUOffsetTRelativeSlot(
0, data, 0
)
builder.PrependInt32Slot(
1, WIDTH, -1
)
builder.PrependInt32Slot(
2, HEIGHT, -1
)
return builder.EndObject()
def create_image(builder, raw_image):
builder.StartObject(3)
# RawImage
builder.PrependUint8Slot(
0, 1, 0
)
builder.PrependUOffsetTRelativeSlot(
1, raw_image, 0
)
builder.PrependInt32Slot(
2, -1, -1
)
return builder.EndObject()
def create_request(builder, command_type, command):
builder.StartObject(2)
builder.PrependUint8Slot(
0, command_type, 0
)
builder.PrependUOffsetTRelativeSlot(
1, command, 0
)
return builder.EndObject()
def build_register():
builder = flatbuffers.Builder(1024)
register = create_register(builder)
# Command.Register
request = create_request(
builder,
4,
register
)
builder.Finish(request)
return builder.Output()
def build_image(frame):
builder = flatbuffers.Builder(
FRAME_SIZE + 1024
)
raw_image = create_raw_image(
builder,
frame
)
image = create_image(
builder,
raw_image
)
# Command.Image
request = create_request(
builder,
2,
image
)
builder.Finish(request)
return builder.Output()
def build_clear():
builder = flatbuffers.Builder(256)
builder.StartObject(1)
builder.PrependInt32Slot(
0,
PRIORITY,
0
)
clear = builder.EndObject()
# Command.Clear
request = create_request(
builder,
3,
clear
)
builder.Finish(request)
return builder.Output()
# ============================================================
# TCP
# ============================================================
def send_message(sock, payload):
header = struct.pack(
">I",
len(payload)
)
sock.sendall(header)
sock.sendall(payload)
def read_reply_nonblocking(sock, reply_buffer):
"""
Hyperion Replies nebenbei abholen.
Die Funktion blockiert nicht.
"""
while True:
try:
data = sock.recv(65536)
if not data:
raise ConnectionError(
"Hyperion hat Verbindung geschlossen"
)
reply_buffer.extend(data)
except BlockingIOError:
break
# Replies aus dem Buffer entfernen.
#
# Format:
# 4 Byte Laenge
# Payload
while True:
if len(reply_buffer) < 4:
break
size = struct.unpack(
">I",
reply_buffer[:4]
)[0]
if size > 1024 * 1024:
raise ConnectionError(
f"Ungueltige Reply-Groesse: {size}"
)
if len(reply_buffer) < 4 + size:
break
del reply_buffer[:4 + size]
# ============================================================
# Hyperion Verbindung
# ============================================================
def connect_hyperion():
while running:
sock = None
try:
print(
f"Verbinde mit Hyperion "
f"{HYPERION_HOST}:{HYPERION_PORT} ..."
)
sock = socket.create_connection(
(
HYPERION_HOST,
HYPERION_PORT
),
timeout=3
)
# Wichtig:
# Socket fuer den laufenden Betrieb
# nicht mehr blockierend betreiben.
sock.setblocking(False)
print(
"Hyperion FlatBuffer verbunden."
)
# Register senden
register = build_register()
send_message(
sock,
register
)
# Kurz Zeit geben, damit Hyperion
# die Register-Reply erzeugen kann.
time.sleep(0.01)
reply_buffer = bytearray()
try:
read_reply_nonblocking(
sock,
reply_buffer
)
except BlockingIOError:
pass
print(
f"Hyperion registriert: "
f"priority={PRIORITY}, "
f"origin={ORIGIN}"
)
return sock, reply_buffer
except (
OSError,
ConnectionError
) as e:
print(
f"Hyperion nicht erreichbar: {e}"
)
if sock is not None:
try:
sock.close()
except Exception:
pass
if running:
time.sleep(1)
return None, None
# ============================================================
# FFmpeg
# ============================================================
def start_ffmpeg():
cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-f", "kmsgrab",
"-device", "/dev/dri/card1",
"-framerate", str(FPS),
"-i", "-",
"-vf",
(
"hwdownload,"
"format=bgr0,"
f"scale={WIDTH}:{HEIGHT},"
"format=rgb24"
),
"-f", "rawvideo",
"-pix_fmt", "rgb24",
"pipe:1",
]
print("Starte FFmpeg:")
print(" ".join(cmd))
print()
return subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=FRAME_SIZE * 4,
)
# ============================================================
# Main
# ============================================================
def main():
global running
print()
print(
"VDR KMSgrab -> Hyperion FlatBuffer"
)
print("-----------------------------------")
print("/dev/dri/card1")
print(f"{WIDTH}x{HEIGHT}")
print(f"{FPS} fps")
print(f"{FRAME_SIZE} Bytes RGB24")
print(
f"{HYPERION_HOST}:{HYPERION_PORT}"
)
print()
ffmpeg = start_ffmpeg()
sock = None
reply_buffer = bytearray()
frame_count = 0
start_time = time.monotonic()
last_report = start_time
try:
while running:
# ------------------------------------------------
# Verbindung herstellen
# ------------------------------------------------
if sock is None:
sock, reply_buffer = (
connect_hyperion()
)
if sock is None:
break
# ------------------------------------------------
# Hyperion Replies nebenbei abholen
# ------------------------------------------------
try:
read_reply_nonblocking(
sock,
reply_buffer
)
except (
ConnectionError,
OSError
) as e:
print(
f"Hyperion-Verbindung verloren: {e}"
)
try:
sock.close()
except Exception:
pass
sock = None
continue
# ------------------------------------------------
# Einen Frame lesen
# ------------------------------------------------
frame = ffmpeg.stdout.read(
FRAME_SIZE
)
if len(frame) != FRAME_SIZE:
print(
f"FFmpeg: erwartet "
f"{FRAME_SIZE} Bytes, "
f"erhalten {len(frame)}"
)
break
# ------------------------------------------------
# FlatBuffer bauen
# ------------------------------------------------
payload = build_image(frame)
# ------------------------------------------------
# Frame senden
# ------------------------------------------------
try:
send_message(
sock,
payload
)
frame_count += 1
except (
BrokenPipeError,
ConnectionResetError,
ConnectionAbortedError,
OSError
) as e:
print(
f"Hyperion-Verbindung verloren: {e}"
)
try:
sock.close()
except Exception:
pass
sock = None
continue
# ------------------------------------------------
# Statistik
# ------------------------------------------------
now = time.monotonic()
if now - last_report >= 5:
elapsed = (
now - start_time
)
real_fps = (
frame_count / elapsed
if elapsed > 0
else 0
)
print(
f"{frame_count} Frames | "
f"{real_fps:.1f} fps | "
f"{len(payload)} Byte FlatBuffer | "
f"Replies im Puffer: "
f"{len(reply_buffer)}"
)
last_report = now
finally:
running = False
# ----------------------------------------------------
# Priority freigeben
# ----------------------------------------------------
if sock is not None:
try:
clear = build_clear()
send_message(
sock,
clear
)
except Exception:
pass
try:
sock.close()
except Exception:
pass
# ----------------------------------------------------
# FFmpeg beenden
# ----------------------------------------------------
try:
ffmpeg.terminate()
ffmpeg.wait(timeout=2)
except Exception:
try:
ffmpeg.kill()
except Exception:
pass
print()
print("Grabber beendet.")
if __name__ == "__main__":
main()Außer hyperion-ng müssen folgende MLD-Pakete installiert sein:
dann im Terminal:
python3 -m pip install flatbuffers
python3 /usr/share/hyperion/vdr-kmsgrab-flatbuf.pyAntwort etwa:
VDR KMSgrab -> Hyperion FlatBuffer
-----------------------------------
/dev/dri/card1
160x90
25 fps
43200 Bytes RGB24
127.0.0.1:19400
Starte FFmpeg:
...
Verbinde mit Hyperion 127.0.0.1:19400 ...
Hyperion FlatBuffer verbunden.
Hyperion registriert: priority=100, origin=VDR-KMSgrab
124 Frames | 24.7 fps | 43272 Byte FlatBuffer | Replies im Puffer: 0
249 Frames | 24.8 fps | 43272 Byte FlatBuffer | Replies im Puffer: 0
...Die in Hyperion eingestellten LEDs sollten je nach Bildschirminhalt leuchten.
Wenn alles funktioniert am besten einen systemd-Service anlegen, der das Script beim Booten startet:
nano /etc/systemd/system/vdr-kmsgrab.serviceInhalt:
[Unit]
Description=VDR KMSgrab to Hyperion FlatBuffer
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 -u /usr/share/hyperion/vdr-kmsgrab-flatbuf.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetsystemd neu laden
systemctl daemon-reloadDienst testweise starten
systemctl start vdr-kmsgrab.serviceDann:
systemctl status vdr-kmsgrab.serviceAusgabe etwa:
Active: active (running)Live-Log ansehen:
journalctl -u vdr-kmsgrab.service -fEs sollten die bisherigen Meldungen erscheinen:
Hyperion FlatBuffer verbunden.
Hyperion registriert: priority=100, origin=VDR-KMSgrab
124 Frames | 24.7 fps | 43272 Byte FlatBuffer
249 Frames | 24.8 fps | 43272 Byte FlatBuffer
...Wenn das funktioniert und die LEDs leuchten, automatischen Start aktivieren:
systemctl enable vdr-kmsgrab.serviceOder direkt aktivieren und starten:
systemctl enable --now vdr-kmsgrab.serviceDamit läuft das Grabber-Script nach einem Neustart automatisch.
Ich wollte diesen Text eigentlich im Wiki ergänzen, aber die Syntax dort schreckt mich ab!
Schöne Grüße
Uwe