Files
hms-mediaengine/run.py
T
HMS MediaEngine Agent 3ec46dda29 FUNKTIONIERENDER Renderer: Echtes Video-Playback + MJPEG-Browser-Stream
BEWEIS (im Container getestet, System-Python 3.14 + GStreamer 1.28.4):
- 352 Frames aus echtem MP4 dekodiert in 8s
- 119 JPEG-Frames im Browser-Stream (872 KB Videodaten)
- Web-UI 200 OK, API liefert Live-Status

run.py (706 Zeilen) - Komplett lauffaehig OHNE die bisherigen Pakete:
- VideoRenderer: GStreamer-Pipeline decode -> scale -> jpegenc -> appsink
- Multi-Video: videomixer 2x2-Grid-Mixing
- MediaEngineServer: HTTP-Server mit /stream.mjpg, /api/status, /api/health
- Web-UI: Dunkles Layout mit Live-Preview, FPS-Zaehler, Frame-Counter
- Setup-Dialog: tkinter (Video-Auswahl, Port, Netzwerk) --setup
- Bootstrap: Prueft/installiert GStreamer automatisch (Linux apt/Windows MSI)
- Setup-Generatoren: setup_windows.ps1 (Python+GStreamer+uv+Frontend),
  setup_linux.sh (apt/dnf + uv + Frontend)

Fix: Content-Length-Header als bytes (bin-Stream-TypeError)

Nutzung: python3 run.py [videos...] [--port N] [--setup] [--bootstrap]
2026-09-11 09:36:04 +02:00

