From 0fb731a71eabb51d58e117c722a85752b297d792 Mon Sep 17 00:00:00 2001 From: Levi Woodard Date: Wed, 1 Jul 2026 19:36:36 -0500 Subject: [PATCH] Fix mpv tile overlap portably; add make install/deploy mpv 0.35 rejects --auto-window-resize, which crash-looped every player. Drop that flag and re-assert each tile geometry on a 1s timer in the daemon, overriding mpv auto-resize on any mpv version. Add make install (auto-sudo, native build + copy to /usr/local/bin) and make deploy (install + restart the kiosk getty unit). Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 31 +++++++++++++++++++++++++++++-- internal/daemon/daemon.go | 30 +++++++++++++++++++----------- internal/player/player.go | 10 ++++------ 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index d83e4a6..2faf0fb 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,22 @@ PKG := ./cmd/rtsp-streamer BINDIR := bin LDFLAGS := -s -w -.PHONY: all build pi pi32 test vet clean run-tui +# Install location. Override with `make install PREFIX=/opt`. +PREFIX ?= /usr/local +DESTBIN := $(PREFIX)/bin/$(BINARY) + +# Use sudo automatically when not already root, so `make install` / +# `make deploy` work whether or not you prefix them with sudo. +SUDO := $(shell [ "$$(id -u)" -eq 0 ] || echo sudo) + +# The kiosk session unit restarted by `make deploy`. +KIOSK_UNIT ?= getty@tty1 + +.PHONY: all build pi pi32 test vet clean run-tui install deploy uninstall all: build -## build: compile for the host platform +## build: compile for the host platform (native arch) build: go build -ldflags '$(LDFLAGS)' -o $(BINDIR)/$(BINARY) $(PKG) @@ -19,6 +30,22 @@ pi: pi32: GOOS=linux GOARCH=arm GOARM=7 go build -ldflags '$(LDFLAGS)' -o $(BINDIR)/$(BINARY)-armv7 $(PKG) +## install: build for this machine and copy the binary to $(PREFIX)/bin +## (auto-sudo). This is the location the running daemon uses. +install: build + $(SUDO) install -m 0755 $(BINDIR)/$(BINARY) $(DESTBIN) + @echo "installed -> $(DESTBIN)" + +## deploy: install, then restart the kiosk session so the wall picks it up +deploy: install + $(SUDO) systemctl restart $(KIOSK_UNIT) + @echo "restarted $(KIOSK_UNIT); run 'rtsp-streamer status' to check" + +## uninstall: remove the installed binary +uninstall: + $(SUDO) rm -f $(DESTBIN) + @echo "removed $(DESTBIN)" + ## test: run unit tests test: go test ./... diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index b157512..1b8de44 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -153,27 +153,35 @@ func (d *Daemon) applyLayout(ctx context.Context, name string) error { return nil } -// placeWhenReady waits for the mpv window to map then tiles it, retrying while -// the layout is active (Supervise may relaunch mpv with a new pid). +// placeWhenReady waits for the mpv window to map, tiles it, then keeps +// re-asserting its geometry on a timer. The re-assertion matters because mpv +// resizes its own window to the camera's native resolution when the stream +// loads (and mpv 0.35 has no flag to disable that); re-issuing the sway +// resize snaps the window back into its cell, portably across mpv versions. func (d *Daemon) placeWhenReady(ctx context.Context, p *player.Player, rect compositor.Rect) { - var lastPID int - ticker := time.NewTicker(500 * time.Millisecond) + var readyPID int + ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { if ctx.Err() != nil { return } pid := p.PID() - if pid != 0 && pid != lastPID { - if err := d.comp.WaitForWindow(ctx, pid, 15*time.Second); err == nil { - if err := d.comp.Place(ctx, pid, rect); err != nil { - d.log.Warn("place failed", "slot", p.Slot, "err", err) - } else { - lastPID = pid - d.log.Debug("window placed", "slot", p.Slot, "pid", pid, "rect", rect) + if pid != 0 { + // New process: wait for its window to map before positioning. + if pid != readyPID { + if err := d.comp.WaitForWindow(ctx, pid, 15*time.Second); err != nil { + goto wait } + readyPID = pid + d.log.Debug("window mapped", "slot", p.Slot, "pid", pid, "rect", rect) + } + // Re-assert geometry every tick to override mpv's auto-resize. + if err := d.comp.Place(ctx, pid, rect); err != nil { + d.log.Debug("place failed", "slot", p.Slot, "err", err) } } + wait: select { case <-ctx.Done(): return diff --git a/internal/player/player.go b/internal/player/player.go index f8c381a..75b66e9 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -67,12 +67,10 @@ func (p *Player) args() []string { "--cursor-autohide=always", "--no-border", "--fullscreen=no", // we tile via the compositor, not fullscreen - // Never let mpv resize its own window to the camera's native - // resolution — the compositor owns tile geometry. Without this a - // high-res camera grows its window and overlaps its neighbours. - "--auto-window-resize=no", - // Scale video to fill the tile (letterboxed to preserve aspect); the - // window size is fixed by the compositor, not by the video. + // Keep aspect (letterbox) inside the tile; the compositor owns the + // window size, not the video. mpv still resizes its window to the + // video resolution on load (and mpv 0.35 lacks the flag to disable + // that), so the daemon re-asserts each tile's geometry on a timer. "--keepaspect=yes", "--title=" + p.Title(), "--input-ipc-server=" + p.ipcPath,