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:
+83
-11
@@ -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))))
|
||||
|
||||
Reference in New Issue
Block a user