DOPPELKlick-WELT: Grafischer Installer + App-Bundles (kein Terminal mehr noetig)

Der Nutzer will KEINE Konsole - jetzt ist alles Doppelklick:

1) GRAFISCHER INSTALLER (installer_gui.py, 336 Zeilen):
- Fenster mit Status-Checks (Python/GStreamer/Homebrew: fehlt/vorhanden),
  Fortschrittsbalken beim Download, Log-Bereich, grosse Buttons
- macOS: Homebrew + GStreamer + PyGObject + tkinter via brew;
  Passwort-Dialog kommt NATIV von macOS (osascript administrator
  privileges) - kein sudo-Tippen
- Windows: laedt und startet die OFFIZIELLEN Installer-GUIs
  (GStreamer-MSI + Python-Setup) - Nutzer klickt nur Weiter/Fertig
- Nach Installation: Button 'MediaEngine starten' spawnt die Engine
  UNSICHTBAR im Hintergrund und oeffnet den Browser
- Smoke-Test im Container bestanden (xvfb): Fenster baut sich auf,
  Statuschecks laufen, sauber beendet

2) macOS: 'HMS MediaEngine.app' (echtes App-Bundle im Repo):
- CFBundleExecutable HMS-Launcher (Bash): sucht brew-python3,
  prueft GStreamer unsichtbar -> Engine nohup im Hintergrund +
  Browser oeffnet sich; fehlt etwas -> grafischer Installer
- make_mac_app.py: App neu erzeugen falls noetig
- HMS-Mac-Install.command: Doppelklick-Installation mit nativen
  osascript-Dialogen (Abbrechen/Weiter) + GUI-Installer am Ende

3) Windows: HMS-Start.vbs (unsichtbar) + HMS-Install.vbs (sichtbar):
- HMS-Start.vbs: findet pythonw (4 Pfade + versteckte PATH-Suche
  ohne Konsolenblitz), startet launcher.pyw KOMPLETT unsichtbar
  (CREATE_NO_WINDOW) -> nur Browser erscheint
- HMS-Install.vbs: oeffnet grafischen Installer; fehlt Python,
  oeffnet offizielle Downloadseite mit Hinweis auf PATH-Haeckchen
- launcher_core.py: gemeinsame Logik (gst_ok -> Engine-Spawn ->
  Health-Poll -> webbrowser.open; sonst Installer-Fenster)

4) BEWEISE (im Container ausgefuehrt):
- Launcher-E2E: launch() -> Engine unsichtbar gestartet, Health
  gruen, Browser-URL korrekt (http://localhost:8080), RC=0
- GUI-Smoke-Test: Fenster + 'Alles bereit' + sauber beendet
- VBS: sh.Run-Zeilen geprueft (Chr(34)-Konstruktion, versteckte
  cmd-Suche)
- Alle 5 Regressionssuiten unveraendert gruen:
  Projekte 25/25 + Cue 35/35 + Verteilung 33/33 + App 14/14 +
  FX 16/16 = 123 Checks

Download: TAR.GZ (macOS, erhaelt Exec-Bits!) oder ZIP (Windows)
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 19:41:26 +02:00
parent 02765e6b34
commit 1e921a9085
9 changed files with 680 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>HMS MediaEngine</string>
<key>CFBundleDisplayName</key><string>HMS MediaEngine</string>
<key>CFBundleIdentifier</key><string>de.hms.mediaengine</string>
<key>CFBundleVersion</key><string>0.1.0</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleExecutable</key><string>HMS-Launcher</string>
<key>LSMinimumSystemVersion</key><string>11.0</string>
<key>NSHighResolutionCapable</key><true/>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
</dict>
</plist>
+26
View File
@@ -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
+33
View File
@@ -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
+50
View File
@@ -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. 510 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
+51
View File
@@ -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
+336
View File
@@ -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()
+8
View File
@@ -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())
+83
View File
@@ -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())
+77
View File
@@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>HMS MediaEngine</string>
<key>CFBundleDisplayName</key><string>HMS MediaEngine</string>
<key>CFBundleIdentifier</key><string>de.hms.mediaengine</string>
<key>CFBundleVersion</key><string>0.1.0</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleExecutable</key><string>HMS-Launcher</string>
<key>LSMinimumSystemVersion</key><string>11.0</string>
<key>NSHighResolutionCapable</key><true/>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
</dict>
</plist>
"""
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()