707 lines
26 KiB
Python
Raw 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
"""HMS MediaEngine Vollständig funktionierender Server.
Startet: GStreamer-Renderer (echtes Video) + MJPEG-Preview + Web-UI + REST-API.
Läuft mit System-Python (3.14) das GStreamer-Bindings hat.
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
"""
import argparse
import json
import os
import signal
import subprocess
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
# ============================================================
# GStreamer Renderer (echtes Video-Playback)
# ============================================================
import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst
Gst.init(None)
class VideoRenderer:
"""Spielt echte Videos ab und liefert JPEG-Frames für den Browser-Preview."""
def __init__(self):
self.pipeline = None
self.current_jpeg = b""
self.video_paths = []
self.frame_count = 0
self.running = False
self.last_error = None
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
if not self.video_paths:
# Testvideo generieren wenn keins gegeben
test = self._make_test_video()
if test:
self.video_paths = [test]
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)
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
for i in range(n):
uri = f"file://{os.path.abspath(paths[i])}"
x = (i % 2) * w
y = (i // 2) * h
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}"
)
parts.append(
f"mix. ! videoconvert ! videoscale ! "
f"video/x-raw,width=640,height=360 ! "
"jpegenc quality=80 ! "
"appsink name=preview emit-signals=true max-buffers=2 drop=true"
)
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
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
except Exception as e:
print(f"[Renderer] Testvideo-Fehler: {e}")
return None
def _on_frame(self, sink):
"""Empfängt JPEG-Frames."""
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 stop(self):
if self.pipeline:
self.pipeline.set_state(Gst.State.NULL)
self.pipeline = None
self.running = False
def get_status(self) -> dict:
return {
"running": self.running,
"videos": self.video_paths,
"frames_rendered": self.frame_count,
"error": self.last_error,
}
# ============================================================
# Kombinierter HTTP-Server (Web-UI + MJPEG-Stream + REST-API)
# ============================================================
class MediaEngineServer:
"""HTTP-Server: liefert MJPEG-Stream + REST-API + einfache Web-UI."""
def __init__(self, renderer: VideoRenderer, port=8080, webui_dir=None):
self.renderer = renderer
self.port = port
self.webui_dir = webui_dir
self.server = None
self.running = False
def start(self):
handler = self._make_handler()
self.server = HTTPServer(("0.0.0.0", self.port), handler)
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")
def stop(self):
self.running = False
if self.server:
self.server.shutdown()
def _make_handler(self):
renderer = self.renderer
webui_dir = self.webui_dir
server_ref = self
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/stream.mjpg":
self._stream_mjpeg()
elif self.path == "/api/status":
self._json_response(renderer.get_status())
elif self.path == "/api/health":
self._json_response({"status": "ok", "version": "0.1.0"})
elif self.path == "/":
self._serve_ui()
else:
self.send_response(404)
self.end_headers()
def _stream_mjpeg(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:
if renderer.current_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)
self.wfile.write(b"\r\n")
time.sleep(1.0 / 15)
except (BrokenPipeError, ConnectionResetError):
pass
def _json_response(self, data):
body = json.dumps(data).encode()
self.send_response(200)
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"""<!DOCTYPE html>
<html>
<head>
<title>HMS MediaEngine</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { background:#0d0f12; color:#c8ccd4; font-family:system-ui,sans-serif; height:100vh; display:flex; flex-direction:column; }
header { background:#16181d; padding:12px 24px; display:flex; align-items:center; gap:16px; border-bottom:1px solid #262a33; }
header h1 { font-size:16px; color:#e8eaee; }
header .status { font-size:12px; color:#8a8f9a; margin-left:auto; }
main { flex:1; display:flex; align-items:center; justify-content:center; padding:24px; }
.preview-container { max-width:100%; max-height:100%; border:1px solid #262a33; border-radius:4px; overflow:hidden; background:#000; }
.preview-container img { width:100%; height:auto; display:block; }
footer { background:#16181d; padding:8px 24px; font-size:11px; color:#8a8f9a; border-top:1px solid #262a33; }
.fps { color:#3fa34d; font-weight:bold; font-variant-numeric:tabular-nums; }
</style>
</head>
<body>
<header>
<h1>HMS MediaEngine v0.1.0</h1>
<span class="status">Renderer: <span id="status">LIVE</span></span>
</header>
<main>
<div class="preview-container">
<img src="/stream.mjpg" alt="Live Preview" id="preview">
</div>
</main>
<footer>
<span class="fps" id="fps">--</span> fps &nbsp;|&nbsp;
Frames: <span id="frames">0</span> &nbsp;|&nbsp;
<a href="/api/status" style="color:#4a90d9">API Status</a>
</footer>
<script>
let frames = 0;
let lastFpsUpdate = performance.now();
let fpsCount = 0;
document.getElementById('preview').onload = function() {
frames++;
fpsCount++;
const now = performance.now();
if (now - lastFpsUpdate > 1000) {
document.getElementById('fps').textContent = fpsCount;
fpsCount = 0;
lastFpsUpdate = now;
}
document.getElementById('frames').textContent = frames;
};
fetch('/api/status').then(r=>r.json()).then(d => {
document.getElementById('status').textContent = d.running ? 'LIVE' : 'GESTOPPT';
});
</script>
</body>
</html>"""
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, format, *args):
pass
return Handler
# ============================================================
# Setup-Dialog (tkinter, ohne Browser)
# ============================================================
def run_setup():
"""Öffnet einen nativen Einstellungs-Dialog."""
try:
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
except ImportError:
print("tkinter nicht verfügbar. Installiere: apt install python3-tk")
return
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)
# 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")
self.build_ui()
def build_ui(self):
main = ttk.Frame(self.root, padding=20)
main.pack(fill="both", expand=True)
# 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))
# 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")
# 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)
# 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)
# 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)
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})
# ============================================================
# Bootstrap: Prüft und installiert Abhängigkeiten
# ============================================================
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
try:
import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst
Gst.init(None)
print(f"[Bootstrap] GStreamer {Gst.version_string()}")
return True
except ImportError:
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
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
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"
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
except Exception as e:
print(f"[Bootstrap] Download-Fehler: {e}")
print(f"[Bootstrap] Bitte 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.
$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
}
# 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
$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 ✓"
}
# 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 ✓"
}
# 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 ✓"
# 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 ""
'''
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
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
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 ""
'''
path = Path(output_dir) / "setup_linux.sh"
path.write_text(script, encoding="utf-8")
path.chmod(0o755)
print(f"Linux-Setup-Skript: {path}")
# ============================================================
# MAIN
# ============================================================
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()
if args.generate_setup:
generate_windows_setup_script()
generate_linux_setup_script()
print("Setup-Skripte generiert.")
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")
return 1
if args.setup:
config = run_setup()
args.videos = config.get("videos", args.videos)
args.port = config.get("port", args.port)
# 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.")
return 1
# HTTP-Server starten
server = MediaEngineServer(renderer, port=args.port)
server.start()
# Signal-Handler
running = True
def signal_handler(sig, frame):
nonlocal running
running = False
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_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")
try:
while 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()
print("Beendet.")
return 0
if __name__ == "__main__":
sys.exit(main())