diff --git a/run.py b/run.py index 99b344a..f86b3da 100644 --- a/run.py +++ b/run.py @@ -1,29 +1,54 @@ #!/usr/bin/env python3 -"""HMS MediaEngine – Vollständig funktionierender Server. +"""HMS MediaEngine – Vollständig funktionierender Medienserver (Single-File). -Startet: GStreamer-Renderer (echtes Video) + MJPEG-Preview + Web-UI + REST-API. -Läuft mit System-Python (3.14) das GStreamer-Bindings hat. +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 # Standardstart mit Testvideo - python3 run.py video1.mp4 video2.mp4 # Eigene Videos - python3 run.py --port 8080 # Anderer Web-UI Port - python3 run.py --setup # Einstellungs-Dialog + 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, HTTPServer +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path # ============================================================ -# GStreamer Renderer (echtes Video-Playback) +# GStreamer # ============================================================ import gi @@ -32,113 +57,146 @@ from gi.repository import Gst Gst.init(None) +MAX_LAYERS = 4 +PREVIEW_W, PREVIEW_H = 640, 360 -class VideoRenderer: - """Spielt echte Videos ab und liefert JPEG-Frames für den Browser-Preview.""" - def __init__(self): +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.video_paths = [] self.frame_count = 0 self.running = False - self.last_error = None + self.last_error: str | None = None + self.fullscreen = fullscreen - def start(self, video_paths: list[str] | None = None): - """Startet Video-Playback mit Browser-Preview.""" - self.stop() - self.video_paths = video_paths or [] - self.frame_count = 0 + # 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() - if not self.video_paths: - # Testvideo generieren wenn keins gegeben - test = self._make_test_video() - if test: - self.video_paths = [test] + # ---------------- Pipeline-Bau ---------------- - if not self.video_paths: - # Fallback: Testpattern direkt streamen - pipeline_str = ( - "videotestsrc is-live=true pattern=0 ! " - "video/x-raw,width=640,height=360,framerate=15/1 ! " - "videoconvert ! jpegenc ! appsink name=preview emit-signals=true max-buffers=2 drop=true" - ) - elif len(self.video_paths) == 1: - pipeline_str = self._single_video_pipeline(self.video_paths[0]) - else: - pipeline_str = self._multi_video_pipeline(self.video_paths) + 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 - try: - self.pipeline = Gst.parse_launch(pipeline_str) - sink = self.pipeline.get_by_name("preview") - if sink: - sink.connect("new-sample", self._on_frame) - self.pipeline.set_state(Gst.State.PLAYING) - self.running = True - print(f"[Renderer] Gestartet: {len(self.video_paths)} Video(s)") - return True - except Exception as e: - self.last_error = str(e) - print(f"[Renderer] FEHLER: {e}") - return False - - def _single_video_pipeline(self, path: str) -> str: - uri = f"file://{os.path.abspath(path)}" - return ( - f"uridecodebin uri={uri} ! " - "queue ! videoconvert ! videoscale ! " - "video/x-raw,width=640,height=360 ! " - "jpegenc quality=80 ! " - "appsink name=preview emit-signals=true max-buffers=2 drop=true" - ) - - def _multi_video_pipeline(self, paths: list[str]) -> str: - """Mischt mehrere Videos in ein 2x2 Grid.""" - parts = [] - n = min(len(paths), 4) - w, h = 320, 180 + # 1) Compositor ZUERST definieren (benannte Request-Pads + Position) + pad_defs = [] for i in range(n): - uri = f"file://{os.path.abspath(paths[i])}" - x = (i % 2) * w - y = (i // 2) * h + 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={w},height={h} ! " - f"videomixer name=mix sink_{i}::xpos={x} sink_{i}::ypos={y} sink_{i}::zorder={i}" + 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( - f"mix. ! videoconvert ! videoscale ! " - f"video/x-raw,width=640,height=360 ! " - "jpegenc quality=80 ! " + "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 _make_test_video(self) -> str | None: - """Erstellt ein Testvideo mit animiertem Ball.""" - test_path = "/tmp/hms_test_video.mp4" - if os.path.exists(test_path): - return test_path + 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: - gen = Gst.parse_launch( - "videotestsrc pattern=ball num-buffers=900 ! " - "video/x-raw,width=640,height=480,framerate=30/1 ! " - "x264enc tune=zerolatency bitrate=2000 ! " - "mp4mux ! " - f"filesink location={test_path}" - ) - gen.set_state(Gst.State.PLAYING) - bus = gen.get_bus() - bus.timed_pop_filtered(Gst.CLOCK_TIME_NONE, Gst.MessageType.EOS) - gen.set_state(Gst.State.NULL) - print(f"[Renderer] Testvideo erstellt: {test_path}") - return test_path + self.pipeline = Gst.parse_launch(desc) except Exception as e: - print(f"[Renderer] Testvideo-Fehler: {e}") - return None + 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): - """Empfängt JPEG-Frames.""" sample = sink.emit("pull-sample") if sample: buf = sample.get_buffer() @@ -146,480 +204,621 @@ class VideoRenderer: 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 - self.running = False - def get_status(self) -> dict: + def status(self) -> dict: return { "running": self.running, - "videos": self.video_paths, + "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 + # ============================================================ -# Kombinierter HTTP-Server (Web-UI + MJPEG-Stream + REST-API) +# Art-Net-Empfang (ArtDMX, echtes Protokoll) # ============================================================ -class MediaEngineServer: - """HTTP-Server: liefert MJPEG-Stream + REST-API + einfache Web-UI.""" +class ArtNetInput: + """ArtDMX-Empfaenger auf UDP 6454. """ - def __init__(self, renderer: VideoRenderer, port=8080, webui_dir=None): - self.renderer = renderer - self.port = port - self.webui_dir = webui_dir - self.server = None + 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): - handler = self._make_handler() - self.server = HTTPServer(("0.0.0.0", self.port), handler) + 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 - thread = threading.Thread(target=self.server.serve_forever, daemon=True) - thread.start() - print(f"[Server] Web-UI: http://0.0.0.0:{self.port}") - print(f"[Server] Preview: http://0.0.0.0:{self.port}/stream.mjpg") - print(f"[Server] API: http://0.0.0.0:{self.port}/api/status") + 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.server: - self.server.shutdown() + 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): - renderer = self.renderer - webui_dir = self.webui_dir + engine = self.engine server_ref = self class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/stream.mjpg": - self._stream_mjpeg() + self._stream() elif self.path == "/api/status": - self._json_response(renderer.get_status()) + self._json(engine.status()) elif self.path == "/api/health": - self._json_response({"status": "ok", "version": "0.1.0"}) + self._json({"status": "ok", "version": "0.1.0"}) elif self.path == "/": - self._serve_ui() + self._html() else: self.send_response(404) self.end_headers() - def _stream_mjpeg(self): + 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( + "Content-Type", + "multipart/x-mixed-replace; boundary=frame") self.send_header("Cache-Control", "no-cache") self.end_headers() try: while server_ref.running: - if renderer.current_jpeg: + 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(renderer.current_jpeg)}\r\n\r\n".encode("ascii") - ) - self.wfile.write(renderer.current_jpeg) + 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_response(self, data): + def _json(self, data, code=200): body = json.dumps(data).encode() - self.send_response(200) + 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 _serve_ui(self): - # Einfache HTML-UI mit Live-Preview - html = b""" - - - HMS MediaEngine - - - -
-

