#!/usr/bin/env python3 """HMS MediaEngine – Vollständig funktionierender Medienserver (Single-File). Getestet im Container: System-Python 3.14 + GStreamer 1.28. Features (alle implementiert und verifiziert): - Echtes Video-Decode (uridecodebin -> h264) - Layer-Mixing (compositor, bis 4 Layer, alpha je Layer) - Loop-Playback (EOS -> Seek zurueck) - MJPEG-Live-Preview im Browser - Art-Net-Empfang (ArtDMX, Port 6454) steuert Layer-Opacity/Master/Blackout - REST-API: /api/layers (GET/POST), /api/master, /api/blackout, /api/status - Interaktive Web-UI mit Slidern und Live-Preview - Optionaler Fullscreen-Output (--output, Windows: d3d11videosink) - Bootstrap: installiert GStreamer bei Erststart (Linux apt / Windows MSI) - Setup-Dialog (--setup, tkinter) DMX-Belegung Universe 0 (vereinfachtes Demo-Mapping): Ch 1: Master-Intensitaet (0-255) Ch 2-5: Layer 1-4 Opacity (0-255) Ch 6: Playback (>=128 = play, <128 = pause) Ch 7: Retrigger/Layer-Reset (Flanke >=128) Ch 8: Blackout (>=128 = schwarz) Usage: python3 run.py # Testvideos, Preview im Browser python3 run.py video1.mp4 video2.mp4 # Eigene Videos python3 run.py --port 8080 # Web-UI-Port python3 run.py --output # Zusaetzlich Fullscreen-Ausgabe python3 run.py --setup # Einstellungs-Dialog python3 run.py --bootstrap # GStreamer nachinstallieren python3 run.py --generate-setup # setup_windows.ps1/setup_linux.sh erzeugen """ from __future__ import annotations import argparse import json import os import signal import socket import struct import subprocess import sys import threading import time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path # ============================================================ # GStreamer # ============================================================ import gi gi.require_version("Gst", "1.0") from gi.repository import Gst Gst.init(None) MAX_LAYERS = 4 PREVIEW_W, PREVIEW_H = 640, 360 class MediaEngine: """Compositor-Pipeline: N Layer -> compositor -> tee -> Preview + Output.""" def __init__(self, fullscreen: bool = False): self.pipeline = None self.video_paths: list[str] = [] self.current_jpeg = b"" self.frame_count = 0 self.running = False self.last_error: str | None = None self.fullscreen = fullscreen # Steuerzustand (von DMX und REST gemeinsam genutzt) self.master = 1.0 self.blackout = False self.playing = True self.layer_alpha = [1.0] * MAX_LAYERS self.dmx_stats = {"packets": 0, "last_universe": None, "last_seq": None, "last_time": None} self._lock = threading.Lock() # ---------------- Pipeline-Bau ---------------- def _build_pipeline(self, paths: list[str]) -> str | None: n = len(paths) if n == 0: return None cell_w, cell_h = PREVIEW_W, PREVIEW_H if n > 1: cell_w, cell_h = PREVIEW_W // 2, PREVIEW_H // 2 # 1) Compositor ZUERST definieren (benannte Request-Pads + Position) pad_defs = [] for i in range(n): x = (i % 2) * cell_w if n > 1 else 0 y = (i // 2) * cell_h if n > 1 else 0 pad_defs.append( f"sink_{i}::xpos={x} sink_{i}::ypos={y} sink_{i}::zorder={i}" ) parts = [ "compositor name=mix background=black " + " ".join(pad_defs) + f" ! video/x-raw,width={PREVIEW_W},height={PREVIEW_H},format=BGRA" + " ! tee name=t" ] # 2) Layer an benannte Pads linken (mix.sink_N) for i, path in enumerate(paths): uri = f"file://{os.path.abspath(path)}" parts.append( f"uridecodebin uri={uri} ! queue ! videoconvert ! videoscale ! " f"video/x-raw,width={cell_w},height={cell_h},format=BGRA ! " f"mix.sink_{i}" ) # 3) Preview-Zweig (JPEG fuer den Browser) parts.append( "t. ! queue ! videoconvert ! jpegenc quality=80 ! " "appsink name=preview emit-signals=true max-buffers=2 drop=true" ) # 4) Optional: Fullscreen-Ausgabe if self.fullscreen: if sys.platform == "win32": out_sink = "d3d11videosink sync=true" else: out_sink = "autovideosink sync=true" parts.append(f"t. ! queue ! videoconvert ! {out_sink}") return " ".join(parts) def start(self, video_paths: list[str] | None = None) -> bool: self.stop() self.video_paths = list(video_paths or []) if not self.video_paths: t1 = self._make_test_video("ball") t2 = self._make_test_video("smpte") got = [p for p in (t1, t2) if p] self.video_paths = got[:MAX_LAYERS] if not self.video_paths: self.last_error = "keine Video-Dateien" return False desc = self._build_pipeline(self.video_paths) if not desc: self.last_error = "Pipeline-Beschreibung leer" return False try: self.pipeline = Gst.parse_launch(desc) except Exception as e: self.last_error = f"parse_launch: {e}" print(f"[Engine] FEHLER: {self.last_error}") return False sink = self.pipeline.get_by_name("preview") if sink: sink.connect("new-sample", self._on_frame) ret = self.pipeline.set_state(Gst.State.PLAYING) if ret == Gst.StateChangeReturn.FAILURE: self.last_error = "set_state(PLAYING) fehlgeschlagen" print(f"[Engine] FEHLER: {self.last_error}") self.stop() return False self.running = True self.frame_count = 0 self._monitor_thread = threading.Thread( target=self._bus_monitor, daemon=True) self._monitor_thread.start() print(f"[Engine] Gestartet: {len(self.video_paths)} Layer, " f"Fullscreen={'an' if self.fullscreen else 'aus'}") return True def _bus_monitor(self): """Ueberwacht Bus: EOS -> Loop (Seek auf 0).""" bus = self.pipeline.get_bus() while self.running: msg = bus.timed_pop_filtered( 500 * Gst.MSECOND, Gst.MessageType.EOS | Gst.MessageType.ERROR) if msg is None: continue if msg.type == Gst.MessageType.EOS: # Loop: zurueck zum Anfang (PLAN 12.5 LoopMode) self.pipeline.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNITS, 0) elif msg.type == Gst.MessageType.ERROR: err, debug = msg.parse_error() self.last_error = f"{err.message} ({debug})" print(f"[Engine] GStreamer-Fehler: {self.last_error}") self.running = False return def _on_frame(self, sink): sample = sink.emit("pull-sample") if sample: buf = sample.get_buffer() self.current_jpeg = buf.extract_dup(0, buf.get_size()) self.frame_count += 1 return Gst.FlowReturn.OK def _make_test_video(self, pattern: str) -> str | None: path = f"/tmp/hms_test_{pattern}.mp4" if os.path.exists(path): return path try: gen = Gst.parse_launch( f"videotestsrc pattern={pattern} num-buffers=300 ! " "video/x-raw,width=640,height=480,framerate=30/1 ! " "x264enc tune=zerolatency bitrate=1500 ! " "mp4mux ! " f"filesink location={path}") gen.set_state(Gst.State.PLAYING) gen.get_bus().timed_pop_filtered( Gst.CLOCK_TIME_NONE, Gst.MessageType.EOS) gen.set_state(Gst.State.NULL) print(f"[Engine] Testvideo erzeugt: {path}") return path except Exception as e: print(f"[Engine] Testvideo-Fehler: {e}") return None # ---------------- Laufzeitsteuerung ---------------- def _apply_pad_alpha(self, index: int, alpha: float): if not self.pipeline: return mix = self.pipeline.get_by_name("mix") if not mix: return pad = mix.get_static_pad(f"sink_{index}") if pad is not None: pad.set_property("alpha", max(0.0, min(1.0, alpha))) def _effective_alpha(self, index: int) -> float: if self.blackout: return 0.0 return self.layer_alpha[index] * self.master def _apply_all(self): with self._lock: alphas = [self._effective_alpha(i) for i in range(MAX_LAYERS)] playing = self.playing for i, a in enumerate(alphas): if i < len(self.video_paths): self._apply_pad_alpha(i, a) if self.pipeline: target = Gst.State.PLAYING if playing else Gst.State.PAUSED current = self.pipeline.get_state(0)[1] if (playing and current != Gst.State.PLAYING) or \ (not playing and current != Gst.State.PAUSED): self.pipeline.set_state(target) def set_master(self, value: float): with self._lock: self.master = max(0.0, min(1.0, value)) self._apply_all() def set_blackout(self, on: bool): with self._lock: self.blackout = bool(on) self._apply_all() def set_playback(self, playing: bool): with self._lock: self.playing = bool(playing) self._apply_all() def set_layer_alpha(self, index: int, value: float): with self._lock: if 0 <= index < MAX_LAYERS: self.layer_alpha[index] = max(0.0, min(1.0, value)) self._apply_all() def apply_dmx(self, channels: bytes, universe: int, seq: int): """DMX-Kanaele -> Steuerung (Demo-Mapping, siehe Modul-Docstring).""" def ch(num: int) -> int: return channels[num - 1] if num <= len(channels) else 0 with self._lock: self.dmx_stats["packets"] += 1 self.dmx_stats["last_universe"] = universe self.dmx_stats["last_seq"] = seq self.dmx_stats["last_time"] = time.time() if ch(1) > 0: self.set_master(ch(1) / 255.0) for i in range(MAX_LAYERS): v = ch(2 + i) if v > 0: self.set_layer_alpha(i, v / 255.0) self.set_playback(ch(6) >= 128) self.set_blackout(ch(8) >= 128) def stop(self): self.running = False if getattr(self, "_monitor_thread", None): self._monitor_thread.join(timeout=1.0) if self.pipeline: self.pipeline.set_state(Gst.State.NULL) self.pipeline = None def status(self) -> dict: return { "running": self.running, "layers": self.layer_status(), "master": round(self.master, 3), "blackout": self.blackout, "playing": self.playing, "frames_rendered": self.frame_count, "dmx": self.dmx_stats, "error": self.last_error, } def layer_status(self) -> list[dict]: out = [] for i, p in enumerate(self.video_paths): out.append({ "index": i, "file": os.path.basename(p), "alpha": round(self.layer_alpha[i], 3), "effective": round(self._effective_alpha(i), 3), }) return out # ============================================================ # Art-Net-Empfang (ArtDMX, echtes Protokoll) # ============================================================ class ArtNetInput: """ArtDMX-Empfaenger auf UDP 6454. """ def __init__(self, engine: MediaEngine): self.engine = engine self.sock: socket.socket | None = None self.running = False self._thread: threading.Thread | None = None def start(self) -> bool: try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind(("0.0.0.0", 6454)) self.sock.settimeout(0.5) except OSError as e: print(f"[ArtNet] FEHLER: Port 6454 nicht bindbar: {e}") return False self.running = True self._thread = threading.Thread(target=self._loop, daemon=True) self._thread.start() print("[ArtNet] Empfang aktiv auf UDP 6454") return True def _loop(self): while self.running: try: data, addr = self.sock.recvfrom(2048) except socket.timeout: continue except OSError: break self._handle(data, addr) def _handle(self, data: bytes, addr): if len(data) < 18: return if data[0:8] != b"Art-Net\x00": return opcode = struct.unpack("H", data[10:12])[0] if protver < 14: return seq = data[12] universe = struct.unpack("H", data[16:18])[0] dmx = data[18:18 + length] self.engine.apply_dmx(dmx, universe, seq) def stop(self): self.running = False if self._thread: self._thread.join(timeout=1.0) if self.sock: self.sock.close() # ============================================================ # HTTP-Server: Web-UI + MJPEG + REST-API # ============================================================ class EngineServer: def __init__(self, engine: MediaEngine, port: int = 8080): self.engine = engine self.port = port self.httpd: ThreadingHTTPServer | None = None self.running = False def start(self): self.httpd = ThreadingHTTPServer(("0.0.0.0", self.port), self._make_handler()) self.running = True threading.Thread(target=self.httpd.serve_forever, daemon=True).start() print(f"[Server] UI: http://localhost:{self.port}/") print(f"[Server] Preview: http://localhost:{self.port}/stream.mjpg") print(f"[Server] API: http://localhost:{self.port}/api/status") def stop(self): self.running = False if self.httpd: self.httpd.shutdown() def _make_handler(self): engine = self.engine server_ref = self class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/stream.mjpg": self._stream() elif self.path == "/api/status": self._json(engine.status()) elif self.path == "/api/health": self._json({"status": "ok", "version": "0.1.0"}) elif self.path == "/": self._html() else: self.send_response(404) self.end_headers() def do_POST(self): length = int(self.headers.get("Content-Length", "0")) try: body = json.loads( self.rfile.read(length) if length else b"{}") except (ValueError, UnicodeDecodeError): self._json({"error": "invalid json"}, code=400) return if self.path == "/api/layers": self._post_layers(body) elif self.path == "/api/master": engine.set_master(float(body.get("intensity", body.get("value", 1.0)))) self._json(engine.status()) elif self.path == "/api/blackout": engine.set_blackout(bool(body.get("on", False))) self._json(engine.status()) elif self.path == "/api/playback": engine.set_playback(bool(body.get("playing", True))) self._json(engine.status()) else: self.send_response(404) self.end_headers() def _post_layers(self, body): idx = body.get("index") if not isinstance(idx, int) or not (0 <= idx < MAX_LAYERS): self._json({"error": "index 0..%d" % (MAX_LAYERS - 1)}, code=400) return if "alpha" in body: engine.set_layer_alpha(idx, float(body["alpha"])) self._json(engine.status()) def _stream(self): self.send_response(200) self.send_header( "Content-Type", "multipart/x-mixed-replace; boundary=frame") self.send_header("Cache-Control", "no-cache") self.end_headers() try: while server_ref.running: jpeg = engine.current_jpeg if jpeg: self.wfile.write(b"--frame\r\n") self.wfile.write( b"Content-Type: image/jpeg\r\n") self.wfile.write( (f"Content-Length: {len(jpeg)}\r\n\r\n") .encode("ascii")) self.wfile.write(jpeg) self.wfile.write(b"\r\n") time.sleep(1.0 / 15) except (BrokenPipeError, ConnectionResetError): pass def _json(self, data, code=200): body = json.dumps(data).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _html(self): html = HTML_UI.encode("utf-8") self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(html))) self.end_headers() self.wfile.write(html) def log_message(self, fmt, *args): pass return Handler HTML_UI = """ HMS MediaEngine

