diff --git a/player/agent/internal/mqttsubscriber/subscriber.go b/player/agent/internal/mqttsubscriber/subscriber.go index 1770533..5adc026 100644 --- a/player/agent/internal/mqttsubscriber/subscriber.go +++ b/player/agent/internal/mqttsubscriber/subscriber.go @@ -12,7 +12,8 @@ import ( const ( // debounceDuration is the minimum interval between two callback invocations. // Any MQTT message arriving while the timer is still running resets it. - debounceDuration = 3 * time.Second + // 500ms reicht aus um Bursts zu absorbieren, ohne die Latenz merklich zu erhöhen. + debounceDuration = 500 * time.Millisecond // playlistChangedTopicTemplate is the topic the backend publishes to. playlistChangedTopic = "signage/screen/%s/playlist-changed" diff --git a/player/agent/internal/playerserver/server.go b/player/agent/internal/playerserver/server.go index 4c3997b..7cb7e77 100644 --- a/player/agent/internal/playerserver/server.go +++ b/player/agent/internal/playerserver/server.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io/fs" + "math/rand" "net" "net/http" "os" @@ -48,14 +49,16 @@ type SysInfo struct { // Server serves the local player UI to Chromium. type Server struct { - listenAddr string - nowFn func() NowPlaying + listenAddr string + nowFn func() NowPlaying + startupToken string // zufälliger Token der sich bei jedem Start ändert } // New creates a Server. listenAddr is e.g. "127.0.0.1:8090". // nowFn is called on each request and returns the current playback state. func New(listenAddr string, nowFn func() NowPlaying) *Server { - return &Server{listenAddr: listenAddr, nowFn: nowFn} + token := fmt.Sprintf("%016x", rand.Uint64()) + return &Server{listenAddr: listenAddr, nowFn: nowFn, startupToken: token} } // Run starts the HTTP server and blocks until ctx is cancelled. @@ -69,6 +72,7 @@ func (s *Server) Run(ctx context.Context) error { mux.HandleFunc("GET /player", s.handlePlayer) mux.HandleFunc("GET /api/now-playing", s.handleNowPlaying) mux.HandleFunc("GET /api/sysinfo", handleSysInfo) + mux.HandleFunc("GET /api/startup-token", s.handleStartupToken) mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub)))) srv := &http.Server{Handler: mux} @@ -99,6 +103,14 @@ func (s *Server) handleNowPlaying(w http.ResponseWriter, _ *http.Request) { json.NewEncoder(w).Encode(s.nowFn()) //nolint:errcheck } +// handleStartupToken gibt einen zufälligen Token zurück der sich bei jedem +// Agent-Start ändert. Der Browser erkennt daran, dass der Agent neu gestartet +// wurde und lädt die Seite automatisch neu. +func (s *Server) handleStartupToken(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"token": s.startupToken}) //nolint:errcheck +} + func handleSysInfo(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(collectSysInfo()) //nolint:errcheck @@ -326,6 +338,11 @@ const playerHTML = ` // Versteckt alle Content-Elemente vor dem Anzeigen des richtigen Typs. // Blendet zunächst auf opacity:0 aus und entfernt display erst nach der // Transition (500ms), damit der Fade-Out sichtbar ist. + // + // Race-Condition-Fix: Das setTimeout-Callback prüft vor dem display=none, + // ob das Element noch opacity=0 hat. Falls displayItem() das Element + // inzwischen wieder auf display=block+opacity=1 gesetzt hat, wird es + // nicht fälschlicherweise versteckt. function hideAllContent() { // Laufendes Video sofort stoppen damit kein Audio weiterläuft. videoView.pause(); @@ -335,7 +352,14 @@ const playerHTML = ` if (el.style.display !== 'none') { el.style.opacity = '0'; (function(e) { - setTimeout(function() { e.style.display = 'none'; }, 500); + setTimeout(function() { + // Nur verstecken wenn das Element noch ausgeblendet ist + // (opacity=0 oder leer). Falls displayItem() es inzwischen + // wieder sichtbar gemacht hat, nicht anfassen. + if (e.style.opacity === '0' || e.style.opacity === '') { + e.style.display = 'none'; + } + }, 500); })(el); } }); @@ -533,10 +557,11 @@ const playerHTML = ` function startSlowPoll() { if (slowPollInterval) return; + // Playlist alle 5s prüfen (fängt MQTT-getriggerte Backend-Änderungen schnell ab). + // Sysinfo läuft separat alle 30s (weiter unten). slowPollInterval = setInterval(function() { pollNowPlaying(); - pollSysInfo(); - }, 30000); + }, 5000); } function pollNowPlaying() { @@ -567,6 +592,30 @@ const playerHTML = ` startSlowPoll(); } }, 60000); + + // ── Auto-Reload bei Agent-Neustart ─────────────────────────────── + // Der Agent gibt bei jedem Start einen neuen zufälligen Token zurück. + // Falls sich der Token ändert, hat der Agent neu gestartet → Seite neu laden. + var knownStartupToken = null; + + function pollStartupToken() { + fetch('/api/startup-token') + .then(function(r) { return r.json(); }) + .then(function(d) { + if (!d || !d.token) return; + if (knownStartupToken === null) { + // Erster Aufruf: Token merken, kein Reload. + knownStartupToken = d.token; + } else if (knownStartupToken !== d.token) { + // Token hat sich geändert → Agent wurde neu gestartet. + window.location.reload(); + } + }) + .catch(function() {}); // Agent offline → ignorieren + } + + pollStartupToken(); + setInterval(pollStartupToken, 5000); // alle 5s prüfen `