Files
hms-mediaengine/hms_app/setup.py
T
HMS MediaEngine Agent 68ce87e5ac ECHTES PROGRAMM: Medienverwaltung + Upload + konfigurierbares Art-Net (14/14 E2E bestanden)
Vollständige Anwendung in hms_app/ (6 Module) + schlanker run.py:
- config.py: config.json-Persistenz, Defaults, strikte Validierung
  (Bereiche, Universes, Kanal-Mapping; ungültige Settings => 400)
- media.py: Upload-Streaming (chunkweise, Größenlimit, .part-Sicherung),
  Duplikat-Umbenennung, Thumbnail-Erzeugung per GStreamer,
  Metadaten-Cache (Dauer via query_duration), Löschen, Pfad-Schutz
- artnet.py: ArtDMX voll KONFIGURIERBAR (Port, Universes, Kanäle),
  Sequenz-/Duplikat-Filter, Signalverlust-Policy hold/fade_black,
  Watchdog, Live-Restart nach Settings-Änderung
- engine.py: dynamische Layer (Video + Bild via imagefreeze),
  atomarer Rebuild mit Rollback (alte Pipeline läuft bei Fehler weiter),
  Positions/Größen/Z-Order/Alpha je Layer, EOS-Loop, DMX-Mapping
- server.py: REST komplett (upload/delete, layers add/remove/update,
  master/blackout/playback, settings GET/POST mit Validierung),
  Thumbnails + Datei-Download, Traversal-Schutz
- ui.py: 4-Tabs-WebUI: Live (Preview+Slider+Diagnose), Medien
  (Drag&Drop-Upload mit Fortschritt, Thumbnails, Als-Layer/Delete),
  Layer-Editor (Alpha/Pos/Größe/Z), Einstellungen (Art-Net komplett
  umstellbar inkl. Port/Universes/Kanäle, Preview, Engine, Upload-Limit)
- setup.py: Bootstrap + setup_windows.ps1/setup_linux.sh Generierung
- run.py: Einstieg (--port, --setup, --bootstrap, --generate-setup,
  Datei-Import via CLI)

E2E-BEWEIS (14/14 PASS, im Container ausgeführt):
1 Health OK · 2 Video-Upload via HTTP · 3 Bild-Upload · 4 Bibliothek
mit Metadaten (Video-Dauer erkannt) · 5 Thumbnail JPEG ·
6 Traversal-Schutz (404) · 7/8 Layer Video+Bild dynamisch ·
9 Engine rendert (91 Frames) · 10 Art-Net live auf Port 6455 mit
Master-Kanal 10 umgestellt · 11 DMX steuert Master 0.251 auf NEUEM
Port/Kanal · 12 Layer-Alpha via DMX · 13 config.json persistiert ·
14 Layer-Remove ohne Absturz (258 Frames, atomar) ·
Ungültige Settings werden mit 400 + Feldname abgewiesen

Fixes: Bild-Caps kombiniert (parse-Fehler), Thumbnail-Namens-Mapping,
kind_of-Methode, atomarer Rebuild mit Rollback, DMX-accepted-Zähler
2026-09-11 11:05:01 +02:00

