GENERATOREN + MEDIEN-VERTEILUNG + AUTOSTART-SETTINGS (33/33 E2E)

1) MADRIX-artige Generator-Layer (15 prozedurale Quellen):
- engine.GENERATORS: ball (animiert), gradient, pinwheel (animiert),
  zone-plate (animiert), checkers, snow (Rauschen animiert), circular,
  spokes, smpte, gamut, red/green/blue/black/white - alle per
  gst-launch verifiziert, kombinierbar mit FX1/FX2 (Generator+FX
  gerendert, 28 Frames im Test)
- add_generator + Pattern-Wechsel zur Laufzeit (ball->snow) mit
  Rebuild, Katalog-Endpoint /api/generators, UI-Dropdown im Layer-Tab
- Generatoren in Projekten speicher-/ladbar (gradient nach Load aktiv)
- Ungueltige Generator-IDs werden mit 400 abgewiesen

2) Medien-Verteilung ueber mehrere Server:
- hms_app/distribution.py: Server-Check (/api/health),
  Remote-Medien-Liste, Streaming-Import mit Upload-Sicherung
- config.servers: Remote-Server registrieren (Name+URL, HTTP-only,
  Duplikat-Schutz), /api/servers (online/offline + Medien),
  servers/import: Datei von Remote ins lokale Archiv ziehen
- UI: Remote-Server in Einstellungen verwalten, Remote-Medien im
  Medien-Tab mit 'Ins Archiv ziehen'-Button
- run.py --data-root: mehrere Instanzen auf einer Maschine mit
  getrennten config/media/projects-Ordnern
- E2E mit ZWEI echten Servern: A registriert B, sieht B-Medien,
  zieht clip byte-identisch (SHA verifiziert) und spielt ihn als
  Layer; Offline-Erkennung nach B-Stop; 404/400-Faelle

3) Autostart in den Einstellungen:
- Einstellungen: Autostart-Projekt-Dropdown (aus Projektliste),
  abwaehlbar; Projekte-Tab weiterhin mit Autostart-Button
- FIX Settings-Save-Merge: POST /api/settings merged jetzt auf die
  AKTUELLEN Settings - Autostart und registrierte Server gehen beim
  Speichern anderer Einstellungen nicht mehr verloren (E2E: Autostart
  ueberlebt Settings-Save, FPS uebernommen)

E2E: 33/33 PASS (Verteilung/Generatoren) + Regressionen:
Projekte 25/25, App 14/14, FX 16/16 - alle gruen
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 13:11:54 +02:00
parent 70502458d8
commit 7e191ff6c9
7 changed files with 550 additions and 21 deletions
+83 -11
View File
@@ -80,6 +80,26 @@ FX_CATALOG: dict[str, dict] = {
"int": True},
}
# Generator-Katalog (prozedurale Quellen, verifiziert per gst-launch):
# MADRIX-artige Inhalte: Generator-Layer + Effekte kombinierbar.
GENERATORS: dict[str, str] = {
"ball": "Springender Ball (animiert)",
"gradient": "Farbverlauf",
"pinwheel": "Pinwheel (animiert)",
"zone-plate": "Zonenplatte (animiert)",
"checkers-2": "Schachbrett",
"snow": "Rauschen (animiert)",
"circular": "Kreismuster",
"spokes": "Speichen",
"smpte": "SMPTE-Farbbalken",
"gamut": "Farbraum-Gamut",
"red": "Vollrot",
"green": "Vollgrün",
"blue": "Vollblau",
"black": "Schwarz",
"white": "Weiß",
}
class Layer:
"""Ein aktiver Layer im Compositor."""
@@ -153,6 +173,12 @@ class MediaEngine:
elem += f" {spec['prop']}={val}"
fx_chain += f" ! videoconvert ! {elem}"
if layer.kind == "generator":
caps = (f"video/x-raw,width={layer.width},"
f"height={layer.height},framerate=15/1")
return (f"videotestsrc is-live=true pattern={layer.name} ! "
f"{caps}{fx_chain} ! videoconvert ! mix.sink_{layer.id}")
if layer.kind == "video":
caps = (f"video/x-raw,width={layer.width},"
f"height={layer.height},format=BGRA")
@@ -298,6 +324,33 @@ class MediaEngine:
return None
return layer.to_dict()
def add_generator(self, gen_id: str) -> dict | None:
"""Fuegt einen Generator-Layer hinzu (prozedurale Quelle)."""
with self._lock:
if gen_id not in GENERATORS:
self.last_error = f"unbekannter Generator: {gen_id}"
return None
max_l = int(self.settings["engine"].get("max_layers", 8))
if len(self.layers) >= max_l:
self.last_error = f"max. {max_l} Layer"
return None
layer = Layer(self._next_id, gen_id, "generator")
self._next_id += 1
pv = self.settings["preview"]
layer.width = pv["width"]
layer.height = pv["height"]
layer.z = len(self.layers)
self.layers.append(layer)
if not self.rebuild():
self.layers.remove(layer)
return None
return layer.to_dict()
def generators(self) -> list[dict]:
"""Generator-Katalog fuer die UI."""
return [{"id": g, "label": lbl}
for g, lbl in GENERATORS.items()]
def remove_layer(self, layer_id: int) -> bool:
with self._lock:
kept = [l for l in self.layers if l.id != layer_id]
@@ -358,11 +411,22 @@ class MediaEngine:
self.rebuild() # Struktur geaendert -> Pipeline neu
return self.get_layer(layer.id)
if "name" in patch and patch["name"] != layer.name:
f = self.library.file_path(str(patch["name"]))
if not f.exists():
new_name = str(patch["name"])
if layer.kind == "generator":
if new_name not in GENERATORS:
return None
layer.name = new_name
else:
f = self.library.file_path(new_name)
if not f.exists():
return None
layer.name = new_name
layer.kind = (self.library.kind_of(new_name)
or layer.kind)
# Quell-Wechsel braucht einen Pipeline-Rebuild
if not self.rebuild():
return None
layer.name = str(patch["name"])
layer.kind = self.library.kind_of(layer.name) or layer.kind
return self.get_layer(layer.id)
# Pad-Eigenschaften live setzen (ohne Rebuild)
self._apply_pad_props(layer)
return layer
@@ -539,13 +603,21 @@ class MediaEngine:
if not isinstance(raw, dict):
continue
name = str(raw.get("name", ""))
f = self.library.file_path(name)
kind = self.library.kind_of(name)
if not f.exists() or kind is None:
print(f"[Projects] WARNUNG: Medium fehlt, Layer "
f"uebersprungen: {name}")
continue
layer = Layer(self._next_id, name, kind)
kind_raw = str(raw.get("kind", ""))
if kind_raw == "generator":
if name not in GENERATORS:
print(f"[Projects] WARNUNG: unbekannter Generator, "
f"Layer uebersprungen: {name}")
continue
layer = Layer(self._next_id, name, "generator")
else:
f = self.library.file_path(name)
kind = self.library.kind_of(name)
if not f.exists() or kind is None:
print(f"[Projects] WARNUNG: Medium fehlt, Layer "
f"uebersprungen: {name}")
continue
layer = Layer(self._next_id, name, kind)
self._next_id += 1
layer.alpha = max(0.0, min(1.0,
float(raw.get("alpha", 1.0))))