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:
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+50
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user