HMS MediaEngine v0.1.0

- Renderer: LIVE -
-
-
- Live Preview -
-
- - - -""" + 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-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, format, *args): + 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, ohne Browser) +# Setup-Dialog (tkinter) # ============================================================ -def run_setup(): - """Öffnet einen nativen Einstellungs-Dialog.""" +def run_setup() -> dict: try: import tkinter as tk - from tkinter import ttk, filedialog, messagebox + from tkinter import ttk, filedialog except ImportError: - print("tkinter nicht verfügbar. Installiere: apt install python3-tk") - return + print("tkinter nicht verfuegbar (apt install python3-tk).") + return {"videos": [], "port": 8080, "output": False} - class SetupDialog: - def __init__(self): - self.root = tk.Tk() - self.root.title("HMS MediaEngine – Einstellungen") - self.root.geometry("480x520") - self.root.configure(bg="#16181d") - self.root.resizable(False, False) + result: dict = {"videos": [], "port": 8080, "output": False} - # Dark Theme - style = ttk.Style() - style.theme_use("clam") - style.configure("TLabel", background="#16181d", foreground="#c8ccd4", font=("System", 10)) - style.configure("TButton", background="#262a33", foreground="#c8ccd4") - style.configure("TEntry", fieldbackground="#0d0f12", foreground="#e8eaee") + 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") - self.build_ui() + 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)) - def build_ui(self): - main = ttk.Frame(self.root, padding=20) - main.pack(fill="both", expand=True) + 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) - # Titel - ttk.Label(main, text="HMS MediaEngine", font=("System", 18, "bold")).pack(pady=(0, 4)) - ttk.Label(main, text="Ersteinrichtung", foreground="#8a8f9a").pack(pady=(0, 20)) + def add(): + for f in filedialog.askopenfilenames( + title="Videos waehlen", + filetypes=[("Video", "*.mp4 *.avi *.mkv *.mov *.webm"), + ("Alle", "*.*")]): + lst.insert("end", f) - # Video-Auswahl - video_frame = ttk.LabelFrame(main, text=" Video-Dateien ", padding=10) - video_frame.pack(fill="x", pady=(0, 10)) - self.video_list = tk.Listbox(video_frame, height=4, bg="#0d0f12", fg="#c8ccd4", selectbackground="#4a90d9") - self.video_list.pack(fill="x") - btn_frame = ttk.Frame(video_frame) - btn_frame.pack(fill="x", pady=5) - ttk.Button(btn_frame, text="Video hinzufügen…", command=self.add_video).pack(side="left", padx=(0, 5)) - ttk.Button(btn_frame, text="Entfernen", command=self.remove_video).pack(side="left") + def rem(): + s = lst.curselection() + if s: + lst.delete(s[0]) - # Port - port_frame = ttk.Frame(main) - port_frame.pack(fill="x", pady=(0, 10)) - ttk.Label(port_frame, text="Web-UI Port:").pack(side="left") - self.port_var = tk.StringVar(value="8080") - ttk.Entry(port_frame, textvariable=self.port_var, width=8).pack(side="left", padx=8) + ttk.Button(bf, text="Hinzufuegen…", command=add).pack(side="left") + ttk.Button(bf, text="Entfernen", command=rem).pack(side="left", padx=6) - # Netzwerk - ttk.Label(main, text="Netzwerk-Modus:").pack(anchor="w") - self.network_var = tk.StringVar(value="local") - net_frame = ttk.Frame(main) - net_frame.pack(fill="x", pady=(0, 15)) - ttk.Radiobutton(net_frame, text="Nur dieser Rechner", variable=self.network_var, value="local").pack(side="left") - ttk.Radiobutton(net_frame, text="Show-LAN (alle Geräte)", variable=self.network_var, value="lan").pack(side="left", padx=10) + 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) - # Start-Button - self.start_btn = ttk.Button(main, text="▶ MediaEngine starten", command=self.start_engine) - self.start_btn.pack(fill="x", pady=(10, 5), ipady=6) + 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() - self.status_label = ttk.Label(main, text="", foreground="#3fa34d") - self.status_label.pack() - - def add_video(self): - files = filedialog.askopenfilenames( - title="Video-Dateien wählen", - filetypes=[("Video", "*.mp4 *.avi *.mkv *.mov *.webm"), ("Alle", "*.*")] - ) - for f in files: - self.video_list.insert("end", f) - - def remove_video(self): - sel = self.video_list.curselection() - if sel: - self.video_list.delete(sel[0]) - - def start_engine(self): - videos = list(self.video_list.get(0, "end")) - port = int(self.port_var.get() or "8080") - self.root.destroy() - self.result = {"videos": videos, "port": port} - - dialog = SetupDialog() - dialog.root.mainloop() - return getattr(dialog, "result", {"videos": [], "port": 8080}) + ttk.Button(main, text="\u25b6 MediaEngine starten", + command=go).pack(fill="x", pady=(12, 4), ipady=6) + root.mainloop() + return result # ============================================================ -# Bootstrap: Prüft und installiert Abhängigkeiten +# 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: - """Prüft ob GStreamer und Python verfügbar sind; installiert falls nötig.""" - print("[Bootstrap] Prüfe Abhängigkeiten...") - - # GStreamer prüfen + print("[Bootstrap] Pruefe GStreamer...") try: - import gi + import gi # noqa: F401 gi.require_version("Gst", "1.0") - from gi.repository import Gst - Gst.init(None) - print(f"[Bootstrap] GStreamer {Gst.version_string()} ✓") + from gi.repository import Gst as _Gst + _Gst.init(None) + print(f"[Bootstrap] GStreamer {_Gst.version_string()} OK") return True - except ImportError: + except (ImportError, ValueError): pass - # Installieren je nach OS if sys.platform == "linux": - print("[Bootstrap] Installiere GStreamer (sudo erforderlich)...") - 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", - ] - cmd = ["sudo", "apt-get", "install", "-y"] + pkgs + print("[Bootstrap] Installiere GStreamer (sudo apt-get)...") try: - subprocess.run(cmd, check=True, timeout=300) - print("[Bootstrap] GStreamer installiert ✓") - return True - except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): - print("[Bootstrap] MANUELLE INSTALLATION NÖTIG:") - print(f" sudo apt-get install {' '.join(pkgs)}") - return False + 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 - elif sys.platform == "win32": - print("[Bootstrap] Windows: GStreamer muss manuell installiert werden.") - print("[Bootstrap] Lade Installer herunter...") - url = "https://gstreamer.freedesktop.org/data/pkg/windows/1.28.4/msvc/gstreamer-1.0-msvc-x86_64-1.28.4.msi" + 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 - msi_path = Path.home() / "Downloads" / "gstreamer-1.0-msvc-x86_64-1.28.4.msi" - msi_path.parent.mkdir(exist_ok=True) - if not msi_path.exists(): - print(f"[Bootstrap] Lade {url}...") - urllib.request.urlretrieve(url, msi_path) - print(f"[Bootstrap] GStreamer-Installer: {msi_path}") - subprocess.run(["msiexec", "/i", str(msi_path), "/quiet"], check=False) - print("[Bootstrap] GStreamer-MSI gestartet. Bitte Installation abschließen.") - return False # Nach Installation neu starten + 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] Bitte manuell installieren: {url}") - return False + print(f"[Bootstrap] Manuell installieren: {url}") + return False return False -# ============================================================ -# Windows Setup-Skript Generator -# ============================================================ - - -def generate_windows_setup_script(output_dir: str = "."): - """Erstellt setup_windows.ps1 für die Windows-Installation.""" - script = r'''# HMS MediaEngine – Windows Setup -# Führt EINMALIG die komplette Installation durch. -# Danach startet hms-mediaengine.exe direkt. - +WINDOWS_SETUP = r'''# HMS MediaEngine - Windows Setup (einmalig ausfuehren) $ErrorActionPreference = "Stop" Write-Host "=== HMS MediaEngine Windows Setup ===" -ForegroundColor Cyan -# 1. Prüfe Admin-Rechte $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { - Write-Host "FEHLER: Als Administrator ausführen (Rechtsklick -> Als Administrator ausführen)" -ForegroundColor Red - exit 1 + Write-Host "Als Administrator ausfuehren (Rechtsklick -> Als Administrator ausfuehren)" -ForegroundColor Red + exit 1 } -# 2. Python 3.13 installieren (wenn nicht vorhanden) -Write-Host "[1/5] Prüfe Python..." -ForegroundColor Yellow -$pythonVersion = python --version 2>$null -if ($pythonVersion -notmatch "3.13") { - Write-Host " Installiere Python 3.13..." - $pythonUrl = "https://www.python.org/ftp/python/3.13.2/python-3.13.2-amd64.exe" - $pythonExe = "$env:TEMP\python-installer.exe" - Invoke-WebRequest -Uri $pythonUrl -OutFile $pythonExe - Start-Process -FilePath $pythonExe -ArgumentList "/quiet InstallAllUsers=1 PrependPath=1" -Wait - Write-Host " Python 3.13 installiert ✓" -} else { - Write-Host " Python bereits vorhanden ✓" -} - -# 3. GStreamer installieren -Write-Host "[2/5] Prüfe GStreamer..." -ForegroundColor Yellow +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) { - Write-Host " Installiere GStreamer 1.28.4 MSVC..." - $gstUrl = "https://gstreamer.freedesktop.org/data/pkg/windows/1.28.4/msvc/gstreamer-1.0-msvc-x86_64-1.28.4.msi" - $gstMsi = "$env:TEMP\gstreamer.msi" - Invoke-WebRequest -Uri $gstUrl -OutFile $gstMsi - Start-Process -FilePath "msiexec.exe" -ArgumentList "/i `"$gstMsi`" /quiet ADDLOCAL=ALL" -Wait - Write-Host " GStreamer installiert ✓" - # Umgebungsvariable aktualisieren - $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";$env:Path" -} else { - Write-Host " GStreamer bereits vorhanden ✓" -} + $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" } -# 4. uv (Python-Paketmanager) installieren -Write-Host "[3/5] Installiere uv..." -ForegroundColor Yellow -$uvInstalled = Get-Command uv -ErrorAction SilentlyContinue -if (-not $uvInstalled) { - irm https://astral.sh/uv/install.ps1 | iex - $env:Path = "$env:USERPROFILE\.local\bin;$env:Path" - Write-Host " uv installiert ✓" -} else { - Write-Host " uv 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" } -# 5. Projekt-Abhängigkeiten -Write-Host "[4/5] Installiere Projekt-Abhängigkeiten..." -ForegroundColor Yellow -$repoDir = Split-Path -Parent $MyInvocation.MyCommand.Path -Set-Location $repoDir -uv sync -Write-Host " Abhängigkeiten installiert ✓" +Write-Host "[3/3] GStreamer-Pfad fuer diese Shell..." -ForegroundColor Yellow +$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";$env:Path" -# 6. Frontend bauen -Write-Host "[5/5] Baue Web-Frontend..." -ForegroundColor Yellow -Push-Location apps\web -corepack enable pnpm 2>$null -pnpm install --frozen-lockfile 2>$null -pnpm build -Pop-Location -Write-Host " Frontend gebaut ✓" - -Write-Host "" -Write-Host "=== Installation abgeschlossen ===" -ForegroundColor Green -Write-Host "" -Write-Host "Starten mit:" -Write-Host " .\run.py" -Write-Host "" -Write-Host "Oder Einstellungen öffnen:" -Write-Host " .\run.py --setup" -Write-Host "" +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" ''' - path = Path(output_dir) / "setup_windows.ps1" - path.write_text(script, encoding="utf-8") - print(f"Windows-Setup-Skript: {path}") - -# ============================================================ -# Linux Setup-Skript Generator -# ============================================================ - - -def generate_linux_setup_script(output_dir: str = "."): - """Erstellt setup_linux.sh für die Linux-Installation.""" - script = r'''#!/bin/bash -# HMS MediaEngine – Linux Setup +LINUX_SETUP = r'''#!/bin/bash +# HMS MediaEngine - Linux Setup (einmalig ausfuehren) set -e - echo "=== HMS MediaEngine Linux Setup ===" - -# 1. System-Pakete -echo "[1/4] Installiere System-Pakete..." -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-* python3-gstreamer python3-tkinter +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 " System-Pakete installiert ✓" - -# 2. uv installieren -echo "[2/4] Installiere uv..." -if ! command -v uv &>/dev/null; then - curl -LsSf https://astral.sh/uv/install.sh | sh - export PATH="$HOME/.local/bin:$PATH" -fi -echo " uv ✓" - -# 3. Projekt-Abhängigkeiten -echo "[3/4] Installiere Python-Abhängigkeiten..." -REPO_DIR="$(dirname "$(realpath "$0")")" -cd "$REPO_DIR" -uv sync -echo " Abhängigkeiten ✓" - -# 4. Frontend -echo "[4/4] Baue Frontend..." -if command -v pnpm &>/dev/null || command -v npx &>/dev/null; then - cd apps/web - (pnpm install --frozen-lockfile 2>/dev/null || npx pnpm install) && \ - (pnpm build 2>/dev/null || npx pnpm build) - cd ../.. - echo " Frontend ✓" -else - echo " WARNUNG: pnpm nicht gefunden. Frontend wird nicht gebaut." - echo " Installiere mit: npm install -g pnpm" -fi - -echo "" -echo "=== Installation abgeschlossen ===" -echo "Starten: ./run.py" -echo "Einstellungen: ./run.py --setup" -echo "" +echo "=== Fertig ===" +echo "Start: python3 run.py" +echo "Dialog: python3 run.py --setup" +echo "Output: python3 run.py --output" ''' - path = Path(output_dir) / "setup_linux.sh" - path.write_text(script, encoding="utf-8") - path.chmod(0o755) - print(f"Linux-Setup-Skript: {path}") + + +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}") # ============================================================ @@ -627,78 +826,76 @@ echo "" # ============================================================ -def main(): - parser = argparse.ArgumentParser(description="HMS MediaEngine") - parser.add_argument("videos", nargs="*", help="Video-Dateien zum Abspielen") - parser.add_argument("--port", type=int, default=8080, help="Web-UI Port (default: 8080)") - parser.add_argument("--setup", action="store_true", help="Einstellungs-Dialog öffnen") - parser.add_argument("--bootstrap", action="store_true", help="Abhängigkeiten prüfen/installieren") - parser.add_argument("--generate-setup", action="store_true", help="Setup-Skripte für Windows/Linux generieren") - args = parser.parse_args() +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_windows_setup_script() - generate_linux_setup_script() - print("Setup-Skripte generiert.") + generate_setup_scripts() return 0 if args.bootstrap: - if not check_and_install_dependencies(): - print("\nAbhängigkeiten nicht vollständig. Setup-Skript ausführen:") - if sys.platform == "win32": - print(" .\\setup_windows.ps1") - else: - print(" ./setup_linux.sh") + ok = check_and_install_dependencies() + if not ok: return 1 if args.setup: - config = run_setup() - args.videos = config.get("videos", args.videos) - args.port = config.get("port", args.port) + cfg = run_setup() + args.videos = cfg["videos"] or args.videos + args.port = cfg["port"] + args.output = args.output or cfg["output"] - # Renderer starten - renderer = VideoRenderer() - video_paths = args.videos if args.videos else None - if not renderer.start(video_paths): - print("Renderer konnte nicht gestartet werden.") + 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 - # HTTP-Server starten - server = MediaEngineServer(renderer, port=args.port) + artnet = ArtNetInput(engine) + artnet.start() + + server = EngineServer(engine, port=args.port) server.start() - # Signal-Handler running = True - def signal_handler(sig, frame): + + def stop_handler(sig, frame): nonlocal running running = False - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, stop_handler) + signal.signal(signal.SIGTERM, stop_handler) - print("\n" + "=" * 60) - print(" HMS MediaEngine läuft!") - print(f" Web-UI: http://localhost:{args.port}") - print(f" Preview: http://localhost:{args.port}/stream.mjpg") - print(f" API: http://localhost:{args.port}/api/status") - print(f" Videos: {len(renderer.video_paths)} aktiv") - print(f" Frames: {renderer.frame_count}") - print(" Strg+C zum Beenden") - print("=" * 60 + "\n") + 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: + while running and engine.running: time.sleep(1) - if renderer.frame_count % 150 == 0 and renderer.frame_count > 0: - pass # Status-Update alle 10 Sekunden bei 15fps except KeyboardInterrupt: pass finally: print("\nBeende...") - renderer.stop() server.stop() + artnet.stop() + engine.stop() print("Beendet.") - return 0 diff --git a/setup_linux.sh b/setup_linux.sh new file mode 100755 index 0000000..323fecd --- /dev/null +++ b/setup_linux.sh @@ -0,0 +1,18 @@ +#!/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" diff --git a/setup_windows.ps1 b/setup_windows.ps1 new file mode 100644 index 0000000..a864361 --- /dev/null +++ b/setup_windows.ps1 @@ -0,0 +1,38 @@ +# 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"