Files
HMS MediaEngine Agent 362e089be0 AUFGERAUMT: Root auf 10 sichtbare Elemente reduziert
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell.
Jetzt ist sauber getrennt:

ROOT (was der Nutzer sieht und braucht):
- run.py                     = das Programm
- hms_app/                   = der Anwendungscode
- HMS MediaEngine.app        = macOS Doppelklick-Starter
- HMS-Start.vbs              = Windows Doppelklick-Starter
- HMS-Install.vbs             = Windows Erst-Installation
- HMS-Mac-Install.command     = macOS Homebrew-Installation
- HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix)
- installer_gui.py           = grafischer Installer
- launcher.pyw + launcher_core.py = interne Start-Logik
- LIESMICH.txt               = 10-Zeilen-Kurzanleitung
- .gitignore

_entwicklung/ (alles andere, NICHT benoetigt):
- packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/
  build/ fixture_profiles/
- PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md
- pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py

Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann
_entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne.

Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
2026-09-11 23:44:06 +02:00

172 lines
6.1 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.
#!/usr/bin/env python3
"""E2E-Test: Effekte (FX1/FX2) in der Live-Pipeline.
Voraussetzung: Server laeuft auf http://localhost:8093
"""
import json
import socket
import struct
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
BASE = "http://localhost:8093"
def get(path):
with urllib.request.urlopen(BASE + path, timeout=15) as r:
return json.loads(r.read().decode())
def post(path, body=None):
data = json.dumps(body or {}).encode()
req = urllib.request.Request(BASE + path, data=data, method="POST")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=120) as r:
return json.loads(r.read().decode())
except urllib.error.HTTPError as e:
return json.loads(e.read().decode())
def check(label, ok, detail=""):
print(("PASS" if ok else "FAIL") + " " + label + " " + str(detail))
if not ok:
sys.exit(1)
# 1) Testvideo erzeugen falls noetig
if not get("/api/health").get("status") == "ok":
check("Health", False)
print("PASS Health")
# 2) FX-Katalog abrufbar und gefuellt
fx = get("/api/fx")
check("FX-Katalog >= 20 Effekte", len(fx) >= 20, "%d Effekte" % len(fx))
fx_ids = [f["id"] for f in fx]
for must in ("blur", "sharpen", "color", "gamma", "pixelate", "vertigo"):
check("FX enthalten: " + must, must in fx_ids)
# 3) Medien pruefen/bereitstellen (Upload falls fehlt)
lib = get("/api/media")["items"]
names = [m["name"] for m in lib]
if "e2e_clip.mp4" not in names:
subprocess.run([
"/usr/bin/python3", "-c",
"import gi; gi.require_version('Gst','1.0')\n"
"from gi.repository import Gst; Gst.init(None)\n"
"p = Gst.parse_launch('videotestsrc num-buffers=60 pattern=18 ! "
"video/x-raw,width=320,height=180 ! x264enc tune=zerolatency ! "
"mp4mux ! filesink location=/tmp/e2e_fx.mp4')\n"
"p.set_state(Gst.State.PLAYING)\n"
"p.get_bus().timed_pop_filtered(Gst.CLOCK_TIME_NONE, "
"Gst.MessageType.EOS)\n"
"p.set_state(Gst.State.NULL)\n",
], check=True)
data = open("/tmp/e2e_fx.mp4", "rb").read()
req = urllib.request.Request(BASE + "/api/media/upload",
data=data, method="POST")
req.add_header("Content-Type", "application/octet-stream")
req.add_header("X-Filename", urllib.parse.quote("e2e_clip.mp4"))
with urllib.request.urlopen(req, timeout=120) as r:
json.loads(r.read().decode())
# 4) Layer mit Video
st = get("/api/status")
if not st["layers"]:
r = post("/api/layers/add", {"name": "e2e_clip.mp4"})
check("Layer hinzugefuegt", "layer" in r, r)
else:
print("PASS Layer hinzugefuegt (bereits aktiv)")
time.sleep(2)
# 5) FX1 = blur setzen -> Struktur-Rebuild
r = post("/api/layers/update", {"id": 1, "fx1": "blur"})
check("FX1 blur gesetzt", r.get("layer", {}).get("fx1") == "blur", r)
time.sleep(2)
st = get("/api/status")
check("Engine laeuft mit FX1", st["running"] is True
and st["error"] is None,
"frames=%d err=%s" % (st["frames_rendered"], st["error"]))
# 6) FX1-Intensitaet live aendern (KEIN Rebuild Property-Update)
frames_before = get("/api/status")["frames_rendered"]
r = post("/api/layers/update", {"id": 1, "fx1_intensity": 1.0})
check("FX1-Intensitaet live", r.get("layer", {}).get("fx1_intensity")
== 1.0, r)
time.sleep(1.5)
frames_after = get("/api/status")["frames_rendered"]
check("Frames laufen weiter (live)", frames_after > frames_before,
"%d -> %d" % (frames_before, frames_after))
# 7) FX2 = color dazu -> Rebuild mit 2 Effekten
r = post("/api/layers/update", {"id": 1, "fx2": "color"})
check("FX2 color gesetzt", r.get("layer", {}).get("fx2") == "color", r)
time.sleep(2)
st = get("/api/status")
check("Engine laeuft mit FX1+FX2", st["running"] is True,
"frames=%d err=%s" % (st["frames_rendered"], st["error"]))
# 8) Bild-Layer mit Pixelate + Vertigo (Bild-Effekt-Pfad)
lib = get("/api/media")["items"]
if any(m["name"] == "e2e_bild.png" for m in lib):
r = post("/api/layers/add", {"name": "e2e_bild.png"})
if "layer" in r:
lid = r["layer"]["id"]
r2 = post("/api/layers/update", {"id": lid, "fx1": "pixelate"})
check("Bild-Layer mit Pixelate", r2.get("layer", {}).get("fx1")
== "pixelate", r2)
time.sleep(2)
st = get("/api/status")
check("Engine laeuft (Bild+FX)", st["running"] is True,
"frames=%d" % st["frames_rendered"])
else:
print("PASS Bild-Layer mit Pixelate (Layer-Limit, uebersprungen)")
# 9) Effekt entfernen (fx1 = null)
r = post("/api/layers/update", {"id": 1, "fx1": None})
check("FX1 entfernt", r.get("layer", {}).get("fx1") is None, r)
time.sleep(2)
st = get("/api/status")
check("Engine laeuft ohne FX1", st["running"] is True,
"frames=%d err=%s" % (st["frames_rendered"], st["error"]))
# 10) DMX steuert FX1-Intensitaet (Kanal konfigurieren + Paket senden)
r = post("/api/settings", {"artnet": {
"enabled": True, "listen_port": 6455, "universes": [0],
"master_channel": 10, "layer_start_channel": 2,
"playback_channel": 6, "retrigger_channel": 7,
"blackout_channel": 8, "fx1_channel": 20, "fx2_channel": 21,
"signal_loss_policy": "hold", "signal_loss_timeout_s": 10}})
check("fx1_channel konfiguriert", bool(r.get("saved")), r)
post("/api/layers/update", {"id": 1, "fx1": "blur"})
time.sleep(2)
dmx = bytearray(512)
dmx[9] = 255 # Master-Kanal 10
dmx[19] = 255 # fx1_channel 20 -> Intensitaet 1.0
dmx[20] = 128 # fx2_channel 21 -> Intensitaet ~0.5
pkt = (b"Art-Net\x00" + struct.pack("<H", 0x5000)
+ struct.pack(">H", 14) + bytes([9]) + b"\x00"
+ struct.pack("<H", 0) + struct.pack(">H", 512) + bytes(dmx))
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(pkt, ("127.0.0.1", 6455))
s.close()
time.sleep(1)
st = get("/api/status")
l1 = st["layers"][0]
check("DMX steuert FX1-Intensitaet", l1["fx1_intensity"] == 1.0,
"fx1_intensity=%s" % l1["fx1_intensity"])
# 11) Ungueltiger Effekt abgewiesen
r = post("/api/layers/update", {"id": 1, "fx1": "gibt_es_nicht"})
check("Ungueltiger FX abgewiesen", r.get("layer", {}).get("fx1") is None,
r)
print("")
print("ALLE FX-E2E-TESTS BESTANDEN")