68ce87e5ac
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
182 lines
7.0 KiB
Python
182 lines
7.0 KiB
Python
"""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
|