From 9985a938510aa3de7e5bfc36a26aa9d89a4f161d Mon Sep 17 00:00:00 2001 From: Levi Woodard Date: Thu, 2 Jul 2026 09:50:39 -0500 Subject: [PATCH] Place windows at map time so stream restarts never flash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnosis: individual tiles flickered when their stream died and mpv relaunched (confirmed via status: a PID dropping to 0 and coming back). The restarted window mapped at the video's native size wherever sway dropped it, and the corrective loop only snapped it into its cell on the next tick — up to 2s later on the relaxed cadence. Fix, at the source instead of racing the map: - Pre-install a per-slot for_window rule (matched on each mpv's unique window title, anchored so slot-1 never matches slot-10) so sway positions and sizes the window synchronously the moment it maps. - Pass --geometry=WxH per tile so mpv opens at the tile size rather than resizing itself to the video's native size on load. The placeLoop remains as a corrective backstop for mid-life drift. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/compositor/compositor.go | 14 ++++++++++++++ internal/daemon/daemon.go | 7 +++++++ internal/player/player.go | 10 ++++++++++ 3 files changed, 31 insertions(+) diff --git a/internal/compositor/compositor.go b/internal/compositor/compositor.go index add2907..462b85c 100644 --- a/internal/compositor/compositor.go +++ b/internal/compositor/compositor.go @@ -185,3 +185,17 @@ func (c *Client) Place(ctx context.Context, pid int, r Rect) error { func (c *Client) PrepareForMPV(ctx context.Context) error { return c.run(ctx, `for_window [app_id="mpv"] floating enable, border none`) } + +// PlaceOnMap installs a for_window rule that positions any window with the +// given title at rect the moment it maps. This is what keeps a restarting +// stream from flashing at the wrong place/size: sway applies the geometry +// synchronously at map time, before the daemon's corrective loop ever sees +// the window. Re-installing a rule for the same title replaces it. The title +// is anchored (^...$) so slot-1 never matches slot-10. +func (c *Client) PlaceOnMap(ctx context.Context, title string, r Rect) error { + cmd := fmt.Sprintf( + `for_window [title="^%s$"] floating enable, border none, move absolute position %d %d, resize set %d %d`, + title, r.X, r.Y, r.W, r.H, + ) + return c.run(ctx, cmd) +} diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 454aabc..df4fc15 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -177,6 +177,13 @@ func (d *Daemon) applyLayout(ctx context.Context, name string) error { for _, s := range specs { p := player.New(s.slot, s.name, s.url, cfg.Player, d.runDir, d.log) + p.TileW, p.TileH = s.rect.W, s.rect.H + // Pre-install the placement rule so sway positions this slot's window + // (including after mid-life mpv restarts) the moment it maps — no + // wrong-place flash while waiting for the corrective loop. + if err := d.comp.PlaceOnMap(ctx, p.Title(), s.rect); err != nil { + d.log.Warn("could not preinstall placement rule", "slot", s.slot, "err", err) + } players = append(players, p) places = append(places, placement{p: p, rect: s.rect}) diff --git a/internal/player/player.go b/internal/player/player.go index a999400..ceffed1 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -25,6 +25,10 @@ type Player struct { Slot int // grid cell index (row-major) Name string // camera name, for logs/titles URL string // RTSP(S) source + // TileW/TileH, when set (before Supervise), are passed as --geometry so + // mpv opens its window at the tile size instead of the video's native + // size — a restarting stream then maps already-sized for its cell. + TileW, TileH int cfg config.Player ipcPath string @@ -94,6 +98,12 @@ func (p *Player) args() []string { // the audio decoder saves CPU per stream on the Pi. args = append(args, "--no-audio") } + if p.TileW > 0 && p.TileH > 0 { + // Open the window at the tile size; --geometry overrides mpv's + // resize-to-video-size on load, so the compositor never has to snap + // the window back into its cell. + args = append(args, fmt.Sprintf("--geometry=%dx%d", p.TileW, p.TileH)) + } if p.cfg.MaxFPS > 0 { args = append(args, fmt.Sprintf("--vf=fps=%d", p.cfg.MaxFPS)) }