182 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Bootstrap, Setup-Skripte für Windows/Linux, Einstellungs-Dialog."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
LINUX_PKGS = [
"gstreamer1.0-tools", "gstreamer1.0-plugins-base",
"gstreamer1.0-plugins-good", "gstreamer1.0-plugins-bad",
"gstreamer1.0-plugins-ugly", "gstreamer1.0-libav",
"python3-gst-1.0", "gir1.2-gst-1.0", "python3-tk",
]
def check_and_install_dependencies() -> bool:
"""Prüft GStreamer; installiert bei Bedarf (Linux) bzw. lädt MSI (Windows)."""
print("[Bootstrap] Prüfe GStreamer...")
try:
import gi # noqa: F401
gi.require_version("Gst", "1.0")
from gi.repository import Gst as _Gst
_Gst.init(None)
print(f"[Bootstrap] GStreamer {_Gst.version_string()} OK")
return True
except (ImportError, ValueError):
pass
if sys.platform == "linux":
print("[Bootstrap] Installiere GStreamer (sudo apt-get)...")
try:
subprocess.run(["sudo", "apt-get", "update", "-qq"],
check=True, timeout=180)
subprocess.run(
["sudo", "apt-get", "install", "-y", "-qq"] + LINUX_PKGS,
check=True, timeout=600)
print("[Bootstrap] GStreamer installiert. Programm neu starten.")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired,
FileNotFoundError):
print("[Bootstrap] Manuell installieren:")
print(" sudo apt-get install " + " ".join(LINUX_PKGS))
return False
if sys.platform == "win32":
url = ("https://gstreamer.freedesktop.org/data/pkg/windows/"
"1.28.4/msvc/"
"gstreamer-1.0-msvc-x86_64-1.28.4.msi")
msi = Path.home() / "Downloads" / "gstreamer-1.0-msvc-x86_64.msi"
try:
import urllib.request
if not msi.exists():
print(f"[Bootstrap] Lade {url}")
urllib.request.urlretrieve(url, msi)
subprocess.run(["msiexec", "/i", str(msi), "/quiet",
"ADDLOCAL=ALL"], check=False)
print("[Bootstrap] GStreamer-MSI gestartet. "
"Nach Installation neu starten.")
except Exception as e: # noqa: BLE001
print(f"[Bootstrap] Download-Fehler: {e}")
print(f"[Bootstrap] Manuell installieren: {url}")
return False
return False
WINDOWS_SETUP = r'''# HMS MediaEngine - Windows Setup (einmalig ausfuehren)
$ErrorActionPreference = "Stop"
Write-Host "=== HMS MediaEngine Windows Setup ===" -ForegroundColor Cyan
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Host "Als Administrator ausfuehren (Rechtsklick -> Als Administrator ausfuehren)" -ForegroundColor Red
exit 1
}
Write-Host "[1/2] GStreamer 1.28.4 MSVC..." -ForegroundColor Yellow
$gstRoot = [Environment]::GetEnvironmentVariable("GSTREAMER_1_0_ROOT_MSVC_X86_64", "Machine")
if (-not $gstRoot) {
$url = "https://gstreamer.freedesktop.org/data/pkg/windows/1.28.4/msvc/gstreamer-1.0-msvc-x86_64-1.28.4.msi"
$msi = "$env:TEMP\gstreamer.msi"
Invoke-WebRequest -Uri $url -OutFile $msi
Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /quiet ADDLOCAL=ALL" -Wait
Write-Host " GStreamer installiert"
} else { Write-Host " bereits vorhanden" }
Write-Host "[2/2] Python 3.13 (falls fehlt) mit tkinter..." -ForegroundColor Yellow
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) {
$pyUrl = "https://www.python.org/ftp/python/3.13.2/python-3.13.2-amd64.exe"
$exe = "$env:TEMP\python-installer.exe"
Invoke-WebRequest -Uri $pyUrl -OutFile $exe
Start-Process $exe -ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_tcltk=1" -Wait
Write-Host " Python installiert"
} else { Write-Host " bereits vorhanden" }
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";$env:Path"
Write-Host ""
Write-Host "=== Fertig ===" -ForegroundColor Green
Write-Host "Start: python run.py"
Write-Host "Dialog: python run.py --setup"
Write-Host "Media-Verwaltung, Layer und Einstellungen: Web-UI im Browser"
'''
LINUX_SETUP = r'''#!/bin/bash
# HMS MediaEngine - Linux Setup (einmalig ausfuehren)
set -e
echo "=== HMS MediaEngine Linux Setup ==="
if command -v apt-get >/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq gstreamer1.0-tools gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
gstreamer1.0-libav python3-gst-1.0 gir1.2-gst-1.0 python3-tk xvfb
elif command -v dnf >/dev/null; then
sudo dnf install -y gstreamer1 gstreamer1-plugins-base gstreamer1-plugins-good \
gstreamer1-plugins-bad-free gstreamer1-plugins-ugly gstreamer1-libav \
python3-gobject python3-tkinter
fi
echo "=== Fertig ==="
echo "Start: python3 run.py"
echo "Media-Verwaltung, Layer und Einstellungen: Web-UI im Browser"
'''
def generate_setup_scripts(root: Path) -> None:
p1 = root / "setup_windows.ps1"
p1.write_text(WINDOWS_SETUP, encoding="utf-8")
p2 = root / "setup_linux.sh"
p2.write_text(LINUX_SETUP, encoding="utf-8")
p2.chmod(0o755)
print(f"Erzeugt: {p1}")
print(f"Erzeugt: {p2}")
def run_setup(settings: dict) -> dict:
"""Nativer Dialog für Port/Fullscreen; Medien-Verwaltung bleibt im Web."""
result = {"port": int(settings["web"]["port"]),
"fullscreen": bool(settings["output"]["fullscreen"])}
try:
import tkinter as tk
from tkinter import ttk
except ImportError:
print("tkinter nicht verfügbar (Linux: apt install python3-tk).")
return result
root = tk.Tk()
root.title("HMS MediaEngine Einstellungen")
root.geometry("440x240")
root.configure(bg="#16181d")
style = ttk.Style(root)
style.theme_use("clam")
style.configure("TLabel", background="#16181d", foreground="#c8ccd4")
main = ttk.Frame(root, padding=18)
main.pack(fill="both", expand=True)
ttk.Label(main, text="HMS MediaEngine",
font=("System", 15, "bold")).pack(pady=(0, 2))
ttk.Label(main, text="Grundeinstellungen",
foreground="#8a8f9a").pack(pady=(0, 12))
row = ttk.Frame(main)
row.pack(fill="x", pady=4)
ttk.Label(row, text="Web-UI Port:").pack(side="left")
port_var = tk.StringVar(value=str(result["port"]))
ttk.Entry(row, textvariable=port_var, width=7).pack(
side="left", padx=8)
fs_var = tk.BooleanVar(value=result["fullscreen"])
ttk.Checkbutton(row, text="Fullscreen-Output",
variable=fs_var).pack(side="left", padx=10)
def go():
try:
result["port"] = int(port_var.get() or 8080)
except ValueError:
pass
result["fullscreen"] = bool(fs_var.get())
root.destroy()
ttk.Button(main, text="Starten", command=go).pack(
fill="x", pady=(14, 0), ipady=6)
root.mainloop()
return result