// Crossbuild for Windows: GOFLAGS=-buildvcs=false GOOS=windows GOARCH=386 go build -ldflags -H=windowsgui Main.go
// crossbuild for win: GOFLAGS=-buildvcs=false GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -tags gl -ldflags -H=windowsgui Main.go
// Todo:
// - mpv parameter "--ontop" optional über config
// - checken ob mpv erreichbar is
// - logging des streams in unterordner - nur die letzten 10 (config.txt)
// - Mailbenachrichtigung inkl. screenshot
// - code aufsplitten in
// - ?Main
// - ?Server
// - ?mpv
// - ?helpfile
// - ?logging
// - mehrere streams gleichzeitig? mpv rtsp://foo:[email protected] --external-file=rtsp://foo:[email protected] --lavfi-complex='[vid1] scale=960x540 [vid1_scale]; [vid2] scale=960x540 [vid2_scale]; [vid1_scale][vid2_scale] vstack [vo]' --geometry=75%+10+10/2 --profile=low-latency --ontop
// Stand 2026.06.15 - v0.4 demlak
package main
import (
_ "embed"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/getlantern/systray"
)
//go:embed assets/icons/icon.png
var iconpng []byte
//go:embed assets/icons/icon.ico
var iconico []byte
type Config map[string]string
var (
mpvCmd *exec.Cmd
mpvMu sync.Mutex
logFile *os.File
)
func initLogFile(dir string) error {
var err error
logPath := filepath.Join(dir, "mpv-controller.log")
logFile, err = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("Logdatei konnte nicht geöffnet werden: %v", err)
}
return nil
}
func logMsg(msg string) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
line := fmt.Sprintf("[%s] %s", timestamp, msg)
log.Println(msg)
if logFile != nil {
fmt.Fprintln(logFile, line)
}
}
func loadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
cfg := Config{}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if idx := strings.Index(line, "="); idx >= 0 {
k := strings.TrimSpace(line[:idx])
v := strings.TrimSpace(line[idx+1:])
v = strings.Trim(v, `"'`)
cfg[k] = v
}
}
return cfg, nil
}
func mpvStart(cfg Config, source string) error {
mpvMu.Lock()
defer mpvMu.Unlock()
if mpvCmd != nil && mpvCmd.Process != nil {
pid := mpvCmd.Process.Pid
if err := mpvCmd.Process.Kill(); err != nil {
return fmt.Errorf("Fehler beim Beenden der laufenden mpv-Instanz (PID %d): %v", pid, err)
}
logMsg(fmt.Sprintf("laufende mpv-Instanz präventiv beendet (PID %d)", pid))
mpvCmd = nil
}
link := cfg["STREAM_LINK"]
maxSec := cfg["MAX_PLAY_SECONDS"]
if link == "" {
return fmt.Errorf("STREAM_LINK fehlt in config")
}
args := []string{link, "--geometry=75%+10+10/2", "--profile=low-latency", "--ontop"}
if maxSec != "" {
args = append(args, "--end="+maxSec)
}
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("Fehler beim Ermitteln des aktuellen Verzeichnisses: %v", err)
}
if runtime.GOOS == "windows" {
mpvCmd = exec.Command(filepath.Join(wd, "mpv.exe"), args...)
} else {
mpvCmd = exec.Command("mpv", args...)
}
if err := mpvCmd.Start(); err != nil {
mpvCmd = nil
return fmt.Errorf("Fehler beim Starten von mpv: %v", err)
}
logMsg(fmt.Sprintf("mpv gestartet (PID %d) [Quelle: %s]", mpvCmd.Process.Pid, source))
return nil
}
func mpvStop(source string) error {
mpvMu.Lock()
defer mpvMu.Unlock()
if mpvCmd == nil || mpvCmd.Process == nil {
return fmt.Errorf("kein laufender mpv-Prozess gefunden")
}
pid := mpvCmd.Process.Pid
if err := mpvCmd.Process.Kill(); err != nil {
return fmt.Errorf("Fehler beim Beenden von mpv (PID %d): %v", pid, err)
}
logMsg(fmt.Sprintf("mpv gestoppt (PID %d) [Quelle: %s]", pid, source))
mpvCmd = nil
return nil
}
func senderIP(r *http.Request) string {
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
return forwarded
}
return r.RemoteAddr
}
func main() {
exe, err := os.Executable()
if err != nil {
log.Fatal("kann Pfad zur ausführbaren Datei nicht ermitteln:", err)
}
dir := filepath.Dir(exe)
if err := initLogFile(dir); err != nil {
log.Println("Warnung:", err)
}
defer logFile.Close()
logMsg("Programm gestartet")
cfgPath := filepath.Join(dir, "config.txt")
cfg, err := loadConfig(cfgPath)
if err != nil {
log.Fatalf("Konfigurationsdatei config.txt nicht gefunden oder nicht lesbar: %v", err)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
password := r.URL.Query().Get("password")
calledNumber := r.URL.Query().Get("calledNumber")
calledCommand := r.URL.Query().Get("calledCommand")
if username == "" || password == "" || calledNumber == "" || calledCommand == "" {
http.Error(w, "Bitte Benutzername, Passwort, Absender-Telefonnummer und Kommando in der URL angeben.", http.StatusBadRequest)
return
}
if username != cfg["VALID_USERNAME"] || password != cfg["VALID_PASSWORD"] || calledNumber != cfg["RING_NUMBER"] {
http.Error(w, "Kombination aus Benutzername, Passwort und Absender-Telefonnummer falsch", http.StatusUnauthorized)
return
}
ip := senderIP(r)
source := fmt.Sprintf("Webhook [IP: %s]", ip)
switch calledCommand {
case "CamStart":
logMsg(fmt.Sprintf("CamStart empfangen [IP: %s]", ip))
if err := mpvStart(cfg, source); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "Befehl CamStart wurde erfolgreich ausgeführt.")
case "CamStop":
logMsg(fmt.Sprintf("CamStop empfangen [IP: %s]", ip))
if err := mpvStop(source); err != nil {
fmt.Fprintln(w, "Fehler beim Stoppen von mpv:", err)
return
}
fmt.Fprintln(w, "Befehl CamStop wurde erfolgreich ausgeführt.")
default:
http.Error(w, "Ungültiger Befehl", http.StatusBadRequest)
}
})
go func() {
port := cfg["PORT"]
if port == "" {
port = "8080"
}
logMsg(fmt.Sprintf("HTTP-Server läuft auf :%s", port))
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal("HTTP-Server Fehler:", err)
}
}()
onReady := func() {
if runtime.GOOS == "windows" {
systray.SetIcon(iconico)
} else {
systray.SetIcon(iconpng)
}
systray.SetTitle("mpv-controller")
systray.SetTooltip("mpv remote controller (HTTP)")
mStart := systray.AddMenuItem("Start mpv", "Startet mpv manuell")
mStop := systray.AddMenuItem("Stop mpv", "Beendet mpv manuell")
systray.AddSeparator()
mQuit := systray.AddMenuItem("Beenden", "Beendet das Programm")
go func() {
for {
select {
case <-mStart.ClickedCh:
if err := mpvStart(cfg, "Systray"); err != nil {
logMsg("Start-Fehler: " + err.Error())
}
case <-mStop.ClickedCh:
if err := mpvStop("Systray"); err != nil {
logMsg("Stop-Fehler: " + err.Error())
}
case <-mQuit.ClickedCh:
systray.Quit()
return
}
}
}()
}
onExit := func() {
logMsg("Programm beendet")
}
systray.Run(onReady, onExit)
}