diff --git a/HMS MediaEngine.app/Contents/Info.plist b/HMS MediaEngine.app/Contents/Info.plist
new file mode 100644
index 0000000..bdcbe60
--- /dev/null
+++ b/HMS MediaEngine.app/Contents/Info.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ CFBundleNameHMS MediaEngine
+ CFBundleDisplayNameHMS MediaEngine
+ CFBundleIdentifierde.hms.mediaengine
+ CFBundleVersion0.1.0
+ CFBundleShortVersionString0.1.0
+ CFBundlePackageTypeAPPL
+ CFBundleExecutableHMS-Launcher
+ LSMinimumSystemVersion11.0
+ NSHighResolutionCapable
+ CFBundleInfoDictionaryVersion6.0
+
+
diff --git a/HMS MediaEngine.app/Contents/MacOS/HMS-Launcher b/HMS MediaEngine.app/Contents/MacOS/HMS-Launcher
new file mode 100755
index 0000000..f393f05
--- /dev/null
+++ b/HMS MediaEngine.app/Contents/MacOS/HMS-Launcher
@@ -0,0 +1,26 @@
+#!/bin/bash
+# HMS MediaEngine App-Start (vom Finder, ohne sichtbares Terminal)
+DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
+PY=""
+for p in /opt/homebrew/bin/python3 /usr/local/bin/python3 python3; do
+ if command -v "$p" >/dev/null 2>&1; then PY="$p"; break; fi
+done
+if [ -z "$PY" ]; then
+ osascript -e 'display alert "Python nicht gefunden" message "Bitte HMS-Mac-Install.command im Projektordner einmal ausfuehren - es installiert alles Grafische." as critical' >/dev/null 2>&1
+ exit 1
+fi
+cd "$DIR"
+if "$PY" -c "import gi; gi.require_version('Gst','1.0')" >/dev/null 2>&1; then
+ nohup "$PY" "$DIR/run.py" >>/tmp/hms-mediaengine.log 2>&1 &
+ PORT=8080
+ for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \
+ 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40; do
+ curl -s -o /dev/null "http://localhost:$PORT/api/health" && break
+ sleep 0.5
+ done
+ open "http://localhost:$PORT"
+ exit 0
+else
+ nohup "$PY" "$DIR/installer_gui.py" >/tmp/hms-installer.log 2>&1 &
+ exit 0
+fi
diff --git a/HMS-Install.vbs b/HMS-Install.vbs
new file mode 100644
index 0000000..164bb66
--- /dev/null
+++ b/HMS-Install.vbs
@@ -0,0 +1,33 @@
+' HMS MediaEngine - Erstinstallation (Windows, Doppelklick)
+' Oeffnet den grafischen Installer (Fenster, kein Tippen noetig).
+Option Explicit
+Dim fso, sh, py, cand, dir_, candidates
+Set fso = CreateObject("Scripting.FileSystemObject")
+Set sh = CreateObject("WScript.Shell")
+dir_ = fso.GetParentFolderName(WScript.ScriptFullName)
+
+py = ""
+On Error Resume Next
+candidates = Array( _
+ sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\Programs\Python\Python313\pythonw.exe", _
+ sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\Programs\Python\Python312\pythonw.exe", _
+ "C:\Python313\pythonw.exe", _
+ "C:\Python312\pythonw.exe")
+For Each cand In candidates
+ If py = "" Then
+ If fso.FileExists(cand) Then py = cand
+ End If
+Next
+On Error GoTo 0
+
+If py = "" Then
+ MsgBox "Python fehlt noch." & vbCrLf & vbCrLf & _
+ "Der Browser oeffnet jetzt die offizielle Python-Downloadseite." & vbCrLf & _
+ "Nach der Python-Installation (Haeckchen bei 'Add python.exe to PATH'!)" & vbCrLf & _
+ "bitte HMS-Install.vbs erneut doppelklicken.", 64, "HMS MediaEngine"
+ sh.Run "cmd /c start https://www.python.org/downloads/", 0, False
+ WScript.Quit 1
+End If
+
+sh.CurrentDirectory = dir_
+sh.Run Chr(34) & py & Chr(34) & " " & Chr(34) & dir_ & "\installer_gui.py" & Chr(34), 1, False
diff --git a/HMS-Mac-Install.command b/HMS-Mac-Install.command
new file mode 100755
index 0000000..42855c8
--- /dev/null
+++ b/HMS-Mac-Install.command
@@ -0,0 +1,50 @@
+#!/bin/bash
+# HMS MediaEngine – macOS-Installation per Doppelklick (Finder)
+# Ablauf: brew pruefen/installieren -> python3+tk pruefen ->
+# GUI-Installer starten (Passwort-Dialog kommt von macOS selbst).
+# Falls python3/tkinter fehlt: Fallback-Dialoge nativ per osascript.
+
+cd "$(dirname "$0")"
+
+echo "HMS MediaEngine – Installation"
+
+eingabe() {
+ osascript -e 'display alert "'$1'" message "'$2'" buttons {"Abbrechen","Weiter"} default button "Weiter" as informational' >/dev/null 2>&1
+ [ $? -eq 0 ]
+}
+
+# --- Homebrew ---
+BREW=""
+for p in /opt/homebrew/bin/brew /usr/local/bin/brew; do
+ if [ -x "$p" ]; then BREW="$p"; break; fi
+done
+if [ -z "$BREW" ]; then
+ if eingabe "Homebrew installieren?" "HMS MediaEngine benötigt Homebrew (der Paketmanager für macOS). Es wird jetzt installiert – danach geht es automatisch weiter."; then
+ NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+ BREW=/opt/homebrew/bin/brew
+ else
+ exit 1
+ fi
+fi
+
+# --- Python 3.12 + tkinter ---
+"$BREW" list python-tk >/dev/null 2>&1 || {
+ if eingabe "Python-Komponenten installieren?" "Es wird python@3.12 mit tkinter installiert (für die grafische Oberfläche)."; then
+ "$BREW" install python@3.12 python-tk
+ fi
+}
+
+# --- GStreamer ---
+"$BREW" list gstreamer >/dev/null 2>&1 || {
+ if eingabe "GStreamer installieren?" "Es werden GStreamer und alle Video-/Audio-Plugins installiert (ca. 5–10 Minuten)."; then
+ "$BREW" install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav pygobject
+ fi
+}
+
+# --- GUI-Installer starten ---
+PY=/opt/homebrew/bin/python3
+if [ ! -x "$PY" ]; then PY=/usr/local/bin/python3; fi
+
+echo ""
+echo "Starte grafischen Installer (Fenster öffnet sich)..."
+"$PY" installer_gui.py
diff --git a/HMS-Start.vbs b/HMS-Start.vbs
new file mode 100644
index 0000000..c50191f
--- /dev/null
+++ b/HMS-Start.vbs
@@ -0,0 +1,51 @@
+' HMS MediaEngine - Windows-Starter (unsichtbar, Doppelklick)
+' Startet die Engine im Hintergrund (nur Browser sichtbar).
+' Fehlen Abhaengigkeiten, oeffnet sich der grafische Installer.
+Option Explicit
+Dim fso, sh, py, cand, dir_, tmpFile, ts, out
+Dim candidates(3)
+Set fso = CreateObject("Scripting.FileSystemObject")
+Set sh = CreateObject("WScript.Shell")
+dir_ = fso.GetParentFolderName(WScript.ScriptFullName)
+
+py = ""
+candidates(0) = sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\Programs\Python\Python313\pythonw.exe"
+candidates(1) = sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\Programs\Python\Python312\pythonw.exe"
+candidates(2) = "C:\Python313\pythonw.exe"
+candidates(3) = "C:\Python312\pythonw.exe"
+For Each cand In candidates
+ If py = "" Then
+ If fso.FileExists(cand) Then py = cand
+ End If
+Next
+
+If py = "" Then
+ ' PATH-Suche VERSTECKT (sh.Exec wuerde ein Konsolenfenster zeigen):
+ ' cmd versteckt laufen lassen, Ausgabe in Temp-Datei
+ tmpFile = sh.ExpandEnvironmentStrings("%TEMP%") & "\hms_py_path.txt"
+ On Error Resume Next
+ sh.Run "cmd /c where pythonw > """ & tmpFile & """ 2>nul", 0, True
+ If fso.FileExists(tmpFile) Then
+ Set ts = fso.OpenTextFile(tmpFile, 1)
+ If Not ts.AtEndOfStream Then
+ out = Trim(ts.ReadLine())
+ If Len(out) > 4 Then
+ If fso.FileExists(out) Then py = out
+ End If
+ End If
+ ts.Close
+ fso.DeleteFile tmpFile, True
+ End If
+ On Error GoTo 0
+End If
+
+If py = "" Then
+ MsgBox "Python wurde nicht gefunden." & vbCrLf & vbCrLf & _
+ "Bitte einmal 'HMS-Install.vbs' doppelklicken -" & vbCrLf & _
+ "es installiert Python und GStreamer ueber grafische Installer.", _
+ 48, "HMS MediaEngine"
+ WScript.Quit 1
+End If
+
+sh.CurrentDirectory = dir_
+sh.Run Chr(34) & py & Chr(34) & " " & Chr(34) & dir_ & "\launcher.pyw" & Chr(34), 0, False
diff --git a/installer_gui.py b/installer_gui.py
new file mode 100644
index 0000000..00b409f
--- /dev/null
+++ b/installer_gui.py
@@ -0,0 +1,336 @@
+#!/usr/bin/env python3
+"""HMS MediaEngine – Grafischer Installer (Fenster statt Konsole).
+
+Wird vom macOS-App-Bundle oder HMS-Start.vbs (Windows) geöffnet, wenn
+Abhaengigkeiten fehlen. Der Nutzer sieht ein Fenster – kein Terminal:
+
+- macOS: GStreamer/PyGObject via Homebrew. Native Passwort-Eingabe durch
+ macOS-Systemdialog (osascript 'with administrator privileges').
+- Windows: Offizielle Installer-GUIs (GStreamer-MSI, Python-Setup) werden
+ gestartet – der Nutzer klickt nur 'Weiter/Fertig'.
+
+Nach der Installation: Button 'MediaEngine starten' – die App laeuft
+ danach unsichtbar im Hintergrund, nur der Browser ist sichtbar.
+"""
+
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+import tempfile
+import threading
+import urllib.request
+import webbrowser
+from pathlib import Path
+
+import tkinter as tk
+from tkinter import ttk, messagebox
+
+ROOT = Path(__file__).resolve().parent
+IS_MAC = sys.platform == "darwin"
+IS_WIN = sys.platform == "win32"
+
+GST_MSI = ("https://gstreamer.freedesktop.org/data/pkg/windows/"
+ "1.28.4/msvc/gstreamer-1.0-msvc-x86_64-1.28.4.msi")
+PY_EXE = "https://www.python.org/ftp/python/3.13.2/python-3.13.2-amd64.exe"
+BREW_PKGS = [
+ "python@3.12", "python-tk", "gstreamer", "gst-plugins-base",
+ "gst-plugins-good", "gst-plugins-bad", "gst-plugins-ugly",
+ "gst-libav", "pygobject",
+]
+
+
+def brew_path() -> str | None:
+ for p in ("/opt/homebrew/bin/brew", "/usr/local/bin/brew"):
+ if Path(p).exists():
+ return p
+ return None
+
+
+def gst_ok() -> bool:
+ try:
+ import gi # noqa: F401
+ gi.require_version("Gst", "1.0")
+ from gi.repository import Gst
+ Gst.init(None)
+ return True
+ except Exception: # noqa: BLE001
+ return False
+
+
+def py_ok() -> bool:
+ try:
+ r = subprocess.run(["python", "--version"], capture_output=True,
+ timeout=10,
+ creationflags=0x08000000 if IS_WIN else 0)
+ return r.returncode == 0
+ except Exception: # noqa: BLE001
+ return False
+
+
+def mac_python_ok() -> bool:
+ """Ein python3, das spaeter auch gi importieren kann (brew-Python)."""
+ for cand in ("/opt/homebrew/bin/python3", "/usr/local/bin/python3",
+ "python3"):
+ try:
+ r = subprocess.run([cand, "-c", "import tkinter"],
+ capture_output=True, timeout=10)
+ if r.returncode == 0:
+ return True
+ except Exception: # noqa: BLE001
+ continue
+ return False
+
+
+def dl(url: str, dest: Path) -> None:
+ def hook(n, bs, total):
+ if total > 0:
+ pct = min(100, n * bs * 100 // total)
+ app.on_progress(pct)
+ urllib.request.urlretrieve(url, dest, hook)
+
+
+def run_quiet(cmd: list[str]) -> None:
+ subprocess.run(cmd, check=False,
+ creationflags=0x08000000 if IS_WIN else 0)
+
+
+class InstallerApp:
+ def __init__(self):
+ self.root = tk.Tk()
+ self.root.title("HMS MediaEngine – Installation")
+ self.root.geometry("680x520")
+ self.root.configure(bg="#16181d")
+ try:
+ style = ttk.Style(self.root)
+ style.theme_use("clam")
+ except Exception: # noqa: BLE001
+ pass
+
+ self.steps: list[tuple[str, bool]] = []
+ self.log_lines: list[str] = []
+ self._build()
+ self.refresh_status()
+
+ # ---------- UI ----------
+
+ def _build(self):
+ head = tk.Frame(self.root, bg="#16181d")
+ head.pack(fill="x", padx=16, pady=(12, 6))
+ tk.Label(head, text="HMS MediaEngine",
+ font=("System", 17, "bold"),
+ bg="#16181d", fg="#e8eaee").pack(anchor="w")
+ tk.Label(head, text="Grafische Installation – kein Terminal nötig",
+ bg="#16181d", fg="#8a8f9a").pack(anchor="w")
+
+ self.status_var = tk.StringVar()
+ tk.Label(self.root, textvariable=self.status_var,
+ bg="#16181d", fg="#c8ccd4").pack(fill="x", padx=16)
+
+ self.bar = ttk.Progressbar(self.root, mode="determinate",
+ length=640)
+ self.bar.pack(fill="x", padx=16, pady=8)
+
+ logbox = tk.Frame(self.root, bg="#0d0f12")
+ logbox.pack(fill="both", expand=True, padx=16, pady=6)
+ self.log = tk.Text(logbox, bg="#0d0f12", fg="#c8ccd4",
+ insertbackground="#c8ccd4", height=14,
+ relief="flat", font=("Menlo" if IS_MAC
+ else "Consolas", 10))
+ self.log.pack(fill="both", expand=True, side="left")
+ scroll = ttk.Scrollbar(logbox, command=self.log.yview)
+ scroll.pack(side="right", fill="y")
+ self.log.configure(yscrollcommand=scroll.set)
+
+ btns = tk.Frame(self.root, bg="#16181d")
+ btns.pack(fill="x", padx=16, pady=10)
+ self.install_btn = tk.Button(
+ btns, text="Fehlendes installieren",
+ command=self.start_install, bg="#1d3a5f", fg="#dbe9ff",
+ activebackground="#2a4f7f", relief="flat", padx=14, pady=7)
+ self.install_btn.pack(side="left")
+ self.start_btn = tk.Button(
+ btns, text="▶ MediaEngine starten",
+ command=self.start_engine, bg="#1f4d2a", fg="#d8ffd8",
+ activebackground="#2a6a3a", relief="flat", padx=14, pady=7)
+ self.start_btn.pack(side="left", padx=8)
+ tk.Button(btns, text="Beenden", command=self.root.destroy,
+ bg="#262a33", fg="#c8ccd4", relief="flat",
+ padx=14, pady=7).pack(side="right")
+
+ def on_log(self, text: str):
+ self.log_lines.append(text)
+ self.log.insert("end", text + "\n")
+ self.log.see("end")
+ self.root.update_idletasks()
+
+ def on_progress(self, pct: int):
+ self.bar["value"] = pct
+ self.root.update_idletasks()
+
+ def on_status(self, text: str):
+ self.status_var.set(text)
+ self.root.update_idletasks()
+
+ # ---------- Status ----------
+
+ def refresh_status(self):
+ self.steps = []
+ if IS_WIN:
+ self.steps.append(("Python 3.13", py_ok()))
+ gs = (os.environ.get("GSTREAMER_1_0_ROOT_MSVC_X86_64")
+ or "")
+ self.steps.append(("GStreamer 1.28",
+ bool(gs) or Path(
+ "C:\\gstreamer\\1.0\\msvc_x86_64"
+ ).exists()))
+ elif IS_MAC:
+ self.steps.append(("Homebrew", brew_path() is not None))
+ self.steps.append(("Python 3 (mit tkinter)", mac_python_ok()))
+ self.steps.append(("GStreamer + PyGObject", gst_ok()))
+ else:
+ self.steps.append(("GStreamer (PyGObject)", gst_ok()))
+
+ self.log.delete("1.0", "end")
+ for name, ok in self.steps:
+ mark = "✓" if ok else "✗"
+ self.on_log(f"{mark} {name}: "
+ + ("vorhanden" if ok else "fehlt – wird installiert"))
+ missing = [n for n, ok in self.steps if not ok]
+ if not missing:
+ self.on_status("Alles bereit – du kannst MediaEngine starten.")
+ self.install_btn.config(state="disabled")
+ self.start_btn.config(state="normal", bg="#1f4d2a")
+ else:
+ self.on_status("Fehlt: " + ", ".join(missing)
+ + " → Button links drücken.")
+ self.install_btn.config(state="normal")
+ self.start_btn.config(state="disabled", bg="#262a33")
+
+ # ---------- Installation ----------
+
+ def start_install(self):
+ self.install_btn.config(state="disabled")
+ threading.Thread(target=self._install_worker, daemon=True).start()
+
+ def _install_worker(self):
+ try:
+ if IS_MAC:
+ self._install_mac()
+ elif IS_WIN:
+ self._install_win()
+ self.on_log("")
+ self.on_log("Installation abgeschlossen.")
+ self.on_log("Falls gerade etwas installiert wurde, das Python "
+ "betrifft: dieses Fenster einmal schließen und "
+ "die App erneut starten.")
+ self.refresh_status()
+ except Exception as e: # noqa: BLE001
+ self.on_log(f"FEHLER: {e}")
+ self.on_status("Installation fehlgeschlagen – siehe Log. "
+ "(Meldung an den Support reicht zum Fixen.)")
+ self.install_btn.config(state="normal")
+
+ def _osascript_admin(self, script: str, prompt: str) -> str:
+ esc = script.replace('\\', '\\\\').replace('"', '\\"')
+ code = (f'do shell script "{esc}" '
+ f'with administrator privileges '
+ f'with prompt "{prompt}"')
+ r = subprocess.run(["osascript", "-e", code],
+ capture_output=True, text=True, timeout=3600)
+ if r.returncode != 0:
+ raise RuntimeError(r.stderr.strip() or "osascript fehlgeschlagen")
+ return r.stdout
+
+ def _install_mac(self):
+ self.on_status("macOS: Installation läuft (Passwort-Dialog folgt)…")
+ lines = ["set -e"]
+ brew = brew_path()
+ if brew is None:
+ self.on_log("Homebrew fehlt – wird installiert (mehrere Minuten)…")
+ lines.append(
+ 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL '
+ 'https://raw.githubusercontent.com/Homebrew/'
+ 'install/HEAD/install.sh)"')
+ # brew-Pfad robust im Skript auswaehlen (Apple Silicon vs. Intel);
+ # absolute Pfade machen ein eval/shellenv ueberfluessig
+ lines.append(
+ 'if [ -x /opt/homebrew/bin/brew ]; then '
+ 'BREW=/opt/homebrew/bin/brew; '
+ 'else BREW=/usr/local/bin/brew; fi')
+ lines.append('"$BREW" install ' + " ".join(BREW_PKGS))
+ script = "\n".join(lines)
+ with tempfile.NamedTemporaryFile("w", suffix=".sh",
+ delete=False) as f:
+ f.write(script)
+ sh = f.name
+ os.chmod(sh, 0o700)
+ self.on_log("Starte Installation (macOS fragt nach deinem "
+ "Administrator-Passwort)…")
+ out = self._osascript_admin(f"/bin/bash {sh}",
+ "HMS MediaEngine: GStreamer und "
+ "Python-Komponenten installieren")
+ for line in (out or "").strip().splitlines()[-12:]:
+ self.on_log(" " + line)
+ Path(sh).unlink(missing_ok=True)
+
+ def _install_win(self):
+ if not Path("C:\\gstreamer\\1.0\\msvc_x86_64").exists() \
+ and not os.environ.get("GSTREAMER_1_0_ROOT_MSVC_X86_64"):
+ self.on_status("Windows: GStreamer-Installer (GUI) – bitte "
+ "mit 'Weiter/Fertig' bestätigen…")
+ dest = Path(tempfile.gettempdir()) / "gstreamer.msi"
+ self.on_log("Lade GStreamer-Installer herunter…")
+ dl(GST_MSI, dest)
+ self.on_log("Öffne GStreamer-Installation (Fenster erscheint)…")
+ r = subprocess.run(["msiexec", "/i", str(dest)],
+ capture_output=False, timeout=3600)
+ self.on_log(f"GStreamer-Installer beendet (Code {r.returncode}).")
+ if not py_ok():
+ self.on_status("Windows: Python-Installer (GUI) – bitte "
+ "'Add python.exe to PATH' anhaken und installieren…")
+ dest = Path(tempfile.gettempdir()) / "python-setup.exe"
+ self.on_log("Lade Python-Installer herunter…")
+ dl(PY_EXE, dest)
+ self.on_log("Öffne Python-Installation (Fenster erscheint)…")
+ subprocess.run([str(dest)], timeout=3600)
+ self.on_log("Python-Installation abgeschlossen.")
+ self.on_log("WICHTIG: Falls PATH soeben geändert wurde, dieses "
+ "Fenster schließen und HMS-Start.vbs erneut "
+ "doppelklicken.")
+
+ # ---------- Start ----------
+
+ def start_engine(self):
+ try:
+ if IS_WIN:
+ pyw = "pythonw"
+ subprocess.Popen(
+ [pyw if Path(sys.executable).stem == "python"
+ else sys.executable, str(ROOT / "run.py")],
+ cwd=str(ROOT),
+ creationflags=0x08000000,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ else:
+ subprocess.Popen(
+ ["python3", str(ROOT / "run.py")], cwd=str(ROOT),
+ stdout=open("/tmp/hms-mediaengine.log", "ab"),
+ stderr=subprocess.STDOUT,
+ start_new_session=True)
+ self.on_status("MediaEngine startet – Browser öffnet sich…")
+ self.root.after(2500, lambda: webbrowser.open(
+ "http://localhost:8080"))
+ self.root.after(4000, self.root.destroy)
+ except Exception as e: # noqa: BLE001
+ messagebox.showerror("HMS MediaEngine",
+ f"Start fehlgeschlagen:\n{e}")
+
+ def run(self):
+ self.root.mainloop()
+
+
+if __name__ == "__main__":
+ app = InstallerApp()
+ app.run()
diff --git a/launcher.pyw b/launcher.pyw
new file mode 100644
index 0000000..613462d
--- /dev/null
+++ b/launcher.pyw
@@ -0,0 +1,8 @@
+# Unsichtbarer Windows-Launcher (pythonw ohne Konsolenfenster).
+# Wird von HMS-Start.vbs aufgerufen.
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from launcher_core import launch
+sys.exit(launch())
diff --git a/launcher_core.py b/launcher_core.py
new file mode 100644
index 0000000..57be452
--- /dev/null
+++ b/launcher_core.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+"""Gemeinsame Launcher-Logik fuer Windows-Starter und macOS.
+
+Unsichtbarer Ablauf: GStreamer vorhanden -> Engine im Hintergrund
+starten, auf die Web-UI warten, Browser oeffnen.
+Fehlt etwas -> grafischer Installer (installer_gui.py) oeffnen.
+"""
+
+from __future__ import annotations
+
+import json
+import subprocess
+import sys
+import time
+import urllib.request
+import webbrowser
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent
+IS_WIN = sys.platform == "win32"
+
+
+def web_port() -> int:
+ try:
+ cfg = json.loads((ROOT / "config.json").read_text("utf-8"))
+ return int(cfg.get("web", {}).get("port", 8080))
+ except Exception: # noqa: BLE001
+ return 8080
+
+
+def gst_ok() -> bool:
+ try:
+ import gi # noqa: F401
+ gi.require_version("Gst", "1.0")
+ from gi.repository import Gst
+ Gst.init(None)
+ return True
+ except Exception: # noqa: BLE001
+ return False
+
+
+def _spawn(py: str, target: str, hidden: bool) -> None:
+ if IS_WIN and hidden:
+ subprocess.Popen(
+ [py, str(ROOT / target)], cwd=str(ROOT),
+ creationflags=0x08000000, # CREATE_NO_WINDOW
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ else:
+ subprocess.Popen(
+ [py, str(ROOT / target)], cwd=str(ROOT),
+ stdout=open("/tmp/hms-mediaengine.log", "ab"),
+ stderr=subprocess.STDOUT, start_new_session=not IS_WIN)
+
+
+def wait_health(port: int, timeout_s: float = 25.0) -> bool:
+ deadline = time.monotonic() + timeout_s
+ while time.monotonic() < deadline:
+ try:
+ with urllib.request.urlopen(
+ f"http://localhost:{port}/api/health",
+ timeout=1.5) as r:
+ if json.loads(r.read().decode()).get("status") == "ok":
+ return True
+ except Exception: # noqa: BLE001
+ time.sleep(0.4)
+ return False
+
+
+def launch() -> int:
+ if gst_ok():
+ _spawn(sys.executable, "run.py", hidden=True)
+ port = web_port()
+ if wait_health(port):
+ webbrowser.open(f"http://localhost:{port}")
+ return 0
+ print("[Launcher] Engine antwortete nicht rechtzeitig – "
+ "oeffne Installer.", file=sys.stderr)
+ _spawn(sys.executable, "installer_gui.py", hidden=False)
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(launch())
diff --git a/make_mac_app.py b/make_mac_app.py
new file mode 100644
index 0000000..08e10e7
--- /dev/null
+++ b/make_mac_app.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+"""Erzeugt 'HMS MediaEngine.app' im Projektordner (macOS App-Bundle).
+
+Aufruf: python3 make_mac_app.py
+Danach: 'HMS MediaEngine.app' per Doppelklick im Finder starten.
+Der Launcher prueft unsichtbar: fehlt GStreamer, oeffnet sich der
+grafische Installer; ist alles da, startet die Engine im Hintergrund
+und der Browser oeffnet sich.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent
+APP = ROOT / "HMS MediaEngine.app"
+
+INFO_PLIST = """
+
+
+
+ CFBundleNameHMS MediaEngine
+ CFBundleDisplayNameHMS MediaEngine
+ CFBundleIdentifierde.hms.mediaengine
+ CFBundleVersion0.1.0
+ CFBundleShortVersionString0.1.0
+ CFBundlePackageTypeAPPL
+ CFBundleExecutableHMS-Launcher
+ LSMinimumSystemVersion11.0
+ NSHighResolutionCapable
+ CFBundleInfoDictionaryVersion6.0
+
+
+"""
+
+LAUNCHER = r'''#!/bin/bash
+# HMS MediaEngine App-Start (vom Finder, ohne sichtbares Terminal)
+DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
+PY=""
+for p in /opt/homebrew/bin/python3 /usr/local/bin/python3 python3; do
+ if command -v "$p" >/dev/null 2>&1; then PY="$p"; break; fi
+done
+if [ -z "$PY" ]; then
+ osascript -e 'display alert "Python nicht gefunden" message "Bitte HMS-Mac-Install.command im Projektordner einmal ausfuehren - es installiert alles Grafische." as critical' >/dev/null 2>&1
+ exit 1
+fi
+cd "$DIR"
+if "$PY" -c "import gi; gi.require_version('Gst','1.0')" >/dev/null 2>&1; then
+ nohup "$PY" "$DIR/run.py" >>/tmp/hms-mediaengine.log 2>&1 &
+ PORT=8080
+ for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \
+ 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40; do
+ curl -s -o /dev/null "http://localhost:$PORT/api/health" && break
+ sleep 0.5
+ done
+ open "http://localhost:$PORT"
+ exit 0
+else
+ nohup "$PY" "$DIR/installer_gui.py" >/tmp/hms-installer.log 2>&1 &
+ exit 0
+fi
+'''
+
+
+def build() -> None:
+ macos = APP / "Contents" / "MacOS"
+ macos.mkdir(parents=True, exist_ok=True)
+ (APP / "Contents" / "Info.plist").write_text(INFO_PLIST, "utf-8")
+ launcher = macos / "HMS-Launcher"
+ launcher.write_text(LAUNCHER, "utf-8")
+ launcher.chmod(0o755)
+ print(f"App erstellt: {APP}")
+ print("Doppelklick im Finder: 'HMS MediaEngine.app'")
+
+
+if __name__ == "__main__":
+ build()