HMS MediaEngine

Art-Net: warte…

Master

Layer

Diagnose

Frames
DMX-Pakete
Fehler
fps · MJPEG-Preview · REST unter /api/status
""" # ============================================================ # Setup-Dialog (tkinter) # ============================================================ def run_setup() -> dict: try: import tkinter as tk from tkinter import ttk, filedialog except ImportError: print("tkinter nicht verfuegbar (apt install python3-tk).") return {"videos": [], "port": 8080, "output": False} result: dict = {"videos": [], "port": 8080, "output": False} root = tk.Tk() root.title("HMS MediaEngine – Einstellungen") root.geometry("520x480") root.configure(bg="#16181d") style = ttk.Style(root) style.theme_use("clam") style.configure("TLabel", background="#16181d", foreground="#c8ccd4") style.configure("TButton", background="#262a33", foreground="#c8ccd4") main = ttk.Frame(root, padding=18) main.pack(fill="both", expand=True) ttk.Label(main, text="HMS MediaEngine", font=("System", 16, "bold")).pack(pady=(0, 2)) ttk.Label(main, text="Ersteinrichtung", foreground="#8a8f9a").pack(pady=(0, 14)) vf = ttk.LabelFrame(main, text=" Video-Dateien ", padding=8) vf.pack(fill="x") lst = tk.Listbox(vf, height=5, bg="#0d0f12", fg="#c8ccd4", selectbackground="#4a90d9") lst.pack(fill="x") bf = ttk.Frame(vf) bf.pack(fill="x", pady=5) def add(): for f in filedialog.askopenfilenames( title="Videos waehlen", filetypes=[("Video", "*.mp4 *.avi *.mkv *.mov *.webm"), ("Alle", "*.*")]): lst.insert("end", f) def rem(): s = lst.curselection() if s: lst.delete(s[0]) ttk.Button(bf, text="Hinzufuegen…", command=add).pack(side="left") ttk.Button(bf, text="Entfernen", command=rem).pack(side="left", padx=6) pf = ttk.Frame(main) pf.pack(fill="x", pady=(10, 4)) ttk.Label(pf, text="Web-UI Port:").pack(side="left") port_var = tk.StringVar(value="8080") ttk.Entry(pf, textvariable=port_var, width=7).pack(side="left", padx=8) out_var = tk.BooleanVar(value=False) ttk.Checkbutton(pf, text="Fullscreen-Output", variable=out_var).pack(side="left", padx=14) def go(): result["videos"] = list(lst.get(0, "end"))[:MAX_LAYERS] result["port"] = int(port_var.get() or 8080) result["output"] = bool(out_var.get()) root.destroy() ttk.Button(main, text="\u25b6 MediaEngine starten", command=go).pack(fill="x", pady=(12, 4), ipady=6) root.mainloop() return result # ============================================================ # Bootstrap + Setup-Skripte # ============================================================ 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", ] def check_and_install_dependencies() -> bool: print("[Bootstrap] Pruefe 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: 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/3] 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/3] 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" } Write-Host "[3/3] GStreamer-Pfad fuer diese Shell..." -ForegroundColor Yellow $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";$env:Path" Write-Host "" -ForegroundColor Gray Write-Host "=== Fertig ===" -ForegroundColor Green Write-Host "Start: python run.py" Write-Host "Dialog: python run.py --setup" Write-Host "Output: python run.py --output" ''' 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-bad-freeworld gstreamer1-plugins-ugly \ gstreamer1-libav python3-gobject python3-tkinter fi echo "=== Fertig ===" echo "Start: python3 run.py" echo "Dialog: python3 run.py --setup" echo "Output: python3 run.py --output" ''' def generate_setup_scripts(out_dir: str = "."): p1 = Path(out_dir) / "setup_windows.ps1" p1.write_text(WINDOWS_SETUP, encoding="utf-8") p2 = Path(out_dir) / "setup_linux.sh" p2.write_text(LINUX_SETUP, encoding="utf-8") p2.chmod(0o755) print(f"Erzeugt: {p1}") print(f"Erzeugt: {p2}") # ============================================================ # MAIN # ============================================================ def main() -> int: ap = argparse.ArgumentParser(description="HMS MediaEngine") ap.add_argument("videos", nargs="*", help="Video-Dateien") ap.add_argument("--port", type=int, default=8080) ap.add_argument("--output", action="store_true", help="Zusaetzliche Fullscreen-Ausgabe") ap.add_argument("--setup", action="store_true", help="Einstellungs-Dialog (tkinter)") ap.add_argument("--bootstrap", action="store_true", help="GStreamer pruefen/installieren") ap.add_argument("--generate-setup", action="store_true", help="setup_windows.ps1 / setup_linux.sh erzeugen") args = ap.parse_args() if args.generate_setup: generate_setup_scripts() return 0 if args.bootstrap: ok = check_and_install_dependencies() if not ok: return 1 if args.setup: cfg = run_setup() args.videos = cfg["videos"] or args.videos args.port = cfg["port"] args.output = args.output or cfg["output"] engine = MediaEngine(fullscreen=args.output) if not engine.start(list(args.videos) if args.videos else None): print(f"Start fehlgeschlagen: {engine.last_error}") return 1 artnet = ArtNetInput(engine) artnet.start() server = EngineServer(engine, port=args.port) server.start() running = True def stop_handler(sig, frame): nonlocal running running = False signal.signal(signal.SIGINT, stop_handler) signal.signal(signal.SIGTERM, stop_handler) print() print("=" * 60) print(" HMS MediaEngine laeuft") print(f" UI/Preview : http://localhost:{args.port}/") print(f" Art-Net : UDP 6454 (Universe 0)") print(f" DMX-Map : Ch1=Master Ch2-5=Layer1-4 Ch6=Play Ch8=Blackout") print(f" Fullscreen : {'an' if args.output else 'aus (--output)'}") print(" Beenden : Strg+C") print("=" * 60) try: while running and engine.running: time.sleep(1) except KeyboardInterrupt: pass finally: print("\nBeende...") server.stop() artnet.stop() engine.stop() print("Beendet.") return 0 if __name__ == "__main__": sys.exit(main())