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)) }