Files
HMS MediaEngine Agent 02765e6b34 CUE-SZENENLISTE PRO LAYER + macOS-SUPPORT (35/35 E2E, 123 Checks gesamt)
1) Cue-/Szenenliste JE Layer (35/35 E2E):
- Cue sichern = Snapshot (Quelle, Alpha, Position, Groesse, Z-Order,
  FX1/FX2 + Intensitaeten); Name + fade_ms + follow_ms pro Cue
- GO vor/zurueck (steps), Goto (Index), Wrap-around am Listenende
- FADE: Quellwechsel = dip-to-black (Phase1 runter, Wechsel bei 0,
  Phase2 hoch; E2E: alpha 0.49 -> 0.32 -> Ziel 0.8), sonst sanfter
  Tween von Alpha + FX-Intensitaeten; laufende Fades abbrechbar
- AUTO-FOLLOW: follow_ms schaltet nach Ablauf automatisch weiter
  (E2E: Cue mit 600ms -> Wrap zum naechsten)
- DMX CUE-GO: cue_go_channel (Settings + UI), steigende Flanke
  >=128 = GO auf allen Layern; ohne Flanke kein GO (E2E: 0->1,
  1==1 bei High-Halten, neue Flanke 1->0); Sequenzfilter aktiv
- Cue-Entfernen mit Indexkorrektur; Cues in Projekten
  speicher-/ladbar (3 Cues nach Projekt-Load erhalten)
- UI: Cue-Bereich je Layer (Cue sichern / GO / Zurueck / Goto /
  Loeschen, aktiver Cue markiert, Fade+Follow-Anzeige)
- Fehlerfaelle: unbekannter Layer 400, fehlendes Medium 400,
  unbekannter Generator 400

2) macOS (Apple Silicon + Intel) mit Skript:
- setup_macos.sh: Homebrew installiert python@3.12, python-tk,
  GStreamer + alle Plugin-Sets + PyGObject; Apple-Silicon-PATH-
  Hinweis (/opt/homebrew); Erklaerung warum Homebrew statt
  gstreamer-.pkg (keine PyGObject-Bindings in .pkg)
- Bootstrap (--bootstrap): darwin-Zweig mit brew-Installationspfad
- run.py --generate-setup erzeugt jetzt 3 Skripte
  (Windows .ps1 / Linux .sh / macOS .sh); Bash-Syntax geprueft

3) Gefundene + behobene FEHLER:
- WEB-UI-ABSTURZ (echter JS-Ternary-Klammerfehler in loadServers,
  seit Verteilungs-Commit unentdeckt, weil API-Tests den Browser-
  Code nie parsen): Klammerstruktur korrigiert + JS-Syntax nun per
  node --check verifizierbar (Syntax OK) + Remote-Buttons robust
  per data-Attribute (kein Quote-Escaping mehr)
- update_layer blockierte Quellwechsel Video->Generator (Cues
  brauchen beides): Name entscheidet jetzt den Typ, Wechsel in
  beide Richtungen
- Cue-E2E-Timing: dip-to-black hat 2 Phasen (Pruefung 0.2s/0.6s),
  Auto-Follow-Index-Fix, Art-Net-Sequenznummern im Test
  inkrementierend (Sequenzfilter verwirft Duplikate korrekt)

E2E-Bilanz: Cue 35/35 + Regressionen Projekte 25/25,
Verteilung 33/33, App 14/14, FX 16/16 = 123 Checks gruen
2026-09-11 13:34:43 +02:00

249 lines
9.6 KiB
Python
Raw Permalink 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 shutil
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 == "darwin":
pkgs = ["python@3.12", "python-tk", "gstreamer",
"gst-plugins-base", "gst-plugins-good",
"gst-plugins-bad", "gst-plugins-ugly",
"gst-libav", "pygobject"]
brew = shutil.which("brew")
if brew:
print("[Bootstrap] Installiere GStreamer + PyGObject "
"via Homebrew (dauert evtl. einige Minuten)...")
try:
subprocess.run([brew, "install"] + pkgs,
check=False, timeout=1800)
print("[Bootstrap] Fertig. Programm mit brew-python3 "
"neu starten.")
except subprocess.TimeoutExpired:
print("[Bootstrap] Timeout. Manuell: brew install "
+ " ".join(pkgs))
else:
print("[Bootstrap] Homebrew nicht gefunden.")
print(" Erst Homebrew installieren:")
print(' /bin/bash -c "$(curl -fsSL '
'https://raw.githubusercontent.com/Homebrew/'
'install/HEAD/install.sh)"')
print(" Danach: ./setup_macos.sh")
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
MACOS_SETUP = r'''#!/bin/bash
# HMS MediaEngine - macOS Setup (Apple Silicon & Intel, einmalig ausfuehren)
set -e
echo "=== HMS MediaEngine macOS Setup ==="
echo "Architektur: $(uname -m)"
if ! command -v brew >/dev/null; then
echo "Homebrew fehlt. Zunaechst installieren:"
echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
echo "Dann dieses Skript erneut ausfuehren."
exit 1
fi
echo "[1/2] Installiere GStreamer, PyGObject und tkinter (brew)..."
brew install python@3.12 python-tk gstreamer gst-plugins-base \
gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav pygobject
echo "[2/2] Umgebung pruefen..."
if [ "$(uname -m)" = "arm64" ] && [ -f /opt/homebrew/bin/brew ]; then
echo "Apple Silicon: Homebrew-Umgebung einbinden mit:"
echo ' eval "$(/opt/homebrew/bin/brew shellenv)"'
fi
echo "Pruefung:"
python3 -c "import gi; gi.require_version('Gst','1.0'); from gi.repository import Gst; Gst.init(None); print('GStreamer OK:', Gst.version_string())" || \
{ echo "PyGObject fehlt noch -> brew python3 verwenden (PATH!)"; exit 1; }
echo ""
echo "Hinweis: Die .pkg-Pakete von gstreamer.freedesktop.org/data/pkg/macos/"
echo "enthalten KEINE PyGObject-Bindings - Homebrew ist der empfohlene Weg."
echo ""
echo "=== Fertig ==="
echo "Start: python3 run.py"
echo "Dialog: python3 run.py --setup"
'''
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)
p3 = root / "setup_macos.sh"
p3.write_text(MACOS_SETUP, encoding="utf-8")
p3.chmod(0o755)
print(f"Erzeugt: {p1}")
print(f"Erzeugt: {p2}")
print(f"Erzeugt: {p3}")
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